From bba74ad3b52ed169d5acfd02b687247e4c172fde Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 14:53:55 -0400 Subject: [PATCH] v4 new release (#232) --- .gitignore | 2 + lib/index.js | 909 ++++++++++-------- package.json | 1 + src/actions/deploy.ts | 23 +- src/actions/promote.ts | 92 +- src/actions/reject.ts | 33 +- src/inputUtils.ts | 16 + src/run.ts | 10 +- .../blueGreen/blueGreenHelper.test.ts | 204 ++-- .../blueGreen/blueGreenHelper.ts | 215 ++--- src/strategyHelpers/blueGreen/deploy.test.ts | 75 ++ src/strategyHelpers/blueGreen/deploy.ts | 136 +++ .../blueGreen/ingressBlueGreenHelper.test.ts | 93 +- .../blueGreen/ingressBlueGreenHelper.ts | 258 ++--- src/strategyHelpers/blueGreen/promote.test.ts | 158 +++ src/strategyHelpers/blueGreen/promote.ts | 81 ++ src/strategyHelpers/blueGreen/reject.test.ts | 66 ++ src/strategyHelpers/blueGreen/reject.ts | 81 ++ src/strategyHelpers/blueGreen/route.test.ts | 119 +++ src/strategyHelpers/blueGreen/route.ts | 141 +++ .../blueGreen/serviceBlueGreenHelper.test.ts | 65 ++ .../blueGreen/serviceBlueGreenHelper.ts | 120 +-- .../blueGreen/smiBlueGreenHelper.test.ts | 203 ++++ .../blueGreen/smiBlueGreenHelper.ts | 235 ++--- src/strategyHelpers/canary/smiCanaryHelper.ts | 6 +- src/strategyHelpers/deploymentHelper.ts | 34 +- src/types/annotations.ts | 6 +- src/types/blueGreenTypes.ts | 21 + src/types/deployResult.ts | 6 + src/types/k8sObject.ts | 57 ++ src/utilities/fileUtils.test.ts | 4 +- src/utilities/workflowAnnotationUtils.test.ts | 5 + src/utilities/workflowAnnotationUtils.ts | 6 +- test/unit/manifests/anomaly-objects-test.yml | 23 + 34 files changed, 2256 insertions(+), 1248 deletions(-) create mode 100644 src/inputUtils.ts create mode 100644 src/strategyHelpers/blueGreen/deploy.test.ts create mode 100644 src/strategyHelpers/blueGreen/deploy.ts create mode 100644 src/strategyHelpers/blueGreen/promote.test.ts create mode 100644 src/strategyHelpers/blueGreen/promote.ts create mode 100644 src/strategyHelpers/blueGreen/reject.test.ts create mode 100644 src/strategyHelpers/blueGreen/reject.ts create mode 100644 src/strategyHelpers/blueGreen/route.test.ts create mode 100644 src/strategyHelpers/blueGreen/route.ts create mode 100644 src/strategyHelpers/blueGreen/serviceBlueGreenHelper.test.ts create mode 100644 src/strategyHelpers/blueGreen/smiBlueGreenHelper.test.ts create mode 100644 src/types/blueGreenTypes.ts create mode 100644 src/types/deployResult.ts create mode 100644 src/types/k8sObject.ts create mode 100644 test/unit/manifests/anomaly-objects-test.yml diff --git a/.gitignore b/.gitignore index 7c7fb26dc..fe11cae3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store .idea + +coverage/ \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index d27123db9..96987ad73 100644 --- a/lib/index.js +++ b/lib/index.js @@ -19259,22 +19259,19 @@ const core = __nccwpck_require__(6024); const models = __nccwpck_require__(6583); const KubernetesConstants = __nccwpck_require__(6583); const manifestUpdateUtils_1 = __nccwpck_require__(6352); -const blueGreenHelper_1 = __nccwpck_require__(2662); const deploymentHelper_1 = __nccwpck_require__(5703); -const deploymentStrategy_1 = __nccwpck_require__(7023); const trafficSplitMethod_1 = __nccwpck_require__(9125); -const routeStrategy_1 = __nccwpck_require__(7194); -function deploy(kubectl, manifestFilePaths, deploymentStrategy, annotations = {}) { +function deploy(kubectl, manifestFilePaths, deploymentStrategy) { return __awaiter(this, void 0, void 0, function* () { // update manifests const inputManifestFiles = manifestUpdateUtils_1.updateManifestFiles(manifestFilePaths); - core.debug('Input manifest files: ' + inputManifestFiles); + core.debug(`Input manifest files: ${inputManifestFiles}`); // deploy manifests core.startGroup('Deploying manifests'); const trafficSplitMethod = trafficSplitMethod_1.parseTrafficSplitMethod(core.getInput('traffic-split-method', { required: true })); - const deployedManifestFiles = yield deploymentHelper_1.deployManifests(inputManifestFiles, deploymentStrategy, kubectl, trafficSplitMethod, annotations); + const deployedManifestFiles = yield deploymentHelper_1.deployManifests(inputManifestFiles, deploymentStrategy, kubectl, trafficSplitMethod); + core.debug(`Deployed manifest files: ${deployedManifestFiles}`); core.endGroup(); - core.debug('Deployed manifest files: ' + deployedManifestFiles); // check manifest stability core.startGroup('Checking manifest stability'); const resourceTypes = manifestUpdateUtils_1.getResources(deployedManifestFiles, models.DEPLOYMENT_TYPES.concat([ @@ -19282,12 +19279,6 @@ function deploy(kubectl, manifestFilePaths, deploymentStrategy, annotations = {} ])); yield deploymentHelper_1.checkManifestStability(kubectl, resourceTypes); core.endGroup(); - if (deploymentStrategy == deploymentStrategy_1.DeploymentStrategy.BLUE_GREEN) { - core.startGroup('Routing blue green'); - const routeStrategy = routeStrategy_1.parseRouteStrategy(core.getInput('route-method', { required: true })); - yield blueGreenHelper_1.routeBlueGreen(kubectl, inputManifestFiles, routeStrategy); - core.endGroup(); - } // print ingresses core.startGroup('Printing ingresses'); const ingressResources = manifestUpdateUtils_1.getResources(deployedManifestFiles, [ @@ -19304,7 +19295,7 @@ function deploy(kubectl, manifestFilePaths, deploymentStrategy, annotations = {} allPods = JSON.parse((yield kubectl.getAllPods()).stdout); } catch (e) { - core.debug('Unable to parse pods: ' + e); + core.debug(`Unable to parse pods: ${e}`); } yield deploymentHelper_1.annotateAndLabelResources(deployedManifestFiles, kubectl, resourceTypes, allPods); core.endGroup(); @@ -19339,20 +19330,20 @@ const manifestUpdateUtils_1 = __nccwpck_require__(6352); const models = __nccwpck_require__(6583); const KubernetesManifestUtility = __nccwpck_require__(8246); const blueGreenHelper_1 = __nccwpck_require__(2662); -const serviceBlueGreenHelper_1 = __nccwpck_require__(4800); -const ingressBlueGreenHelper_1 = __nccwpck_require__(1721); +const promote_1 = __nccwpck_require__(9701); +const route_1 = __nccwpck_require__(7692); const smiBlueGreenHelper_1 = __nccwpck_require__(7506); const deploymentStrategy_1 = __nccwpck_require__(7023); const trafficSplitMethod_1 = __nccwpck_require__(9125); const routeStrategy_1 = __nccwpck_require__(7194); -function promote(kubectl, manifests, deploymentStrategy, annotations = {}) { +function promote(kubectl, manifests, deploymentStrategy) { return __awaiter(this, void 0, void 0, function* () { switch (deploymentStrategy) { case deploymentStrategy_1.DeploymentStrategy.CANARY: yield promoteCanary(kubectl, manifests); break; case deploymentStrategy_1.DeploymentStrategy.BLUE_GREEN: - yield promoteBlueGreen(kubectl, manifests, annotations); + yield promoteBlueGreen(kubectl, manifests); break; default: throw Error('Invalid promote deployment strategy'); @@ -19388,33 +19379,32 @@ function promoteCanary(kubectl, manifests) { yield canaryDeploymentHelper.deleteCanaryDeployment(kubectl, manifests, includeServices); } catch (ex) { - core.warning('Exception occurred while deleting canary and baseline workloads: ' + - ex); + core.warning(`Exception occurred while deleting canary and baseline workloads: ${ex}`); } core.endGroup(); }); } -function promoteBlueGreen(kubectl, manifests, annotations = {}) { +function promoteBlueGreen(kubectl, manifests) { return __awaiter(this, void 0, void 0, function* () { // update container images and pull secrets const inputManifestFiles = manifestUpdateUtils_1.updateManifestFiles(manifests); const manifestObjects = blueGreenHelper_1.getManifestObjects(inputManifestFiles); const routeStrategy = routeStrategy_1.parseRouteStrategy(core.getInput('route-method', { required: true })); - core.startGroup('Deleting old deployment and making new one'); - let result; - if (routeStrategy == routeStrategy_1.RouteStrategy.INGRESS) { - result = yield ingressBlueGreenHelper_1.promoteBlueGreenIngress(kubectl, manifestObjects); - } - else if (routeStrategy == routeStrategy_1.RouteStrategy.SMI) { - result = yield smiBlueGreenHelper_1.promoteBlueGreenSMI(kubectl, manifestObjects); - } - else { - result = yield serviceBlueGreenHelper_1.promoteBlueGreenService(kubectl, manifestObjects); - } + core.startGroup('Deleting old deployment and making new stable deployment'); + const { deployResult } = yield (() => __awaiter(this, void 0, void 0, function* () { + switch (routeStrategy) { + case routeStrategy_1.RouteStrategy.INGRESS: + return yield promote_1.promoteBlueGreenIngress(kubectl, manifestObjects); + case routeStrategy_1.RouteStrategy.SMI: + return yield promote_1.promoteBlueGreenSMI(kubectl, manifestObjects); + default: + return yield promote_1.promoteBlueGreenService(kubectl, manifestObjects); + } + }))(); core.endGroup(); // checking stability of newly created deployments core.startGroup('Checking manifest stability'); - const deployedManifestFiles = result.newFilePaths; + const deployedManifestFiles = deployResult.manifestFiles; const resources = manifestUpdateUtils_1.getResources(deployedManifestFiles, models.DEPLOYMENT_TYPES.concat([ models.DiscoveryAndLoadBalancerResource.SERVICE ])); @@ -19422,17 +19412,17 @@ function promoteBlueGreen(kubectl, manifests, annotations = {}) { core.endGroup(); core.startGroup('Routing to new deployments and deleting old workloads and services'); if (routeStrategy == routeStrategy_1.RouteStrategy.INGRESS) { - yield ingressBlueGreenHelper_1.routeBlueGreenIngress(kubectl, null, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList); - yield blueGreenHelper_1.deleteWorkloadsAndServicesWithLabel(kubectl, blueGreenHelper_1.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList, manifestObjects.serviceEntityList); + yield route_1.routeBlueGreenIngressUnchanged(kubectl, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList); + yield blueGreenHelper_1.deleteGreenObjects(kubectl, [].concat(manifestObjects.deploymentEntityList, manifestObjects.serviceEntityList)); } else if (routeStrategy == routeStrategy_1.RouteStrategy.SMI) { - yield smiBlueGreenHelper_1.routeBlueGreenSMI(kubectl, blueGreenHelper_1.NONE_LABEL_VALUE, manifestObjects.serviceEntityList, annotations); - yield blueGreenHelper_1.deleteWorkloadsWithLabel(kubectl, blueGreenHelper_1.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList); + yield route_1.routeBlueGreenSMI(kubectl, blueGreenHelper_1.NONE_LABEL_VALUE, manifestObjects.serviceEntityList); + yield blueGreenHelper_1.deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList); yield smiBlueGreenHelper_1.cleanupSMI(kubectl, manifestObjects.serviceEntityList); } else { - yield serviceBlueGreenHelper_1.routeBlueGreenService(kubectl, blueGreenHelper_1.NONE_LABEL_VALUE, manifestObjects.serviceEntityList); - yield blueGreenHelper_1.deleteWorkloadsWithLabel(kubectl, blueGreenHelper_1.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList); + yield route_1.routeBlueGreenService(kubectl, blueGreenHelper_1.NONE_LABEL_VALUE, manifestObjects.serviceEntityList); + yield blueGreenHelper_1.deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList); } core.endGroup(); }); @@ -19460,20 +19450,19 @@ exports.reject = void 0; const core = __nccwpck_require__(6024); const canaryDeploymentHelper = __nccwpck_require__(4063); const SMICanaryDeploymentHelper = __nccwpck_require__(8139); -const serviceBlueGreenHelper_1 = __nccwpck_require__(4800); -const ingressBlueGreenHelper_1 = __nccwpck_require__(1721); -const smiBlueGreenHelper_1 = __nccwpck_require__(7506); +const reject_1 = __nccwpck_require__(5308); +const blueGreenHelper_1 = __nccwpck_require__(2662); const deploymentStrategy_1 = __nccwpck_require__(7023); const trafficSplitMethod_1 = __nccwpck_require__(9125); const routeStrategy_1 = __nccwpck_require__(7194); -function reject(kubectl, manifests, deploymentStrategy, annotations = {}) { +function reject(kubectl, manifests, deploymentStrategy) { return __awaiter(this, void 0, void 0, function* () { switch (deploymentStrategy) { case deploymentStrategy_1.DeploymentStrategy.CANARY: yield rejectCanary(kubectl, manifests); break; case deploymentStrategy_1.DeploymentStrategy.BLUE_GREEN: - yield rejectBlueGreen(kubectl, manifests, annotations); + yield rejectBlueGreen(kubectl, manifests); break; default: throw 'Invalid delete deployment strategy'; @@ -19496,24 +19485,47 @@ function rejectCanary(kubectl, manifests) { core.endGroup(); }); } -function rejectBlueGreen(kubectl, manifests, annotations = {}) { +function rejectBlueGreen(kubectl, manifests) { return __awaiter(this, void 0, void 0, function* () { - core.startGroup('Rejecting deployment with blue green strategy'); const routeStrategy = routeStrategy_1.parseRouteStrategy(core.getInput('route-method', { required: true })); + core.startGroup('Rejecting deployment with blue green strategy'); + core.info(`using routeMethod ${routeStrategy}`); + const manifestObjects = blueGreenHelper_1.getManifestObjects(manifests); if (routeStrategy == routeStrategy_1.RouteStrategy.INGRESS) { - yield ingressBlueGreenHelper_1.rejectBlueGreenIngress(kubectl, manifests); + yield reject_1.rejectBlueGreenIngress(kubectl, manifestObjects); } else if (routeStrategy == routeStrategy_1.RouteStrategy.SMI) { - yield smiBlueGreenHelper_1.rejectBlueGreenSMI(kubectl, manifests, annotations); + yield reject_1.rejectBlueGreenSMI(kubectl, manifestObjects); } else { - yield serviceBlueGreenHelper_1.rejectBlueGreenService(kubectl, manifests); + yield reject_1.rejectBlueGreenService(kubectl, manifestObjects); } core.endGroup(); }); } +/***/ }), + +/***/ 5442: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getBufferTime = exports.inputAnnotations = void 0; +const core = __nccwpck_require__(6024); +const annotations_1 = __nccwpck_require__(7522); +exports.inputAnnotations = annotations_1.parseAnnotations(core.getInput('annotations', { required: false })); +function getBufferTime() { + const inputBufferTime = parseInt(core.getInput('version-switch-buffer') || '0'); + if (inputBufferTime < 0 || inputBufferTime > 300) + throw Error('Version switch buffer must be between 0 and 300 (inclusive)'); + return inputBufferTime; +} +exports.getBufferTime = getBufferTime; + + /***/ }), /***/ 34: @@ -19541,7 +19553,6 @@ const action_1 = __nccwpck_require__(2868); const deploymentStrategy_1 = __nccwpck_require__(7023); const fileUtils_1 = __nccwpck_require__(7446); const privatekubectl_1 = __nccwpck_require__(8437); -const annotations_1 = __nccwpck_require__(7522); function run() { return __awaiter(this, void 0, void 0, function* () { // verify kubeconfig is set @@ -19549,7 +19560,6 @@ function run() { core.warning('KUBECONFIG env is not explicitly set. Ensure cluster context is set by using k8s-set-context action.'); // get inputs const action = action_1.parseAction(core.getInput('action', { required: true })); - const annotations = annotations_1.parseAnnotations(core.getInput('annotations', { required: false })); const strategy = deploymentStrategy_1.parseDeploymentStrategy(core.getInput('strategy')); const manifestsInput = core.getInput('manifests', { required: true }); const manifestFilePaths = manifestsInput @@ -19568,15 +19578,15 @@ function run() { // run action switch (action) { case action_1.Action.DEPLOY: { - yield deploy_1.deploy(kubectl, fullManifestFilePaths, strategy, annotations); + yield deploy_1.deploy(kubectl, fullManifestFilePaths, strategy); break; } case action_1.Action.PROMOTE: { - yield promote_1.promote(kubectl, fullManifestFilePaths, strategy, annotations); + yield promote_1.promote(kubectl, fullManifestFilePaths, strategy); break; } case action_1.Action.REJECT: { - yield reject_1.reject(kubectl, fullManifestFilePaths, strategy, annotations); + yield reject_1.reject(kubectl, fullManifestFilePaths, strategy); break; } default: { @@ -19606,103 +19616,35 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.fetchResource = exports.isServiceSelectorSubsetOfMatchLabel = exports.getServiceSelector = exports.getDeploymentMatchLabels = exports.getBlueGreenResourceName = exports.addBlueGreenLabelsAndAnnotations = exports.getNewBlueGreenObject = exports.createWorkloadsWithLabel = exports.isServiceRouted = exports.getManifestObjects = exports.deleteObjects = exports.deleteWorkloadsAndServicesWithLabel = exports.deleteWorkloadsWithLabel = exports.routeBlueGreen = exports.STABLE_SUFFIX = exports.GREEN_SUFFIX = exports.BLUE_GREEN_VERSION_LABEL = exports.NONE_LABEL_VALUE = exports.GREEN_LABEL_VALUE = void 0; +exports.deployObjects = exports.fetchResource = exports.isServiceSelectorSubsetOfMatchLabel = exports.getServiceSelector = exports.getDeploymentMatchLabels = exports.getBlueGreenResourceName = exports.addBlueGreenLabelsAndAnnotations = exports.getNewBlueGreenObject = exports.deployWithLabel = exports.isServiceRouted = exports.getManifestObjects = exports.deleteObjects = exports.deleteGreenObjects = exports.STABLE_SUFFIX = exports.GREEN_SUFFIX = exports.BLUE_GREEN_VERSION_LABEL = exports.NONE_LABEL_VALUE = exports.GREEN_LABEL_VALUE = void 0; const core = __nccwpck_require__(6024); const fs = __nccwpck_require__(7147); const yaml = __nccwpck_require__(3607); const kubernetesTypes_1 = __nccwpck_require__(6583); const fileHelper = __nccwpck_require__(7446); -const serviceBlueGreenHelper_1 = __nccwpck_require__(4800); -const ingressBlueGreenHelper_1 = __nccwpck_require__(1721); -const smiBlueGreenHelper_1 = __nccwpck_require__(7506); -const manifestUpdateUtils_1 = __nccwpck_require__(6352); const manifestSpecLabelUtils_1 = __nccwpck_require__(895); const kubectlUtils_1 = __nccwpck_require__(5474); -const timeUtils_1 = __nccwpck_require__(4046); -const routeStrategy_1 = __nccwpck_require__(7194); +const manifestUpdateUtils_1 = __nccwpck_require__(6352); exports.GREEN_LABEL_VALUE = 'green'; exports.NONE_LABEL_VALUE = 'None'; exports.BLUE_GREEN_VERSION_LABEL = 'k8s.deploy.color'; exports.GREEN_SUFFIX = '-green'; exports.STABLE_SUFFIX = '-stable'; -function routeBlueGreen(kubectl, inputManifestFiles, routeStrategy, annotations = {}) { - return __awaiter(this, void 0, void 0, function* () { - // sleep for buffer time - const bufferTime = parseInt(core.getInput('version-switch-buffer') || '0'); - if (bufferTime < 0 || bufferTime > 300) - throw Error('Version switch buffer must be between 0 and 300 (inclusive)'); - const startSleepDate = new Date(); - core.info(`Starting buffer time of ${bufferTime} minute(s) at ${startSleepDate.toISOString()}`); - yield timeUtils_1.sleep(bufferTime * 1000 * 60); - const endSleepDate = new Date(); - core.info(`Stopping buffer time of ${bufferTime} minute(s) at ${endSleepDate.toISOString()}`); - const manifestObjects = getManifestObjects(inputManifestFiles); - core.debug('Manifest objects: ' + JSON.stringify(manifestObjects)); - // route to new deployments - if (routeStrategy == routeStrategy_1.RouteStrategy.INGRESS) { - yield ingressBlueGreenHelper_1.routeBlueGreenIngress(kubectl, exports.GREEN_LABEL_VALUE, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList); - } - else if (routeStrategy == routeStrategy_1.RouteStrategy.SMI) { - yield smiBlueGreenHelper_1.routeBlueGreenSMI(kubectl, exports.GREEN_LABEL_VALUE, manifestObjects.serviceEntityList, annotations); - } - else { - yield serviceBlueGreenHelper_1.routeBlueGreenService(kubectl, exports.GREEN_LABEL_VALUE, manifestObjects.serviceEntityList); - } - }); -} -exports.routeBlueGreen = routeBlueGreen; -function deleteWorkloadsWithLabel(kubectl, deleteLabel, deploymentEntityList) { - return __awaiter(this, void 0, void 0, function* () { - const resourcesToDelete = []; - deploymentEntityList.forEach((inputObject) => { - const name = inputObject.metadata.name; - const kind = inputObject.kind; - if (deleteLabel === exports.NONE_LABEL_VALUE) { - // delete stable deployments - const resourceToDelete = { name, kind }; - resourcesToDelete.push(resourceToDelete); - } - else { - // delete new green deployments - const resourceToDelete = { - name: getBlueGreenResourceName(name, exports.GREEN_SUFFIX), - kind: kind - }; - resourcesToDelete.push(resourceToDelete); - } - }); - yield deleteObjects(kubectl, resourcesToDelete); - return resourcesToDelete; - }); -} -exports.deleteWorkloadsWithLabel = deleteWorkloadsWithLabel; -function deleteWorkloadsAndServicesWithLabel(kubectl, deleteLabel, deploymentEntityList, serviceEntityList) { +function deleteGreenObjects(kubectl, toDelete) { return __awaiter(this, void 0, void 0, function* () { - // need to delete services and deployments - const deletionEntitiesList = deploymentEntityList.concat(serviceEntityList); - const resourcesToDelete = []; - deletionEntitiesList.forEach((inputObject) => { - const name = inputObject.metadata.name; - const kind = inputObject.kind; - if (deleteLabel === exports.NONE_LABEL_VALUE) { - // delete stable objects - const resourceToDelete = { name, kind }; - resourcesToDelete.push(resourceToDelete); - } - else { - // delete green labels - const resourceToDelete = { - name: getBlueGreenResourceName(name, exports.GREEN_SUFFIX), - kind: kind - }; - resourcesToDelete.push(resourceToDelete); - } + // const resourcesToDelete: K8sDeleteObject[] = [] + const resourcesToDelete = toDelete.map((obj) => { + return { + name: getBlueGreenResourceName(obj.metadata.name, exports.GREEN_SUFFIX), + kind: obj.kind + }; }); + core.debug(`deleting green objects: ${JSON.stringify(resourcesToDelete)}`); yield deleteObjects(kubectl, resourcesToDelete); return resourcesToDelete; }); } -exports.deleteWorkloadsAndServicesWithLabel = deleteWorkloadsAndServicesWithLabel; +exports.deleteGreenObjects = deleteGreenObjects; function deleteObjects(kubectl, deleteList) { return __awaiter(this, void 0, void 0, function* () { // delete services and deployments @@ -19712,7 +19654,7 @@ function deleteObjects(kubectl, deleteList) { kubectlUtils_1.checkForErrors([result]); } catch (ex) { - // Ignore failures of delete if it doesn't exist + core.debug(`failed to delete object ${delObject.name}: ${ex}`); } } }); @@ -19764,35 +19706,25 @@ function getManifestObjects(filePaths) { } exports.getManifestObjects = getManifestObjects; function isServiceRouted(serviceObject, deploymentEntityList) { - let shouldBeRouted = false; const serviceSelector = getServiceSelector(serviceObject); - if (serviceSelector) { - if (deploymentEntityList.some((depObject) => { + return (serviceSelector && + deploymentEntityList.some((depObject) => { // finding if there is a deployment in the given manifests the service targets const matchLabels = getDeploymentMatchLabels(depObject); return (matchLabels && isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels)); - })) { - shouldBeRouted = true; - } - } - return shouldBeRouted; + })); } exports.isServiceRouted = isServiceRouted; -function createWorkloadsWithLabel(kubectl, deploymentObjectList, nextLabel) { +function deployWithLabel(kubectl, deploymentObjectList, nextLabel) { return __awaiter(this, void 0, void 0, function* () { - const newObjectsList = []; - deploymentObjectList.forEach((inputObject) => { - // creating deployment with label - const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel); - newObjectsList.push(newBlueGreenObject); - }); - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - const result = yield kubectl.apply(manifestFiles); - return { result: result, newFilePaths: manifestFiles }; + const newObjectsList = deploymentObjectList.map((inputObject) => getNewBlueGreenObject(inputObject, nextLabel)); + core.debug(`objects deployed with label are ${JSON.stringify(newObjectsList)}`); + const deployResult = yield deployObjects(kubectl, newObjectsList); + return { deployResult, objects: newObjectsList }; }); } -exports.createWorkloadsWithLabel = createWorkloadsWithLabel; +exports.deployWithLabel = deployWithLabel; function getNewBlueGreenObject(inputObject, labelValue) { const newObject = JSON.parse(JSON.stringify(inputObject)); // Updating name only if label is green label is given @@ -19876,11 +19808,19 @@ function fetchResource(kubectl, kind, name) { }); } exports.fetchResource = fetchResource; +function deployObjects(kubectl, objectsList) { + return __awaiter(this, void 0, void 0, function* () { + const manifestFiles = fileHelper.writeObjectsToFile(objectsList); + const execResult = yield kubectl.apply(manifestFiles); + return { execResult, manifestFiles }; + }); +} +exports.deployObjects = deployObjects; /***/ }), -/***/ 1721: +/***/ 1494: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -19895,126 +19835,107 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.updateIngressBackend = exports.updateIngressBackendBetaV1 = exports.getUpdatedBlueGreenIngress = exports.isIngressRouted = exports.validateIngresses = exports.routeBlueGreenIngress = exports.rejectBlueGreenIngress = exports.promoteBlueGreenIngress = exports.deployBlueGreenIngress = void 0; -const fileHelper = __nccwpck_require__(7446); -const blueGreenHelper_1 = __nccwpck_require__(2662); +exports.deployBlueGreenService = exports.deployBlueGreenIngress = exports.deployBlueGreenSMI = exports.deployBlueGreen = void 0; const core = __nccwpck_require__(6024); -const BACKEND = 'backend'; -function deployBlueGreenIngress(kubectl, filePaths) { +const routeStrategy_1 = __nccwpck_require__(7194); +const blueGreenHelper_1 = __nccwpck_require__(2662); +const smiBlueGreenHelper_1 = __nccwpck_require__(7506); +const route_1 = __nccwpck_require__(7692); +function deployBlueGreen(kubectl, files, routeStrategy) { return __awaiter(this, void 0, void 0, function* () { - // get all kubernetes objects defined in manifest files - const manifestObjects = blueGreenHelper_1.getManifestObjects(filePaths); - // create deployments with green label value - const workloadDeployment = yield blueGreenHelper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.GREEN_LABEL_VALUE); - let newObjectsList = []; - manifestObjects.serviceEntityList.forEach((inputObject) => { - const newBlueGreenObject = blueGreenHelper_1.getNewBlueGreenObject(inputObject, blueGreenHelper_1.GREEN_LABEL_VALUE); - newObjectsList.push(newBlueGreenObject); - }); - newObjectsList = newObjectsList - .concat(manifestObjects.otherObjects) - .concat(manifestObjects.unroutedServiceEntityList); - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - yield kubectl.apply(manifestFiles); - core.debug('new objects after processing services and other objects: \n' + - JSON.stringify(newObjectsList)); - return { workloadDeployment, newObjectsList }; + const blueGreenDeployment = yield (() => __awaiter(this, void 0, void 0, function* () { + switch (routeStrategy) { + case routeStrategy_1.RouteStrategy.INGRESS: + return yield deployBlueGreenIngress(kubectl, files); + case routeStrategy_1.RouteStrategy.SMI: + return yield deployBlueGreenSMI(kubectl, files); + default: + return yield deployBlueGreenService(kubectl, files); + } + }))(); + core.startGroup('Routing blue green'); + yield route_1.routeBlueGreenForDeploy(kubectl, files, routeStrategy); + core.endGroup(); + return blueGreenDeployment; }); } -exports.deployBlueGreenIngress = deployBlueGreenIngress; -function promoteBlueGreenIngress(kubectl, manifestObjects) { +exports.deployBlueGreen = deployBlueGreen; +function deployBlueGreenSMI(kubectl, filePaths) { return __awaiter(this, void 0, void 0, function* () { - //checking if anything to promote - const { areValid, invalidIngresses } = validateIngresses(kubectl, manifestObjects.ingressEntityList, manifestObjects.serviceNameMap); - if (!areValid) { - throw 'Ingresses are not in promote state' + invalidIngresses.toString(); - } - // create stable deployments with new configuration - const result = blueGreenHelper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.NONE_LABEL_VALUE); - // create stable services with new configuration - const newObjectsList = []; - manifestObjects.serviceEntityList.forEach((inputObject) => { - const newBlueGreenObject = blueGreenHelper_1.getNewBlueGreenObject(inputObject, blueGreenHelper_1.NONE_LABEL_VALUE); - newObjectsList.push(newBlueGreenObject); - }); - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - yield kubectl.apply(manifestFiles); - return result; + // get all kubernetes objects defined in manifest files + const manifestObjects = blueGreenHelper_1.getManifestObjects(filePaths); + // create services and other objects + const newObjectsList = [].concat(manifestObjects.otherObjects, manifestObjects.serviceEntityList, manifestObjects.ingressEntityList, manifestObjects.unroutedServiceEntityList); + yield blueGreenHelper_1.deployObjects(kubectl, newObjectsList); + // make extraservices and trafficsplit + yield smiBlueGreenHelper_1.setupSMI(kubectl, manifestObjects.serviceEntityList); + // create new deloyments + const blueGreenDeployment = yield blueGreenHelper_1.deployWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.GREEN_LABEL_VALUE); + return { + deployResult: blueGreenDeployment.deployResult, + objects: [].concat(blueGreenDeployment.objects, newObjectsList) + }; }); } -exports.promoteBlueGreenIngress = promoteBlueGreenIngress; -function rejectBlueGreenIngress(kubectl, filePaths) { +exports.deployBlueGreenSMI = deployBlueGreenSMI; +function deployBlueGreenIngress(kubectl, filePaths) { return __awaiter(this, void 0, void 0, function* () { // get all kubernetes objects defined in manifest files const manifestObjects = blueGreenHelper_1.getManifestObjects(filePaths); - // route ingress to stables services - yield routeBlueGreenIngress(kubectl, null, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList); - // delete green services and deployments - yield blueGreenHelper_1.deleteWorkloadsAndServicesWithLabel(kubectl, blueGreenHelper_1.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList, manifestObjects.serviceEntityList); + // create deployments with green label value + const servicesAndDeployments = [].concat(manifestObjects.deploymentEntityList, manifestObjects.serviceEntityList); + const workloadDeployment = yield blueGreenHelper_1.deployWithLabel(kubectl, servicesAndDeployments, blueGreenHelper_1.GREEN_LABEL_VALUE); + const otherObjects = [].concat(manifestObjects.otherObjects, manifestObjects.unroutedServiceEntityList); + yield blueGreenHelper_1.deployObjects(kubectl, otherObjects); + core.debug(`new objects after processing services and other objects: \n + ${JSON.stringify(servicesAndDeployments)}`); + return { + deployResult: workloadDeployment.deployResult, + objects: [].concat(workloadDeployment.objects, otherObjects) + }; }); } -exports.rejectBlueGreenIngress = rejectBlueGreenIngress; -function routeBlueGreenIngress(kubectl, nextLabel, serviceNameMap, ingressEntityList) { +exports.deployBlueGreenIngress = deployBlueGreenIngress; +function deployBlueGreenService(kubectl, filePaths) { return __awaiter(this, void 0, void 0, function* () { - let newObjectsList = []; - if (!nextLabel) { - newObjectsList = ingressEntityList.filter((ingress) => isIngressRouted(ingress, serviceNameMap)); - } - else { - ingressEntityList.forEach((inputObject) => { - if (isIngressRouted(inputObject, serviceNameMap)) { - const newBlueGreenIngressObject = getUpdatedBlueGreenIngress(inputObject, serviceNameMap, blueGreenHelper_1.GREEN_LABEL_VALUE); - newObjectsList.push(newBlueGreenIngressObject); - } - else { - newObjectsList.push(inputObject); - } - }); - } - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - yield kubectl.apply(manifestFiles); - return newObjectsList; + const manifestObjects = blueGreenHelper_1.getManifestObjects(filePaths); + // create deployments with green label value + const blueGreenDeployment = yield blueGreenHelper_1.deployWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.GREEN_LABEL_VALUE); + // create other non deployment and non service entities + const newObjectsList = [].concat(manifestObjects.otherObjects, manifestObjects.ingressEntityList, manifestObjects.unroutedServiceEntityList); + yield blueGreenHelper_1.deployObjects(kubectl, newObjectsList); + // returning deployment details to check for rollout stability + return { + deployResult: blueGreenDeployment.deployResult, + objects: [].concat(blueGreenDeployment.objects, newObjectsList) + }; }); } -exports.routeBlueGreenIngress = routeBlueGreenIngress; -function validateIngresses(kubectl, ingressEntityList, serviceNameMap) { - let areValid = true; - const invalidIngresses = []; - ingressEntityList.forEach((inputObject) => __awaiter(this, void 0, void 0, function* () { - var _a; - if (isIngressRouted(inputObject, serviceNameMap)) { - //querying existing ingress - const existingIngress = yield blueGreenHelper_1.fetchResource(kubectl, inputObject.kind, inputObject.metadata.name); - let isValid = !!existingIngress && - ((_a = existingIngress === null || existingIngress === void 0 ? void 0 : existingIngress.metadata) === null || _a === void 0 ? void 0 : _a.labels[blueGreenHelper_1.BLUE_GREEN_VERSION_LABEL]) === - blueGreenHelper_1.GREEN_LABEL_VALUE; - if (!isValid) { - invalidIngresses.push(inputObject.metadata.name); - } - // to be valid, ingress should exist and should be green - areValid = areValid && isValid; - } - })); - return { areValid, invalidIngresses }; -} -exports.validateIngresses = validateIngresses; -function isIngressRouted(ingressObject, serviceNameMap) { - let isIngressRouted = false; - // check if ingress targets a service in the given manifests - JSON.parse(JSON.stringify(ingressObject), (key, value) => { - isIngressRouted = - isIngressRouted || (key === 'service' && value.hasOwnProperty('name')); - isIngressRouted = - isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value)); - return value; +exports.deployBlueGreenService = deployBlueGreenService; + + +/***/ }), + +/***/ 1721: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); }); - return isIngressRouted; -} -exports.isIngressRouted = isIngressRouted; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.validateIngresses = exports.isIngressRouted = exports.updateIngressBackend = exports.updateIngressBackendBetaV1 = exports.getUpdatedBlueGreenIngress = void 0; +const core = __nccwpck_require__(6024); +const blueGreenHelper_1 = __nccwpck_require__(2662); +const BACKEND = 'backend'; function getUpdatedBlueGreenIngress(inputObject, serviceNameMap, type) { - if (!type) { - return inputObject; - } const newObject = JSON.parse(JSON.stringify(inputObject)); // add green labels and values blueGreenHelper_1.addBlueGreenLabelsAndAnnotations(newObject, type); @@ -20041,9 +19962,8 @@ function updateIngressBackendBetaV1(inputObject, serviceNameMap) { exports.updateIngressBackendBetaV1 = updateIngressBackendBetaV1; function updateIngressBackend(inputObject, serviceNameMap) { inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => { - var _a; if (key.toLowerCase() === BACKEND && - serviceNameMap.has((_a = value === null || value === void 0 ? void 0 : value.service) === null || _a === void 0 ? void 0 : _a.name)) { + serviceNameMap.has(value.service.name)) { value.service.name = serviceNameMap.get(value.service.name); } return value; @@ -20051,11 +19971,51 @@ function updateIngressBackend(inputObject, serviceNameMap) { return inputObject; } exports.updateIngressBackend = updateIngressBackend; +function isIngressRouted(ingressObject, serviceNameMap) { + let isIngressRouted = false; + // check if ingress targets a service in the given manifests + JSON.parse(JSON.stringify(ingressObject), (key, value) => { + isIngressRouted = + isIngressRouted || + (key === 'service' && + value.hasOwnProperty('name') && + serviceNameMap.has(value.name)); + isIngressRouted = + isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value)); + return value; + }); + return isIngressRouted; +} +exports.isIngressRouted = isIngressRouted; +function validateIngresses(kubectl, ingressEntityList, serviceNameMap) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + let areValid = true; + const invalidIngresses = []; + for (const inputObject of ingressEntityList) { + if (isIngressRouted(inputObject, serviceNameMap)) { + //querying existing ingress + const existingIngress = yield blueGreenHelper_1.fetchResource(kubectl, inputObject.kind, inputObject.metadata.name); + const isValid = !!existingIngress && + ((_a = existingIngress === null || existingIngress === void 0 ? void 0 : existingIngress.metadata) === null || _a === void 0 ? void 0 : _a.labels[blueGreenHelper_1.BLUE_GREEN_VERSION_LABEL]) === + blueGreenHelper_1.GREEN_LABEL_VALUE; + if (!isValid) { + core.debug(`Invalid ingress detected (must be in green state): ${JSON.stringify(inputObject)}`); + invalidIngresses.push(inputObject.metadata.name); + } + // to be valid, ingress should exist and should be green + areValid = areValid && isValid; + } + } + return { areValid, invalidIngresses }; + }); +} +exports.validateIngresses = validateIngresses; /***/ }), -/***/ 4800: +/***/ 9701: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -20070,60 +20030,222 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getServiceSpecLabel = exports.validateServicesState = exports.routeBlueGreenService = exports.rejectBlueGreenService = exports.promoteBlueGreenService = exports.deployBlueGreenService = void 0; -const fileHelper = __nccwpck_require__(7446); +exports.promoteBlueGreenSMI = exports.promoteBlueGreenService = exports.promoteBlueGreenIngress = void 0; const blueGreenHelper_1 = __nccwpck_require__(2662); -function deployBlueGreenService(kubectl, filePaths) { +const ingressBlueGreenHelper_1 = __nccwpck_require__(1721); +const serviceBlueGreenHelper_1 = __nccwpck_require__(4800); +const smiBlueGreenHelper_1 = __nccwpck_require__(7506); +function promoteBlueGreenIngress(kubectl, manifestObjects) { return __awaiter(this, void 0, void 0, function* () { - const manifestObjects = blueGreenHelper_1.getManifestObjects(filePaths); - // create deployments with green label value - const workloadDeployment = yield blueGreenHelper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.GREEN_LABEL_VALUE); - const newObjectsList = manifestObjects.otherObjects - .concat(manifestObjects.ingressEntityList) - .concat(manifestObjects.unroutedServiceEntityList); - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - if (manifestFiles.length > 0) - yield kubectl.apply(manifestFiles); - // returning deployment details to check for rollout stability - return { workloadDeployment, newObjectsList }; + //checking if anything to promote + const { areValid, invalidIngresses } = yield ingressBlueGreenHelper_1.validateIngresses(kubectl, manifestObjects.ingressEntityList, manifestObjects.serviceNameMap); + if (!areValid) { + throw new Error(`Ingresses are not in promote state: ${invalidIngresses.toString()}`); + } + // create stable deployments with new configuration + const result = yield blueGreenHelper_1.deployWithLabel(kubectl, [].concat(manifestObjects.deploymentEntityList, manifestObjects.serviceEntityList), blueGreenHelper_1.NONE_LABEL_VALUE); + // create stable services with new configuration + return result; }); } -exports.deployBlueGreenService = deployBlueGreenService; +exports.promoteBlueGreenIngress = promoteBlueGreenIngress; function promoteBlueGreenService(kubectl, manifestObjects) { return __awaiter(this, void 0, void 0, function* () { // checking if services are in the right state ie. targeting green deployments - if (!(yield validateServicesState(kubectl, manifestObjects.serviceEntityList))) { - throw 'Not inP promote state'; + if (!(yield serviceBlueGreenHelper_1.validateServicesState(kubectl, manifestObjects.serviceEntityList))) { + throw new Error('Found services not in promote state'); } // creating stable deployments with new configurations - return yield blueGreenHelper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.NONE_LABEL_VALUE); + return yield blueGreenHelper_1.deployWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.NONE_LABEL_VALUE); }); } exports.promoteBlueGreenService = promoteBlueGreenService; -function rejectBlueGreenService(kubectl, filePaths) { +function promoteBlueGreenSMI(kubectl, manifestObjects) { + return __awaiter(this, void 0, void 0, function* () { + // checking if there is something to promote + if (!(yield smiBlueGreenHelper_1.validateTrafficSplitsState(kubectl, manifestObjects.serviceEntityList))) { + throw Error('Not in promote state SMI'); + } + // create stable deployments with new configuration + return yield blueGreenHelper_1.deployWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.NONE_LABEL_VALUE); + }); +} +exports.promoteBlueGreenSMI = promoteBlueGreenSMI; + + +/***/ }), + +/***/ 5308: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.rejectBlueGreenSMI = exports.rejectBlueGreenService = exports.rejectBlueGreenIngress = void 0; +const blueGreenHelper_1 = __nccwpck_require__(2662); +const route_1 = __nccwpck_require__(7692); +const smiBlueGreenHelper_1 = __nccwpck_require__(7506); +const route_2 = __nccwpck_require__(7692); +function rejectBlueGreenIngress(kubectl, manifestObjects) { return __awaiter(this, void 0, void 0, function* () { // get all kubernetes objects defined in manifest files - const manifestObjects = blueGreenHelper_1.getManifestObjects(filePaths); + // route ingress to stables services + const routeResult = yield route_2.routeBlueGreenIngressUnchanged(kubectl, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList); + // delete green services and deployments + const deleteResult = yield blueGreenHelper_1.deleteGreenObjects(kubectl, [].concat(manifestObjects.deploymentEntityList, manifestObjects.serviceEntityList)); + return { routeResult, deleteResult }; + }); +} +exports.rejectBlueGreenIngress = rejectBlueGreenIngress; +function rejectBlueGreenService(kubectl, manifestObjects) { + return __awaiter(this, void 0, void 0, function* () { // route to stable objects - yield routeBlueGreenService(kubectl, blueGreenHelper_1.NONE_LABEL_VALUE, manifestObjects.serviceEntityList); + const routeResult = yield route_2.routeBlueGreenService(kubectl, blueGreenHelper_1.NONE_LABEL_VALUE, manifestObjects.serviceEntityList); // delete new deployments with green suffix - yield blueGreenHelper_1.deleteWorkloadsWithLabel(kubectl, blueGreenHelper_1.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList); + const deleteResult = yield blueGreenHelper_1.deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList); + return { routeResult, deleteResult }; }); } exports.rejectBlueGreenService = rejectBlueGreenService; -function routeBlueGreenService(kubectl, nextLabel, serviceEntityList) { +function rejectBlueGreenSMI(kubectl, manifestObjects) { return __awaiter(this, void 0, void 0, function* () { - const newObjectsList = []; - serviceEntityList.forEach((serviceObject) => { - const newBlueGreenServiceObject = getUpdatedBlueGreenService(serviceObject, nextLabel); - newObjectsList.push(newBlueGreenServiceObject); + // route trafficsplit to stable deployments + const routeResult = yield route_1.routeBlueGreenSMI(kubectl, blueGreenHelper_1.NONE_LABEL_VALUE, manifestObjects.serviceEntityList); + // delete rejected new bluegreen deployments + const deletedObjects = yield blueGreenHelper_1.deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList); + // delete trafficsplit and extra services + const cleanupResult = yield smiBlueGreenHelper_1.cleanupSMI(kubectl, manifestObjects.serviceEntityList); + return { routeResult, deleteResult: [].concat(deletedObjects, cleanupResult) }; + }); +} +exports.rejectBlueGreenSMI = rejectBlueGreenSMI; + + +/***/ }), + +/***/ 7692: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.routeBlueGreenSMI = exports.routeBlueGreenService = exports.routeBlueGreenIngressUnchanged = exports.routeBlueGreenIngress = exports.routeBlueGreenForDeploy = void 0; +const timeUtils_1 = __nccwpck_require__(4046); +const routeStrategy_1 = __nccwpck_require__(7194); +const blueGreenHelper_1 = __nccwpck_require__(2662); +const ingressBlueGreenHelper_1 = __nccwpck_require__(1721); +const serviceBlueGreenHelper_1 = __nccwpck_require__(4800); +const smiBlueGreenHelper_1 = __nccwpck_require__(7506); +const core = __nccwpck_require__(6024); +const inputUtils_1 = __nccwpck_require__(5442); +function routeBlueGreenForDeploy(kubectl, inputManifestFiles, routeStrategy) { + return __awaiter(this, void 0, void 0, function* () { + // sleep for buffer time + const bufferTime = inputUtils_1.getBufferTime(); + const startSleepDate = new Date(); + core.info(`Starting buffer time of ${bufferTime} minute(s) at ${startSleepDate.toISOString()}`); + yield timeUtils_1.sleep(bufferTime * 1000 * 60); + const endSleepDate = new Date(); + core.info(`Stopping buffer time of ${bufferTime} minute(s) at ${endSleepDate.toISOString()}`); + const manifestObjects = blueGreenHelper_1.getManifestObjects(inputManifestFiles); + // route to new deployments + if (routeStrategy == routeStrategy_1.RouteStrategy.INGRESS) { + return yield routeBlueGreenIngress(kubectl, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList); + } + else if (routeStrategy == routeStrategy_1.RouteStrategy.SMI) { + return yield routeBlueGreenSMI(kubectl, blueGreenHelper_1.GREEN_LABEL_VALUE, manifestObjects.serviceEntityList); + } + else { + return yield routeBlueGreenService(kubectl, blueGreenHelper_1.GREEN_LABEL_VALUE, manifestObjects.serviceEntityList); + } + }); +} +exports.routeBlueGreenForDeploy = routeBlueGreenForDeploy; +function routeBlueGreenIngress(kubectl, serviceNameMap, ingressEntityList) { + return __awaiter(this, void 0, void 0, function* () { + // const newObjectsList = [] + const newObjectsList = ingressEntityList.map((obj) => { + if (ingressBlueGreenHelper_1.isIngressRouted(obj, serviceNameMap)) { + const newBlueGreenIngressObject = ingressBlueGreenHelper_1.getUpdatedBlueGreenIngress(obj, serviceNameMap, blueGreenHelper_1.GREEN_LABEL_VALUE); + return newBlueGreenIngressObject; + } + else { + core.debug(`unrouted ingress detected ${obj.metadata.name}`); + return obj; + } }); - // configures the services - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - yield kubectl.apply(manifestFiles); + const deployResult = yield blueGreenHelper_1.deployObjects(kubectl, newObjectsList); + return { deployResult, objects: newObjectsList }; + }); +} +exports.routeBlueGreenIngress = routeBlueGreenIngress; +function routeBlueGreenIngressUnchanged(kubectl, serviceNameMap, ingressEntityList) { + return __awaiter(this, void 0, void 0, function* () { + const objects = ingressEntityList.filter((ingress) => ingressBlueGreenHelper_1.isIngressRouted(ingress, serviceNameMap)); + const deployResult = yield blueGreenHelper_1.deployObjects(kubectl, objects); + return { deployResult, objects }; + }); +} +exports.routeBlueGreenIngressUnchanged = routeBlueGreenIngressUnchanged; +function routeBlueGreenService(kubectl, nextLabel, serviceEntityList) { + return __awaiter(this, void 0, void 0, function* () { + const objects = serviceEntityList.map((serviceObject) => serviceBlueGreenHelper_1.getUpdatedBlueGreenService(serviceObject, nextLabel)); + const deployResult = yield blueGreenHelper_1.deployObjects(kubectl, objects); + return { deployResult, objects }; }); } exports.routeBlueGreenService = routeBlueGreenService; +function routeBlueGreenSMI(kubectl, nextLabel, serviceEntityList) { + return __awaiter(this, void 0, void 0, function* () { + // let tsObjects: TrafficSplitObject[] = [] + const tsObjects = yield Promise.all(serviceEntityList.map((serviceObject) => __awaiter(this, void 0, void 0, function* () { + const tsObject = yield smiBlueGreenHelper_1.createTrafficSplitObject(kubectl, serviceObject.metadata.name, nextLabel); + return tsObject; + }))); + const deployResult = yield blueGreenHelper_1.deployObjects(kubectl, tsObjects); + return { deployResult, objects: tsObjects }; + }); +} +exports.routeBlueGreenSMI = routeBlueGreenSMI; + + +/***/ }), + +/***/ 4800: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getServiceSpecLabel = exports.validateServicesState = exports.getUpdatedBlueGreenService = void 0; +const blueGreenHelper_1 = __nccwpck_require__(2662); // add green labels to configure existing service function getUpdatedBlueGreenService(inputObject, labelValue) { const newObject = JSON.parse(JSON.stringify(inputObject)); @@ -20131,34 +20253,24 @@ function getUpdatedBlueGreenService(inputObject, labelValue) { blueGreenHelper_1.addBlueGreenLabelsAndAnnotations(newObject, labelValue); return newObject; } +exports.getUpdatedBlueGreenService = getUpdatedBlueGreenService; function validateServicesState(kubectl, serviceEntityList) { return __awaiter(this, void 0, void 0, function* () { let areServicesGreen = true; for (const serviceObject of serviceEntityList) { // finding the existing routed service const existingService = yield blueGreenHelper_1.fetchResource(kubectl, serviceObject.kind, serviceObject.metadata.name); - if (!!existingService) { - const currentLabel = getServiceSpecLabel(existingService); - if (currentLabel != blueGreenHelper_1.GREEN_LABEL_VALUE) { - // service should be targeting deployments with green label - areServicesGreen = false; - } - } - else { - // service targeting deployment doesn't exist - areServicesGreen = false; - } + let isServiceGreen = !!existingService && + getServiceSpecLabel(existingService) == + blueGreenHelper_1.GREEN_LABEL_VALUE; + areServicesGreen = areServicesGreen && isServiceGreen; } return areServicesGreen; }); } exports.validateServicesState = validateServicesState; function getServiceSpecLabel(inputObject) { - var _a; - if ((_a = inputObject === null || inputObject === void 0 ? void 0 : inputObject.spec) === null || _a === void 0 ? void 0 : _a.selector[blueGreenHelper_1.BLUE_GREEN_VERSION_LABEL]) { - return inputObject.spec.selector[blueGreenHelper_1.BLUE_GREEN_VERSION_LABEL]; - } - return ''; + return inputObject.spec.selector[blueGreenHelper_1.BLUE_GREEN_VERSION_LABEL]; } exports.getServiceSpecLabel = getServiceSpecLabel; @@ -20180,58 +20292,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.cleanupSMI = exports.validateTrafficSplitsState = exports.routeBlueGreenSMI = exports.getSMIServiceResource = exports.setupSMI = exports.rejectBlueGreenSMI = exports.promoteBlueGreenSMI = exports.deployBlueGreenSMI = void 0; +exports.cleanupSMI = exports.validateTrafficSplitsState = exports.getGreenSMIServiceResource = exports.getStableSMIServiceResource = exports.createTrafficSplitObject = exports.setupSMI = exports.MAX_VAL = exports.MIN_VAL = exports.TRAFFIC_SPLIT_OBJECT = exports.TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = void 0; +const core = __nccwpck_require__(6024); const kubectlUtils = __nccwpck_require__(7584); -const fileHelper = __nccwpck_require__(7446); const blueGreenHelper_1 = __nccwpck_require__(2662); -const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'; -const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'; -const MIN_VAL = 0; -const MAX_VAL = 100; -function deployBlueGreenSMI(kubectl, filePaths, annotations = {}) { - return __awaiter(this, void 0, void 0, function* () { - // get all kubernetes objects defined in manifest files - const manifestObjects = blueGreenHelper_1.getManifestObjects(filePaths); - // create services and other objects - const newObjectsList = manifestObjects.otherObjects - .concat(manifestObjects.serviceEntityList) - .concat(manifestObjects.ingressEntityList) - .concat(manifestObjects.unroutedServiceEntityList); - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - yield kubectl.apply(manifestFiles); - // make extraservices and trafficsplit - yield setupSMI(kubectl, manifestObjects.serviceEntityList, annotations); - // create new deloyments - const workloadDeployment = yield blueGreenHelper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.GREEN_LABEL_VALUE); - return { workloadDeployment, newObjectsList }; - }); -} -exports.deployBlueGreenSMI = deployBlueGreenSMI; -function promoteBlueGreenSMI(kubectl, manifestObjects) { - return __awaiter(this, void 0, void 0, function* () { - // checking if there is something to promote - if (!(yield validateTrafficSplitsState(kubectl, manifestObjects.serviceEntityList))) { - throw Error('Not in promote state SMI'); - } - // create stable deployments with new configuration - return yield blueGreenHelper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blueGreenHelper_1.NONE_LABEL_VALUE); - }); -} -exports.promoteBlueGreenSMI = promoteBlueGreenSMI; -function rejectBlueGreenSMI(kubectl, filePaths, annotations = {}) { - return __awaiter(this, void 0, void 0, function* () { - // get all kubernetes objects defined in manifest files - const manifestObjects = blueGreenHelper_1.getManifestObjects(filePaths); - // route trafficsplit to stable deployments - yield routeBlueGreenSMI(kubectl, blueGreenHelper_1.NONE_LABEL_VALUE, manifestObjects.serviceEntityList, annotations); - // delete rejected new bluegreen deployments - yield blueGreenHelper_1.deleteWorkloadsWithLabel(kubectl, blueGreenHelper_1.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList); - // delete trafficsplit and extra services - yield cleanupSMI(kubectl, manifestObjects.serviceEntityList); - }); -} -exports.rejectBlueGreenSMI = rejectBlueGreenSMI; -function setupSMI(kubectl, serviceEntityList, annotations = {}) { +const inputUtils_1 = __nccwpck_require__(5442); +exports.TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'; +exports.TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'; +exports.MIN_VAL = 0; +exports.MAX_VAL = 100; +function setupSMI(kubectl, serviceEntityList) { return __awaiter(this, void 0, void 0, function* () { const newObjectsList = []; const trafficObjectList = []; @@ -20239,36 +20309,45 @@ function setupSMI(kubectl, serviceEntityList, annotations = {}) { // create a trafficsplit for service trafficObjectList.push(serviceObject); // set up the services for trafficsplit - const newStableService = getSMIServiceResource(serviceObject, blueGreenHelper_1.STABLE_SUFFIX); - const newGreenService = getSMIServiceResource(serviceObject, blueGreenHelper_1.GREEN_SUFFIX); + const newStableService = getStableSMIServiceResource(serviceObject); + const newGreenService = getGreenSMIServiceResource(serviceObject); newObjectsList.push(newStableService); newObjectsList.push(newGreenService); }); - // create services - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - yield kubectl.apply(manifestFiles); + const tsObjects = []; // route to stable service - trafficObjectList.forEach((inputObject) => { - createTrafficSplitObject(kubectl, inputObject.metadata.name, blueGreenHelper_1.NONE_LABEL_VALUE, annotations); - }); + for (const svc of trafficObjectList) { + const tsObject = yield createTrafficSplitObject(kubectl, svc.metadata.name, blueGreenHelper_1.NONE_LABEL_VALUE); + tsObjects.push(tsObject); + } + const objectsToDeploy = [].concat(newObjectsList, tsObjects); + // create services + const smiDeploymentResult = yield blueGreenHelper_1.deployObjects(kubectl, objectsToDeploy); + return { + objects: objectsToDeploy, + deployResult: smiDeploymentResult + }; }); } exports.setupSMI = setupSMI; let trafficSplitAPIVersion = ''; -function createTrafficSplitObject(kubectl, name, nextLabel, annotations = {}) { +function createTrafficSplitObject(kubectl, name, nextLabel) { return __awaiter(this, void 0, void 0, function* () { // cache traffic split api version if (!trafficSplitAPIVersion) trafficSplitAPIVersion = yield kubectlUtils.getTrafficSplitAPIVersion(kubectl); + // retrieve annotations for TS object + const annotations = inputUtils_1.inputAnnotations; // decide weights based on nextlabel - const stableWeight = nextLabel === blueGreenHelper_1.GREEN_LABEL_VALUE ? MIN_VAL : MAX_VAL; - const greenWeight = nextLabel === blueGreenHelper_1.GREEN_LABEL_VALUE ? MAX_VAL : MIN_VAL; - const trafficSplitObject = JSON.stringify({ + const stableWeight = nextLabel === blueGreenHelper_1.GREEN_LABEL_VALUE ? exports.MIN_VAL : exports.MAX_VAL; + const greenWeight = nextLabel === blueGreenHelper_1.GREEN_LABEL_VALUE ? exports.MAX_VAL : exports.MIN_VAL; + const trafficSplitObject = { apiVersion: trafficSplitAPIVersion, - kind: 'TrafficSplit', + kind: exports.TRAFFIC_SPLIT_OBJECT, metadata: { - name: blueGreenHelper_1.getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), - annotations: annotations + name: blueGreenHelper_1.getBlueGreenResourceName(name, exports.TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), + annotations: annotations, + labels: new Map() }, spec: { service: name, @@ -20283,54 +20362,44 @@ function createTrafficSplitObject(kubectl, name, nextLabel, annotations = {}) { } ] } - }); - // create traffic split object - const trafficSplitManifestFile = fileHelper.writeManifestToFile(trafficSplitObject, TRAFFIC_SPLIT_OBJECT, blueGreenHelper_1.getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)); - yield kubectl.apply(trafficSplitManifestFile); + }; + return trafficSplitObject; }); } -function getSMIServiceResource(inputObject, suffix) { +exports.createTrafficSplitObject = createTrafficSplitObject; +function getStableSMIServiceResource(inputObject) { const newObject = JSON.parse(JSON.stringify(inputObject)); - if (suffix === blueGreenHelper_1.STABLE_SUFFIX) { - // adding stable suffix to service name - newObject.metadata.name = blueGreenHelper_1.getBlueGreenResourceName(inputObject.metadata.name, blueGreenHelper_1.STABLE_SUFFIX); - return blueGreenHelper_1.getNewBlueGreenObject(newObject, blueGreenHelper_1.NONE_LABEL_VALUE); - } - else { - // green label will be added for these - return blueGreenHelper_1.getNewBlueGreenObject(newObject, blueGreenHelper_1.GREEN_LABEL_VALUE); - } + // adding stable suffix to service name + newObject.metadata.name = blueGreenHelper_1.getBlueGreenResourceName(inputObject.metadata.name, blueGreenHelper_1.STABLE_SUFFIX); + return blueGreenHelper_1.getNewBlueGreenObject(newObject, blueGreenHelper_1.NONE_LABEL_VALUE); } -exports.getSMIServiceResource = getSMIServiceResource; -function routeBlueGreenSMI(kubectl, nextLabel, serviceEntityList, annotations = {}) { - return __awaiter(this, void 0, void 0, function* () { - for (const serviceObject of serviceEntityList) { - // route trafficsplit to given label - yield createTrafficSplitObject(kubectl, serviceObject.metadata.name, nextLabel, annotations); - } - }); +exports.getStableSMIServiceResource = getStableSMIServiceResource; +function getGreenSMIServiceResource(inputObject) { + const newObject = JSON.parse(JSON.stringify(inputObject)); + return blueGreenHelper_1.getNewBlueGreenObject(newObject, blueGreenHelper_1.GREEN_LABEL_VALUE); } -exports.routeBlueGreenSMI = routeBlueGreenSMI; +exports.getGreenSMIServiceResource = getGreenSMIServiceResource; function validateTrafficSplitsState(kubectl, serviceEntityList) { return __awaiter(this, void 0, void 0, function* () { let trafficSplitsInRightState = true; for (const serviceObject of serviceEntityList) { const name = serviceObject.metadata.name; - let trafficSplitObject = yield blueGreenHelper_1.fetchResource(kubectl, TRAFFIC_SPLIT_OBJECT, blueGreenHelper_1.getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)); + let trafficSplitObject = yield blueGreenHelper_1.fetchResource(kubectl, exports.TRAFFIC_SPLIT_OBJECT, blueGreenHelper_1.getBlueGreenResourceName(name, exports.TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)); + core.debug(`ts object extracted was ${JSON.stringify(trafficSplitObject)}`); if (!trafficSplitObject) { - // no traffic split exits + core.debug(`no traffic split exits for ${name}`); trafficSplitsInRightState = false; + continue; } - trafficSplitObject = JSON.parse(JSON.stringify(trafficSplitObject)); trafficSplitObject.spec.backends.forEach((element) => { // checking if trafficsplit in right state to deploy if (element.service === blueGreenHelper_1.getBlueGreenResourceName(name, blueGreenHelper_1.GREEN_SUFFIX)) { - if (element.weight != MAX_VAL) - trafficSplitsInRightState = false; + trafficSplitsInRightState = + trafficSplitsInRightState && element.weight == exports.MAX_VAL; } if (element.service === blueGreenHelper_1.getBlueGreenResourceName(name, blueGreenHelper_1.STABLE_SUFFIX)) { - if (element.weight != MIN_VAL) - trafficSplitsInRightState = false; + trafficSplitsInRightState = + trafficSplitsInRightState && element.weight == exports.MIN_VAL; } }); } @@ -20343,8 +20412,8 @@ function cleanupSMI(kubectl, serviceEntityList) { const deleteList = []; serviceEntityList.forEach((serviceObject) => { deleteList.push({ - name: blueGreenHelper_1.getBlueGreenResourceName(serviceObject.metadata.name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), - kind: TRAFFIC_SPLIT_OBJECT + name: blueGreenHelper_1.getBlueGreenResourceName(serviceObject.metadata.name, exports.TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), + kind: exports.TRAFFIC_SPLIT_OBJECT }); deleteList.push({ name: blueGreenHelper_1.getBlueGreenResourceName(serviceObject.metadata.name, blueGreenHelper_1.GREEN_SUFFIX), @@ -20357,6 +20426,7 @@ function cleanupSMI(kubectl, serviceEntityList) { }); // delete all objects yield blueGreenHelper_1.deleteObjects(kubectl, deleteList); + return deleteList; }); } exports.cleanupSMI = cleanupSMI; @@ -20635,6 +20705,7 @@ const kubectlUtils = __nccwpck_require__(7584); const canaryDeploymentHelper = __nccwpck_require__(4063); const kubernetesTypes_1 = __nccwpck_require__(6583); const kubectlUtils_1 = __nccwpck_require__(5474); +const inputUtils_1 = __nccwpck_require__(5442); const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout'; const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'; function deploySMICanary(filePaths, kubectl) { @@ -20801,7 +20872,7 @@ function createTrafficSplitManifestFile(kubectl, serviceName, stableWeight, base }); } let trafficSplitAPIVersion = ''; -function getTrafficSplitObject(kubectl, name, stableWeight, baselineWeight, canaryWeight, annotations = {}) { +function getTrafficSplitObject(kubectl, name, stableWeight, baselineWeight, canaryWeight) { return __awaiter(this, void 0, void 0, function* () { // cached version if (!trafficSplitAPIVersion) { @@ -20812,7 +20883,7 @@ function getTrafficSplitObject(kubectl, name, stableWeight, baselineWeight, cana kind: 'TrafficSplit', metadata: { name: getTrafficSplitResourceName(name), - annotations: annotations + annotations: inputUtils_1.inputAnnotations }, spec: { backends: [ @@ -20866,9 +20937,7 @@ const fileHelper = __nccwpck_require__(7446); const KubernetesManifestUtility = __nccwpck_require__(8246); const podCanaryHelper_1 = __nccwpck_require__(519); const smiCanaryHelper_1 = __nccwpck_require__(8139); -const serviceBlueGreenHelper_1 = __nccwpck_require__(4800); -const ingressBlueGreenHelper_1 = __nccwpck_require__(1721); -const smiBlueGreenHelper_1 = __nccwpck_require__(7506); +const deploy_1 = __nccwpck_require__(1494); const deploymentStrategy_1 = __nccwpck_require__(7023); const core = __nccwpck_require__(6024); const trafficSplitMethod_1 = __nccwpck_require__(9125); @@ -20877,7 +20946,7 @@ const workflowAnnotationUtils_1 = __nccwpck_require__(6564); const kubectlUtils_1 = __nccwpck_require__(5474); const githubUtils_1 = __nccwpck_require__(9976); const dockerUtils_1 = __nccwpck_require__(6704); -function deployManifests(files, deploymentStrategy, kubectl, trafficSplitMethod, annotations = {}) { +function deployManifests(files, deploymentStrategy, kubectl, trafficSplitMethod) { return __awaiter(this, void 0, void 0, function* () { switch (deploymentStrategy) { case deploymentStrategy_1.DeploymentStrategy.CANARY: { @@ -20889,13 +20958,10 @@ function deployManifests(files, deploymentStrategy, kubectl, trafficSplitMethod, } case deploymentStrategy_1.DeploymentStrategy.BLUE_GREEN: { const routeStrategy = routeStrategy_1.parseRouteStrategy(core.getInput('route-method', { required: true })); - const { workloadDeployment, newObjectsList } = yield Promise.resolve((routeStrategy == routeStrategy_1.RouteStrategy.INGRESS && - ingressBlueGreenHelper_1.deployBlueGreenIngress(kubectl, files)) || - (routeStrategy == routeStrategy_1.RouteStrategy.SMI && - smiBlueGreenHelper_1.deployBlueGreenSMI(kubectl, files, annotations)) || - serviceBlueGreenHelper_1.deployBlueGreenService(kubectl, files)); - kubectlUtils_1.checkForErrors([workloadDeployment.result]); - return workloadDeployment.newFilePaths; + const blueGreenDeployment = yield deploy_1.deployBlueGreen(kubectl, files, routeStrategy); + core.debug(`objects deployed for ${routeStrategy}: ${JSON.stringify(blueGreenDeployment.objects)} `); + kubectlUtils_1.checkForErrors([blueGreenDeployment.deployResult.execResult]); + return blueGreenDeployment.deployResult.manifestFiles; } case deploymentStrategy_1.DeploymentStrategy.BASIC: { const trafficSplitMethod = trafficSplitMethod_1.parseTrafficSplitMethod(core.getInput('traffic-split-method', { required: true })); @@ -21021,11 +21087,11 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.parseAnnotations = void 0; function parseAnnotations(str) { if (str == '') { - return {}; + return new Map(); } else { - const annotaion = JSON.parse(str); - return new Map(annotaion); + const annotation = JSON.parse(str); + return new Map(annotation); } } exports.parseAnnotations = parseAnnotations; @@ -22560,7 +22626,10 @@ exports.getWorkflowAnnotationKeyLabel = getWorkflowAnnotationKeyLabel; * @returns cleaned label */ function cleanLabel(label) { - const removedInvalidChars = label.replace(/[^-A-Za-z0-9_.]/gi, ''); + let removedInvalidChars = label + .replace(/\s/gi, '_') + .replace(/[\/\\\|]/gi, '-') + .replace(/[^-A-Za-z0-9_.]/gi, ''); const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/; return regex.exec(removedInvalidChars)[0] || ''; } diff --git a/package.json b/package.json index 6b86fc46b..493685b63 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "ncc build src/run.ts -o lib", "test": "jest", + "coverage": "jest --coverage=true", "format": "prettier --write .", "format-check": "prettier --check ." }, diff --git a/src/actions/deploy.ts b/src/actions/deploy.ts index 16aaf24a2..e3021217c 100644 --- a/src/actions/deploy.ts +++ b/src/actions/deploy.ts @@ -6,7 +6,6 @@ import { getResources, updateManifestFiles } from '../utilities/manifestUpdateUtils' -import {routeBlueGreen} from '../strategyHelpers/blueGreen/blueGreenHelper' import { annotateAndLabelResources, checkManifestStability, @@ -14,17 +13,15 @@ import { } from '../strategyHelpers/deploymentHelper' import {DeploymentStrategy} from '../types/deploymentStrategy' import {parseTrafficSplitMethod} from '../types/trafficSplitMethod' -import {parseRouteStrategy} from '../types/routeStrategy' export async function deploy( kubectl: Kubectl, manifestFilePaths: string[], - deploymentStrategy: DeploymentStrategy, - annotations: {[key: string]: string} = {} + deploymentStrategy: DeploymentStrategy ) { // update manifests const inputManifestFiles: string[] = updateManifestFiles(manifestFilePaths) - core.debug('Input manifest files: ' + inputManifestFiles) + core.debug(`Input manifest files: ${inputManifestFiles}`) // deploy manifests core.startGroup('Deploying manifests') @@ -35,11 +32,10 @@ export async function deploy( inputManifestFiles, deploymentStrategy, kubectl, - trafficSplitMethod, - annotations + trafficSplitMethod ) + core.debug(`Deployed manifest files: ${deployedManifestFiles}`) core.endGroup() - core.debug('Deployed manifest files: ' + deployedManifestFiles) // check manifest stability core.startGroup('Checking manifest stability') @@ -52,15 +48,6 @@ export async function deploy( await checkManifestStability(kubectl, resourceTypes) core.endGroup() - if (deploymentStrategy == DeploymentStrategy.BLUE_GREEN) { - core.startGroup('Routing blue green') - const routeStrategy = parseRouteStrategy( - core.getInput('route-method', {required: true}) - ) - await routeBlueGreen(kubectl, inputManifestFiles, routeStrategy) - core.endGroup() - } - // print ingresses core.startGroup('Printing ingresses') const ingressResources: Resource[] = getResources(deployedManifestFiles, [ @@ -80,7 +67,7 @@ export async function deploy( try { allPods = JSON.parse((await kubectl.getAllPods()).stdout) } catch (e) { - core.debug('Unable to parse pods: ' + e) + core.debug(`Unable to parse pods: ${e}`) } await annotateAndLabelResources( deployedManifestFiles, diff --git a/src/actions/promote.ts b/src/actions/promote.ts index c567e4d77..39ba199e0 100644 --- a/src/actions/promote.ts +++ b/src/actions/promote.ts @@ -9,26 +9,26 @@ import { import * as models from '../types/kubernetesTypes' import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils' import { - BlueGreenManifests, - deleteWorkloadsAndServicesWithLabel, - deleteWorkloadsWithLabel, + deleteGreenObjects, getManifestObjects, - GREEN_LABEL_VALUE, NONE_LABEL_VALUE } from '../strategyHelpers/blueGreen/blueGreenHelper' -import { - promoteBlueGreenService, - routeBlueGreenService -} from '../strategyHelpers/blueGreen/serviceBlueGreenHelper' + +import {BlueGreenManifests} from '../types/blueGreenTypes' + import { promoteBlueGreenIngress, - routeBlueGreenIngress -} from '../strategyHelpers/blueGreen/ingressBlueGreenHelper' + promoteBlueGreenService, + promoteBlueGreenSMI +} from '../strategyHelpers/blueGreen/promote' + import { - cleanupSMI, - promoteBlueGreenSMI, + routeBlueGreenService, + routeBlueGreenIngressUnchanged, routeBlueGreenSMI -} from '../strategyHelpers/blueGreen/smiBlueGreenHelper' +} from '../strategyHelpers/blueGreen/route' + +import {cleanupSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper' import {Kubectl, Resource} from '../types/kubectl' import {DeploymentStrategy} from '../types/deploymentStrategy' import { @@ -40,15 +40,14 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy' export async function promote( kubectl: Kubectl, manifests: string[], - deploymentStrategy: DeploymentStrategy, - annotations: {[key: string]: string} = {} + deploymentStrategy: DeploymentStrategy ) { switch (deploymentStrategy) { case DeploymentStrategy.CANARY: await promoteCanary(kubectl, manifests) break case DeploymentStrategy.BLUE_GREEN: - await promoteBlueGreen(kubectl, manifests, annotations) + await promoteBlueGreen(kubectl, manifests) break default: throw Error('Invalid promote deployment strategy') @@ -98,18 +97,13 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) { ) } catch (ex) { core.warning( - 'Exception occurred while deleting canary and baseline workloads: ' + - ex + `Exception occurred while deleting canary and baseline workloads: ${ex}` ) } core.endGroup() } -async function promoteBlueGreen( - kubectl: Kubectl, - manifests: string[], - annotations: {[key: string]: string} = {} -) { +async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) { // update container images and pull secrets const inputManifestFiles: string[] = updateManifestFiles(manifests) const manifestObjects: BlueGreenManifests = @@ -119,20 +113,24 @@ async function promoteBlueGreen( core.getInput('route-method', {required: true}) ) - core.startGroup('Deleting old deployment and making new one') - let result - if (routeStrategy == RouteStrategy.INGRESS) { - result = await promoteBlueGreenIngress(kubectl, manifestObjects) - } else if (routeStrategy == RouteStrategy.SMI) { - result = await promoteBlueGreenSMI(kubectl, manifestObjects) - } else { - result = await promoteBlueGreenService(kubectl, manifestObjects) - } + core.startGroup('Deleting old deployment and making new stable deployment') + + const {deployResult} = await (async () => { + switch (routeStrategy) { + case RouteStrategy.INGRESS: + return await promoteBlueGreenIngress(kubectl, manifestObjects) + case RouteStrategy.SMI: + return await promoteBlueGreenSMI(kubectl, manifestObjects) + default: + return await promoteBlueGreenService(kubectl, manifestObjects) + } + })() + core.endGroup() // checking stability of newly created deployments core.startGroup('Checking manifest stability') - const deployedManifestFiles = result.newFilePaths + const deployedManifestFiles = deployResult.manifestFiles const resources: Resource[] = getResources( deployedManifestFiles, models.DEPLOYMENT_TYPES.concat([ @@ -146,30 +144,26 @@ async function promoteBlueGreen( 'Routing to new deployments and deleting old workloads and services' ) if (routeStrategy == RouteStrategy.INGRESS) { - await routeBlueGreenIngress( + await routeBlueGreenIngressUnchanged( kubectl, - null, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList ) - await deleteWorkloadsAndServicesWithLabel( + + await deleteGreenObjects( kubectl, - GREEN_LABEL_VALUE, - manifestObjects.deploymentEntityList, - manifestObjects.serviceEntityList + [].concat( + manifestObjects.deploymentEntityList, + manifestObjects.serviceEntityList + ) ) } else if (routeStrategy == RouteStrategy.SMI) { await routeBlueGreenSMI( kubectl, NONE_LABEL_VALUE, - manifestObjects.serviceEntityList, - annotations - ) - await deleteWorkloadsWithLabel( - kubectl, - GREEN_LABEL_VALUE, - manifestObjects.deploymentEntityList + manifestObjects.serviceEntityList ) + await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList) await cleanupSMI(kubectl, manifestObjects.serviceEntityList) } else { await routeBlueGreenService( @@ -177,11 +171,7 @@ async function promoteBlueGreen( NONE_LABEL_VALUE, manifestObjects.serviceEntityList ) - await deleteWorkloadsWithLabel( - kubectl, - GREEN_LABEL_VALUE, - manifestObjects.deploymentEntityList - ) + await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList) } core.endGroup() } diff --git a/src/actions/reject.ts b/src/actions/reject.ts index bbcb4772c..0da91c74d 100644 --- a/src/actions/reject.ts +++ b/src/actions/reject.ts @@ -2,9 +2,13 @@ import * as core from '@actions/core' import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper' import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper' import {Kubectl} from '../types/kubectl' -import {rejectBlueGreenService} from '../strategyHelpers/blueGreen/serviceBlueGreenHelper' -import {rejectBlueGreenIngress} from '../strategyHelpers/blueGreen/ingressBlueGreenHelper' -import {rejectBlueGreenSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper' +import {BlueGreenManifests} from '../types/blueGreenTypes' +import { + rejectBlueGreenIngress, + rejectBlueGreenService, + rejectBlueGreenSMI +} from '../strategyHelpers/blueGreen/reject' +import {getManifestObjects} from '../strategyHelpers/blueGreen/blueGreenHelper' import {DeploymentStrategy} from '../types/deploymentStrategy' import { parseTrafficSplitMethod, @@ -15,15 +19,14 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy' export async function reject( kubectl: Kubectl, manifests: string[], - deploymentStrategy: DeploymentStrategy, - annotations: {[key: string]: string} = {} + deploymentStrategy: DeploymentStrategy ) { switch (deploymentStrategy) { case DeploymentStrategy.CANARY: await rejectCanary(kubectl, manifests) break case DeploymentStrategy.BLUE_GREEN: - await rejectBlueGreen(kubectl, manifests, annotations) + await rejectBlueGreen(kubectl, manifests) break default: throw 'Invalid delete deployment strategy' @@ -55,22 +58,20 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) { core.endGroup() } -async function rejectBlueGreen( - kubectl: Kubectl, - manifests: string[], - annotations: {[key: string]: string} = {} -) { - core.startGroup('Rejecting deployment with blue green strategy') - +async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) { const routeStrategy = parseRouteStrategy( core.getInput('route-method', {required: true}) ) + core.startGroup('Rejecting deployment with blue green strategy') + core.info(`using routeMethod ${routeStrategy}`) + const manifestObjects: BlueGreenManifests = getManifestObjects(manifests) + if (routeStrategy == RouteStrategy.INGRESS) { - await rejectBlueGreenIngress(kubectl, manifests) + await rejectBlueGreenIngress(kubectl, manifestObjects) } else if (routeStrategy == RouteStrategy.SMI) { - await rejectBlueGreenSMI(kubectl, manifests, annotations) + await rejectBlueGreenSMI(kubectl, manifestObjects) } else { - await rejectBlueGreenService(kubectl, manifests) + await rejectBlueGreenService(kubectl, manifestObjects) } core.endGroup() } diff --git a/src/inputUtils.ts b/src/inputUtils.ts new file mode 100644 index 000000000..01b10c5aa --- /dev/null +++ b/src/inputUtils.ts @@ -0,0 +1,16 @@ +import * as core from '@actions/core' +import {parseAnnotations} from './types/annotations' + +export const inputAnnotations = parseAnnotations( + core.getInput('annotations', {required: false}) +) + +export function getBufferTime(): number { + const inputBufferTime = parseInt( + core.getInput('version-switch-buffer') || '0' + ) + if (inputBufferTime < 0 || inputBufferTime > 300) + throw Error('Version switch buffer must be between 0 and 300 (inclusive)') + + return inputBufferTime +} diff --git a/src/run.ts b/src/run.ts index 765314a6c..525390470 100644 --- a/src/run.ts +++ b/src/run.ts @@ -7,7 +7,6 @@ import {Action, parseAction} from './types/action' import {parseDeploymentStrategy} from './types/deploymentStrategy' import {getFilesFromDirectories} from './utilities/fileUtils' import {PrivateKubectl} from './types/privatekubectl' -import {parseAnnotations} from './types/annotations' export async function run() { // verify kubeconfig is set @@ -20,9 +19,6 @@ export async function run() { const action: Action | undefined = parseAction( core.getInput('action', {required: true}) ) - const annotations = parseAnnotations( - core.getInput('annotations', {required: false}) - ) const strategy = parseDeploymentStrategy(core.getInput('strategy')) const manifestsInput = core.getInput('manifests', {required: true}) const manifestFilePaths = manifestsInput @@ -51,15 +47,15 @@ export async function run() { // run action switch (action) { case Action.DEPLOY: { - await deploy(kubectl, fullManifestFilePaths, strategy, annotations) + await deploy(kubectl, fullManifestFilePaths, strategy) break } case Action.PROMOTE: { - await promote(kubectl, fullManifestFilePaths, strategy, annotations) + await promote(kubectl, fullManifestFilePaths, strategy) break } case Action.REJECT: { - await reject(kubectl, fullManifestFilePaths, strategy, annotations) + await reject(kubectl, fullManifestFilePaths, strategy) break } default: { diff --git a/src/strategyHelpers/blueGreen/blueGreenHelper.test.ts b/src/strategyHelpers/blueGreen/blueGreenHelper.test.ts index 26a094eba..6b063ba40 100644 --- a/src/strategyHelpers/blueGreen/blueGreenHelper.test.ts +++ b/src/strategyHelpers/blueGreen/blueGreenHelper.test.ts @@ -1,18 +1,25 @@ import { - createWorkloadsWithLabel, - deleteWorkloadsAndServicesWithLabel, + deployWithLabel, + deleteGreenObjects, + fetchResource, + getDeploymentMatchLabels, getManifestObjects, getNewBlueGreenObject, GREEN_LABEL_VALUE, - isServiceRouted, - NONE_LABEL_VALUE + isServiceRouted } from './blueGreenHelper' +import {BlueGreenDeployment} from '../../types/blueGreenTypes' import * as bgHelper from './blueGreenHelper' import {Kubectl} from '../../types/kubectl' import * as fileHelper from '../../utilities/fileUtils' +import {K8sObject} from '../../types/k8sObject' +import * as manifestUpdateUtils from '../../utilities/manifestUpdateUtils' +import {ExecOutput} from '@actions/exec' jest.mock('../../types/kubectl') +const kubectl = new Kubectl('') + describe('bluegreenhelper functions', () => { let testObjects beforeEach(() => { @@ -25,7 +32,29 @@ describe('bluegreenhelper functions', () => { .mockImplementationOnce(() => ['']) }) - test('it should parse objects correctly from one file', () => { + test('correctly deletes services and workloads according to label', async () => { + jest.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise) + + const value = await deleteGreenObjects( + kubectl, + [].concat( + testObjects.deploymentEntityList, + testObjects.serviceEntityList + ) + ) + + expect(value).toHaveLength(2) + expect(value).toContainEqual({ + name: 'nginx-service-green', + kind: 'Service' + }) + expect(value).toContainEqual({ + name: 'nginx-deployment-green', + kind: 'Deployment' + }) + }) + + test('parses objects correctly from one file (getManifestObjects)', () => { expect(testObjects.deploymentEntityList[0].kind).toBe('Deployment') expect(testObjects.serviceEntityList[0].kind).toBe('Service') expect(testObjects.ingressEntityList[0].kind).toBe('Ingress') @@ -35,40 +64,16 @@ describe('bluegreenhelper functions', () => { ).toBe('nginx') }) - test('correctly makes new blue green object', () => { - const modifiedDeployment = getNewBlueGreenObject( - testObjects.deploymentEntityList[0], - GREEN_LABEL_VALUE - ) - //@ts-ignore - expect(modifiedDeployment.metadata.name).toBe('nginx-deployment-green') - //@ts-ignore - expect(modifiedDeployment.metadata.labels['k8s.deploy.color']).toBe( - 'green' - ) - - const modifiedSvc = getNewBlueGreenObject( - testObjects.serviceEntityList[0], - GREEN_LABEL_VALUE - ) - //@ts-ignore - expect(modifiedSvc.metadata.name).toBe('nginx-service-green') - //@ts-ignore - expect(modifiedSvc.metadata.labels['k8s.deploy.color']).toBe('green') - }) - - test('correctly makes labeled workloads', () => { - const kubectl = new Kubectl('') - expect(Kubectl).toBeCalledTimes(1) - const cwlResult = createWorkloadsWithLabel( - kubectl, - testObjects.deploymentEntityList, - GREEN_LABEL_VALUE + test('parses other kinds of objects (getManifestObjects)', () => { + const otherObjectsCollection = getManifestObjects([ + 'test/unit/manifests/anomaly-objects-test.yml' + ]) + expect( + otherObjectsCollection.unroutedServiceEntityList[0].metadata.name + ).toBe('unrouted-service') + expect(otherObjectsCollection.otherObjects[0].metadata.name).toBe( + 'foobar-rollout' ) - cwlResult.then((value) => { - //@ts-ignore - expect(value.newFilePaths[0]).toBe('') - }) }) test('correctly classifies routed services', () => { @@ -87,42 +92,105 @@ describe('bluegreenhelper functions', () => { ).toBe(false) }) - test('correctly deletes services and workloads according to label', () => { - const kubectl = new Kubectl('') - jest.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise) - - let objectsToDelete = deleteWorkloadsAndServicesWithLabel( + test('correctly makes labeled workloads', async () => { + const cwlResult: BlueGreenDeployment = await deployWithLabel( kubectl, - NONE_LABEL_VALUE, testObjects.deploymentEntityList, - testObjects.serviceEntityList + GREEN_LABEL_VALUE ) - objectsToDelete.then((value) => { - expect(value).toHaveLength(2) - expect(value).toContainEqual - ;({name: 'nginx-service', kind: 'Service'}) - expect(value).toContainEqual({ - name: 'nginx-deployment', - kind: 'Deployment' - }) - }) + expect(cwlResult.deployResult.manifestFiles[0]).toBe('') + }) + + test('correctly makes new blue green object (getNewBlueGreenObject and addBlueGreenLabelsAndAnnotations)', () => { + const modifiedDeployment = getNewBlueGreenObject( + testObjects.deploymentEntityList[0], + GREEN_LABEL_VALUE + ) + + expect(modifiedDeployment.metadata.name).toBe('nginx-deployment-green') + expect(modifiedDeployment.metadata.labels['k8s.deploy.color']).toBe( + 'green' + ) + + const modifiedSvc = getNewBlueGreenObject( + testObjects.serviceEntityList[0], + GREEN_LABEL_VALUE + ) + + expect(modifiedSvc.metadata.name).toBe('nginx-service-green') + expect(modifiedSvc.metadata.labels['k8s.deploy.color']).toBe('green') + }) - objectsToDelete = deleteWorkloadsAndServicesWithLabel( + test('correctly fetches k8s objects', async () => { + const mockExecOutput = { + stderr: '', + stdout: JSON.stringify(testObjects.deploymentEntityList[0]), + exitCode: 0 + } + + jest + .spyOn(kubectl, 'getResource') + .mockImplementation(() => Promise.resolve(mockExecOutput)) + const fetched = await fetchResource( kubectl, - GREEN_LABEL_VALUE, - testObjects.deploymentEntityList, - testObjects.serviceEntityList + 'nginx-deployment', + 'Deployment' ) - objectsToDelete.then((value) => { - expect(value).toHaveLength(2) - expect(value).toContainEqual({ - name: 'nginx-service-green', - kind: 'Service' - }) - expect(value).toContainEqual({ - name: 'nginx-deployment-green', - kind: 'Deployment' + expect(fetched.metadata.name).toBe('nginx-deployment') + }) + + test('exits when fails to fetch k8s objects', async () => { + const mockExecOutput = { + stdout: 'this should not matter', + exitCode: 0, + stderr: 'this is a fake error' + } as ExecOutput + jest + .spyOn(kubectl, 'getResource') + .mockImplementation(() => Promise.resolve(mockExecOutput)) + let fetched = await fetchResource( + kubectl, + 'nginx-deployment', + 'Deployment' + ) + expect(fetched).toBe(null) + + jest.spyOn(kubectl, 'getResource').mockImplementation() + fetched = await fetchResource(kubectl, 'nginx-deployment', 'Deployment') + expect(fetched).toBe(null) + }) + + test('returns null when fetch fails to unset k8s objects', async () => { + const mockExecOutput = { + stdout: 'this should not matter', + exitCode: 0, + stderr: 'this is a fake error' + } as ExecOutput + jest + .spyOn(manifestUpdateUtils, 'UnsetClusterSpecificDetails') + .mockImplementation(() => { + throw new Error('test error') }) - }) + expect( + await fetchResource(kubectl, 'nginx-deployment', 'Deployment') + ).toBe(null) + }) + + test('gets deployment labels', () => { + const mockLabels = new Map() + mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE + const mockPodObject: K8sObject = { + kind: 'Pod', + metadata: {name: 'testPod', labels: mockLabels}, + spec: {} + } + expect( + getDeploymentMatchLabels(mockPodObject)[ + bgHelper.BLUE_GREEN_VERSION_LABEL + ] + ).toBe(GREEN_LABEL_VALUE) + expect( + getDeploymentMatchLabels(testObjects.deploymentEntityList[0])['app'] + ).toBe('nginx') }) }) diff --git a/src/strategyHelpers/blueGreen/blueGreenHelper.ts b/src/strategyHelpers/blueGreen/blueGreenHelper.ts index 8f60cfd14..ac4543637 100644 --- a/src/strategyHelpers/blueGreen/blueGreenHelper.ts +++ b/src/strategyHelpers/blueGreen/blueGreenHelper.ts @@ -1,6 +1,9 @@ import * as core from '@actions/core' import * as fs from 'fs' import * as yaml from 'js-yaml' + +import {DeployResult} from '../../types/deployResult' +import {K8sObject, K8sDeleteObject} from '../../types/k8sObject' import {Kubectl} from '../../types/kubectl' import { isDeploymentEntity, @@ -8,19 +11,18 @@ import { isServiceEntity, KubernetesWorkload } from '../../types/kubernetesTypes' +import { + BlueGreenDeployment, + BlueGreenManifests +} from '../../types/blueGreenTypes' import * as fileHelper from '../../utilities/fileUtils' -import {routeBlueGreenService} from './serviceBlueGreenHelper' -import {routeBlueGreenIngress} from './ingressBlueGreenHelper' -import {routeBlueGreenSMI} from './smiBlueGreenHelper' +import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils' +import {checkForErrors} from '../../utilities/kubectlUtils' import { UnsetClusterSpecificDetails, updateObjectLabels, updateSelectorLabels } from '../../utilities/manifestUpdateUtils' -import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils' -import {checkForErrors} from '../../utilities/kubectlUtils' -import {sleep} from '../../utilities/timeUtils' -import {RouteStrategy} from '../../types/routeStrategy' export const GREEN_LABEL_VALUE = 'green' export const NONE_LABEL_VALUE = 'None' @@ -28,144 +30,46 @@ export const BLUE_GREEN_VERSION_LABEL = 'k8s.deploy.color' export const GREEN_SUFFIX = '-green' export const STABLE_SUFFIX = '-stable' -export interface BlueGreenManifests { - serviceEntityList: any[] - serviceNameMap: Map - unroutedServiceEntityList: any[] - deploymentEntityList: any[] - ingressEntityList: any[] - otherObjects: any[] -} - -export async function routeBlueGreen( +export async function deleteGreenObjects( kubectl: Kubectl, - inputManifestFiles: string[], - routeStrategy: RouteStrategy, - annotations: {[key: string]: string} = {} -) { - // sleep for buffer time - const bufferTime: number = parseInt( - core.getInput('version-switch-buffer') || '0' - ) - if (bufferTime < 0 || bufferTime > 300) - throw Error('Version switch buffer must be between 0 and 300 (inclusive)') - const startSleepDate = new Date() - core.info( - `Starting buffer time of ${bufferTime} minute(s) at ${startSleepDate.toISOString()}` - ) - await sleep(bufferTime * 1000 * 60) - const endSleepDate = new Date() - core.info( - `Stopping buffer time of ${bufferTime} minute(s) at ${endSleepDate.toISOString()}` - ) - - const manifestObjects: BlueGreenManifests = - getManifestObjects(inputManifestFiles) - core.debug('Manifest objects: ' + JSON.stringify(manifestObjects)) - - // route to new deployments - if (routeStrategy == RouteStrategy.INGRESS) { - await routeBlueGreenIngress( - kubectl, - GREEN_LABEL_VALUE, - manifestObjects.serviceNameMap, - manifestObjects.ingressEntityList - ) - } else if (routeStrategy == RouteStrategy.SMI) { - await routeBlueGreenSMI( - kubectl, - GREEN_LABEL_VALUE, - manifestObjects.serviceEntityList, - annotations - ) - } else { - await routeBlueGreenService( - kubectl, - GREEN_LABEL_VALUE, - manifestObjects.serviceEntityList - ) - } -} - -export async function deleteWorkloadsWithLabel( - kubectl: Kubectl, - deleteLabel: string, - deploymentEntityList: any[] -) { - const resourcesToDelete = [] - deploymentEntityList.forEach((inputObject) => { - const name = inputObject.metadata.name - const kind = inputObject.kind - - if (deleteLabel === NONE_LABEL_VALUE) { - // delete stable deployments - const resourceToDelete = {name, kind} - resourcesToDelete.push(resourceToDelete) - } else { - // delete new green deployments - const resourceToDelete = { - name: getBlueGreenResourceName(name, GREEN_SUFFIX), - kind: kind - } - resourcesToDelete.push(resourceToDelete) + toDelete: K8sObject[] +): Promise { + // const resourcesToDelete: K8sDeleteObject[] = [] + const resourcesToDelete: K8sDeleteObject[] = toDelete.map((obj) => { + return { + name: getBlueGreenResourceName(obj.metadata.name, GREEN_SUFFIX), + kind: obj.kind } }) + core.debug(`deleting green objects: ${JSON.stringify(resourcesToDelete)}`) + await deleteObjects(kubectl, resourcesToDelete) return resourcesToDelete } -export async function deleteWorkloadsAndServicesWithLabel( +export async function deleteObjects( kubectl: Kubectl, - deleteLabel: string, - deploymentEntityList: any[], - serviceEntityList: any[] + deleteList: K8sDeleteObject[] ) { - // need to delete services and deployments - const deletionEntitiesList = deploymentEntityList.concat(serviceEntityList) - const resourcesToDelete = [] - - deletionEntitiesList.forEach((inputObject) => { - const name = inputObject.metadata.name - const kind = inputObject.kind - - if (deleteLabel === NONE_LABEL_VALUE) { - // delete stable objects - const resourceToDelete = {name, kind} - resourcesToDelete.push(resourceToDelete) - } else { - // delete green labels - const resourceToDelete = { - name: getBlueGreenResourceName(name, GREEN_SUFFIX), - kind: kind - } - resourcesToDelete.push(resourceToDelete) - } - }) - - await deleteObjects(kubectl, resourcesToDelete) - return resourcesToDelete -} - -export async function deleteObjects(kubectl: Kubectl, deleteList: any[]) { // delete services and deployments for (const delObject of deleteList) { try { const result = await kubectl.delete([delObject.kind, delObject.name]) checkForErrors([result]) } catch (ex) { - // Ignore failures of delete if it doesn't exist + core.debug(`failed to delete object ${delObject.name}: ${ex}`) } } } // other common functions export function getManifestObjects(filePaths: string[]): BlueGreenManifests { - const deploymentEntityList = [] - const routedServiceEntityList = [] - const unroutedServiceEntityList = [] - const ingressEntityList = [] - const otherEntitiesList = [] + const deploymentEntityList: K8sObject[] = [] + const routedServiceEntityList: K8sObject[] = [] + const unroutedServiceEntityList: K8sObject[] = [] + const ingressEntityList: K8sObject[] = [] + const otherEntitiesList: K8sObject[] = [] const serviceNameMap = new Map() filePaths.forEach((filePath: string) => { @@ -210,48 +114,41 @@ export function isServiceRouted( serviceObject: any[], deploymentEntityList: any[] ): boolean { - let shouldBeRouted: boolean = false const serviceSelector: any = getServiceSelector(serviceObject) - if (serviceSelector) { - if ( - deploymentEntityList.some((depObject) => { - // finding if there is a deployment in the given manifests the service targets - const matchLabels: any = getDeploymentMatchLabels(depObject) - return ( - matchLabels && - isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels) - ) - }) - ) { - shouldBeRouted = true - } - } - return shouldBeRouted + return ( + serviceSelector && + deploymentEntityList.some((depObject) => { + // finding if there is a deployment in the given manifests the service targets + const matchLabels: any = getDeploymentMatchLabels(depObject) + return ( + matchLabels && + isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels) + ) + }) + ) } -export async function createWorkloadsWithLabel( +export async function deployWithLabel( kubectl: Kubectl, deploymentObjectList: any[], nextLabel: string -) { - const newObjectsList = [] - deploymentObjectList.forEach((inputObject) => { - // creating deployment with label - const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel) - newObjectsList.push(newBlueGreenObject) - }) - - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) - const result = await kubectl.apply(manifestFiles) +): Promise { + const newObjectsList = deploymentObjectList.map((inputObject) => + getNewBlueGreenObject(inputObject, nextLabel) + ) - return {result: result, newFilePaths: manifestFiles} + core.debug( + `objects deployed with label are ${JSON.stringify(newObjectsList)}` + ) + const deployResult = await deployObjects(kubectl, newObjectsList) + return {deployResult, objects: newObjectsList} } export function getNewBlueGreenObject( inputObject: any, labelValue: string -): object { +): K8sObject { const newObject = JSON.parse(JSON.stringify(inputObject)) // Updating name only if label is green label is given @@ -338,14 +235,14 @@ export async function fetchResource( kubectl: Kubectl, kind: string, name: string -) { +): Promise { const result = await kubectl.getResource(kind, name) if (result == null || !!result.stderr) { return null } if (!!result.stdout) { - const resource = JSON.parse(result.stdout) + const resource = JSON.parse(result.stdout) as K8sObject try { UnsetClusterSpecificDetails(resource) @@ -357,3 +254,13 @@ export async function fetchResource( } } } + +export async function deployObjects( + kubectl: Kubectl, + objectsList: any[] +): Promise { + const manifestFiles = fileHelper.writeObjectsToFile(objectsList) + const execResult = await kubectl.apply(manifestFiles) + + return {execResult, manifestFiles} +} diff --git a/src/strategyHelpers/blueGreen/deploy.test.ts b/src/strategyHelpers/blueGreen/deploy.test.ts new file mode 100644 index 000000000..769bcb5ab --- /dev/null +++ b/src/strategyHelpers/blueGreen/deploy.test.ts @@ -0,0 +1,75 @@ +import {getManifestObjects} from './blueGreenHelper' +import {BlueGreenDeployment} from '../../types/blueGreenTypes' +import {deployBlueGreen, deployBlueGreenIngress} from './deploy' +import * as routeTester from './route' +import {Kubectl} from '../../types/kubectl' +import {RouteStrategy} from '../../types/routeStrategy' +import * as TSutils from '../../utilities/trafficSplitUtils' + +const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml'] + +jest.mock('../../types/kubectl') + +describe('deploy tests', () => { + let testObjects + beforeEach(() => { + //@ts-ignore + Kubectl.mockClear() + testObjects = getManifestObjects(ingressFilepath) + }) + + test('correctly determines deploy type and acts accordingly', async () => { + const kubectl = new Kubectl('') + const mockBgDeployment: BlueGreenDeployment = { + deployResult: { + execResult: {exitCode: 0, stderr: '', stdout: ''}, + manifestFiles: [] + }, + objects: [] + } + + jest + .spyOn(routeTester, 'routeBlueGreenForDeploy') + .mockImplementation(() => Promise.resolve(mockBgDeployment)) + jest + .spyOn(TSutils, 'getTrafficSplitAPIVersion') + .mockImplementation(() => Promise.resolve('v1alpha3')) + + const ingressResult = await deployBlueGreen( + kubectl, + ingressFilepath, + RouteStrategy.INGRESS + ) + + expect(ingressResult.objects.length).toBe(2) + + const result = await deployBlueGreen( + kubectl, + ingressFilepath, + RouteStrategy.SERVICE + ) + + expect(result.objects.length).toBe(2) + + const smiResult = await deployBlueGreen( + kubectl, + ingressFilepath, + RouteStrategy.SMI + ) + + expect(smiResult.objects.length).toBe(3) + }) + + test('correctly deploys blue/green ingress', async () => { + const kc = new Kubectl('') + const value = await deployBlueGreenIngress(kc, ingressFilepath) + const nol = value.objects.map((obj) => { + if (obj.kind === 'Service') { + expect(obj.metadata.name).toBe('nginx-service-green') + } + if (obj.kind === 'Deployment') { + expect(obj.metadata.name).toBe('nginx-deployment-green') + } + }) + }) +}) diff --git a/src/strategyHelpers/blueGreen/deploy.ts b/src/strategyHelpers/blueGreen/deploy.ts new file mode 100644 index 000000000..bee08a770 --- /dev/null +++ b/src/strategyHelpers/blueGreen/deploy.ts @@ -0,0 +1,136 @@ +import * as core from '@actions/core' + +import {Kubectl} from '../../types/kubectl' +import { + BlueGreenDeployment, + BlueGreenManifests +} from '../../types/blueGreenTypes' + +import {RouteStrategy} from '../../types/routeStrategy' + +import { + deployWithLabel, + getManifestObjects, + GREEN_LABEL_VALUE, + deployObjects +} from './blueGreenHelper' +import {setupSMI} from './smiBlueGreenHelper' + +import {routeBlueGreenForDeploy} from './route' + +export async function deployBlueGreen( + kubectl: Kubectl, + files: string[], + routeStrategy: RouteStrategy +): Promise { + const blueGreenDeployment = await (async () => { + switch (routeStrategy) { + case RouteStrategy.INGRESS: + return await deployBlueGreenIngress(kubectl, files) + case RouteStrategy.SMI: + return await deployBlueGreenSMI(kubectl, files) + default: + return await deployBlueGreenService(kubectl, files) + } + })() + + core.startGroup('Routing blue green') + await routeBlueGreenForDeploy(kubectl, files, routeStrategy) + core.endGroup() + + return blueGreenDeployment +} + +export async function deployBlueGreenSMI( + kubectl: Kubectl, + filePaths: string[] +): Promise { + // get all kubernetes objects defined in manifest files + const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) + + // create services and other objects + const newObjectsList = [].concat( + manifestObjects.otherObjects, + manifestObjects.serviceEntityList, + manifestObjects.ingressEntityList, + manifestObjects.unroutedServiceEntityList + ) + + await deployObjects(kubectl, newObjectsList) + + // make extraservices and trafficsplit + await setupSMI(kubectl, manifestObjects.serviceEntityList) + + // create new deloyments + const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel( + kubectl, + manifestObjects.deploymentEntityList, + GREEN_LABEL_VALUE + ) + return { + deployResult: blueGreenDeployment.deployResult, + objects: [].concat(blueGreenDeployment.objects, newObjectsList) + } +} + +export async function deployBlueGreenIngress( + kubectl: Kubectl, + filePaths: string[] +): Promise { + // get all kubernetes objects defined in manifest files + const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) + + // create deployments with green label value + const servicesAndDeployments = [].concat( + manifestObjects.deploymentEntityList, + manifestObjects.serviceEntityList + ) + const workloadDeployment: BlueGreenDeployment = await deployWithLabel( + kubectl, + servicesAndDeployments, + GREEN_LABEL_VALUE + ) + + const otherObjects = [].concat( + manifestObjects.otherObjects, + manifestObjects.unroutedServiceEntityList + ) + await deployObjects(kubectl, otherObjects) + core.debug( + `new objects after processing services and other objects: \n + ${JSON.stringify(servicesAndDeployments)}` + ) + + return { + deployResult: workloadDeployment.deployResult, + objects: [].concat(workloadDeployment.objects, otherObjects) + } +} + +export async function deployBlueGreenService( + kubectl: Kubectl, + filePaths: string[] +): Promise { + const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) + + // create deployments with green label value + const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel( + kubectl, + manifestObjects.deploymentEntityList, + GREEN_LABEL_VALUE + ) + + // create other non deployment and non service entities + const newObjectsList = [].concat( + manifestObjects.otherObjects, + manifestObjects.ingressEntityList, + manifestObjects.unroutedServiceEntityList + ) + + await deployObjects(kubectl, newObjectsList) + // returning deployment details to check for rollout stability + return { + deployResult: blueGreenDeployment.deployResult, + objects: [].concat(blueGreenDeployment.objects, newObjectsList) + } +} diff --git a/src/strategyHelpers/blueGreen/ingressBlueGreenHelper.test.ts b/src/strategyHelpers/blueGreen/ingressBlueGreenHelper.test.ts index 0994ab2df..9f654d8f5 100644 --- a/src/strategyHelpers/blueGreen/ingressBlueGreenHelper.test.ts +++ b/src/strategyHelpers/blueGreen/ingressBlueGreenHelper.test.ts @@ -1,20 +1,20 @@ import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper' +import * as bgHelper from './blueGreenHelper' import { - deployBlueGreenIngress, getUpdatedBlueGreenIngress, isIngressRouted, - routeBlueGreenIngress + validateIngresses } from './ingressBlueGreenHelper' import {Kubectl} from '../../types/kubectl' import * as fileHelper from '../../utilities/fileUtils' +const betaFilepath = ['test/unit/manifests/test-ingress.yml'] +const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml'] +const kubectl = new Kubectl('') jest.mock('../../types/kubectl') describe('ingress blue green helpers', () => { let testObjects - const betaFilepath = ['test/unit/manifests/test-ingress.yml'] - const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml'] - beforeEach(() => { //@ts-ignore Kubectl.mockClear() @@ -38,7 +38,6 @@ describe('ingress blue green helpers', () => { testObjects.serviceNameMap ) ).toBe(false) - expect( isIngressRouted( getManifestObjects(betaFilepath).ingressEntityList[0], @@ -53,36 +52,72 @@ describe('ingress blue green helpers', () => { testObjects.serviceNameMap, GREEN_LABEL_VALUE ) - //@ts-ignore + expect(updatedIng.metadata.name).toBe('nginx-ingress') expect(updatedIng.metadata.labels['k8s.deploy.color']).toBe('green') - //@ts-ignore expect(updatedIng.spec.rules[0].http.paths[0].backend.service.name).toBe( 'nginx-service-green' ) - }) - test('correctly prepares blue/green ingresses for deployment', () => { - const kc = new Kubectl('') - const generatedObjects = routeBlueGreenIngress( - kc, - GREEN_LABEL_VALUE, - testObjects.serviceNameMap, - testObjects.ingressEntityList + const oldIngObjects = getManifestObjects(betaFilepath) + const oldIng = getUpdatedBlueGreenIngress( + oldIngObjects.ingressEntityList[0], + oldIngObjects.serviceNameMap, + GREEN_LABEL_VALUE + ) + expect(updatedIng.metadata.labels['k8s.deploy.color']).toBe('green') + expect(updatedIng.spec.rules[0].http.paths[0].backend.service.name).toBe( + 'nginx-service-green' ) - generatedObjects.then((value) => { - expect(value).toHaveLength(1) - //@ts-ignore - expect(value[0].metadata.name).toBe('nginx-ingress') - }) }) - test('correctly deploys services', () => { - const kc = new Kubectl('') - const result = deployBlueGreenIngress(kc, ingressFilepath) - result.then((value) => { - const nol = value.newObjectsList - //@ts-ignore - expect(nol[0].metadata.name).toBe('nginx-service-green') - }) + test('it should validate ingresses', async () => { + // what if nothing gets returned from fetchResource? + jest.spyOn(bgHelper, 'fetchResource').mockImplementation() + let validResponse = await validateIngresses( + kubectl, + testObjects.ingressEntityList, + testObjects.serviceNameMap + ) + expect(validResponse.areValid).toBe(false) + + // test valid ingress + let mockIngress = JSON.parse( + JSON.stringify(testObjects.ingressEntityList[0]) + ) + mockIngress.spec.rules[0].http.paths[0].backend.service.name = + 'nginx-service-green' + const mockLabels = new Map() + mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE + mockIngress.metadata.labels = mockLabels + jest + .spyOn(bgHelper, 'fetchResource') + .mockImplementation(() => Promise.resolve(mockIngress)) + validResponse = await validateIngresses( + kubectl, + testObjects.ingressEntityList, + testObjects.serviceNameMap + ) + expect(validResponse.areValid).toBe(true) + + // test invalid labels + mockIngress.metadata.labels[bgHelper.BLUE_GREEN_VERSION_LABEL] = + bgHelper.NONE_LABEL_VALUE + mockIngress.spec.rules[0].http.paths[0].backend.service.name = + 'nginx-service' + validResponse = await validateIngresses( + kubectl, + testObjects.ingressEntityList, + testObjects.serviceNameMap + ) + expect(validResponse.areValid).toBe(false) + + // test missing fields + mockIngress = {} + validResponse = await validateIngresses( + kubectl, + testObjects.ingressEntityList, + testObjects.serviceNameMap + ) + expect(validResponse.areValid).toBe(false) }) }) diff --git a/src/strategyHelpers/blueGreen/ingressBlueGreenHelper.ts b/src/strategyHelpers/blueGreen/ingressBlueGreenHelper.ts index 28685f917..17625d7dd 100644 --- a/src/strategyHelpers/blueGreen/ingressBlueGreenHelper.ts +++ b/src/strategyHelpers/blueGreen/ingressBlueGreenHelper.ts @@ -1,209 +1,20 @@ -import {Kubectl} from '../../types/kubectl' -import * as fileHelper from '../../utilities/fileUtils' +import * as core from '@actions/core' +import {K8sIngress} from '../../types/k8sObject' import { addBlueGreenLabelsAndAnnotations, BLUE_GREEN_VERSION_LABEL, - BlueGreenManifests, - createWorkloadsWithLabel, - deleteWorkloadsAndServicesWithLabel, - fetchResource, - getManifestObjects, - getNewBlueGreenObject, GREEN_LABEL_VALUE, - NONE_LABEL_VALUE + fetchResource } from './blueGreenHelper' -import * as core from '@actions/core' +import {Kubectl} from '../../types/kubectl' const BACKEND = 'backend' -export async function deployBlueGreenIngress( - kubectl: Kubectl, - filePaths: string[] -) { - // get all kubernetes objects defined in manifest files - const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) - - // create deployments with green label value - const workloadDeployment = await createWorkloadsWithLabel( - kubectl, - manifestObjects.deploymentEntityList, - GREEN_LABEL_VALUE - ) - - let newObjectsList = [] - manifestObjects.serviceEntityList.forEach((inputObject) => { - const newBlueGreenObject = getNewBlueGreenObject( - inputObject, - GREEN_LABEL_VALUE - ) - newObjectsList.push(newBlueGreenObject) - }) - newObjectsList = newObjectsList - .concat(manifestObjects.otherObjects) - .concat(manifestObjects.unroutedServiceEntityList) - - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) - await kubectl.apply(manifestFiles) - - core.debug( - 'new objects after processing services and other objects: \n' + - JSON.stringify(newObjectsList) - ) - - return {workloadDeployment, newObjectsList} -} - -export async function promoteBlueGreenIngress( - kubectl: Kubectl, - manifestObjects -) { - //checking if anything to promote - const {areValid, invalidIngresses} = validateIngresses( - kubectl, - manifestObjects.ingressEntityList, - manifestObjects.serviceNameMap - ) - if (!areValid) { - throw 'Ingresses are not in promote state' + invalidIngresses.toString() - } - - // create stable deployments with new configuration - const result = createWorkloadsWithLabel( - kubectl, - manifestObjects.deploymentEntityList, - NONE_LABEL_VALUE - ) - - // create stable services with new configuration - const newObjectsList = [] - manifestObjects.serviceEntityList.forEach((inputObject) => { - const newBlueGreenObject = getNewBlueGreenObject( - inputObject, - NONE_LABEL_VALUE - ) - newObjectsList.push(newBlueGreenObject) - }) - - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) - await kubectl.apply(manifestFiles) - - return result -} - -export async function rejectBlueGreenIngress( - kubectl: Kubectl, - filePaths: string[] -) { - // get all kubernetes objects defined in manifest files - const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) - - // route ingress to stables services - await routeBlueGreenIngress( - kubectl, - null, - manifestObjects.serviceNameMap, - manifestObjects.ingressEntityList - ) - - // delete green services and deployments - await deleteWorkloadsAndServicesWithLabel( - kubectl, - GREEN_LABEL_VALUE, - manifestObjects.deploymentEntityList, - manifestObjects.serviceEntityList - ) -} - -export async function routeBlueGreenIngress( - kubectl: Kubectl, - nextLabel: string, - serviceNameMap: Map, - ingressEntityList: any[] -) { - let newObjectsList = [] - - if (!nextLabel) { - newObjectsList = ingressEntityList.filter((ingress) => - isIngressRouted(ingress, serviceNameMap) - ) - } else { - ingressEntityList.forEach((inputObject) => { - if (isIngressRouted(inputObject, serviceNameMap)) { - const newBlueGreenIngressObject = getUpdatedBlueGreenIngress( - inputObject, - serviceNameMap, - GREEN_LABEL_VALUE - ) - newObjectsList.push(newBlueGreenIngressObject) - } else { - newObjectsList.push(inputObject) - } - }) - } - - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) - await kubectl.apply(manifestFiles) - return newObjectsList -} - -export function validateIngresses( - kubectl: Kubectl, - ingressEntityList: any[], - serviceNameMap: Map -): {areValid: boolean; invalidIngresses: string[]} { - let areValid: boolean = true - const invalidIngresses = [] - ingressEntityList.forEach(async (inputObject) => { - if (isIngressRouted(inputObject, serviceNameMap)) { - //querying existing ingress - const existingIngress = await fetchResource( - kubectl, - inputObject.kind, - inputObject.metadata.name - ) - - let isValid = - !!existingIngress && - existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL] === - GREEN_LABEL_VALUE - if (!isValid) { - invalidIngresses.push(inputObject.metadata.name) - } - // to be valid, ingress should exist and should be green - areValid = areValid && isValid - } - }) - - return {areValid, invalidIngresses} -} - -export function isIngressRouted( - ingressObject: any, - serviceNameMap: Map -): boolean { - let isIngressRouted: boolean = false - // check if ingress targets a service in the given manifests - JSON.parse(JSON.stringify(ingressObject), (key, value) => { - isIngressRouted = - isIngressRouted || (key === 'service' && value.hasOwnProperty('name')) - isIngressRouted = - isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value)) - - return value - }) - - return isIngressRouted -} - export function getUpdatedBlueGreenIngress( inputObject: any, serviceNameMap: Map, type: string -): object { - if (!type) { - return inputObject - } - +): K8sIngress { const newObject = JSON.parse(JSON.stringify(inputObject)) // add green labels and values addBlueGreenLabelsAndAnnotations(newObject, type) @@ -241,7 +52,7 @@ export function updateIngressBackend( inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => { if ( key.toLowerCase() === BACKEND && - serviceNameMap.has(value?.service?.name) + serviceNameMap.has(value.service.name) ) { value.service.name = serviceNameMap.get(value.service.name) } @@ -250,3 +61,60 @@ export function updateIngressBackend( return inputObject } + +export function isIngressRouted( + ingressObject: any, + serviceNameMap: Map +): boolean { + let isIngressRouted: boolean = false + // check if ingress targets a service in the given manifests + JSON.parse(JSON.stringify(ingressObject), (key, value) => { + isIngressRouted = + isIngressRouted || + (key === 'service' && + value.hasOwnProperty('name') && + serviceNameMap.has(value.name)) + isIngressRouted = + isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value)) + + return value + }) + + return isIngressRouted +} + +export async function validateIngresses( + kubectl: Kubectl, + ingressEntityList: any[], + serviceNameMap: Map +): Promise<{areValid: boolean; invalidIngresses: string[]}> { + let areValid: boolean = true + const invalidIngresses = [] + + for (const inputObject of ingressEntityList) { + if (isIngressRouted(inputObject, serviceNameMap)) { + //querying existing ingress + const existingIngress = await fetchResource( + kubectl, + inputObject.kind, + inputObject.metadata.name + ) + + const isValid = + !!existingIngress && + existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL] === + GREEN_LABEL_VALUE + if (!isValid) { + core.debug( + `Invalid ingress detected (must be in green state): ${JSON.stringify( + inputObject + )}` + ) + invalidIngresses.push(inputObject.metadata.name) + } + // to be valid, ingress should exist and should be green + areValid = areValid && isValid + } + } + return {areValid, invalidIngresses} +} diff --git a/src/strategyHelpers/blueGreen/promote.test.ts b/src/strategyHelpers/blueGreen/promote.test.ts new file mode 100644 index 000000000..ab32c206f --- /dev/null +++ b/src/strategyHelpers/blueGreen/promote.test.ts @@ -0,0 +1,158 @@ +import * as core from '@actions/core' +import {getManifestObjects} from './blueGreenHelper' +import { + promoteBlueGreenIngress, + promoteBlueGreenService, + promoteBlueGreenSMI +} from './promote' +import {TrafficSplitObject} from '../../types/k8sObject' +import * as servicesTester from './serviceBlueGreenHelper' +import {Kubectl} from '../../types/kubectl' +import {MAX_VAL, MIN_VAL, TRAFFIC_SPLIT_OBJECT} from './smiBlueGreenHelper' +import * as smiTester from './smiBlueGreenHelper' +import * as bgHelper from './blueGreenHelper' + +let testObjects +const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml'] +jest.mock('../../types/kubectl') +const kubectl = new Kubectl('') + +describe('promote tests', () => { + beforeEach(() => { + //@ts-ignore + Kubectl.mockClear() + testObjects = getManifestObjects(ingressFilepath) + }) + + test('promote blue/green ingress', async () => { + const mockLabels = new Map() + mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE + + jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() => + Promise.resolve({ + kind: 'Ingress', + spec: {}, + metadata: {labels: mockLabels, name: 'nginx-ingress-green'} + }) + ) + const value = await promoteBlueGreenIngress(kubectl, testObjects) + + const objects = value.objects + expect(objects).toHaveLength(2) + + for (const obj of objects) { + if (obj.kind === 'Service') { + expect(obj.metadata.name).toBe('nginx-service') + } else if (obj.kind == 'Deployment') { + expect(obj.metadata.name).toBe('nginx-deployment') + } + expect(obj.metadata.labels['k8s.deploy.color']).toBe('None') + } + }) + + test('fail to promote invalid blue/green ingress', async () => { + const mockLabels = new Map() + mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE + jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() => + Promise.resolve({ + kind: 'Ingress', + spec: {}, + metadata: {labels: mockLabels, name: 'nginx-ingress-green'} + }) + ) + + await expect( + promoteBlueGreenIngress(kubectl, testObjects) + ).rejects.toThrowError() + }) + + test('promote blue/green service', async () => { + const mockLabels = new Map() + mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE + jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() => + Promise.resolve({ + kind: 'Service', + spec: {selector: mockLabels}, + metadata: {labels: mockLabels, name: 'nginx-service-green'} + }) + ) + + let value = await promoteBlueGreenService(kubectl, testObjects) + + expect(value.objects).toHaveLength(1) + expect( + value.objects[0].metadata.labels[bgHelper.BLUE_GREEN_VERSION_LABEL] + ).toBe(bgHelper.NONE_LABEL_VALUE) + expect(value.objects[0].metadata.name).toBe('nginx-deployment') + }) + + test('fail to promote invalid blue/green service', async () => { + const mockLabels = new Map() + mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE + jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() => + Promise.resolve({ + kind: 'Service', + spec: {}, + metadata: {labels: mockLabels, name: 'nginx-ingress-green'} + }) + ) + jest + .spyOn(servicesTester, 'validateServicesState') + .mockImplementationOnce(() => Promise.resolve(false)) + + await expect( + promoteBlueGreenService(kubectl, testObjects) + ).rejects.toThrowError() + }) + + test('promote blue/green SMI', async () => { + const mockLabels = new Map() + mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE + + const mockTsObject: TrafficSplitObject = { + apiVersion: 'v1alpha3', + kind: TRAFFIC_SPLIT_OBJECT, + metadata: { + name: 'nginx-service-trafficsplit', + labels: new Map(), + annotations: new Map() + }, + spec: { + service: 'nginx-service', + backends: [ + { + service: 'nginx-service-stable', + weight: MIN_VAL + }, + { + service: 'nginx-service-green', + weight: MAX_VAL + } + ] + } + } + jest + .spyOn(bgHelper, 'fetchResource') + .mockImplementation(() => Promise.resolve(mockTsObject)) + + const deployResult = await promoteBlueGreenSMI(kubectl, testObjects) + + expect(deployResult.objects).toHaveLength(1) + expect(deployResult.objects[0].metadata.name).toBe('nginx-deployment') + expect( + deployResult.objects[0].metadata.labels[ + bgHelper.BLUE_GREEN_VERSION_LABEL + ] + ).toBe(bgHelper.NONE_LABEL_VALUE) + }) + + test('promote blue/green SMI with bad trafficsplit', async () => { + const mockLabels = new Map() + mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE + jest + .spyOn(smiTester, 'validateTrafficSplitsState') + .mockImplementation(() => Promise.resolve(false)) + + expect(promoteBlueGreenSMI(kubectl, testObjects)).rejects.toThrowError() + }) +}) diff --git a/src/strategyHelpers/blueGreen/promote.ts b/src/strategyHelpers/blueGreen/promote.ts new file mode 100644 index 000000000..857a4498b --- /dev/null +++ b/src/strategyHelpers/blueGreen/promote.ts @@ -0,0 +1,81 @@ +import * as core from '@actions/core' + +import {Kubectl} from '../../types/kubectl' + +import {BlueGreenDeployment} from '../../types/blueGreenTypes' +import {deployWithLabel, NONE_LABEL_VALUE} from './blueGreenHelper' + +import {validateIngresses} from './ingressBlueGreenHelper' +import {validateServicesState} from './serviceBlueGreenHelper' +import {validateTrafficSplitsState} from './smiBlueGreenHelper' + +export async function promoteBlueGreenIngress( + kubectl: Kubectl, + manifestObjects +): Promise { + //checking if anything to promote + const {areValid, invalidIngresses} = await validateIngresses( + kubectl, + manifestObjects.ingressEntityList, + manifestObjects.serviceNameMap + ) + if (!areValid) { + throw new Error( + `Ingresses are not in promote state: ${invalidIngresses.toString()}` + ) + } + + // create stable deployments with new configuration + const result: BlueGreenDeployment = await deployWithLabel( + kubectl, + [].concat( + manifestObjects.deploymentEntityList, + manifestObjects.serviceEntityList + ), + NONE_LABEL_VALUE + ) + + // create stable services with new configuration + return result +} + +export async function promoteBlueGreenService( + kubectl: Kubectl, + manifestObjects +): Promise { + // checking if services are in the right state ie. targeting green deployments + if ( + !(await validateServicesState(kubectl, manifestObjects.serviceEntityList)) + ) { + throw new Error('Found services not in promote state') + } + + // creating stable deployments with new configurations + return await deployWithLabel( + kubectl, + manifestObjects.deploymentEntityList, + NONE_LABEL_VALUE + ) +} + +export async function promoteBlueGreenSMI( + kubectl: Kubectl, + manifestObjects +): Promise { + // checking if there is something to promote + if ( + !(await validateTrafficSplitsState( + kubectl, + manifestObjects.serviceEntityList + )) + ) { + throw Error('Not in promote state SMI') + } + + // create stable deployments with new configuration + return await deployWithLabel( + kubectl, + manifestObjects.deploymentEntityList, + NONE_LABEL_VALUE + ) +} diff --git a/src/strategyHelpers/blueGreen/reject.test.ts b/src/strategyHelpers/blueGreen/reject.test.ts new file mode 100644 index 000000000..9c1cf2fac --- /dev/null +++ b/src/strategyHelpers/blueGreen/reject.test.ts @@ -0,0 +1,66 @@ +import {getManifestObjects} from './blueGreenHelper' +import {Kubectl} from '../../types/kubectl' +import {BlueGreenRejectResult} from '../../types/blueGreenTypes' + +import * as TSutils from '../../utilities/trafficSplitUtils' +import { + rejectBlueGreenIngress, + rejectBlueGreenService, + rejectBlueGreenSMI +} from './reject' + +const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml'] +const kubectl = new Kubectl('') + +jest.mock('../../types/kubectl') + +describe('reject tests', () => { + let testObjects + + beforeEach(() => { + //@ts-ignore + Kubectl.mockClear() + testObjects = getManifestObjects(ingressFilepath) + }) + + test('reject blue/green ingress', async () => { + const value = await rejectBlueGreenIngress(kubectl, testObjects) + + const bgDeployment = value.routeResult + const deleteResult = value.deleteResult + + expect(deleteResult).toHaveLength(2) + for (const obj of deleteResult) { + if (obj.kind == 'Service') { + expect(obj.name).toBe('nginx-service-green') + } + if (obj.kind == 'Deployment') { + expect(obj.name).toBe('nginx-deployment-green') + } + } + + expect(bgDeployment.objects).toHaveLength(1) + expect(bgDeployment.objects[0].metadata.name).toBe('nginx-ingress') + }) + + test('reject blue/green service', async () => { + const value = await rejectBlueGreenService(kubectl, testObjects) + + const bgDeployment = value.routeResult + const deleteResult = value.deleteResult + + expect(deleteResult).toHaveLength(1) + expect(deleteResult[0].name).toBe('nginx-deployment-green') + + expect(bgDeployment.objects).toHaveLength(1) + expect(bgDeployment.objects[0].metadata.name).toBe('nginx-service') + }) + + test('reject blue/green SMI', async () => { + jest + .spyOn(TSutils, 'getTrafficSplitAPIVersion') + .mockImplementation(() => Promise.resolve('v1alpha3')) + const rejectResult = await rejectBlueGreenSMI(kubectl, testObjects) + expect(rejectResult.deleteResult).toHaveLength(4) + }) +}) diff --git a/src/strategyHelpers/blueGreen/reject.ts b/src/strategyHelpers/blueGreen/reject.ts new file mode 100644 index 000000000..c0be91e28 --- /dev/null +++ b/src/strategyHelpers/blueGreen/reject.ts @@ -0,0 +1,81 @@ +import {K8sDeleteObject} from '../../types/k8sObject' +import {Kubectl} from '../../types/kubectl' +import { + BlueGreenDeployment, + BlueGreenManifests, + BlueGreenRejectResult +} from '../../types/blueGreenTypes' +import {deleteGreenObjects, NONE_LABEL_VALUE} from './blueGreenHelper' +import {routeBlueGreenSMI} from './route' +import {cleanupSMI} from './smiBlueGreenHelper' +import {routeBlueGreenIngressUnchanged, routeBlueGreenService} from './route' + +export async function rejectBlueGreenIngress( + kubectl: Kubectl, + manifestObjects: BlueGreenManifests +): Promise { + // get all kubernetes objects defined in manifest files + // route ingress to stables services + const routeResult = await routeBlueGreenIngressUnchanged( + kubectl, + manifestObjects.serviceNameMap, + manifestObjects.ingressEntityList + ) + + // delete green services and deployments + const deleteResult = await deleteGreenObjects( + kubectl, + [].concat( + manifestObjects.deploymentEntityList, + manifestObjects.serviceEntityList + ) + ) + + return {routeResult, deleteResult} +} + +export async function rejectBlueGreenService( + kubectl: Kubectl, + manifestObjects: BlueGreenManifests +): Promise { + // route to stable objects + const routeResult = await routeBlueGreenService( + kubectl, + NONE_LABEL_VALUE, + manifestObjects.serviceEntityList + ) + + // delete new deployments with green suffix + const deleteResult = await deleteGreenObjects( + kubectl, + manifestObjects.deploymentEntityList + ) + + return {routeResult, deleteResult} +} + +export async function rejectBlueGreenSMI( + kubectl: Kubectl, + manifestObjects: BlueGreenManifests +): Promise { + // route trafficsplit to stable deployments + const routeResult = await routeBlueGreenSMI( + kubectl, + NONE_LABEL_VALUE, + manifestObjects.serviceEntityList + ) + + // delete rejected new bluegreen deployments + const deletedObjects = await deleteGreenObjects( + kubectl, + manifestObjects.deploymentEntityList + ) + + // delete trafficsplit and extra services + const cleanupResult = await cleanupSMI( + kubectl, + manifestObjects.serviceEntityList + ) + + return {routeResult, deleteResult: [].concat(deletedObjects, cleanupResult)} +} diff --git a/src/strategyHelpers/blueGreen/route.test.ts b/src/strategyHelpers/blueGreen/route.test.ts new file mode 100644 index 000000000..7a84b770f --- /dev/null +++ b/src/strategyHelpers/blueGreen/route.test.ts @@ -0,0 +1,119 @@ +import * as core from '@actions/core' +import {K8sIngress, TrafficSplitObject} from '../../types/k8sObject' +import {Kubectl} from '../../types/kubectl' +import * as fileHelper from '../../utilities/fileUtils' +import * as TSutils from '../../utilities/trafficSplitUtils' +import {RouteStrategy} from '../../types/routeStrategy' +import {getBufferTime} from '../../inputUtils' +import * as inputUtils from '../../inputUtils' +import {BlueGreenManifests} from '../../types/blueGreenTypes' + +import { + BLUE_GREEN_VERSION_LABEL, + getManifestObjects, + GREEN_LABEL_VALUE +} from './blueGreenHelper' +import { + routeBlueGreenIngress, + routeBlueGreenService, + routeBlueGreenForDeploy +} from './route' + +jest.mock('../../types/kubectl') +const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml'] +const kc = new Kubectl('') + +describe('route function tests', () => { + let testObjects: BlueGreenManifests + beforeEach(() => { + //@ts-ignore + Kubectl.mockClear() + + testObjects = getManifestObjects(ingressFilepath) + jest + .spyOn(fileHelper, 'writeObjectsToFile') + .mockImplementationOnce(() => ['']) + }) + + test('correctly prepares blue/green ingresses for deployment', async () => { + const unroutedIngCopy: K8sIngress = JSON.parse( + JSON.stringify(testObjects.ingressEntityList[0]) + ) + unroutedIngCopy.metadata.name = 'nginx-ingress-unrouted' + unroutedIngCopy.spec.rules[0].http.paths[0].backend.service.name = + 'fake-service' + testObjects.ingressEntityList.push(unroutedIngCopy) + const value = await routeBlueGreenIngress( + kc, + testObjects.serviceNameMap, + testObjects.ingressEntityList + ) + + expect(value.objects).toHaveLength(2) + expect(value.objects[0].metadata.name).toBe('nginx-ingress') + expect( + (value.objects[0] as K8sIngress).spec.rules[0].http.paths[0].backend + .service.name + ).toBe('nginx-service-green') + + expect(value.objects[1].metadata.name).toBe('nginx-ingress-unrouted') + // unrouted services shouldn't get their service name changed + expect( + (value.objects[1] as K8sIngress).spec.rules[0].http.paths[0].backend + .service.name + ).toBe('fake-service') + }) + + test('correctly prepares blue/green services for deployment', async () => { + const value = await routeBlueGreenService( + kc, + GREEN_LABEL_VALUE, + testObjects.serviceEntityList + ) + + expect(value.objects).toHaveLength(1) + expect(value.objects[0].metadata.name).toBe('nginx-service') + + expect(value.objects[0].metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe( + GREEN_LABEL_VALUE + ) + }) + + test('correctly identifies route pattern and acts accordingly', async () => { + jest + .spyOn(TSutils, 'getTrafficSplitAPIVersion') + .mockImplementation(() => Promise.resolve('v1alpha3')) + + const ingressResult = await routeBlueGreenForDeploy( + kc, + ingressFilepath, + RouteStrategy.INGRESS + ) + + expect(ingressResult.objects.length).toBe(1) + expect(ingressResult.objects[0].metadata.name).toBe('nginx-ingress') + + const serviceResult = await routeBlueGreenForDeploy( + kc, + ingressFilepath, + RouteStrategy.SERVICE + ) + + expect(serviceResult.objects.length).toBe(1) + expect(serviceResult.objects[0].metadata.name).toBe('nginx-service') + + const smiResult = await routeBlueGreenForDeploy( + kc, + ingressFilepath, + RouteStrategy.SMI + ) + + expect(smiResult.objects).toHaveLength(1) + expect(smiResult.objects[0].metadata.name).toBe( + 'nginx-service-trafficsplit' + ) + expect( + (smiResult.objects as TrafficSplitObject[])[0].spec.backends + ).toHaveLength(2) + }) +}) diff --git a/src/strategyHelpers/blueGreen/route.ts b/src/strategyHelpers/blueGreen/route.ts new file mode 100644 index 000000000..f0c385517 --- /dev/null +++ b/src/strategyHelpers/blueGreen/route.ts @@ -0,0 +1,141 @@ +import {sleep} from '../../utilities/timeUtils' +import {RouteStrategy} from '../../types/routeStrategy' +import {Kubectl} from '../../types/kubectl' +import { + BlueGreenDeployment, + BlueGreenManifests +} from '../../types/blueGreenTypes' +import { + getManifestObjects, + GREEN_LABEL_VALUE, + deployObjects +} from './blueGreenHelper' + +import { + getUpdatedBlueGreenIngress, + isIngressRouted +} from './ingressBlueGreenHelper' +import {getUpdatedBlueGreenService} from './serviceBlueGreenHelper' +import {createTrafficSplitObject} from './smiBlueGreenHelper' + +import * as core from '@actions/core' +import {K8sObject, TrafficSplitObject} from '../../types/k8sObject' +import {getBufferTime} from '../../inputUtils' + +export async function routeBlueGreenForDeploy( + kubectl: Kubectl, + inputManifestFiles: string[], + routeStrategy: RouteStrategy +): Promise { + // sleep for buffer time + const bufferTime: number = getBufferTime() + const startSleepDate = new Date() + core.info( + `Starting buffer time of ${bufferTime} minute(s) at ${startSleepDate.toISOString()}` + ) + await sleep(bufferTime * 1000 * 60) + const endSleepDate = new Date() + core.info( + `Stopping buffer time of ${bufferTime} minute(s) at ${endSleepDate.toISOString()}` + ) + + const manifestObjects: BlueGreenManifests = + getManifestObjects(inputManifestFiles) + + // route to new deployments + if (routeStrategy == RouteStrategy.INGRESS) { + return await routeBlueGreenIngress( + kubectl, + manifestObjects.serviceNameMap, + manifestObjects.ingressEntityList + ) + } else if (routeStrategy == RouteStrategy.SMI) { + return await routeBlueGreenSMI( + kubectl, + GREEN_LABEL_VALUE, + manifestObjects.serviceEntityList + ) + } else { + return await routeBlueGreenService( + kubectl, + GREEN_LABEL_VALUE, + manifestObjects.serviceEntityList + ) + } +} + +export async function routeBlueGreenIngress( + kubectl: Kubectl, + serviceNameMap: Map, + ingressEntityList: any[] +): Promise { + // const newObjectsList = [] + const newObjectsList: K8sObject[] = ingressEntityList.map((obj) => { + if (isIngressRouted(obj, serviceNameMap)) { + const newBlueGreenIngressObject = getUpdatedBlueGreenIngress( + obj, + serviceNameMap, + GREEN_LABEL_VALUE + ) + return newBlueGreenIngressObject + } else { + core.debug(`unrouted ingress detected ${obj.metadata.name}`) + return obj + } + }) + + const deployResult = await deployObjects(kubectl, newObjectsList) + + return {deployResult, objects: newObjectsList} +} + +export async function routeBlueGreenIngressUnchanged( + kubectl: Kubectl, + serviceNameMap: Map, + ingressEntityList: any[] +): Promise { + const objects = ingressEntityList.filter((ingress) => + isIngressRouted(ingress, serviceNameMap) + ) + + const deployResult = await deployObjects(kubectl, objects) + return {deployResult, objects} +} + +export async function routeBlueGreenService( + kubectl: Kubectl, + nextLabel: string, + serviceEntityList: any[] +): Promise { + const objects = serviceEntityList.map((serviceObject) => + getUpdatedBlueGreenService(serviceObject, nextLabel) + ) + + const deployResult = await deployObjects(kubectl, objects) + + return {deployResult, objects} +} + +export async function routeBlueGreenSMI( + kubectl: Kubectl, + nextLabel: string, + serviceEntityList: any[] +): Promise { + // let tsObjects: TrafficSplitObject[] = [] + + const tsObjects: TrafficSplitObject[] = await Promise.all( + serviceEntityList.map(async (serviceObject) => { + const tsObject: TrafficSplitObject = await createTrafficSplitObject( + kubectl, + serviceObject.metadata.name, + nextLabel + ) + + return tsObject + }) + ) + + const deployResult = await deployObjects(kubectl, tsObjects) + + return {deployResult, objects: tsObjects} +} diff --git a/src/strategyHelpers/blueGreen/serviceBlueGreenHelper.test.ts b/src/strategyHelpers/blueGreen/serviceBlueGreenHelper.test.ts new file mode 100644 index 000000000..7978890db --- /dev/null +++ b/src/strategyHelpers/blueGreen/serviceBlueGreenHelper.test.ts @@ -0,0 +1,65 @@ +import * as core from '@actions/core' +import { + BLUE_GREEN_VERSION_LABEL, + getManifestObjects, + GREEN_LABEL_VALUE +} from './blueGreenHelper' +import * as bgHelper from './blueGreenHelper' +import {Kubectl} from '../../types/kubectl' +import { + getServiceSpecLabel, + getUpdatedBlueGreenService, + validateServicesState +} from './serviceBlueGreenHelper' + +let testObjects +const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml'] +jest.mock('../../types/kubectl') +const kubectl = new Kubectl('') + +describe('blue/green service helper tests', () => { + beforeEach(() => { + //@ts-ignore + Kubectl.mockClear() + testObjects = getManifestObjects(ingressFilepath) + }) + + test('getUpdatedBlueGreenService', () => { + const newService = getUpdatedBlueGreenService( + testObjects.serviceEntityList[0], + GREEN_LABEL_VALUE + ) + expect(newService.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe( + GREEN_LABEL_VALUE + ) + expect(newService.spec.selector[BLUE_GREEN_VERSION_LABEL]).toBe( + GREEN_LABEL_VALUE + ) + }) + + test('validateServicesState', async () => { + const mockLabels = new Map() + mockLabels[BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE + const mockSelectors = new Map() + mockSelectors[BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE + jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() => + Promise.resolve({ + kind: 'Service', + spec: {selector: mockSelectors}, + metadata: {labels: mockLabels, name: 'nginx-service-green'} + }) + ) + expect( + await validateServicesState(kubectl, testObjects.serviceEntityList) + ).toBe(true) + }) + + test('getServiceSpecLabel', () => { + testObjects.serviceEntityList[0].spec.selector[BLUE_GREEN_VERSION_LABEL] = + GREEN_LABEL_VALUE + + expect(getServiceSpecLabel(testObjects.serviceEntityList[0])).toBe( + GREEN_LABEL_VALUE + ) + }) +}) diff --git a/src/strategyHelpers/blueGreen/serviceBlueGreenHelper.ts b/src/strategyHelpers/blueGreen/serviceBlueGreenHelper.ts index 3b2297b85..8fde8c409 100644 --- a/src/strategyHelpers/blueGreen/serviceBlueGreenHelper.ts +++ b/src/strategyHelpers/blueGreen/serviceBlueGreenHelper.ts @@ -1,105 +1,18 @@ +import * as core from '@actions/core' +import {K8sServiceObject} from '../../types/k8sObject' import {Kubectl} from '../../types/kubectl' -import * as fileHelper from '../../utilities/fileUtils' import { addBlueGreenLabelsAndAnnotations, BLUE_GREEN_VERSION_LABEL, - BlueGreenManifests, - createWorkloadsWithLabel, - deleteWorkloadsWithLabel, fetchResource, - getManifestObjects, - GREEN_LABEL_VALUE, - NONE_LABEL_VALUE + GREEN_LABEL_VALUE } from './blueGreenHelper' -export async function deployBlueGreenService( - kubectl: Kubectl, - filePaths: string[] -) { - const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) - - // create deployments with green label value - const workloadDeployment = await createWorkloadsWithLabel( - kubectl, - manifestObjects.deploymentEntityList, - GREEN_LABEL_VALUE - ) - - const newObjectsList = manifestObjects.otherObjects - .concat(manifestObjects.ingressEntityList) - .concat(manifestObjects.unroutedServiceEntityList) - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) - - if (manifestFiles.length > 0) await kubectl.apply(manifestFiles) - - // returning deployment details to check for rollout stability - return {workloadDeployment, newObjectsList} -} - -export async function promoteBlueGreenService( - kubectl: Kubectl, - manifestObjects -) { - // checking if services are in the right state ie. targeting green deployments - if ( - !(await validateServicesState(kubectl, manifestObjects.serviceEntityList)) - ) { - throw 'Not inP promote state' - } - - // creating stable deployments with new configurations - return await createWorkloadsWithLabel( - kubectl, - manifestObjects.deploymentEntityList, - NONE_LABEL_VALUE - ) -} - -export async function rejectBlueGreenService( - kubectl: Kubectl, - filePaths: string[] -) { - // get all kubernetes objects defined in manifest files - const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) - - // route to stable objects - await routeBlueGreenService( - kubectl, - NONE_LABEL_VALUE, - manifestObjects.serviceEntityList - ) - - // delete new deployments with green suffix - await deleteWorkloadsWithLabel( - kubectl, - GREEN_LABEL_VALUE, - manifestObjects.deploymentEntityList - ) -} -export async function routeBlueGreenService( - kubectl: Kubectl, - nextLabel: string, - serviceEntityList: any[] -) { - const newObjectsList = [] - serviceEntityList.forEach((serviceObject) => { - const newBlueGreenServiceObject = getUpdatedBlueGreenService( - serviceObject, - nextLabel - ) - newObjectsList.push(newBlueGreenServiceObject) - }) - - // configures the services - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) - await kubectl.apply(manifestFiles) -} - // add green labels to configure existing service -function getUpdatedBlueGreenService( +export function getUpdatedBlueGreenService( inputObject: any, labelValue: string -): object { +): K8sServiceObject { const newObject = JSON.parse(JSON.stringify(inputObject)) // Adding labels and annotations. @@ -121,25 +34,16 @@ export async function validateServicesState( serviceObject.metadata.name ) - if (!!existingService) { - const currentLabel: string = getServiceSpecLabel(existingService) - if (currentLabel != GREEN_LABEL_VALUE) { - // service should be targeting deployments with green label - areServicesGreen = false - } - } else { - // service targeting deployment doesn't exist - areServicesGreen = false - } + let isServiceGreen = + !!existingService && + getServiceSpecLabel(existingService as K8sServiceObject) == + GREEN_LABEL_VALUE + areServicesGreen = areServicesGreen && isServiceGreen } return areServicesGreen } -export function getServiceSpecLabel(inputObject: any): string { - if (inputObject?.spec?.selector[BLUE_GREEN_VERSION_LABEL]) { - return inputObject.spec.selector[BLUE_GREEN_VERSION_LABEL] - } - - return '' +export function getServiceSpecLabel(inputObject: K8sServiceObject): string { + return inputObject.spec.selector[BLUE_GREEN_VERSION_LABEL] } diff --git a/src/strategyHelpers/blueGreen/smiBlueGreenHelper.test.ts b/src/strategyHelpers/blueGreen/smiBlueGreenHelper.test.ts new file mode 100644 index 000000000..938f5763c --- /dev/null +++ b/src/strategyHelpers/blueGreen/smiBlueGreenHelper.test.ts @@ -0,0 +1,203 @@ +import * as core from '@actions/core' +import {TrafficSplitObject} from '../../types/k8sObject' +import {Kubectl} from '../../types/kubectl' +import * as fileHelper from '../../utilities/fileUtils' +import * as TSutils from '../../utilities/trafficSplitUtils' + +import {BlueGreenManifests} from '../../types/blueGreenTypes' +import { + BLUE_GREEN_VERSION_LABEL, + getManifestObjects, + GREEN_LABEL_VALUE, + NONE_LABEL_VALUE +} from './blueGreenHelper' + +import { + cleanupSMI, + createTrafficSplitObject, + getGreenSMIServiceResource, + getStableSMIServiceResource, + MAX_VAL, + MIN_VAL, + setupSMI, + TRAFFIC_SPLIT_OBJECT, + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX, + validateTrafficSplitsState +} from './smiBlueGreenHelper' +import * as bgHelper from './blueGreenHelper' + +jest.mock('../../types/kubectl') + +const kc = new Kubectl('') +const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml'] +const mockTsObject: TrafficSplitObject = { + apiVersion: 'v1alpha3', + kind: TRAFFIC_SPLIT_OBJECT, + metadata: { + name: 'nginx-service-trafficsplit', + labels: new Map(), + annotations: new Map() + }, + spec: { + service: 'nginx-service', + backends: [ + { + service: 'nginx-service-stable', + weight: MIN_VAL + }, + { + service: 'nginx-service-green', + weight: MAX_VAL + } + ] + } +} + +describe('SMI Helper tests', () => { + let testObjects: BlueGreenManifests + beforeEach(() => { + //@ts-ignore + Kubectl.mockClear() + + jest + .spyOn(TSutils, 'getTrafficSplitAPIVersion') + .mockImplementation(() => Promise.resolve('')) + + testObjects = getManifestObjects(ingressFilepath) + jest + .spyOn(fileHelper, 'writeObjectsToFile') + .mockImplementationOnce(() => ['']) + }) + + test('setupSMI tests', async () => { + const smiResults = await setupSMI(kc, testObjects.serviceEntityList) + + let found = 0 + for (const obj of smiResults.objects) { + if (obj.metadata.name === 'nginx-service-stable') { + expect(obj.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe( + NONE_LABEL_VALUE + ) + expect(obj.spec.selector.app).toBe('nginx') + found++ + } + + if (obj.metadata.name === 'nginx-service-green') { + expect(obj.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe( + GREEN_LABEL_VALUE + ) + found++ + } + + if (obj.metadata.name === 'nginx-service-trafficsplit') { + found++ + // expect stable weight to be max val + const casted = obj as TrafficSplitObject + expect(casted.spec.backends).toHaveLength(2) + for (const be of casted.spec.backends) { + if (be.service === 'nginx-service-stable') { + expect(be.weight).toBe(MAX_VAL) + } + if (be.service === 'nginx-service-green') { + expect(be.weight).toBe(MIN_VAL) + } + } + } + } + + expect(found).toBe(3) + }) + + test('createTrafficSplitObject tests', async () => { + const noneTsObject: TrafficSplitObject = await createTrafficSplitObject( + kc, + testObjects.serviceEntityList[0].metadata.name, + NONE_LABEL_VALUE + ) + expect(noneTsObject.metadata.name).toBe('nginx-service-trafficsplit') + for (let be of noneTsObject.spec.backends) { + if (be.service === 'nginx-service-stable') { + expect(be.weight).toBe(MAX_VAL) + } + if (be.service === 'nginx-service-green') { + expect(be.weight).toBe(MIN_VAL) + } + } + + const greenTsObject: TrafficSplitObject = await createTrafficSplitObject( + kc, + testObjects.serviceEntityList[0].metadata.name, + GREEN_LABEL_VALUE + ) + expect(greenTsObject.metadata.name).toBe('nginx-service-trafficsplit') + for (const be of greenTsObject.spec.backends) { + if (be.service === 'nginx-service-stable') { + expect(be.weight).toBe(MIN_VAL) + } + if (be.service === 'nginx-service-green') { + expect(be.weight).toBe(MAX_VAL) + } + } + }) + + test('getSMIServiceResource test', () => { + const stableResult = getStableSMIServiceResource( + testObjects.serviceEntityList[0] + ) + const greenResult = getGreenSMIServiceResource( + testObjects.serviceEntityList[0] + ) + + expect(stableResult.metadata.name).toBe('nginx-service-stable') + expect(stableResult.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe( + NONE_LABEL_VALUE + ) + + expect(greenResult.metadata.name).toBe('nginx-service-green') + expect(greenResult.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe( + GREEN_LABEL_VALUE + ) + }) + + test('validateTrafficSplitsState', async () => { + jest + .spyOn(bgHelper, 'fetchResource') + .mockImplementation(() => Promise.resolve(mockTsObject)) + + let valResult = await validateTrafficSplitsState( + kc, + testObjects.serviceEntityList + ) + + expect(valResult).toBe(true) + + const mockTsCopy = JSON.parse(JSON.stringify(mockTsObject)) + mockTsCopy.spec.backends[0].weight = MAX_VAL + jest + .spyOn(bgHelper, 'fetchResource') + .mockImplementation(() => Promise.resolve(mockTsCopy)) + + valResult = await validateTrafficSplitsState( + kc, + testObjects.serviceEntityList + ) + expect(valResult).toBe(false) + + jest.spyOn(bgHelper, 'fetchResource').mockImplementation() + valResult = await validateTrafficSplitsState( + kc, + testObjects.serviceEntityList + ) + expect(valResult).toBe(false) + }) + + test('cleanupSMI test', async () => { + const deleteObjects = await cleanupSMI(kc, testObjects.serviceEntityList) + expect(deleteObjects).toHaveLength(3) + expect(deleteObjects[0].name).toBe('nginx-service-trafficsplit') + expect(deleteObjects[1].name).toBe('nginx-service-green') + expect(deleteObjects[1].kind).toBe('Service') + expect(deleteObjects[2].name).toBe('nginx-service-stable') + expect(deleteObjects[2].kind).toBe('Service') + }) +}) diff --git a/src/strategyHelpers/blueGreen/smiBlueGreenHelper.ts b/src/strategyHelpers/blueGreen/smiBlueGreenHelper.ts index 248aa7811..e39865a21 100644 --- a/src/strategyHelpers/blueGreen/smiBlueGreenHelper.ts +++ b/src/strategyHelpers/blueGreen/smiBlueGreenHelper.ts @@ -1,106 +1,35 @@ +import * as core from '@actions/core' import {Kubectl} from '../../types/kubectl' import * as kubectlUtils from '../../utilities/trafficSplitUtils' -import * as fileHelper from '../../utilities/fileUtils' import { - BlueGreenManifests, - createWorkloadsWithLabel, deleteObjects, - deleteWorkloadsWithLabel, + deployObjects, fetchResource, getBlueGreenResourceName, - getManifestObjects, getNewBlueGreenObject, GREEN_LABEL_VALUE, GREEN_SUFFIX, NONE_LABEL_VALUE, STABLE_SUFFIX } from './blueGreenHelper' - -const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit' -const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit' -const MIN_VAL = 0 -const MAX_VAL = 100 - -export async function deployBlueGreenSMI( - kubectl: Kubectl, - filePaths: string[], - annotations: {[key: string]: string} = {} -) { - // get all kubernetes objects defined in manifest files - const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) - - // create services and other objects - const newObjectsList = manifestObjects.otherObjects - .concat(manifestObjects.serviceEntityList) - .concat(manifestObjects.ingressEntityList) - .concat(manifestObjects.unroutedServiceEntityList) - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) - await kubectl.apply(manifestFiles) - - // make extraservices and trafficsplit - await setupSMI(kubectl, manifestObjects.serviceEntityList, annotations) - - // create new deloyments - const workloadDeployment = await createWorkloadsWithLabel( - kubectl, - manifestObjects.deploymentEntityList, - GREEN_LABEL_VALUE - ) - - return {workloadDeployment, newObjectsList} -} - -export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) { - // checking if there is something to promote - if ( - !(await validateTrafficSplitsState( - kubectl, - manifestObjects.serviceEntityList - )) - ) { - throw Error('Not in promote state SMI') - } - - // create stable deployments with new configuration - return await createWorkloadsWithLabel( - kubectl, - manifestObjects.deploymentEntityList, - NONE_LABEL_VALUE - ) -} - -export async function rejectBlueGreenSMI( - kubectl: Kubectl, - filePaths: string[], - annotations: {[key: string]: string} = {} -) { - // get all kubernetes objects defined in manifest files - const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) - - // route trafficsplit to stable deployments - await routeBlueGreenSMI( - kubectl, - NONE_LABEL_VALUE, - manifestObjects.serviceEntityList, - annotations - ) - - // delete rejected new bluegreen deployments - await deleteWorkloadsWithLabel( - kubectl, - GREEN_LABEL_VALUE, - manifestObjects.deploymentEntityList - ) - - // delete trafficsplit and extra services - await cleanupSMI(kubectl, manifestObjects.serviceEntityList) -} +import {BlueGreenDeployment} from '../../types/blueGreenTypes' +import { + K8sDeleteObject, + K8sObject, + TrafficSplitObject +} from '../../types/k8sObject' +import {DeployResult} from '../../types/deployResult' +import {inputAnnotations} from '../../inputUtils' + +export const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit' +export const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit' +export const MIN_VAL = 0 +export const MAX_VAL = 100 export async function setupSMI( kubectl: Kubectl, - serviceEntityList: any[], - annotations: {[key: string]: string} = {} -) { + serviceEntityList: any[] +): Promise { const newObjectsList = [] const trafficObjectList = [] @@ -108,56 +37,66 @@ export async function setupSMI( // create a trafficsplit for service trafficObjectList.push(serviceObject) // set up the services for trafficsplit - const newStableService = getSMIServiceResource( - serviceObject, - STABLE_SUFFIX - ) - const newGreenService = getSMIServiceResource(serviceObject, GREEN_SUFFIX) + const newStableService = getStableSMIServiceResource(serviceObject) + const newGreenService = getGreenSMIServiceResource(serviceObject) newObjectsList.push(newStableService) newObjectsList.push(newGreenService) }) - // create services - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) - await kubectl.apply(manifestFiles) - + const tsObjects: TrafficSplitObject[] = [] // route to stable service - trafficObjectList.forEach((inputObject) => { - createTrafficSplitObject( + for (const svc of trafficObjectList) { + const tsObject = await createTrafficSplitObject( kubectl, - inputObject.metadata.name, - NONE_LABEL_VALUE, - annotations + svc.metadata.name, + NONE_LABEL_VALUE ) - }) + tsObjects.push(tsObject as TrafficSplitObject) + } + + const objectsToDeploy = [].concat(newObjectsList, tsObjects) + + // create services + const smiDeploymentResult: DeployResult = await deployObjects( + kubectl, + objectsToDeploy + ) + + return { + objects: objectsToDeploy, + deployResult: smiDeploymentResult + } } let trafficSplitAPIVersion = '' -async function createTrafficSplitObject( +export async function createTrafficSplitObject( kubectl: Kubectl, name: string, - nextLabel: string, - annotations: {[key: string]: string} = {} -): Promise { + nextLabel: string +): Promise { // cache traffic split api version if (!trafficSplitAPIVersion) trafficSplitAPIVersion = await kubectlUtils.getTrafficSplitAPIVersion( kubectl ) + // retrieve annotations for TS object + const annotations = inputAnnotations + // decide weights based on nextlabel const stableWeight: number = nextLabel === GREEN_LABEL_VALUE ? MIN_VAL : MAX_VAL const greenWeight: number = nextLabel === GREEN_LABEL_VALUE ? MAX_VAL : MIN_VAL - const trafficSplitObject = JSON.stringify({ + const trafficSplitObject: TrafficSplitObject = { apiVersion: trafficSplitAPIVersion, - kind: 'TrafficSplit', + kind: TRAFFIC_SPLIT_OBJECT, metadata: { name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), - annotations: annotations + annotations: annotations, + labels: new Map() }, spec: { service: name, @@ -172,52 +111,24 @@ async function createTrafficSplitObject( } ] } - }) - - // create traffic split object - const trafficSplitManifestFile = fileHelper.writeManifestToFile( - trafficSplitObject, - TRAFFIC_SPLIT_OBJECT, - getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX) - ) + } - await kubectl.apply(trafficSplitManifestFile) + return trafficSplitObject } -export function getSMIServiceResource( - inputObject: any, - suffix: string -): object { +export function getStableSMIServiceResource(inputObject: K8sObject): K8sObject { const newObject = JSON.parse(JSON.stringify(inputObject)) - - if (suffix === STABLE_SUFFIX) { - // adding stable suffix to service name - newObject.metadata.name = getBlueGreenResourceName( - inputObject.metadata.name, - STABLE_SUFFIX - ) - return getNewBlueGreenObject(newObject, NONE_LABEL_VALUE) - } else { - // green label will be added for these - return getNewBlueGreenObject(newObject, GREEN_LABEL_VALUE) - } + // adding stable suffix to service name + newObject.metadata.name = getBlueGreenResourceName( + inputObject.metadata.name, + STABLE_SUFFIX + ) + return getNewBlueGreenObject(newObject, NONE_LABEL_VALUE) } -export async function routeBlueGreenSMI( - kubectl: Kubectl, - nextLabel: string, - serviceEntityList: any[], - annotations: {[key: string]: string} = {} -) { - for (const serviceObject of serviceEntityList) { - // route trafficsplit to given label - await createTrafficSplitObject( - kubectl, - serviceObject.metadata.name, - nextLabel, - annotations - ) - } +export function getGreenSMIServiceResource(inputObject: K8sObject): K8sObject { + const newObject = JSON.parse(JSON.stringify(inputObject)) + return getNewBlueGreenObject(newObject, GREEN_LABEL_VALUE) } export async function validateTrafficSplitsState( @@ -233,32 +144,38 @@ export async function validateTrafficSplitsState( TRAFFIC_SPLIT_OBJECT, getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX) ) - + core.debug( + `ts object extracted was ${JSON.stringify(trafficSplitObject)}` + ) if (!trafficSplitObject) { - // no traffic split exits + core.debug(`no traffic split exits for ${name}`) trafficSplitsInRightState = false + continue } - trafficSplitObject = JSON.parse(JSON.stringify(trafficSplitObject)) trafficSplitObject.spec.backends.forEach((element) => { // checking if trafficsplit in right state to deploy if (element.service === getBlueGreenResourceName(name, GREEN_SUFFIX)) { - if (element.weight != MAX_VAL) trafficSplitsInRightState = false + trafficSplitsInRightState = + trafficSplitsInRightState && element.weight == MAX_VAL } if ( element.service === getBlueGreenResourceName(name, STABLE_SUFFIX) ) { - if (element.weight != MIN_VAL) trafficSplitsInRightState = false + trafficSplitsInRightState = + trafficSplitsInRightState && element.weight == MIN_VAL } }) } - return trafficSplitsInRightState } -export async function cleanupSMI(kubectl: Kubectl, serviceEntityList: any[]) { - const deleteList = [] +export async function cleanupSMI( + kubectl: Kubectl, + serviceEntityList: any[] +): Promise { + const deleteList: K8sDeleteObject[] = [] serviceEntityList.forEach((serviceObject) => { deleteList.push({ @@ -288,4 +205,6 @@ export async function cleanupSMI(kubectl: Kubectl, serviceEntityList: any[]) { // delete all objects await deleteObjects(kubectl, deleteList) + + return deleteList } diff --git a/src/strategyHelpers/canary/smiCanaryHelper.ts b/src/strategyHelpers/canary/smiCanaryHelper.ts index 3e37852ae..0e923eeef 100644 --- a/src/strategyHelpers/canary/smiCanaryHelper.ts +++ b/src/strategyHelpers/canary/smiCanaryHelper.ts @@ -8,6 +8,7 @@ import * as kubectlUtils from '../../utilities/trafficSplitUtils' import * as canaryDeploymentHelper from './canaryHelper' import {isDeploymentEntity, isServiceEntity} from '../../types/kubernetesTypes' import {checkForErrors} from '../../utilities/kubectlUtils' +import {inputAnnotations} from '../../inputUtils' const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout' const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit' @@ -288,8 +289,7 @@ async function getTrafficSplitObject( name: string, stableWeight: number, baselineWeight: number, - canaryWeight: number, - annotations: {[key: string]: string} = {} + canaryWeight: number ): Promise { // cached version if (!trafficSplitAPIVersion) { @@ -303,7 +303,7 @@ async function getTrafficSplitObject( kind: 'TrafficSplit', metadata: { name: getTrafficSplitResourceName(name), - annotations: annotations + annotations: inputAnnotations }, spec: { backends: [ diff --git a/src/strategyHelpers/deploymentHelper.ts b/src/strategyHelpers/deploymentHelper.ts index 69d27c8fb..4705dbf8b 100644 --- a/src/strategyHelpers/deploymentHelper.ts +++ b/src/strategyHelpers/deploymentHelper.ts @@ -10,16 +10,19 @@ import {Kubectl, Resource} from '../types/kubectl' import {deployPodCanary} from './canary/podCanaryHelper' import {deploySMICanary} from './canary/smiCanaryHelper' import {DeploymentConfig} from '../types/deploymentConfig' -import {deployBlueGreenService} from './blueGreen/serviceBlueGreenHelper' -import {deployBlueGreenIngress} from './blueGreen/ingressBlueGreenHelper' -import {deployBlueGreenSMI} from './blueGreen/smiBlueGreenHelper' +import { + deployBlueGreen, + deployBlueGreenIngress, + deployBlueGreenService +} from './blueGreen/deploy' +import {deployBlueGreenSMI} from './blueGreen/deploy' import {DeploymentStrategy} from '../types/deploymentStrategy' import * as core from '@actions/core' import { parseTrafficSplitMethod, TrafficSplitMethod } from '../types/trafficSplitMethod' -import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy' +import {parseRouteStrategy} from '../types/routeStrategy' import {ExecOutput} from '@actions/exec' import { getWorkflowAnnotationKeyLabel, @@ -41,8 +44,7 @@ export async function deployManifests( files: string[], deploymentStrategy: DeploymentStrategy, kubectl: Kubectl, - trafficSplitMethod: TrafficSplitMethod, - annotations: {[key: string]: string} = {} + trafficSplitMethod: TrafficSplitMethod ): Promise { switch (deploymentStrategy) { case DeploymentStrategy.CANARY: { @@ -59,17 +61,19 @@ export async function deployManifests( const routeStrategy = parseRouteStrategy( core.getInput('route-method', {required: true}) ) - - const {workloadDeployment, newObjectsList} = await Promise.resolve( - (routeStrategy == RouteStrategy.INGRESS && - deployBlueGreenIngress(kubectl, files)) || - (routeStrategy == RouteStrategy.SMI && - deployBlueGreenSMI(kubectl, files, annotations)) || - deployBlueGreenService(kubectl, files) + const blueGreenDeployment = await deployBlueGreen( + kubectl, + files, + routeStrategy + ) + core.debug( + `objects deployed for ${routeStrategy}: ${JSON.stringify( + blueGreenDeployment.objects + )} ` ) - checkForErrors([workloadDeployment.result]) - return workloadDeployment.newFilePaths + checkForErrors([blueGreenDeployment.deployResult.execResult]) + return blueGreenDeployment.deployResult.manifestFiles } case DeploymentStrategy.BASIC: { diff --git a/src/types/annotations.ts b/src/types/annotations.ts index affd417aa..484f0453e 100644 --- a/src/types/annotations.ts +++ b/src/types/annotations.ts @@ -1,8 +1,8 @@ export function parseAnnotations(str: string) { if (str == '') { - return {} + return new Map() } else { - const annotaion = JSON.parse(str) - return new Map(annotaion) + const annotation = JSON.parse(str) + return new Map(annotation) } } diff --git a/src/types/blueGreenTypes.ts b/src/types/blueGreenTypes.ts new file mode 100644 index 000000000..b224550ee --- /dev/null +++ b/src/types/blueGreenTypes.ts @@ -0,0 +1,21 @@ +import {DeployResult} from './deployResult' +import {K8sObject, K8sDeleteObject} from './k8sObject' + +export interface BlueGreenDeployment { + deployResult: DeployResult + objects: K8sObject[] +} + +export interface BlueGreenManifests { + serviceEntityList: K8sObject[] + serviceNameMap: Map + unroutedServiceEntityList: K8sObject[] + deploymentEntityList: K8sObject[] + ingressEntityList: K8sObject[] + otherObjects: K8sObject[] +} + +export interface BlueGreenRejectResult { + deleteResult: K8sDeleteObject[] + routeResult: BlueGreenDeployment +} diff --git a/src/types/deployResult.ts b/src/types/deployResult.ts new file mode 100644 index 000000000..39eee7433 --- /dev/null +++ b/src/types/deployResult.ts @@ -0,0 +1,6 @@ +import {ExecOutput} from '@actions/exec' + +export interface DeployResult { + execResult: ExecOutput + manifestFiles: string[] +} diff --git a/src/types/k8sObject.ts b/src/types/k8sObject.ts new file mode 100644 index 000000000..db0c2f755 --- /dev/null +++ b/src/types/k8sObject.ts @@ -0,0 +1,57 @@ +export interface K8sObject { + metadata: { + name: string + labels: Map + } + kind: string + spec: any +} + +export interface K8sServiceObject extends K8sObject { + spec: { + selector: Map + } +} + +export interface K8sDeleteObject { + name: string + kind: string +} + +export interface K8sIngress extends K8sObject { + spec: { + rules: [ + { + http: { + paths: [ + { + backend: { + service: { + name: string + } + } + } + ] + } + } + ] + } +} + +export interface TrafficSplitObject extends K8sObject { + apiVersion: string + metadata: { + name: string + labels: Map + annotations: Map + } + spec: { + service: string + backends: TrafficSplitBackend[] + } +} + +export interface TrafficSplitBackend { + service: string + weight: number +} diff --git a/src/utilities/fileUtils.test.ts b/src/utilities/fileUtils.test.ts index 5c33423c8..9b6d17482 100644 --- a/src/utilities/fileUtils.test.ts +++ b/src/utilities/fileUtils.test.ts @@ -17,7 +17,7 @@ describe('File utils', () => { ] // is there a more efficient way to test equality w random order? - expect(testSearch).toHaveLength(6) + expect(testSearch).toHaveLength(7) expectedManifests.forEach((fileName) => { expect(testSearch).toContain(fileName) }) @@ -54,7 +54,7 @@ describe('File utils', () => { expect( getFilesFromDirectories([outerPath, fileAtOuter, innerPath]) - ).toHaveLength(6) + ).toHaveLength(7) }) }) diff --git a/src/utilities/workflowAnnotationUtils.test.ts b/src/utilities/workflowAnnotationUtils.test.ts index 5fc55475b..bed2bd1a8 100644 --- a/src/utilities/workflowAnnotationUtils.test.ts +++ b/src/utilities/workflowAnnotationUtils.test.ts @@ -11,5 +11,10 @@ describe('WorkflowAnnotationUtils', () => { ) expect(cleanLabel('with⚒️emoji')).toEqual('withemoji') }) + it('should remove slashes from label', () => { + expect( + cleanLabel('Workflow Name / With Slashes / And Spaces') + ).toEqual('Workflow_Name_-_With_Slashes_-_And_Spaces') + }) }) }) diff --git a/src/utilities/workflowAnnotationUtils.ts b/src/utilities/workflowAnnotationUtils.ts index 8d3045946..248b53f33 100644 --- a/src/utilities/workflowAnnotationUtils.ts +++ b/src/utilities/workflowAnnotationUtils.ts @@ -37,7 +37,11 @@ export function getWorkflowAnnotationKeyLabel(): string { * @returns cleaned label */ export function cleanLabel(label: string): string { - const removedInvalidChars = label.replace(/[^-A-Za-z0-9_.]/gi, '') + let removedInvalidChars = label + .replace(/\s/gi, '_') + .replace(/[\/\\\|]/gi, '-') + .replace(/[^-A-Za-z0-9_.]/gi, '') + const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/ return regex.exec(removedInvalidChars)[0] || '' } diff --git a/test/unit/manifests/anomaly-objects-test.yml b/test/unit/manifests/anomaly-objects-test.yml new file mode 100644 index 000000000..0475c1fa3 --- /dev/null +++ b/test/unit/manifests/anomaly-objects-test.yml @@ -0,0 +1,23 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: unrouted-service +spec: + selector: + app: fake-application + ports: + - protocol: TCP + port: 80 + targetPort: 80 +--- +kind: TrafficSplit +metadata: + name: foobar-rollout +spec: + service: foobar + backends: + - service: foobar-v1 + weight: 1000 + - service: foobar-v2 + weight: 500