UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

289 lines 13.4 kB
"use strict"; /* * Copyright © 2020 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultImage = exports.repoSpecsKubernetsApplication = exports.dockerPort = exports.defaultKubernetesApplication = exports.getKubernetesGoalEventData = exports.generateKubernetesGoalEventData = exports.sdmPackK8s = void 0; const logger_1 = require("@atomist/automation-client/lib/util/logger"); const dockerfileParser = require("docker-file-parser"); const stringify = require("json-stringify-safe"); const _ = require("lodash"); const sdmGoal_1 = require("../../../api-helper/goal/sdmGoal"); const projectVersioner_1 = require("../../../core/delivery/build/local/projectVersioner"); const name_1 = require("../kubernetes/name"); const namespace_1 = require("../support/namespace"); const fulfiller_1 = require("./fulfiller"); const goal_1 = require("./goal"); const spec_1 = require("./spec"); /** * JSON propery under the goal event data where the * [[KubernetesDeployment]] goal [[KubernetesApplication]] data are * stored, i.e., the value of `goal.data[sdmPackK8s]`. */ exports.sdmPackK8s = "@atomist/sdm-pack-k8s"; /** * Generate KubernetesApplication from goal, goal registration, * project, and goal invocation. The priority of the various sources * of KubernetesApplication data are, from lowest to highest: * * 1. Starting point is `{ ns: defaultNamespace }` * 2. [[KubernetesDeployDataSources.SdmConfiguration]] * 3. [[defaultKubernetesApplication]] * 4. [[KubernetesDeployDataSources.Dockerfile]] * 5. The various partial Kubernetes specs under `.atomist/kubernetes` * 6. [[KubernetesDeployDataSources.GoalEvent]] * 7. [[KubernetesDeployRegistration.applicationData]] * * Specific sources can be enabled/disabled via the * [[KubernetesDeployRegistration.dataSources]]. * * @param k8Deploy Kubernetes deployment goal * @param registration Goal registration/configuration for `k8Deploy` * @param goalInvocation The Kubernetes deployment goal currently triggered. * @return The SdmGoalEvent augmented with [[KubernetesApplication]] in the data */ function generateKubernetesGoalEventData(k8Deploy, registration, goalInvocation) { return k8Deploy.sdm.configuration.sdm.projectLoader.doWithProject(Object.assign(Object.assign({}, goalInvocation), { readOnly: true }), async (p) => { const { context, goalEvent } = goalInvocation; const sdmConfigAppData = (registration.dataSources.includes(goal_1.KubernetesDeployDataSources.SdmConfiguration)) ? _.get(k8Deploy, "sdm.configuration.sdm.k8s.app", {}) : {}; const defaultAppData = await defaultKubernetesApplication(goalEvent, k8Deploy, context); const dockerfileAppData = (registration.dataSources.includes(goal_1.KubernetesDeployDataSources.Dockerfile)) ? await dockerfileKubernetesApplication(p) : {}; const specAppData = await repoSpecsKubernetsApplication(p, registration); const mergedAppData = _.merge({ ns: namespace_1.K8sDefaultNamespace }, sdmConfigAppData, defaultAppData, dockerfileAppData, specAppData); const eventAppData = {}; eventAppData[exports.sdmPackK8s] = mergedAppData; if (registration.dataSources.includes(goal_1.KubernetesDeployDataSources.GoalEvent)) { const slug = goal_1.goalEventSlug(goalEvent); let eventData; try { eventData = sdmGoal_1.goalData(goalEvent); } catch (e) { logger_1.logger.warn(`Failed to parse goal event data for ${slug} as JSON: ${e.message}`); logger_1.logger.warn(`Ignoring current value of goal event data: ${goalEvent.data}`); eventData = {}; } _.merge(eventAppData, eventData); } if (registration.applicationData) { const callbackAppData = await registration.applicationData(eventAppData[exports.sdmPackK8s], p, k8Deploy, goalEvent, context); eventAppData[exports.sdmPackK8s] = callbackAppData; } goalEvent.data = JSON.stringify(eventAppData); goalEvent.fulfillment = { method: "sdm", name: fulfiller_1.KubernetesDeployFulfillerGoalName, registration: registration.name, }; return goalEvent; }); } exports.generateKubernetesGoalEventData = generateKubernetesGoalEventData; /** * Fetch [[KubernetesApplication]] from goal event. If the goal event * does not contain Kubernetes application information in its data * property, `undefined` is returned. If the value of the goal event * data cannot be parsed as JSON, an error is thrown. * * @param goalEvent SDM goal event to retrieve the Kubernetes application data from * @return Parsed [[KubernetesApplication]] object */ function getKubernetesGoalEventData(goalEvent) { let data; try { data = sdmGoal_1.goalData(goalEvent); } catch (e) { logger_1.logger.error(e.message); throw e; } return data[exports.sdmPackK8s]; } exports.getKubernetesGoalEventData = getKubernetesGoalEventData; /** * Given the goal event, [[KubernetesDeploy]] goal, and * handler context generate a default [[KubernetesApplication]] object. * * This function uses [[defaultImage]] to determine the image. * * @param goalEvent SDM Kubernetes deployment goal event * @param k8Deploy Kubernetes deployment goal configuration * @param context Handler context * @return a valid default KubernetesApplication for this SDM goal deployment event */ async function defaultKubernetesApplication(goalEvent, k8Deploy, context) { const workspaceId = context.workspaceId; const name = name_1.validName(goalEvent.repo.name); const image = await defaultImage(goalEvent, k8Deploy, context); return { workspaceId, name, image, }; } exports.defaultKubernetesApplication = defaultKubernetesApplication; /** * Parse Dockerfile and return port of first argument to the first * EXPOSE command. it can suggessfully convert to an integer. If * there are no EXPOSE commands or if it cannot successfully convert * the EXPOSE arguments to an integer, `undefined` is returned. * * @param p Project to look for Dockerfile in * @return port number or `undefined` if no EXPOSE commands are found */ async function dockerPort(p) { const dockerFile = await p.getFile("Dockerfile"); if (dockerFile) { const dockerFileContents = await dockerFile.getContent(); const commands = dockerfileParser.parse(dockerFileContents, { includeComments: false }); const exposeCommands = commands.filter(c => c.name === "EXPOSE"); for (const exposeCommand of exposeCommands) { if (Array.isArray(exposeCommand.args)) { for (const arg of exposeCommand.args) { const port = parseInt(arg, 10); if (!isNaN(port)) { return port; } } } else if (typeof exposeCommand.args === "string") { const port = parseInt(exposeCommand.args, 10); if (!isNaN(port)) { return port; } } else { logger_1.logger.warn(`Unexpected EXPOSE argument type '${typeof exposeCommand.args}': ${stringify(exposeCommand.args)}`); } } } return undefined; } exports.dockerPort = dockerPort; /** Package return of [[dockerPort]] in [[KubernetesApplication]]. */ async function dockerfileKubernetesApplication(p) { const port = await dockerPort(p); return (port) ? { port } : {}; } /** * Read configured Kubernetes partial specs from repository. * * @param p Project to look for specs * @param registration Configuration of KubernetesDeploy goal * @return KubernetesApplication object with requested specs populated */ async function repoSpecsKubernetsApplication(p, r) { const deploymentSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.DeploymentSpec)) ? await spec_1.loadKubernetesSpec(p, "deployment") : undefined; const serviceSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.ServiceSpec)) ? await spec_1.loadKubernetesSpec(p, "service") : undefined; const ingressSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.IngressSpec)) ? await spec_1.loadKubernetesSpec(p, "ingress") : undefined; const roleSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.RoleSpec)) ? await spec_1.loadKubernetesSpec(p, "role") : undefined; const serviceAccountSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.ServiceAccountSpec)) ? await spec_1.loadKubernetesSpec(p, "service-account") : undefined; const roleBindingSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.RoleBindingSpec)) ? await spec_1.loadKubernetesSpec(p, "role-binding") : undefined; return { deploymentSpec, serviceSpec, ingressSpec, roleSpec, serviceAccountSpec, roleBindingSpec, }; } exports.repoSpecsKubernetsApplication = repoSpecsKubernetsApplication; /** * Remove any invalid characters from Docker image name component * `name` to make it a valid Docker image name component. If * `hubOwner` is true, it ensures the name contains only alphanumeric * characters. * * From https://docs.docker.com/engine/reference/commandline/tag/: * * > An image name is made up of slash-separated name components, * > optionally prefixed by a registry hostname. The hostname must * > comply with standard DNS rules, but may not contain * > underscores. If a hostname is present, it may optionally be * > followed by a port number in the format :8080. If not present, * > the command uses Docker’s public registry located at * > registry-1.docker.io by default. Name components may contain * > lowercase letters, digits and separators. A separator is defined * > as a period, one or two underscores, or one or more dashes. A * > name component may not start or end with a separator. * > * > A tag name must be valid ASCII and may contain lowercase and * > uppercase letters, digits, underscores, periods and dashes. A tag * > name may not start with a period or a dash and may contain a * > maximum of 128 characters. * * @param name Name component to clean up. * @param hubOwner If `true` only allow characters valid for a Docker Hub user/org * @return Valid Docker image name component. */ function dockerImageNameComponent(name, hubOwner = false) { const cleanName = name.toLocaleLowerCase() .replace(/^[^a-z0-9]+/, "") .replace(/[^a-z0-9]+$/, "") .replace(/[^-_/.a-z0-9]+/g, ""); if (hubOwner) { return cleanName.replace(/[^a-z0-9]+/g, ""); } else { return cleanName; } } /** * Determine the best default value for the image property for this * Kubernetes deployment goal event. If there is no image associated * with the after commit of the push, it checks if a Docker registry * is provided at `sdm.configuration.sdm.docker.registry` and uses * that and the repo name to return an image. If neither of those * exist, a Docker Hub-like image name generated from the repository * owner and name. In the latter two cases, it tries to read a * version for this commit from the graph. If it exists it uses it at * the image tag. If it does not, it uses the tag "latest". * * @param goalEvent SDM Kubernetes deployment goal event * @param k8Deploy Kubernetes deployment goal object * @param context Handler context * @return Docker image associated with goal push after commit, or best guess */ async function defaultImage(goalEvent, k8Deploy, context) { if (goalEvent.push && goalEvent.push.after && goalEvent.push.after.images && goalEvent.push.after.images.length > 0) { return goalEvent.push.after.images[0].imageName; } const slug = goal_1.goalEventSlug(goalEvent); let version; try { version = await projectVersioner_1.readSdmVersion(goalEvent.repo.owner, goalEvent.repo.name, goalEvent.repo.providerId, goalEvent.sha, goalEvent.branch, context); } catch (e) { logger_1.logger.warn(`Failed to read version for goal ${slug}:${goalEvent.sha}: ${e.message}`); } if (!version) { version = "latest"; } const tag = version.replace(/^[-.]/, "").replace(/[^-.\w]+/, "").substring(0, 128); const dockerName = dockerImageNameComponent(goalEvent.repo.name); const registry = _.get(k8Deploy, "sdm.configuration.sdm.build.docker.registry", dockerImageNameComponent(goalEvent.repo.owner, true)); return `${registry}/${dockerName}:${tag}`; } exports.defaultImage = defaultImage; //# sourceMappingURL=data.js.map