diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index 339501d2..1abdcdc7 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -6,6 +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 fileHelper from '../src/utilities/files-helper'; +import { workflowAnnotations } from '../src/constants'; import { Kubectl, Resource } from '../src/kubectl-object-model'; @@ -24,9 +26,29 @@ const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/rele var deploymentYaml = ""; +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" }]; + beforeAll(() => { 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 () => { @@ -182,9 +204,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); @@ -193,4 +218,25 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => { await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); expect(readFileSpy).toBeCalledWith("manifests/deployment.yaml"); expect(kubeCtl.getResource).toBeCalledWith("ingress", "AppName"); +}); + +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); + fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue(["~/Deployment_testapp_currentTimestamp"]); + 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(["~/Deployment_testapp_currentTimestamp"], workflowAnnotations, true); + expect(kubeCtl.annotate).toBeCalledTimes(2); }); \ No newline at end of file diff --git a/lib/constants.js b/lib/constants.js index 5367fd9c..3af484d6 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -27,10 +27,12 @@ exports.workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statef exports.workloadTypesWithRolloutStatus = ['deployment', 'daemonset', 'statefulset']; exports.workflowAnnotations = [ `run=${process.env['GITHUB_RUN_ID']}`, - `workflow="${process.env['GITHUB_WORKFLOW']}"`, - `jobName="${process.env['GITHUB_JOB']}"`, + `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']}` + `branch=${process.env['GITHUB_REF']}`, + `deployTimestamp=${Date.now()}` ]; diff --git a/lib/utilities/strategy-helpers/deployment-helper.js b/lib/utilities/strategy-helpers/deployment-helper.js index f94c16ce..978750ca 100644 --- a/lib/utilities/strategy-helpers/deployment-helper.js +++ b/lib/utilities/strategy-helpers/deployment-helper.js @@ -112,6 +112,7 @@ function 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()) { @@ -149,7 +150,7 @@ function updateResourceObjects(filePaths, imagePullSecrets, containers) { } }); }); - 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/utility.js b/lib/utilities/utility.js index 7d68af81..80c3ce47 100644 --- a/lib/utilities/utility.js +++ b/lib/utilities/utility.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.annotateChildPods = 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"); @@ -71,6 +71,20 @@ function annotateChildPods(kubectl, resourceType, resourceName, allPods) { return commandExecutionResults; } exports.annotateChildPods = annotateChildPods; +function annotateNamespace(kubectl, namespaceName) { + let annotate = true; + const result = kubectl.getResource('namespace', namespaceName); + this.checkForErrors([result]); + const annotationsSet = JSON.parse(result.stdout).metadata.annotations; + if (!!annotationsSet && !!annotationsSet.runUri && annotationsSet.runUri.indexOf(process.env['GITHUB_REPOSITORY']) == -1) { + annotate = false; + core.debug(`Skipping 'annotate namespace' as namespace annotated by other workflow`); + } + if (annotate) { + 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 5aa6f845..e9aba2fc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,12 +25,14 @@ export const deploymentTypes: string[] = ['deployment', 'replicaset', 'daemonset export const workloadTypes: string[] = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob']; export const workloadTypesWithRolloutStatus: string[] = ['deployment', 'daemonset', 'statefulset']; -export let workflowAnnotations = [ +export const workflowAnnotations = [ `run=${process.env['GITHUB_RUN_ID']}`, - `workflow="${process.env['GITHUB_WORKFLOW']}"`, - `jobName="${process.env['GITHUB_JOB']}"`, + `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']}` + `branch=${process.env['GITHUB_REF']}`, + `deployTimestamp=${Date.now()}` ]; \ No newline at end of file diff --git a/src/kubectl-object-model.ts b/src/kubectl-object-model.ts index 2dcffa0a..0f9c02df 100644 --- a/src/kubectl-object-model.ts +++ b/src/kubectl-object-model.ts @@ -1,5 +1,4 @@ import { ToolRunner, IExecOptions, IExecSyncResult } from "./utilities/tool-runner"; -import * as core from '@actions/core'; export interface Resource { name: string; diff --git a/src/utilities/strategy-helpers/deployment-helper.ts b/src/utilities/strategy-helpers/deployment-helper.ts index 121dd9d8..a7299cb0 100644 --- a/src/utilities/strategy-helpers/deployment-helper.ts +++ b/src/utilities/strategy-helpers/deployment-helper.ts @@ -17,7 +17,7 @@ import { IExecSyncResult } from '../../utilities/tool-runner'; import { deployPodCanary } from './pod-canary-deployment-helper'; import { deploySMICanary } from './smi-canary-deployment-helper'; -import { checkForErrors, annotateChildPods } from "../utility"; +import { checkForErrors, annotateChildPods, annotateNamespace } from "../utility"; export async function deploy(kubectl: Kubectl, manifestFilePaths: string[], deploymentStrategy: string) { @@ -114,6 +114,7 @@ async function checkManifestStability(kubectl: Kubectl, resources: Resource[]): 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()) { @@ -152,7 +153,7 @@ function updateResourceObjects(filePaths: string[], imagePullSecrets: string[], } }); }); - 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/utility.ts b/src/utilities/utility.ts index c1605220..bf86ba91 100644 --- a/src/utilities/utility.ts +++ b/src/utilities/utility.ts @@ -73,6 +73,20 @@ export function annotateChildPods(kubectl: Kubectl, resourceType: string, resour return commandExecutionResults; } +export function annotateNamespace(kubectl: Kubectl, namespaceName: string): IExecSyncResult { + let annotate = true; + const result = kubectl.getResource('namespace', namespaceName); + this.checkForErrors([result]); + const annotationsSet = JSON.parse(result.stdout).metadata.annotations; + if (!!annotationsSet && !!annotationsSet.runUri && annotationsSet.runUri.indexOf(process.env['GITHUB_REPOSITORY']) == -1) { + annotate = false; + core.debug(`Skipping 'annotate namespace' as namespace annotated by other workflow`); + } + if (annotate) { + return kubectl.annotate('namespace', namespaceName, workflowAnnotations, true); + } +} + export function sleep(timeout: number) { return new Promise(resolve => setTimeout(resolve, timeout)); }