diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index 1abdcdc7..c6e9441f 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -8,6 +8,8 @@ import * as io from '@actions/io'; import * as toolCache from '@actions/tool-cache'; import * as fileHelper from '../src/utilities/files-helper'; import { workflowAnnotations } from '../src/constants'; +import * as utility from '../src/utilities/utility'; +import * as inputParam from '../src/input-parameters'; import { Kubectl, Resource } from '../src/kubectl-object-model'; @@ -18,6 +20,8 @@ var path = require('path'); const coreMock = mocked(core, true); const ioMock = mocked(io, true); +const utilityMock = mocked(utility, true); +const inputParamMock = mocked(inputParam, true); const toolCacheMock = mocked(toolCache, true); const fileUtility = mocked(fs, true); @@ -220,6 +224,40 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => { expect(kubeCtl.getResource).toBeCalledWith("ingress", "AppName"); }); +test("run() - deploy force flag on", async () => { + const kubectlVersion = 'v1.18.0' + //Mocks + coreMock.getInput = jest.fn().mockImplementation((name) => { + if (name == 'manifests') { + return 'manifests/deployment.yaml'; + } + if (name == 'action') { + return 'deploy'; + } + if (name == 'strategy') { + return undefined; + } + if (name == 'force') { + return 'true'; + } + return kubectlVersion; + }); + + inputParamMock.forceDeployment = true; + coreMock.setFailed = jest.fn(); + toolCacheMock.find = jest.fn().mockReturnValue('validPath'); + toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath'); + toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath'); + fileUtility.chmodSync = jest.fn(); + utilityMock.checkForErrors = jest.fn(); + const deploySpy = jest.spyOn(Kubectl.prototype, 'apply').mockImplementation(); + + //Invoke and assert + await expect(action.run()).resolves.not.toThrow(); + expect(deploySpy).toBeCalledWith(expect.anything(), true); + deploySpy.mockRestore(); +}); + test("deployment - deploy() - Annotate resources", async () => { const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); diff --git a/action.yml b/action.yml index 1146e5c1..0a1a40c7 100644 --- a/action.yml +++ b/action.yml @@ -41,6 +41,10 @@ inputs: description: 'deploy/promote/reject' required: true default: 'deploy' + force: + description: 'Deploy when a previous deployment already exists. If true then --force argument is added to the apply command.' + required: false + default: false branding: color: 'green' # optional, decorates the entry in the GitHub Marketplace diff --git a/lib/input-parameters.js b/lib/input-parameters.js index c4903363..21744518 100644 --- a/lib/input-parameters.js +++ b/lib/input-parameters.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.args = exports.baselineAndCanaryReplicas = exports.trafficSplitMethod = exports.deploymentStrategy = exports.canaryPercentage = exports.manifests = exports.imagePullSecrets = exports.containers = exports.namespace = void 0; +exports.forceDeployment = exports.args = exports.baselineAndCanaryReplicas = exports.trafficSplitMethod = exports.deploymentStrategy = exports.canaryPercentage = exports.manifests = exports.imagePullSecrets = exports.containers = exports.namespace = void 0; const core = require("@actions/core"); exports.namespace = core.getInput('namespace'); exports.containers = core.getInput('images').split('\n'); @@ -11,6 +11,7 @@ exports.deploymentStrategy = core.getInput('strategy'); exports.trafficSplitMethod = core.getInput('traffic-split-method'); exports.baselineAndCanaryReplicas = core.getInput('baseline-and-canary-replicas'); exports.args = core.getInput('arguments'); +exports.forceDeployment = core.getInput('force').toLowerCase() == 'true'; if (!exports.namespace) { core.debug('Namespace was not supplied; using "default" namespace instead.'); exports.namespace = 'default'; diff --git a/lib/kubectl-object-model.js b/lib/kubectl-object-model.js index 688a4f72..502a8c83 100644 --- a/lib/kubectl-object-model.js +++ b/lib/kubectl-object-model.js @@ -13,8 +13,13 @@ class Kubectl { this.namespace = 'default'; } } - apply(configurationPaths) { - return this.execute(['apply', '-f', this.createInlineArray(configurationPaths)]); + apply(configurationPaths, force) { + let applyArgs = ['apply', '-f', this.createInlineArray(configurationPaths)]; + if (!!force) { + console.log("force flag is on, deployment will continue even if previous deployment already exists"); + applyArgs.push('--force'); + } + return this.execute(applyArgs); } describe(resourceType, resourceName, silent) { return this.execute(['describe', resourceType, resourceName], silent); diff --git a/lib/utilities/strategy-helpers/deployment-helper.js b/lib/utilities/strategy-helpers/deployment-helper.js index 978750ca..0cf34602 100644 --- a/lib/utilities/strategy-helpers/deployment-helper.js +++ b/lib/utilities/strategy-helpers/deployment-helper.js @@ -76,10 +76,10 @@ function deployManifests(files, kubectl, isCanaryDeploymentStrategy) { else { if (canaryDeploymentHelper.isSMICanaryStrategy()) { const updatedManifests = appendStableVersionLabelToResource(files, kubectl); - result = kubectl.apply(updatedManifests); + result = kubectl.apply(updatedManifests, TaskInputParameters.forceDeployment); } else { - result = kubectl.apply(files); + result = kubectl.apply(files, TaskInputParameters.forceDeployment); } } utility_1.checkForErrors([result]); diff --git a/lib/utilities/strategy-helpers/pod-canary-deployment-helper.js b/lib/utilities/strategy-helpers/pod-canary-deployment-helper.js index 4e0ae883..79235e55 100644 --- a/lib/utilities/strategy-helpers/pod-canary-deployment-helper.js +++ b/lib/utilities/strategy-helpers/pod-canary-deployment-helper.js @@ -48,7 +48,7 @@ function deployPodCanary(kubectl, filePaths) { }); }); const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - const result = kubectl.apply(manifestFiles); + const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment); return { 'result': result, 'newFilePaths': manifestFiles }; } exports.deployPodCanary = deployPodCanary; diff --git a/lib/utilities/strategy-helpers/smi-canary-deployment-helper.js b/lib/utilities/strategy-helpers/smi-canary-deployment-helper.js index fd90abcb..18882d66 100644 --- a/lib/utilities/strategy-helpers/smi-canary-deployment-helper.js +++ b/lib/utilities/strategy-helpers/smi-canary-deployment-helper.js @@ -56,7 +56,7 @@ function deploySMICanary(kubectl, filePaths) { }); }); const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - const result = kubectl.apply(manifestFiles); + const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment); createCanaryService(kubectl, filePaths); return { 'result': result, 'newFilePaths': manifestFiles }; } @@ -111,7 +111,7 @@ function createCanaryService(kubectl, filePaths) { }); const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); manifestFiles.push(...trafficObjectsList); - const result = kubectl.apply(manifestFiles); + const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment); utility_1.checkForErrors([result]); } function redirectTrafficToCanaryDeployment(kubectl, manifestFilePaths) { @@ -144,7 +144,7 @@ function adjustTraffic(kubectl, manifestFilePaths, stableWeight, canaryWeight) { if (trafficSplitManifests.length <= 0) { return; } - const result = kubectl.apply(trafficSplitManifests); + const result = kubectl.apply(trafficSplitManifests, TaskInputParameters.forceDeployment); core.debug('serviceObjects:' + serviceObjects.join(',') + ' result:' + result); utility_1.checkForErrors([result]); } diff --git a/src/input-parameters.ts b/src/input-parameters.ts index da8f73bb..0bec64ab 100644 --- a/src/input-parameters.ts +++ b/src/input-parameters.ts @@ -11,6 +11,7 @@ export const deploymentStrategy: string = core.getInput('strategy'); export const trafficSplitMethod: string = core.getInput('traffic-split-method'); export const baselineAndCanaryReplicas: string = core.getInput('baseline-and-canary-replicas'); export const args: string = core.getInput('arguments'); +export const forceDeployment: boolean = core.getInput('force').toLowerCase() == 'true'; if (!namespace) { core.debug('Namespace was not supplied; using "default" namespace instead.'); diff --git a/src/kubectl-object-model.ts b/src/kubectl-object-model.ts index 0f9c02df..23d1494d 100644 --- a/src/kubectl-object-model.ts +++ b/src/kubectl-object-model.ts @@ -20,8 +20,15 @@ export class Kubectl { } } - public apply(configurationPaths: string | string[]): IExecSyncResult { - return this.execute(['apply', '-f', this.createInlineArray(configurationPaths)]); + public apply(configurationPaths: string | string[], force?: boolean): IExecSyncResult { + let applyArgs: string[] = ['apply', '-f', this.createInlineArray(configurationPaths)]; + + if (!!force) { + console.log("force flag is on, deployment will continue even if previous deployment already exists"); + applyArgs.push('--force'); + } + + return this.execute(applyArgs); } public describe(resourceType: string, resourceName: string, silent?: boolean): IExecSyncResult { diff --git a/src/utilities/strategy-helpers/deployment-helper.ts b/src/utilities/strategy-helpers/deployment-helper.ts index a7299cb0..8e6f1a39 100644 --- a/src/utilities/strategy-helpers/deployment-helper.ts +++ b/src/utilities/strategy-helpers/deployment-helper.ts @@ -76,10 +76,10 @@ function deployManifests(files: string[], kubectl: Kubectl, isCanaryDeploymentSt } else { if (canaryDeploymentHelper.isSMICanaryStrategy()) { const updatedManifests = appendStableVersionLabelToResource(files, kubectl); - result = kubectl.apply(updatedManifests); + result = kubectl.apply(updatedManifests, TaskInputParameters.forceDeployment); } else { - result = kubectl.apply(files); + result = kubectl.apply(files, TaskInputParameters.forceDeployment); } } checkForErrors([result]); diff --git a/src/utilities/strategy-helpers/pod-canary-deployment-helper.ts b/src/utilities/strategy-helpers/pod-canary-deployment-helper.ts index 4c4ca4df..f6b6dcf0 100644 --- a/src/utilities/strategy-helpers/pod-canary-deployment-helper.ts +++ b/src/utilities/strategy-helpers/pod-canary-deployment-helper.ts @@ -52,7 +52,7 @@ export function deployPodCanary(kubectl: Kubectl, filePaths: string[]) { }); const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - const result = kubectl.apply(manifestFiles); + const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment); return { 'result': result, 'newFilePaths': manifestFiles }; } diff --git a/src/utilities/strategy-helpers/smi-canary-deployment-helper.ts b/src/utilities/strategy-helpers/smi-canary-deployment-helper.ts index d06e3899..5239b583 100644 --- a/src/utilities/strategy-helpers/smi-canary-deployment-helper.ts +++ b/src/utilities/strategy-helpers/smi-canary-deployment-helper.ts @@ -60,7 +60,7 @@ export function deploySMICanary(kubectl: Kubectl, filePaths: string[]) { }); const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - const result = kubectl.apply(manifestFiles); + const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment); createCanaryService(kubectl, filePaths); return { 'result': result, 'newFilePaths': manifestFiles }; } @@ -121,7 +121,7 @@ function createCanaryService(kubectl: Kubectl, filePaths: string[]) { const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); manifestFiles.push(...trafficObjectsList); - const result = kubectl.apply(manifestFiles); + const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment); checkForErrors([result]); } @@ -159,7 +159,7 @@ function adjustTraffic(kubectl: Kubectl, manifestFilePaths: string[], stableWeig return; } - const result = kubectl.apply(trafficSplitManifests); + const result = kubectl.apply(trafficSplitManifests, TaskInputParameters.forceDeployment); core.debug('serviceObjects:' + serviceObjects.join(',') + ' result:' + result); checkForErrors([result]); }