@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
247 lines • 13.9 kB
JavaScript
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;
};