UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

247 lines 13.9 kB
import https from 'node:https'; import fs from 'fs'; import path from 'path'; import { resolveRootPath } from '../../utils/atlas.js'; import { normalizeOptionalString } from '../../utils/value.js'; import { getCommandErrorMessage, isGcloudResourceNotFoundError, parseGcloudJsonOutput, runGcloudFileCommand } from '../../utils/gcloud.js'; const DEFAULT_SERVICE_KUBECONFIG_ROOT_DIR = path.join('.atlas', 'kubeconfig'); const EMPTY_KUBECONFIG_CONTENT = `apiVersion: v1 kind: Config preferences: {} clusters: [] contexts: [] current-context: "" users: [] `; const normalizeKubeconfigPathSegment = value => String(value).trim().replace(/[^A-Za-z0-9._-]+/gu, '-').replace(/-{2,}/gu, '-').replace(/^-+|-+$/gu, ''); const createServiceClusterApiOrigin = clusterEndpoint => clusterEndpoint.startsWith('http') ? clusterEndpoint : `https://${clusterEndpoint}`; const resolveServiceClusterConfigSectionKey = service => normalizeOptionalString(service?.configSectionKey) ?? normalizeOptionalString(service?.id) ?? 'service'; const resolveServiceClusterHintValue = (clusterHints, propertyName) => normalizeOptionalString(clusterHints?.[propertyName]); const resolveServiceClusterAccessToken = (dependencies = {}) => { const configuredAccessToken = normalizeOptionalString(dependencies.accessToken); if (configuredAccessToken) { return configuredAccessToken; } const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? ((args, options) => runGcloudFileCommand(args, options, dependencies.runCommand)); return normalizeOptionalString(runGcloudFileCommandImpl(['auth', 'print-access-token'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })); }; export const requestServiceClusterApi = async (terraformConnection, resourcePath, dependencies = {}) => { if (typeof dependencies.requestServiceClusterApi === 'function') { return dependencies.requestServiceClusterApi(terraformConnection, resourcePath); } if (!terraformConnection?.clusterEndpoint || !terraformConnection?.clusterCaCertificate) { throw new Error('Atlas could not inspect existing Kubernetes resources because the cluster connection is incomplete.'); } const accessToken = resolveServiceClusterAccessToken(dependencies); if (!accessToken) { throw new Error('Atlas could not resolve a Google Cloud access token for Kubernetes API inspection.'); } const requestUrl = new URL(resourcePath, createServiceClusterApiOrigin(terraformConnection.clusterEndpoint)); return new Promise((resolve, reject) => { const request = https.request(requestUrl, { ca: Buffer.from(terraformConnection.clusterCaCertificate, 'base64'), headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}` }, method: 'GET' }, response => { const chunks = []; response.on('data', chunk => { chunks.push(chunk); }); response.on('end', () => { resolve({ body: Buffer.concat(chunks).toString('utf8'), statusCode: response.statusCode ?? 0 }); }); }); request.on('error', reject); request.end(); }); }; export const readServiceClusterJsonResource = async (terraformConnection, resourcePath, dependencies = {}) => { const response = await requestServiceClusterApi(terraformConnection, resourcePath, dependencies); if (response?.statusCode === 404) { return null; } if (!response || response.statusCode < 200 || response.statusCode >= 300) { throw new Error(`Could not inspect Kubernetes resource ${resourcePath}. ` + `The Kubernetes API returned ${response?.statusCode ?? 'an unknown status'}${normalizeOptionalString(response?.body) ? `: ${response.body}` : '.'}`); } if (!normalizeOptionalString(response.body)) { return null; } try { return JSON.parse(response.body); } catch (error) { throw new Error(`Could not parse the Kubernetes API response for ${resourcePath}. ${error.message}`); } }; export const readServiceClusterTextResource = async (terraformConnection, resourcePath, dependencies = {}) => { const response = await requestServiceClusterApi(terraformConnection, resourcePath, dependencies); if (response?.statusCode === 404) { return null; } if (!response || response.statusCode < 200 || response.statusCode >= 300) { throw new Error(`Could not inspect Kubernetes resource ${resourcePath}. ` + `The Kubernetes API returned ${response?.statusCode ?? 'an unknown status'}${normalizeOptionalString(response?.body) ? `: ${response.body}` : '.'}`); } return normalizeOptionalString(response.body) ?? null; }; export const readServiceClusterSecret = async (terraformConnection, namespace, secretName, dependencies = {}) => { const secret = await readServiceClusterJsonResource(terraformConnection, `/api/v1/namespaces/${encodeURIComponent(namespace)}/secrets/${encodeURIComponent(secretName)}`, dependencies); if (!secret?.data || typeof secret.data !== 'object') { return null; } return Object.fromEntries(Object.entries(secret.data).map(([key, value]) => [key, Buffer.from(String(value), 'base64').toString('utf8')])); }; export const doesServiceClusterNamespaceExist = async (terraformConnection, namespace, dependencies = {}) => Boolean(await readServiceClusterJsonResource(terraformConnection, `/api/v1/namespaces/${encodeURIComponent(namespace)}`, dependencies)); export const doesServiceClusterServiceAccountExist = async (terraformConnection, namespace, serviceAccountName, dependencies = {}) => Boolean(await readServiceClusterJsonResource(terraformConnection, `/api/v1/namespaces/${encodeURIComponent(namespace)}/serviceaccounts/${encodeURIComponent(serviceAccountName)}`, dependencies)); export const doesServiceClusterRoleExist = async (terraformConnection, namespace, roleName, dependencies = {}) => Boolean(await readServiceClusterJsonResource(terraformConnection, `/apis/rbac.authorization.k8s.io/v1/namespaces/${encodeURIComponent(namespace)}/roles/${encodeURIComponent(roleName)}`, dependencies)); export const doesServiceClusterRoleBindingExist = async (terraformConnection, namespace, roleBindingName, dependencies = {}) => Boolean(await readServiceClusterJsonResource(terraformConnection, `/apis/rbac.authorization.k8s.io/v1/namespaces/${encodeURIComponent(namespace)}/rolebindings/${encodeURIComponent(roleBindingName)}`, dependencies)); export const doesServiceClusterHelmReleaseExist = async (terraformConnection, namespace, releaseName, dependencies = {}) => { const labelSelector = encodeURIComponent(`owner=helm,name=${releaseName}`); const releaseSecrets = await readServiceClusterJsonResource(terraformConnection, `/api/v1/namespaces/${encodeURIComponent(namespace)}/secrets?labelSelector=${labelSelector}`, dependencies); if (Array.isArray(releaseSecrets?.items) && releaseSecrets.items.length > 0) { return true; } const releaseConfigMaps = await readServiceClusterJsonResource(terraformConnection, `/api/v1/namespaces/${encodeURIComponent(namespace)}/configmaps?labelSelector=${labelSelector}`, dependencies); return Array.isArray(releaseConfigMaps?.items) && releaseConfigMaps.items.length > 0; }; export const resolveServiceClusterTarget = (service, serviceConfig, clusterHints = {}) => { const clusterName = normalizeOptionalString(serviceConfig.clusterName) ?? resolveServiceClusterHintValue(clusterHints, 'clusterName'); const clusterLocation = normalizeOptionalString(serviceConfig.clusterLocation) ?? resolveServiceClusterHintValue(clusterHints, 'clusterLocation'); if (!clusterName || !clusterLocation) { const configSectionKey = resolveServiceClusterConfigSectionKey(service); const serviceId = normalizeOptionalString(service?.id) ?? 'service'; throw new Error(`Atlas deploy service ${serviceId} requires a configured GKE cluster name and location. ` + `Set ${configSectionKey}.clusterName and ${configSectionKey}.clusterLocation in services/${configSectionKey}/config.json, ` + 'or pass --cluster-name and --cluster-location.'); } return { clusterLocation, clusterName, source: normalizeOptionalString(serviceConfig.clusterName) || normalizeOptionalString(serviceConfig.clusterLocation) ? 'services-config' : resolveServiceClusterHintValue(clusterHints, 'source') ?? 'inferred' }; }; export const resolveServiceKubeContext = (context, clusterTarget) => `gke_${context.projectId}_${clusterTarget.clusterLocation}_${clusterTarget.clusterName}`; export const resolveServiceKubeconfigPath = (context, clusterTarget, cwd = process.cwd(), options = {}) => { const configuredPath = normalizeOptionalString(options.kubeconfigPath); if (configuredPath) { return resolveRootPath(configuredPath, cwd); } const fileName = [context.projectId, clusterTarget.clusterLocation, clusterTarget.clusterName].map(normalizeKubeconfigPathSegment).filter(Boolean).join('.'); return resolveRootPath(path.join(DEFAULT_SERVICE_KUBECONFIG_ROOT_DIR, `${fileName || 'service'}.yaml`), cwd); }; export const createServiceTerraformConnection = (context, clusterTarget, cwd = process.cwd(), options = {}) => { const kubeconfigPath = resolveServiceKubeconfigPath(context, clusterTarget, cwd, options); return { clusterCaCertificate: normalizeOptionalString(options.clusterCaCertificate), clusterEndpoint: normalizeOptionalString(options.clusterEndpoint), kubeconfigPath }; }; export const doesServiceClusterExist = (context, clusterTarget, dependencies = {}) => { const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? ((args, options) => runGcloudFileCommand(args, options, dependencies.runCommand)); try { runGcloudFileCommandImpl(['container', 'clusters', 'describe', clusterTarget.clusterName, `--project=${context.projectId}`, `--location=${clusterTarget.clusterLocation}`, '--format=json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return true; } catch (error) { if (isGcloudResourceNotFoundError(error)) { return false; } throw new Error(`Atlas could not inspect GKE cluster ${clusterTarget.clusterName}. ${getCommandErrorMessage(error)}`); } }; export const resolveServiceClusterConnection = (context, clusterTarget, dependencies = {}) => { const clusterEndpoint = normalizeOptionalString(dependencies.clusterEndpoint); const clusterCaCertificate = normalizeOptionalString(dependencies.clusterCaCertificate); if (clusterEndpoint && clusterCaCertificate) { return { clusterCaCertificate, clusterEndpoint }; } const clusterDescription = parseGcloudJsonOutput(runGcloudFileCommand(['container', 'clusters', 'describe', clusterTarget.clusterName, `--project=${context.projectId}`, `--location=${clusterTarget.clusterLocation}`, '--format=json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, dependencies.runCommand), `GKE cluster metadata for ${clusterTarget.clusterName}`); const describedClusterEndpoint = normalizeOptionalString(clusterDescription?.endpoint); const describedClusterCaCertificate = normalizeOptionalString(clusterDescription?.masterAuth?.clusterCaCertificate); if (!describedClusterEndpoint || !describedClusterCaCertificate) { throw new Error(`Atlas could not resolve the Kubernetes API endpoint and CA certificate for GKE cluster ${clusterTarget.clusterName}.`); } return { clusterCaCertificate: describedClusterCaCertificate, clusterEndpoint: describedClusterEndpoint }; }; const ensureServiceKubeconfigFile = (kubeconfigPath, dependencies = {}) => { if (!kubeconfigPath) { return null; } const { existsSyncImpl = fs.existsSync, mkdirSyncImpl = fs.mkdirSync, writeFileSyncImpl = fs.writeFileSync } = dependencies; mkdirSyncImpl(path.dirname(kubeconfigPath), { recursive: true }); if (!existsSyncImpl(kubeconfigPath)) { writeFileSyncImpl(kubeconfigPath, EMPTY_KUBECONFIG_CONTENT); } return kubeconfigPath; }; export const bootstrapServiceClusterCredentials = (context, clusterTarget, dependencies = {}) => { const { existsSyncImpl = fs.existsSync, kubeconfigPath, mkdirSyncImpl = fs.mkdirSync, runCommand: runCommandImpl, writeFileSyncImpl = fs.writeFileSync } = dependencies; const resolvedKubeconfigPath = ensureServiceKubeconfigFile(kubeconfigPath, { existsSyncImpl, mkdirSyncImpl, writeFileSyncImpl }); runGcloudFileCommand(['container', 'clusters', 'get-credentials', clusterTarget.clusterName, `--project=${context.projectId}`, `--location=${clusterTarget.clusterLocation}`], { encoding: 'utf8', ...(resolvedKubeconfigPath ? { env: { KUBECONFIG: resolvedKubeconfigPath } } : {}), stdio: 'inherit' }, runCommandImpl); if (resolvedKubeconfigPath && !existsSyncImpl(resolvedKubeconfigPath)) { throw new Error(`Atlas could not initialize the Kubernetes config at ${resolvedKubeconfigPath}.`); } return { clusterLocation: clusterTarget.clusterLocation, clusterName: clusterTarget.clusterName, kubeconfigPath: resolvedKubeconfigPath, status: 'configured', source: clusterTarget.source, warnings: [] }; }; export const shouldBootstrapManagedServiceCluster = (serviceName, managedPlatformClusterConfig, serviceConfig, clusterHints = {}, options = {}) => { if (options.createCluster === true) { if (!managedPlatformClusterConfig) { throw new Error(`Atlas deploy service ${normalizeOptionalString(serviceName) ?? 'service'} does not support --create-cluster.`); } return true; } if (!managedPlatformClusterConfig) { return false; } const clusterName = normalizeOptionalString(serviceConfig.clusterName) ?? resolveServiceClusterHintValue(clusterHints, 'clusterName'); const clusterLocation = normalizeOptionalString(serviceConfig.clusterLocation) ?? resolveServiceClusterHintValue(clusterHints, 'clusterLocation'); return !clusterName || !clusterLocation; };