@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
248 lines (232 loc) • 10.1 kB
text/typescript
/*
* 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.
*/
import { HandlerContext } from "@atomist/automation-client/lib/HandlerContext";
import { GitProject } from "@atomist/automation-client/lib/project/git/GitProject";
import * as _ from "lodash";
import { ExecuteGoalResult } from "../../../api/goal/ExecuteGoalResult";
import {
Goal,
GoalDefinition,
} from "../../../api/goal/Goal";
import {
ExecuteGoal,
GoalInvocation,
} from "../../../api/goal/GoalInvocation";
import { DefaultGoalNameGenerator } from "../../../api/goal/GoalNameGenerator";
import {
FulfillableGoalDetails,
FulfillableGoalWithRegistrations,
getGoalDefinitionFrom,
} from "../../../api/goal/GoalWithFulfillment";
import { SdmGoalEvent } from "../../../api/goal/SdmGoalEvent";
import { SoftwareDeliveryMachine } from "../../../api/machine/SoftwareDeliveryMachine";
import { PushTest } from "../../../api/mapping/PushTest";
import { AnyPush } from "../../../api/mapping/support/commonPushTests";
import { isInLocalMode } from "../../../core/machine/modes";
import { SdmGoalState } from "../../../typings/types";
import { KubernetesApplication } from "../kubernetes/request";
import { getClusterLabel } from "./cluster";
import { generateKubernetesGoalEventData } from "./data";
import { deployApplication } from "./deploy";
/** Return repository slug for SDM goal event. */
export function goalEventSlug(goalEvent: SdmGoalEvent): string {
return `${goalEvent.repo.owner}/${goalEvent.repo.name}`;
}
/**
* Function signature for callback that can modify and return the
* [[KubernetesApplication]] object.
*/
export type KubernetesApplicationDataCallback =
(a: KubernetesApplication, p: GitProject, g: KubernetesDeploy, e: SdmGoalEvent, ctx: HandlerContext) => Promise<KubernetesApplication>;
/**
* Sources of information for Kubernetes goal application data.
*/
export enum KubernetesDeployDataSources {
/** Read deployment spec from `.atomist/kubernetes`. */
DeploymentSpec = "DeploymentSpec",
/** Read EXPOSE from Dockerfile to get service port. */
Dockerfile = "Dockerfile",
/** Parse `goalEvent.data` as JSON. */
GoalEvent = "GoalEvent",
/** Read ingress spec from `.atomist/kubernetes`. */
IngressSpec = "IngressSpec",
/** Read role-binding spec from `.atomist/kubernetes`. */
RoleBindingSpec = "RoleBindingSpec",
/** Read role spec from `.atomist/kubernetes`. */
RoleSpec = "RoleSpec",
/** Load `sdm.configuration.sdm.k8s.app`. */
SdmConfiguration = "SdmConfiguration",
/** Read service-account spec from `.atomist/kubernetes`. */
ServiceAccountSpec = "ServiceAccountSpec",
/** Read service spec from `.atomist/kubernetes`. */
ServiceSpec = "ServiceSpec",
}
/**
* Registration object to pass to KubernetesDeployment goal to
* configure how deployment works.
*/
export interface KubernetesDeployRegistration {
/**
* Allows the user of this pack to modify the default application
* data before execution of deployment.
*/
applicationData?: KubernetesApplicationDataCallback;
/**
* If falsey, this SDM will fulfill its own Kubernetes deployment
* goals. If set, its value defines the name of the SDM that will
* fulfill the goal. In this case, there should be another SDM
* running whose name, i.e., its name as defined in its
* registration/package.json, is the same as this name.
*/
name?: string;
/**
* Optional push test for this goal implementation.
*/
pushTest?: PushTest;
/**
* Determine what parts of the repository to use to generate the
* initial Kubernetes goal application data. It no value is
* provided, all sources are used.
*/
dataSources?: KubernetesDeployDataSources[];
}
/**
* Goal that initiates deploying an application to a Kubernetes
* cluster. Deploying the application is completed by the
* [[kubernetesDeployHandler]] event handler. By default, this goal
* will be configured such that it is fulfilled by the SDM that
* creates it. To have this goal be executed by another SDM, set the
* fulfillment name to the name of that SDM:
*
* const deploy = new KubernetesDeploy()
* .with({ name: otherSdm.configuration.name });
*
*/
export class KubernetesDeploy extends FulfillableGoalWithRegistrations<KubernetesDeployRegistration> {
/**
* Create a KubernetesDeploy object.
*
* @param details Define unique aspects of this Kubernetes deployment, see [[KubernetesDeploy.details]].
* @param dependsOn Other goals that must complete successfully before scheduling this goal.
*/
constructor(public readonly details: FulfillableGoalDetails = {}, ...dependsOn: Goal[]) {
super(getGoalDefinitionFrom(details, DefaultGoalNameGenerator.generateName("kubernetes-deploy")), ...dependsOn);
}
/**
* Register a deployment with the initiator fulfillment.
*/
public with(registration: KubernetesDeployRegistration): this {
const fulfillment = registration.name || this.sdm.configuration.name;
this.addFulfillment({
name: fulfillment,
goalExecutor: initiateKubernetesDeploy(this, registration),
pushTest: registration.pushTest,
});
this.updateGoalName(fulfillment);
return this;
}
/**
* Called by the SDM on initialization. This function calls
* `super.register` and adds a startup listener to the SDM.
*
* The startup listener registers a default goal fulfillment that
* adds itself as fulfiller of its deployment requests if this
* goal has no fulfillments or callbacks at startup.
*/
public register(sdm: SoftwareDeliveryMachine): void {
super.register(sdm);
sdm.addStartupListener(async () => {
if (this.fulfillments.length === 0 && this.callbacks.length === 0) {
this.with({ pushTest: AnyPush });
}
});
}
/**
* Set the goal "name" and goal definition "displayName". If any
* goal definition description is not set, populate it with a
* reasonable default.
*
* @param fulfillment Name of fulfillment, typically the cluster-scoped name of k8s-sdm
* @return object
*/
private updateGoalName(fulfillment: string): this {
const env = (this.details && this.details.environment) ? this.details.environment : this.environment;
const clusterLabel = getClusterLabel(env, fulfillment);
const displayName = this.definition.displayName || "deploy";
const goalName = `${displayName}${clusterLabel}`;
this.definition.displayName = goalName;
const defaultDefinitions: Partial<GoalDefinition> = {
canceledDescription: `Canceled: ${goalName}`,
completedDescription: `Deployed${clusterLabel}`,
failedDescription: `Deployment${clusterLabel} failed`,
plannedDescription: `Planned: ${goalName}`,
requestedDescription: `Requested: ${goalName}`,
skippedDescription: `Skipped: ${goalName}`,
stoppedDescription: `Stopped: ${goalName}`,
waitingForApprovalDescription: `Successfully deployed${clusterLabel}`,
waitingForPreApprovalDescription: `Deploy${clusterLabel} pending approval`,
workingDescription: `Deploying${clusterLabel}`,
};
_.defaultsDeep(this.definition, defaultDefinitions);
return this;
}
}
/**
* Populate data sources properrty of registration with all possible
* KubernetesGoalDataSources if it is not defined. Otherwise, leave
* it as is. The registration object is modified directly and
* returned.
*
* @param registration Kubernetes deploy object registration
* @return registration with data sources
*/
export function defaultDataSources(registration: KubernetesDeployRegistration): KubernetesDeployRegistration {
if (!registration.dataSources) {
registration.dataSources = Object.values(KubernetesDeployDataSources);
}
return registration;
}
/**
* If in SDM team mode, this goal executor generates and stores the
* Kubernetes application data for deploying an application to
* Kubernetes. It returns the augmented SdmGoalEvent with the
* Kubernetes application information in the `data` property and the
* state of the SdmGoalEvent set to "requested". The actual
* deployment is done by the [[kubernetesDeployHandler]] event
* handler.
*
* It will call [[defaultDataSources]] to populate the default
* repository data sources if none are provided in the registration.
*
* If in SDM local mode, generate the Kubernetes application data and
* deploy the application.
*
* @param k8Deploy Kubernetes deploy object
* @param registration Kubernetes deploy object registration
* @return An ExecuteGoal result that is not really a result, but an intermediate state.
*/
export function initiateKubernetesDeploy(k8Deploy: KubernetesDeploy, registration: KubernetesDeployRegistration): ExecuteGoal {
return async (goalInvocation: GoalInvocation): Promise<ExecuteGoalResult> => {
defaultDataSources(registration);
const goalEvent = await generateKubernetesGoalEventData(k8Deploy, registration, goalInvocation);
if (isInLocalMode()) {
return deployApplication(goalEvent, goalInvocation.context, goalInvocation.progressLog);
} else {
goalEvent.state = SdmGoalState.requested;
return goalEvent;
}
};
}