UNPKG

@underpostnet/underpost

Version:
526 lines (488 loc) 19.3 kB
import { buildKindPorts, buildPortProxyRouter, buildProxyRouter, Config, deployRangePortFactory, getDataDeploy, loadReplicas, pathPortAssignmentFactory, } from '../server/conf.js'; import { loggerFactory } from '../server/logger.js'; import { shellExec } from '../server/process.js'; import fs from 'fs-extra'; import dotenv from 'dotenv'; import { DataBaseProvider } from '../db/DataBaseProvider.js'; import UnderpostRootEnv from './env.js'; import UnderpostCluster from './cluster.js'; import Underpost from '../index.js'; const logger = loggerFactory(import.meta); class UnderpostDeploy { static NETWORK = {}; static API = { sync(deployList, { versions, replicas, kubeadm = false }) { const deployGroupId = 'dd.router'; fs.writeFileSync(`./engine-private/deploy/${deployGroupId}`, deployList, 'utf8'); const totalPods = deployList.split(',').length * versions.split(',').length * parseInt(replicas); const limitFactor = 0.8; const reserveFactor = 0.05; const resources = UnderpostCluster.API.getResourcesCapacity(kubeadm); const memory = parseInt(resources.memory.value / totalPods); const cpu = parseInt(resources.cpu.value / totalPods); UnderpostRootEnv.API.set( 'resources.requests.memory', `${parseInt(memory * reserveFactor)}${resources.memory.unit}`, ); UnderpostRootEnv.API.set('resources.requests.cpu', `${parseInt(cpu * reserveFactor)}${resources.cpu.unit}`); UnderpostRootEnv.API.set('resources.limits.memory', `${parseInt(memory * limitFactor)}${resources.memory.unit}`); UnderpostRootEnv.API.set('resources.limits.cpu', `${parseInt(cpu * limitFactor)}${resources.cpu.unit}`); UnderpostRootEnv.API.set('total-pods', totalPods); return getDataDeploy({ buildSingleReplica: true, deployGroupId, }); }, async routerFactory(deployList, env) { const initEnvPath = `./engine-private/conf/${deployList.split(',')[0]}/.env.${env}`; const initEnvObj = dotenv.parse(fs.readFileSync(initEnvPath, 'utf8')); process.env.PORT = initEnvObj.PORT; process.env.NODE_ENV = env; await Config.build(undefined, 'proxy', deployList); return buildPortProxyRouter(env === 'development' ? 80 : 443, buildProxyRouter()); }, deploymentYamlServiceFactory({ deployId, env, port, deploymentVersions }) { return deploymentVersions .map( (version, i) => ` - name: ${deployId}-${env}-${version}-service port: ${port} weight: ${i === 0 ? 100 : 0} `, ) .join(''); }, deploymentYamlPartsFactory({ deployId, env, suffix, resources, replicas }) { return `apiVersion: apps/v1 kind: Deployment metadata: name: ${deployId}-${env}-${suffix} labels: app: ${deployId}-${env}-${suffix} spec: replicas: ${replicas} selector: matchLabels: app: ${deployId}-${env}-${suffix} template: metadata: labels: app: ${deployId}-${env}-${suffix} spec: containers: - name: ${deployId}-${env}-${suffix} image: localhost/rockylinux9-underpost:${Underpost.version} # resources: # requests: # memory: "${resources.requests.memory}" # cpu: "${resources.requests.cpu}" # limits: # memory: "${resources.limits.memory}" # cpu: "${resources.limits.cpu}" command: - /bin/sh - -c - > npm install -g npm@11.2.0 && npm install -g underpost && underpost secret underpost --create-from-file /etc/config/.env.${env} && underpost start --build --run ${deployId} ${env} volumeMounts: - name: config-volume mountPath: /etc/config volumes: - name: config-volume configMap: name: underpost-config --- apiVersion: v1 kind: Service metadata: name: ${deployId}-${env}-${suffix}-service spec: selector: app: ${deployId}-${env}-${suffix} ports: {{ports}} type: LoadBalancer`; }, async buildManifest(deployList, env, options) { const resources = UnderpostDeploy.API.resourcesFactory(); const replicas = options.replicas; for (const _deployId of deployList.split(',')) { const deployId = _deployId.trim(); if (!deployId) continue; const confServer = loadReplicas( JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8')), 'proxy', ); const router = await UnderpostDeploy.API.routerFactory(deployId, env); const pathPortAssignmentData = pathPortAssignmentFactory(router, confServer); const { fromPort, toPort } = deployRangePortFactory(router); const deploymentVersions = options.versions.split(','); fs.mkdirSync(`./engine-private/conf/${deployId}/build/${env}`, { recursive: true }); if (env === 'development') fs.mkdirSync(`./manifests/deployment/${deployId}-${env}`, { recursive: true }); logger.info('port range', { deployId, fromPort, toPort }); let deploymentYamlParts = ''; for (const deploymentVersion of deploymentVersions) { deploymentYamlParts += `--- ${UnderpostDeploy.API.deploymentYamlPartsFactory({ deployId, env, suffix: deploymentVersion, resources, replicas, }).replace('{{ports}}', buildKindPorts(fromPort, toPort))} `; } fs.writeFileSync(`./engine-private/conf/${deployId}/build/${env}/deployment.yaml`, deploymentYamlParts, 'utf8'); let proxyYaml = ''; let secretYaml = ''; for (const host of Object.keys(confServer)) { if (env === 'production') secretYaml += ` --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: ${host} spec: commonName: ${host} dnsNames: - ${host} issuerRef: name: letsencrypt-prod kind: ClusterIssuer secretName: ${host}`; const pathPortAssignment = pathPortAssignmentData[host]; // logger.info('', { host, pathPortAssignment }); proxyYaml += ` --- apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: ${host} spec: virtualhost: fqdn: ${host}${ env === 'development' ? '' : ` tls: secretName: ${host}` } routes:`; for (const conditionObj of pathPortAssignment) { const { path, port } = conditionObj; proxyYaml += ` - conditions: - prefix: ${path} enableWebsockets: true services: ${UnderpostDeploy.API.deploymentYamlServiceFactory({ deployId, env, port, deploymentVersions: options.traffic && typeof options.traffic === 'string' ? options.traffic.split(',') : ['blue'], })}`; } } const yamlPath = `./engine-private/conf/${deployId}/build/${env}/proxy.yaml`; fs.writeFileSync(yamlPath, proxyYaml, 'utf8'); if (env === 'production') { const yamlPath = `./engine-private/conf/${deployId}/build/${env}/secret.yaml`; fs.writeFileSync(yamlPath, secretYaml, 'utf8'); } else { const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml']; for (const file of deploymentsFiles) { if (fs.existsSync(`./engine-private/conf/${deployId}/build/${env}/${file}`)) { fs.copyFileSync( `./engine-private/conf/${deployId}/build/${env}/${file}`, `./manifests/deployment/${deployId}-${env}/${file}`, ); } } } } }, getCurrentTraffic(deployId) { // kubectl get deploy,sts,svc,configmap,secret -n default -o yaml --export > default.yaml const hostTest = Object.keys( JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8')), )[0]; const info = shellExec(`sudo kubectl get HTTPProxy/${hostTest} -o yaml`, { silent: true, stdout: true }); return info.match('blue') ? 'blue' : info.match('green') ? 'green' : null; }, async callback( deployList = 'default', env = 'development', options = { remove: false, infoRouter: false, sync: false, buildManifest: false, infoUtil: false, expose: false, cert: false, versions: '', traffic: '', dashboardUpdate: false, replicas: '', restoreHosts: false, disableUpdateDeployment: false, infoTraffic: false, rebuildClientsBundle: false, }, ) { if (options.infoUtil === true) return logger.info(` kubectl rollout restart deployment/deployment-name kubectl rollout undo deployment/deployment-name kubectl scale statefulsets <stateful-set-name> --replicas=<new-replicas> kubectl get pods -w kubectl patch statefulset valkey-service --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"valkey/valkey:latest"}]' kubectl patch statefulset valkey-service -p '{"spec":{"template":{"spec":{"containers":[{"name":"valkey-service","imagePullPolicy":"Never"}]}}}}' kubectl logs -f <pod-name> kubectl describe pod <pod-name> kubectl exec -it <pod-name> -- bash kubectl exec -it <pod-name> -- sh docker exec -it kind-control-plane bash curl -4 -v google.com kubectl taint nodes <node-name> node-role.kubernetes.io/control-plane:NoSchedule- kubectl run test-pod --image=busybox:latest --restart=Never -- /bin/sh -c "while true; do sleep 30; done;" kubectl run test-pod --image=alpine/curl:latest --restart=Never -- sh -c "sleep infinity" kubectl get ippools -o yaml kubectl get node <node-name> -o jsonpath='{.spec.podCIDR}' kubectl patch ippool default-ipv4-ippool --type='json' -p='[{"op": "replace", "path": "/spec/cidr", "value": "10.244.0.0/16"}]' kubectl patch ippool default-ipv4-ippool --type='json' -p='[{"op": "replace", "path": "/spec/cidr", "value": "192.168.0.0/24"}]' sudo podman run --rm localhost/<image-name>:<image-version> <command> kubectl get configmap kubelet-config -n kube-system -o yaml > kubelet-config.yaml kubectl -n kube-system rollout restart daemonset kube-proxy kubectl get EndpointSlice -o wide --all-namespaces -w kubectl apply -k manifests/deployment/adminer/. kubectl wait --for=condition=Ready pod/busybox1 kubectl wait --for=jsonpath='{.status.phase}'=Running pod/busybox1 kubectl wait --for='jsonpath={.status.conditions[?(@.type=="Ready")].status}=True' pod/busybox1 kubectl wait --for=delete pod/busybox1 --timeout=60s kubectl run --rm -it test-dns --image=busybox:latest --restart=Never -- /bin/sh -c " nslookup kubernetes.default.svc.cluster.local; nslookup mongodb-service.default.svc.cluster.local; nslookup valkey-service.default.svc.cluster.local; nc -vz mongodb-service 27017; nc -vz valkey-service 6379; echo exit code: \\\$? " kubectl apply -f - <<EOF apiVersion: apps/v1 kind: StatefulSet metadata: name: ... EOF https://org.ngc.nvidia.com/setup/api-keys docker login nvcr.io Username: $oauthtoken Password: <Your Key> `); if (deployList === 'dd' && fs.existsSync(`./engine-private/deploy/dd.router`)) deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8'); if (options.infoTraffic === true) { for (const _deployId of deployList.split(',')) { const deployId = _deployId.trim(); logger.info('', { deployId, env, traffic: UnderpostDeploy.API.getCurrentTraffic(deployId), }); } return; } if (options.rebuildClientsBundle === true) await UnderpostDeploy.API.rebuildClientsBundle(deployList); if (!(options.versions && typeof options.versions === 'string')) options.versions = 'blue,green'; if (!options.replicas) options.replicas = 1; if (options.sync) UnderpostDeploy.API.sync(deployList, options); if (options.buildManifest === true) await UnderpostDeploy.API.buildManifest(deployList, env, options); if (options.infoRouter === true) logger.info('router', await UnderpostDeploy.API.routerFactory(deployList, env)); if (options.dashboardUpdate === true) await UnderpostDeploy.API.updateDashboardData(deployList, env, options); if (options.infoRouter === true) return; shellExec(`kubectl delete configmap underpost-config`); shellExec( `kubectl create configmap underpost-config --from-file=/home/dd/engine/engine-private/conf/dd-cron/.env.${env}`, ); let renderHosts = ''; let concatHots = ''; const etcHost = ( concat, ) => `127.0.0.1 ${concat} localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6`; if (options.restoreHosts === true) { renderHosts = etcHost(concatHots); fs.writeFileSync(`/etc/hosts`, renderHosts, 'utf8'); logger.info(renderHosts); return; } for (const _deployId of deployList.split(',')) { const deployId = _deployId.trim(); if (!deployId) continue; if (options.expose === true) { const svc = UnderpostDeploy.API.get(deployId, 'svc')[0]; const port = parseInt(svc[`PORT(S)`].split('/TCP')[0]); logger.info(deployId, { svc, port, }); shellExec(`sudo kubectl port-forward -n default svc/${svc.NAME} ${port}:${port}`, { async: true }); continue; } if (!options.disableUpdateDeployment) for (const version of options.versions.split(',')) { shellExec(`sudo kubectl delete svc ${deployId}-${env}-${version}-service`); shellExec(`sudo kubectl delete deployment ${deployId}-${env}-${version}`); } const confServer = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8')); for (const host of Object.keys(confServer)) { shellExec(`sudo kubectl delete HTTPProxy ${host}`); if (env === 'production' && options.cert === true) shellExec(`sudo kubectl delete Certificate ${host}`); if (!options.remove === true && env === 'development') concatHots += ` ${host}`; } const manifestsPath = env === 'production' ? `engine-private/conf/${deployId}/build/production` : `manifests/deployment/${deployId}-${env}`; if (!options.remove === true) { if (!options.disableUpdateDeployment) shellExec(`sudo kubectl apply -f ./${manifestsPath}/deployment.yaml`); shellExec(`sudo kubectl apply -f ./${manifestsPath}/proxy.yaml`); if (env === 'production' && options.cert === true) shellExec(`sudo kubectl apply -f ./${manifestsPath}/secret.yaml`); } } switch (process.platform) { case 'linux': { switch (env) { case 'development': renderHosts = etcHost(concatHots); fs.writeFileSync(`/etc/hosts`, renderHosts, 'utf8'); break; default: break; } } break; default: break; } if (renderHosts) logger.info( ` ` + renderHosts, ); }, get(deployId, kindType = 'pods') { const raw = shellExec(`sudo kubectl get ${kindType} --all-namespaces -o wide`, { stdout: true, disableLog: true, silent: true, }); const heads = raw .split(`\n`)[0] .split(' ') .filter((_r) => _r.trim()); const pods = raw .split(`\n`) .filter((r) => (deployId ? r.match(deployId) : r.trim() && !r.match('NAME'))) .map((r) => r.split(' ').filter((_r) => _r.trim())); const result = []; for (const row of pods) { const pod = {}; let index = -1; for (const head of heads) { index++; pod[head] = row[index]; } result.push(pod); } return result; }, rebuildClientsBundle(deployList) { for (const _deployId of deployList.split(',')) { const deployId = _deployId.trim(); const repoName = `engine-${deployId.split('-')[1]}`; shellExec(`underpost script set ${deployId}-client-build ' cd /home/dd/engine && git checkout . && underpost pull . underpostnet/${repoName} && underpost pull ./engine-private underpostnet/${repoName}-private && underpost env ${deployId} production && node bin/deploy build-full-client ${deployId} '`); shellExec(`node bin script run ${deployId}-client-build --itc --pod-name ${deployId}`); } }, resourcesFactory() { return { requests: { memory: UnderpostRootEnv.API.get('resources.requests.memory'), cpu: UnderpostRootEnv.API.get('resources.requests.cpu'), }, limits: { memory: UnderpostRootEnv.API.get('resources.limits.memory'), cpu: UnderpostRootEnv.API.get('resources.limits.cpu'), }, totalPods: UnderpostRootEnv.API.get('total-pods'), }; }, async updateDashboardData(deployList, env, options) { try { const deployId = process.env.DEFAULT_DEPLOY_ID; const host = process.env.DEFAULT_DEPLOY_HOST; const path = process.env.DEFAULT_DEPLOY_PATH; const { db } = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8'))[host][ path ]; await DataBaseProvider.load({ apis: ['instance'], host, path, db }); /** @type {import('../api/instance/instance.model.js').InstanceModel} */ const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance; await Instance.deleteMany(); for (const _deployId of deployList.split(',')) { const deployId = _deployId.trim(); if (!deployId) continue; const confServer = loadReplicas( JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8')), 'proxy', ); const router = await UnderpostDeploy.API.routerFactory(deployId, env); const pathPortAssignmentData = pathPortAssignmentFactory(router, confServer); for (const host of Object.keys(confServer)) { for (const { path, port } of pathPortAssignmentData[host]) { if (!confServer[host][path]) continue; const { client, runtime, apis } = confServer[host][path]; const body = { deployId, host, path, port, client, runtime, apis, }; logger.info('save', body); await new Instance(body).save(); } } } await DataBaseProvider.instance[`${host}${path}`].mongoose.close(); } catch (error) { logger.error(error, error.stack); } }, }; } export default UnderpostDeploy;