diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index 1ef25b68..58d844d0 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -6,7 +6,8 @@ import * as deployment from '../src/utilities/strategy-helpers/deployment-helper import * as fs from 'fs'; import * as io from '@actions/io'; import * as toolCache from '@actions/tool-cache'; -import * as utility from '../src/utilities/utility'; +import * as fileHelper from '../src/utilities/files-helper'; +import { workflowAnnotations } from '../src/constants'; import * as inputParam from '../src/input-parameters'; import { Kubectl, Resource } from '../src/kubectl-object-model'; @@ -15,10 +16,10 @@ import { getkubectlDownloadURL } from "../src/utilities/kubectl-util"; import { mocked } from 'ts-jest/utils'; var path = require('path'); +const os = require("os"); 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); @@ -28,9 +29,29 @@ const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/rele var deploymentYaml = ""; -beforeAll(() => { +const getAllPodsMock = { + 'code': 0, + 'stdout': '{"apiVersion": "v1","items": [{"apiVersion": "v1","kind": "Pod","metadata": {"labels": {"app": "testapp","pod-template-hash": "776cbc86f9"},"name": "testpod-776cbc86f9-pjrb6","namespace": "testnamespace","ownerReferences": [{"apiVersion": "apps/v1","blockOwnerDeletion": true,"controller": true,"kind": "ReplicaSet","name": "testpod-776cbc86f9","uid": "de544628-6589-4354-81fe-05faf00d336a"}],"resourceVersion": "12362496","selfLink": "/api/v1/namespaces/akskodey8187/pods/akskodey-776cbc86f9-pjrb6","uid": "c7d5f4c1-11a1-4884-8a66-09b015c72f69"},"spec": {"containers": [{"image": "imageId","imagePullPolicy": "IfNotPresent","name": "containerName","ports": [{"containerPort": 80,"protocol": "TCP"}]}]},"status": {"hostIP": "10.240.0.4","phase": "Running","podIP": "10.244.0.25","qosClass": "BestEffort","startTime": "2020-06-04T07:59:42Z"}}]}' +}; + +const getNamespaceMock = { + 'code': 0, + 'stdout': '{"apiVersion": "v1","kind": "Namespace","metadata": {"annotations": {"workflow": ".github/workflows/workflow.yml","runUri": "https://github.com/testRepo/actions/runs/12345"}},"spec": {"finalizers": ["kubernetes"]},"status": {"phase": "Active"}}' +}; + +const resources: Resource[] = [{ type: "Deployment", name: "AppName" }]; + +beforeEach(() => { deploymentYaml = fs.readFileSync(path.join(__dirname, 'manifests', 'deployment.yml'), 'utf8'); + process.env["KUBECONFIG"] = 'kubeConfig'; + process.env['GITHUB_RUN_ID'] = '12345'; + process.env['GITHUB_WORKFLOW'] = '.github/workflows/workflow.yml'; + process.env['GITHUB_JOB'] = 'build-and-deploy'; + process.env['GITHUB_ACTOR'] = 'testUser'; + process.env['GITHUB_REPOSITORY'] = 'testRepo'; + process.env['GITHUB_SHA'] = 'testCommit'; + process.env['GITHUB_REF'] = 'testBranch'; }) test("setKubectlPath() - install a particular version", async () => { @@ -186,9 +207,12 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => { const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); const kubeCtl: jest.Mocked = new Kubectl("") as any; kubeCtl.apply = jest.fn().mockReturnValue(""); - const resources: Resource[] = [{ type: "Deployment", name: "AppName" }]; KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); - kubeCtl.getResource = jest.fn().mockReturnValue(""); + kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); + kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock); + kubeCtl.describe = jest.fn().mockReturnValue(""); + kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); + kubeCtl.annotate = jest.fn().mockReturnValue(""); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml); @@ -199,36 +223,107 @@ 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' +test("deployment - deploy() - deploy force flag on", async () => { //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(); + const applyResMock = { + 'code': 0, + 'stderr': '', + 'error': Error(""), + 'stdout': 'changes configured' + }; + const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); + const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); + const kubeCtl: jest.Mocked = new Kubectl("") as any; + KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); + kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); + kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock); + kubeCtl.describe = jest.fn().mockReturnValue(""); + kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); + kubeCtl.annotate = jest.fn().mockReturnValue(""); + KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); + + const deploySpy = jest.spyOn(kubeCtl, 'apply').mockImplementation(() => applyResMock); //Invoke and assert - await expect(action.run()).resolves.not.toThrow(); + await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); expect(deploySpy).toBeCalledWith(expect.anything(), true); deploySpy.mockRestore(); +}); + +test("deployment - deploy() - Annotate resources", async () => { + const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); + KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); + const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); + KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); + const fileHelperMock = mocked(fileHelper, true); + const fsMock = (mocked(fs, true)); + fileHelperMock.getTempDirectory = jest.fn().mockReturnValue("Local/Temp/"); + fsMock.writeFileSync =jest.fn().mockReturnValue(""); + const kubeCtl: jest.Mocked = new Kubectl("") as any; + kubeCtl.apply = jest.fn().mockReturnValue(""); + kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); + kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock); + kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9"); + kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); + kubeCtl.annotate = jest.fn().mockReturnValue(""); + + //Invoke and assert + await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); + expect(kubeCtl.annotateFiles).toBeCalledWith(["Local/Temp/deployment.yaml"], workflowAnnotations, true); + expect(kubeCtl.annotate).toBeCalledTimes(2); +}); + +test("deployment - deploy() - Skip Annotate namespace", async () => { + process.env['GITHUB_REPOSITORY'] = 'test1Repo'; + const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); + KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); + const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); + KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); + const fileHelperMock = mocked(fileHelper, true); + const fsMock = (mocked(fs, true)); + fileHelperMock.getTempDirectory = jest.fn().mockReturnValue("Local/Temp/"); + fsMock.writeFileSync =jest.fn().mockReturnValue(""); + const kubeCtl: jest.Mocked = new Kubectl("") as any; + kubeCtl.apply = jest.fn().mockReturnValue(""); + kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); + kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock); + kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9"); + kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); + kubeCtl.annotate = jest.fn().mockReturnValue(""); + + const consoleOutputSpy = jest.spyOn(process.stdout, "write").mockImplementation(); + + //Invoke and assert + await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); + expect(kubeCtl.annotateFiles).toBeCalledWith(["Local/Temp/deployment.yaml"], workflowAnnotations, true); + expect(kubeCtl.annotate).toBeCalledTimes(1); + expect(consoleOutputSpy).toHaveBeenNthCalledWith(1, `##[debug]Skipping 'annotate namespace' as namespace annotated by other workflow` + os.EOL) +}); + +test("deployment - deploy() - Annotate resources failed", async () => { + //Mocks + inputParamMock.forceDeployment = true; + const annotateMock = { + 'code': 1, + 'stderr': 'kubectl annotate failed', + 'error': Error(""), + 'stdout': '' + }; + const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); + const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); + const kubeCtl: jest.Mocked = new Kubectl("") as any; + KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); + kubeCtl.apply = jest.fn().mockReturnValue(""); + kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); + kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock); + kubeCtl.describe = jest.fn().mockReturnValue(""); + kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); + kubeCtl.annotate = jest.fn().mockReturnValue(annotateMock); + KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); + + const consoleOutputSpy = jest.spyOn(process.stdout, "write").mockImplementation(); + //Invoke and assert + await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); + expect(consoleOutputSpy).toHaveBeenNthCalledWith(1, '##[warning]kubectl annotate failed' + os.EOL) }); \ No newline at end of file diff --git a/lib/constants.js b/lib/constants.js index 8f537d25..a45ffc04 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.workloadTypesWithRolloutStatus = exports.workloadTypes = exports.deploymentTypes = exports.ServiceTypes = exports.DiscoveryAndLoadBalancerResource = exports.KubernetesWorkload = void 0; +exports.workflowAnnotations = exports.workloadTypesWithRolloutStatus = exports.workloadTypes = exports.deploymentTypes = exports.ServiceTypes = exports.DiscoveryAndLoadBalancerResource = exports.KubernetesWorkload = void 0; class KubernetesWorkload { } exports.KubernetesWorkload = KubernetesWorkload; @@ -25,3 +25,15 @@ ServiceTypes.clusterIP = 'ClusterIP'; exports.deploymentTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset']; exports.workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob']; exports.workloadTypesWithRolloutStatus = ['deployment', 'daemonset', 'statefulset']; +exports.workflowAnnotations = [ + `run=${process.env['GITHUB_RUN_ID']}`, + `repository=${process.env['GITHUB_REPOSITORY']}`, + `workflow=${process.env['GITHUB_WORKFLOW']}`, + `jobName=${process.env['GITHUB_JOB']}`, + `createdBy=${process.env['GITHUB_ACTOR']}`, + `runUri=https://github.com/${process.env['GITHUB_REPOSITORY']}/actions/runs/${process.env['GITHUB_RUN_ID']}`, + `commit=${process.env['GITHUB_SHA']}`, + `branch=${process.env['GITHUB_REF']}`, + `deployTimestamp=${Date.now()}`, + `provider=GitHub` +]; diff --git a/lib/kubectl-object-model.js b/lib/kubectl-object-model.js index 5c19c509..502a8c83 100644 --- a/lib/kubectl-object-model.js +++ b/lib/kubectl-object-model.js @@ -1,13 +1,4 @@ "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.Kubectl = void 0; const tool_runner_1 = require("./utilities/tool-runner"); @@ -34,19 +25,34 @@ class Kubectl { return this.execute(['describe', resourceType, resourceName], silent); } getNewReplicaSet(deployment) { - return __awaiter(this, void 0, void 0, function* () { - let newReplicaSet = ''; - const result = yield this.describe('deployment', deployment, true); - if (result && result.stdout) { - const stdout = result.stdout.split('\n'); - stdout.forEach((line) => { - if (!!line && line.toLowerCase().indexOf('newreplicaset') > -1) { - newReplicaSet = line.substr(14).trim().split(' ')[0]; - } - }); - } - return newReplicaSet; - }); + let newReplicaSet = ''; + const result = this.describe('deployment', deployment, true); + if (result && result.stdout) { + const stdout = result.stdout.split('\n'); + stdout.forEach((line) => { + if (!!line && line.toLowerCase().indexOf('newreplicaset') > -1) { + newReplicaSet = line.substr(14).trim().split(' ')[0]; + } + }); + } + return newReplicaSet; + } + annotate(resourceType, resourceName, annotations, overwrite) { + let args = ['annotate', resourceType, resourceName]; + args = args.concat(annotations); + if (!!overwrite) { + args.push(`--overwrite`); + } + return this.execute(args); + } + annotateFiles(files, annotations, overwrite) { + let args = ['annotate']; + args = args.concat(['-f', this.createInlineArray(files)]); + args = args.concat(annotations); + if (!!overwrite) { + args.push(`--overwrite`); + } + return this.execute(args); } getAllPods() { return this.execute(['get', 'pods', '-o', 'json'], true); diff --git a/lib/utilities/manifest-utilities.js b/lib/utilities/manifest-utilities.js index e51b12c6..58e828d0 100644 --- a/lib/utilities/manifest-utilities.js +++ b/lib/utilities/manifest-utilities.js @@ -252,7 +252,7 @@ function updateImagePullSecretsInManifestFiles(filePaths, imagePullSecrets) { } }); }); - core.debug('New K8s objects after addin imagePullSecrets are :' + JSON.stringify(newObjectsList)); + core.debug('New K8s objects after adding imagePullSecrets are :' + JSON.stringify(newObjectsList)); const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList); return newFilePaths; } diff --git a/lib/utilities/resource-object-utility.js b/lib/utilities/resource-object-utility.js index e847f985..97aab200 100644 --- a/lib/utilities/resource-object-utility.js +++ b/lib/utilities/resource-object-utility.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getResources = exports.updateSelectorLabels = exports.updateSpecLabels = exports.updateImagePullSecrets = exports.updateObjectLabels = exports.getReplicaCount = exports.isIngressEntity = exports.isServiceEntity = exports.isWorkloadEntity = exports.isDeploymentEntity = void 0; +exports.getResources = exports.updateSelectorLabels = exports.updateSpecLabels = exports.updateImagePullSecrets = exports.updateObjectAnnotations = exports.updateObjectLabels = exports.getReplicaCount = exports.isIngressEntity = exports.isServiceEntity = exports.isWorkloadEntity = exports.isDeploymentEntity = void 0; const fs = require("fs"); const core = require("@actions/core"); const yaml = require("js-yaml"); @@ -78,6 +78,31 @@ function updateObjectLabels(inputObject, newLabels, override) { } } exports.updateObjectLabels = updateObjectLabels; +function updateObjectAnnotations(inputObject, newAnnotations, override) { + if (!inputObject) { + throw ('NullInputObject'); + } + if (!inputObject.metadata) { + throw ('NullInputObjectMetadata'); + } + if (!newAnnotations) { + return; + } + if (override) { + inputObject.metadata.annotations = newAnnotations; + } + else { + let existingAnnotations = inputObject.metadata.annotations; + if (!existingAnnotations) { + existingAnnotations = new Map(); + } + Object.keys(newAnnotations).forEach(function (key) { + existingAnnotations[key] = newAnnotations[key]; + }); + inputObject.metadata.annotations = existingAnnotations; + } +} +exports.updateObjectAnnotations = updateObjectAnnotations; function updateImagePullSecrets(inputObject, newImagePullSecrets, override) { if (!inputObject || !inputObject.spec || !newImagePullSecrets) { return; diff --git a/lib/utilities/strategy-helpers/canary-deployment-helper.js b/lib/utilities/strategy-helpers/canary-deployment-helper.js index 7df15ce1..d4518630 100644 --- a/lib/utilities/strategy-helpers/canary-deployment-helper.js +++ b/lib/utilities/strategy-helpers/canary-deployment-helper.js @@ -155,6 +155,7 @@ function addCanaryLabelsAndAnnotations(inputObject, type) { const newLabels = new Map(); newLabels[exports.CANARY_VERSION_LABEL] = type; helper.updateObjectLabels(inputObject, newLabels, false); + helper.updateObjectAnnotations(inputObject, newLabels, false); helper.updateSelectorLabels(inputObject, newLabels, false); if (!helper.isServiceEntity(inputObject.kind)) { helper.updateSpecLabels(inputObject, newLabels, false); diff --git a/lib/utilities/strategy-helpers/deployment-helper.js b/lib/utilities/strategy-helpers/deployment-helper.js index 2afc0378..b2914a5b 100644 --- a/lib/utilities/strategy-helpers/deployment-helper.js +++ b/lib/utilities/strategy-helpers/deployment-helper.js @@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge Object.defineProperty(exports, "__esModule", { value: true }); exports.getManifestFiles = exports.deploy = void 0; const fs = require("fs"); +const core = require("@actions/core"); const yaml = require("js-yaml"); const canaryDeploymentHelper = require("./canary-deployment-helper"); const KubernetesObjectUtility = require("../resource-object-utility"); @@ -46,6 +47,15 @@ function deploy(kubectl, manifestFilePaths, deploymentStrategy) { ingressResources.forEach(ingressResource => { kubectl.getResource(KubernetesConstants.DiscoveryAndLoadBalancerResource.ingress, ingressResource.name); }); + // annotate resources + let allPods; + try { + allPods = JSON.parse((kubectl.getAllPods()).stdout); + } + catch (e) { + core.debug("Unable to parse pods; Error: " + e); + } + annotateResources(deployedManifestFiles, kubectl, resourceTypes, allPods); }); } exports.deploy = deploy; @@ -121,6 +131,18 @@ function checkManifestStability(kubectl, resources) { yield KubernetesManifestUtility.checkManifestStability(kubectl, resources); }); } +function annotateResources(files, kubectl, resourceTypes, allPods) { + const annotateResults = []; + annotateResults.push(utility_1.annotateNamespace(kubectl, TaskInputParameters.namespace)); + annotateResults.push(kubectl.annotateFiles(files, models.workflowAnnotations, true)); + resourceTypes.forEach(resource => { + if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { + utility_1.annotateChildPods(kubectl, resource.type, resource.name, allPods) + .forEach(execResult => annotateResults.push(execResult)); + } + }); + utility_1.checkForErrors(annotateResults, true); +} function isCanaryDeploymentStrategy(deploymentStrategy) { return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase(); } diff --git a/lib/utilities/utility.js b/lib/utilities/utility.js index db0543a5..203f28e6 100644 --- a/lib/utilities/utility.js +++ b/lib/utilities/utility.js @@ -1,8 +1,9 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0; +exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.annotateNamespace = exports.annotateChildPods = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0; const os = require("os"); const core = require("@actions/core"); +const constants_1 = require("../constants"); function getExecutableExtension() { if (os.type().match(/^Win/)) { return '.exe'; @@ -29,7 +30,7 @@ function checkForErrors(execResults, warnIfError) { if (execResults.length !== 0) { let stderr = ''; execResults.forEach(result => { - if (result.stderr) { + if (result && result.stderr) { if (result.code !== 0) { stderr += result.stderr + '\n'; } @@ -39,7 +40,7 @@ function checkForErrors(execResults, warnIfError) { } }); if (stderr.length > 0) { - if (!!warnIfError) { + if (warnIfError) { core.warning(stderr.trim()); } else { @@ -49,6 +50,50 @@ function checkForErrors(execResults, warnIfError) { } } exports.checkForErrors = checkForErrors; +function annotateChildPods(kubectl, resourceType, resourceName, allPods) { + const commandExecutionResults = []; + let owner = resourceName; + if (resourceType.toLowerCase().indexOf('deployment') > -1) { + owner = kubectl.getNewReplicaSet(resourceName); + } + if (allPods && allPods.items && allPods.items.length > 0) { + allPods.items.forEach((pod) => { + const owners = pod.metadata.ownerReferences; + if (owners) { + for (let ownerRef of owners) { + if (ownerRef.name === owner) { + commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, constants_1.workflowAnnotations, true)); + break; + } + } + } + }); + } + return commandExecutionResults; +} +exports.annotateChildPods = annotateChildPods; +function annotateNamespace(kubectl, namespaceName) { + const result = kubectl.getResource('namespace', namespaceName); + if (!result) { + return { code: 1, stderr: 'Failed to get resource' }; + } + else { + if (result.stderr) { + return result; + } + else if (result.stdout) { + const annotationsSet = JSON.parse(result.stdout).metadata.annotations; + if (annotationsSet && annotationsSet.runUri) { + if (annotationsSet.runUri.indexOf(process.env['GITHUB_REPOSITORY']) == -1) { + core.debug(`Skipping 'annotate namespace' as namespace annotated by other workflow`); + return { code: 0, stdout: '' }; + } + } + return kubectl.annotate('namespace', namespaceName, constants_1.workflowAnnotations, true); + } + } +} +exports.annotateNamespace = annotateNamespace; function sleep(timeout) { return new Promise(resolve => setTimeout(resolve, timeout)); } diff --git a/src/constants.ts b/src/constants.ts index 4d523e56..665e0003 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -24,3 +24,16 @@ export class ServiceTypes { export const deploymentTypes: string[] = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset']; export const workloadTypes: string[] = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob']; export const workloadTypesWithRolloutStatus: string[] = ['deployment', 'daemonset', 'statefulset']; + +export const workflowAnnotations = [ + `run=${process.env['GITHUB_RUN_ID']}`, + `repository=${process.env['GITHUB_REPOSITORY']}`, + `workflow=${process.env['GITHUB_WORKFLOW']}`, + `jobName=${process.env['GITHUB_JOB']}`, + `createdBy=${process.env['GITHUB_ACTOR']}`, + `runUri=https://github.com/${process.env['GITHUB_REPOSITORY']}/actions/runs/${process.env['GITHUB_RUN_ID']}`, + `commit=${process.env['GITHUB_SHA']}`, + `branch=${process.env['GITHUB_REF']}`, + `deployTimestamp=${Date.now()}`, + `provider=GitHub` +]; \ No newline at end of file diff --git a/src/kubectl-object-model.ts b/src/kubectl-object-model.ts index 3293161e..23d1494d 100644 --- a/src/kubectl-object-model.ts +++ b/src/kubectl-object-model.ts @@ -1,4 +1,4 @@ -import { ToolRunner, IExecOptions } from "./utilities/tool-runner"; +import { ToolRunner, IExecOptions, IExecSyncResult } from "./utilities/tool-runner"; export interface Resource { name: string; @@ -20,7 +20,7 @@ export class Kubectl { } } - public apply(configurationPaths: string | string[], force?: boolean) { + public apply(configurationPaths: string | string[], force?: boolean): IExecSyncResult { let applyArgs: string[] = ['apply', '-f', this.createInlineArray(configurationPaths)]; if (!!force) { @@ -31,13 +31,13 @@ export class Kubectl { return this.execute(applyArgs); } - public describe(resourceType: string, resourceName: string, silent?: boolean) { + public describe(resourceType: string, resourceName: string, silent?: boolean): IExecSyncResult { return this.execute(['describe', resourceType, resourceName], silent); } - public async getNewReplicaSet(deployment: string) { + public getNewReplicaSet(deployment: string) { let newReplicaSet = ''; - const result = await this.describe('deployment', deployment, true); + const result = this.describe('deployment', deployment, true); if (result && result.stdout) { const stdout = result.stdout.split('\n'); stdout.forEach((line: string) => { @@ -50,19 +50,34 @@ export class Kubectl { return newReplicaSet; } - public getAllPods() { + public annotate(resourceType: string, resourceName: string, annotations: string[], overwrite?: boolean): IExecSyncResult { + let args = ['annotate', resourceType, resourceName]; + args = args.concat(annotations); + if (!!overwrite) { args.push(`--overwrite`); } + return this.execute(args); + } + + public annotateFiles(files: string | string[], annotations: string[], overwrite?: boolean): IExecSyncResult { + let args = ['annotate']; + args = args.concat(['-f', this.createInlineArray(files)]); + args = args.concat(annotations); + if (!!overwrite) { args.push(`--overwrite`); } + return this.execute(args); + } + + public getAllPods(): IExecSyncResult { return this.execute(['get', 'pods', '-o', 'json'], true); } - public getClusterInfo() { + public getClusterInfo(): IExecSyncResult { return this.execute(['cluster-info'], true); } - public checkRolloutStatus(resourceType: string, name: string) { + public checkRolloutStatus(resourceType: string, name: string): IExecSyncResult { return this.execute(['rollout', 'status', resourceType + '/' + name]); } - public getResource(resourceType: string, name: string) { + public getResource(resourceType: string, name: string): IExecSyncResult { return this.execute(['get', resourceType + '/' + name, '-o', 'json']); } @@ -87,7 +102,7 @@ export class Kubectl { } public executeCommand(customCommand: string, args?: string) { - if(!customCommand) + if (!customCommand) throw new Error('NullCommandForKubectl'); return args ? this.execute([customCommand, args]) : this.execute([customCommand]); } diff --git a/src/utilities/manifest-utilities.ts b/src/utilities/manifest-utilities.ts index 93a3f821..edec4ab1 100644 --- a/src/utilities/manifest-utilities.ts +++ b/src/utilities/manifest-utilities.ts @@ -263,7 +263,7 @@ function updateImagePullSecretsInManifestFiles(filePaths: string[], imagePullSec } }); }); - core.debug('New K8s objects after addin imagePullSecrets are :' + JSON.stringify(newObjectsList)); + core.debug('New K8s objects after adding imagePullSecrets are :' + JSON.stringify(newObjectsList)); const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList); return newFilePaths; } diff --git a/src/utilities/resource-object-utility.ts b/src/utilities/resource-object-utility.ts index 3e0c78b9..a8b2d1e8 100644 --- a/src/utilities/resource-object-utility.ts +++ b/src/utilities/resource-object-utility.ts @@ -90,6 +90,34 @@ export function updateObjectLabels(inputObject: any, newLabels: Map, override: boolean) { + if (!inputObject) { + throw ('NullInputObject'); + } + + if (!inputObject.metadata) { + throw ('NullInputObjectMetadata'); + } + + if (!newAnnotations) { + return; + } + if (override) { + inputObject.metadata.annotations = newAnnotations; + } else { + let existingAnnotations = inputObject.metadata.annotations; + if (!existingAnnotations) { + existingAnnotations = new Map(); + } + + Object.keys(newAnnotations).forEach(function (key) { + existingAnnotations[key] = newAnnotations[key]; + }); + + inputObject.metadata.annotations = existingAnnotations; + } +} + export function updateImagePullSecrets(inputObject: any, newImagePullSecrets: string[], override: boolean) { if (!inputObject || !inputObject.spec || !newImagePullSecrets) { return; diff --git a/src/utilities/strategy-helpers/canary-deployment-helper.ts b/src/utilities/strategy-helpers/canary-deployment-helper.ts index 88fcca1a..541c48c0 100644 --- a/src/utilities/strategy-helpers/canary-deployment-helper.ts +++ b/src/utilities/strategy-helpers/canary-deployment-helper.ts @@ -177,6 +177,7 @@ function addCanaryLabelsAndAnnotations(inputObject: any, type: string) { newLabels[CANARY_VERSION_LABEL] = type; helper.updateObjectLabels(inputObject, newLabels, false); + helper.updateObjectAnnotations(inputObject, newLabels, false); helper.updateSelectorLabels(inputObject, newLabels, false); if (!helper.isServiceEntity(inputObject.kind)) { diff --git a/src/utilities/strategy-helpers/deployment-helper.ts b/src/utilities/strategy-helpers/deployment-helper.ts index 7c91d8df..daa64677 100644 --- a/src/utilities/strategy-helpers/deployment-helper.ts +++ b/src/utilities/strategy-helpers/deployment-helper.ts @@ -1,6 +1,7 @@ 'use strict'; import * as fs from 'fs'; +import * as core from '@actions/core'; import * as yaml from 'js-yaml'; import * as canaryDeploymentHelper from './canary-deployment-helper'; import * as KubernetesObjectUtility from '../resource-object-utility'; @@ -12,9 +13,11 @@ import * as KubernetesManifestUtility from '../manifest-stability-utility'; import * as KubernetesConstants from '../../constants'; import { Kubectl, Resource } from '../../kubectl-object-model'; import { getUpdatedManifestFiles } from '../manifest-utilities'; +import { IExecSyncResult } from '../../utilities/tool-runner'; + import { deployPodCanary } from './pod-canary-deployment-helper'; import { deploySMICanary } from './smi-canary-deployment-helper'; -import { checkForErrors } from "../utility"; +import { checkForErrors, annotateChildPods, annotateNamespace } from "../utility"; import { isBlueGreenDeploymentStrategy, isIngressRoute, isSMIRoute, routeBlueGreen } from './blue-green-helper'; import { deployBlueGreenService } from './service-blue-green-helper'; import { deployBlueGreenIngress } from './ingress-blue-green-helper'; @@ -42,6 +45,16 @@ export async function deploy(kubectl: Kubectl, manifestFilePaths: string[], depl ingressResources.forEach(ingressResource => { kubectl.getResource(KubernetesConstants.DiscoveryAndLoadBalancerResource.ingress, ingressResource.name); }); + + // annotate resources + let allPods: any; + try { + allPods = JSON.parse((kubectl.getAllPods()).stdout); + } catch (e) { + core.debug("Unable to parse pods; Error: " + e); + } + + annotateResources(deployedManifestFiles, kubectl, resourceTypes, allPods); } export function getManifestFiles(manifestFilePaths: string[]): string[] { @@ -115,6 +128,19 @@ async function checkManifestStability(kubectl: Kubectl, resources: Resource[]): await KubernetesManifestUtility.checkManifestStability(kubectl, resources); } +function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any) { + const annotateResults: IExecSyncResult[] = []; + annotateResults.push(annotateNamespace(kubectl, TaskInputParameters.namespace)); + annotateResults.push(kubectl.annotateFiles(files, models.workflowAnnotations, true)); + resourceTypes.forEach(resource => { + if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { + annotateChildPods(kubectl, resource.type, resource.name, allPods) + .forEach(execResult => annotateResults.push(execResult)); + } + }); + checkForErrors(annotateResults, true); +} + function isCanaryDeploymentStrategy(deploymentStrategy: string): boolean { return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase(); } diff --git a/src/utilities/utility.ts b/src/utilities/utility.ts index 75799ab1..9c4c1cce 100644 --- a/src/utilities/utility.ts +++ b/src/utilities/utility.ts @@ -1,5 +1,8 @@ import * as os from 'os'; import * as core from '@actions/core'; +import { IExecSyncResult } from './tool-runner'; +import { Kubectl } from '../kubectl-object-model'; +import { workflowAnnotations } from '../constants'; export function getExecutableExtension(): string { if (os.type().match(/^Win/)) { @@ -25,11 +28,11 @@ export function isEqual(str1: string, str2: string, ignoreCase?: boolean): boole } } -export function checkForErrors(execResults, warnIfError?: boolean) { +export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boolean) { if (execResults.length !== 0) { let stderr = ''; execResults.forEach(result => { - if (result.stderr) { + if (result && result.stderr) { if (result.code !== 0) { stderr += result.stderr + '\n'; } else { @@ -38,7 +41,7 @@ export function checkForErrors(execResults, warnIfError?: boolean) { } }); if (stderr.length > 0) { - if (!!warnIfError) { + if (warnIfError) { core.warning(stderr.trim()); } else { throw new Error(stderr.trim()); @@ -47,6 +50,53 @@ export function checkForErrors(execResults, warnIfError?: boolean) { } } +export function annotateChildPods(kubectl: Kubectl, resourceType: string, resourceName: string, allPods): IExecSyncResult[] { + const commandExecutionResults = []; + let owner = resourceName; + if (resourceType.toLowerCase().indexOf('deployment') > -1) { + owner = kubectl.getNewReplicaSet(resourceName); + } + + if (allPods && allPods.items && allPods.items.length > 0) { + allPods.items.forEach((pod) => { + const owners = pod.metadata.ownerReferences; + if (owners) { + for (let ownerRef of owners) { + if (ownerRef.name === owner) { + commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, workflowAnnotations, true)); + break; + } + } + } + }); + } + + return commandExecutionResults; +} + +export function annotateNamespace(kubectl: Kubectl, namespaceName: string): IExecSyncResult { + const result = kubectl.getResource('namespace', namespaceName); + if (!result) { + return { code: 1, stderr: 'Failed to get resource' } as IExecSyncResult; + } + else { + if (result.stderr) { + return result; + } + + else if (result.stdout) { + const annotationsSet = JSON.parse(result.stdout).metadata.annotations; + if (annotationsSet && annotationsSet.runUri) { + if (annotationsSet.runUri.indexOf(process.env['GITHUB_REPOSITORY']) == -1) { + core.debug(`Skipping 'annotate namespace' as namespace annotated by other workflow`); + return { code: 0, stdout: '' } as IExecSyncResult; + } + } + return kubectl.annotate('namespace', namespaceName, workflowAnnotations, true); + } + } +} + export function sleep(timeout: number) { return new Promise(resolve => setTimeout(resolve, timeout)); }