From 48110f8b943481e6f7f9f74cfee047ea4025fc09 Mon Sep 17 00:00:00 2001 From: Koushik Dey Date: Wed, 22 Jul 2020 20:07:30 +0530 Subject: [PATCH 1/6] GitHub API integration, labelling, annotating namespace --- __tests__/run.test.ts | 26 ++-- action.yml | 4 + lib/constants.js | 37 ++++-- lib/githubClient.js | 38 ++++++ lib/input-parameters.js | 6 +- lib/kubectl-object-model.js | 9 ++ lib/utilities/httpClient.js | 111 +++++++++++++++++ .../strategy-helpers/deployment-helper.js | 34 ++++-- lib/utilities/utility.js | 71 ++++++----- src/constants.ts | 33 +++-- src/githubClient.ts | 30 +++++ src/input-parameters.ts | 5 + src/kubectl-object-model.ts | 8 ++ src/utilities/httpClient.ts | 114 ++++++++++++++++++ .../strategy-helpers/deployment-helper.ts | 24 +++- src/utilities/utility.ts | 60 ++++----- 16 files changed, 500 insertions(+), 110 deletions(-) create mode 100644 lib/githubClient.js create mode 100644 lib/utilities/httpClient.js create mode 100644 src/githubClient.ts create mode 100644 src/utilities/httpClient.ts diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index b28cf93e..05fea60f 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -7,10 +7,11 @@ 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 { getWorkflowAnnotationKeyLabel, getWorkflowAnnotationsJson } from '../src/constants'; import * as inputParam from '../src/input-parameters'; import { Kubectl, Resource } from '../src/kubectl-object-model'; +import { GitHubClient } from '../src/githubClient'; import { getkubectlDownloadURL } from "../src/utilities/kubectl-util"; import { mocked } from 'ts-jest/utils'; @@ -36,7 +37,7 @@ const getAllPodsMock = { 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"}}' + 'stdout': '{"apiVersion": "v1","kind": "Namespace","metadata": {"annotations": { "resourceAnnotations": "[{\'run\':\'152673324\',\'repository\':\'koushdey/hello-kubernetes\',\'workflow\':\'.github/workflows/workflowNew.yml\',\'jobName\':\'build-and-deploy\',\'createdBy\':\'koushdey\',\'runUri\':\'https://github.com/koushdey/hello-kubernetes/actions/runs/152673324\',\'commit\':\'f45c9c04ed6bbd4813019ebc6f5e94f155c974a4\',\'branch\':\'refs/heads/koushdey-rename\',\'deployTimestamp\':\'1593516378601\',\'provider\':\'GitHub\'},{\'run\':\'12345\',\'repository\':\'testRepo\',\'workflow\':\'.github/workflows/workflow.yml\',\'jobName\':\'build-and-deploy\',\'createdBy\':\'koushdey\',\'runUri\':\'https://github.com/testRepo/actions/runs/12345\',\'commit\':\'testCommit\',\'branch\':\'testBranch\',\'deployTimestamp\':\'Now\',\'provider\':\'GitHub\'}]","key":"value"}},"spec": {"finalizers": ["kubernetes"]},"status": {"phase": "Active"}}' }; const resources: Resource[] = [{ type: "Deployment", name: "AppName" }]; @@ -52,6 +53,7 @@ beforeEach(() => { process.env['GITHUB_REPOSITORY'] = 'testRepo'; process.env['GITHUB_SHA'] = 'testCommit'; process.env['GITHUB_REF'] = 'testBranch'; + process.env['GITHUB_TOKEN'] = 'testToken'; }) test("setKubectlPath() - install a particular version", async () => { @@ -213,6 +215,7 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => { kubeCtl.describe = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue(""); + kubeCtl.labelFiles = jest.fn().mockReturnValue(""); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml); @@ -241,6 +244,7 @@ test("deployment - deploy() - deploy force flag on", async () => { kubeCtl.describe = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue(""); + kubeCtl.labelFiles = jest.fn().mockReturnValue(""); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const deploySpy = jest.spyOn(kubeCtl, 'apply').mockImplementation(() => applyResMock); @@ -252,12 +256,14 @@ test("deployment - deploy() - deploy force flag on", async () => { }); test("deployment - deploy() - Annotate resources", async () => { + let annotationKeyValStr = getWorkflowAnnotationKeyLabel() + '=' + '[' + getWorkflowAnnotationsJson('lastSuccessSha') + ']'; 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); @@ -265,15 +271,17 @@ test("deployment - deploy() - Annotate resources", async () => { kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9"); kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue(""); + kubeCtl.labelFiles = 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.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [annotationKeyValStr], true); expect(kubeCtl.annotate).toBeCalledTimes(2); }); test("deployment - deploy() - Skip Annotate namespace", async () => { - process.env['GITHUB_REPOSITORY'] = 'test1Repo'; + process.env['GITHUB_REPOSITORY'] = 'testUser/test1Repo'; + let annotationKeyValStr = getWorkflowAnnotationKeyLabel() + '=' + '[' + getWorkflowAnnotationsJson('lastSuccessSha') + ']'; const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); @@ -287,14 +295,15 @@ test("deployment - deploy() - Skip Annotate namespace", async () => { kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9"); kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue(""); + kubeCtl.labelFiles = 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(["~/Deployment_testapp_currentTimestamp"], workflowAnnotations, true); - expect(kubeCtl.annotate).toBeCalledTimes(1); - expect(consoleOutputSpy).toHaveBeenNthCalledWith(2, `##[debug]Skipping 'annotate namespace' as namespace annotated by other workflow` + os.EOL) + expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [annotationKeyValStr], true); + //expect(kubeCtl.annotate).toBeCalledTimes(1); + //expect(consoleOutputSpy).toHaveBeenNthCalledWith(2, `##[debug]Skipping 'annotate namespace' as namespace annotated by other workflow` + os.EOL) }); test("deployment - deploy() - Annotate resources failed", async () => { @@ -316,10 +325,11 @@ test("deployment - deploy() - Annotate resources failed", async () => { kubeCtl.describe = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue(annotateMock); + kubeCtl.labelFiles = jest.fn().mockReturnValue(""); 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(2, '##[warning]kubectl annotate failed' + os.EOL) + expect(consoleOutputSpy).toHaveBeenNthCalledWith(4, '##[warning]kubectl annotate failed' + os.EOL) }); \ No newline at end of file diff --git a/action.yml b/action.yml index 0a1a40c7..de3033ec 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,10 @@ inputs: description: 'Deploy when a previous deployment already exists. If true then --force argument is added to the apply command.' required: false default: false + token: + description: 'Github token' + default: ${{ github.token }} + required: true branding: color: 'green' # optional, decorates the entry in the GitHub Marketplace diff --git a/lib/constants.js b/lib/constants.js index a45ffc04..421d39b0 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.workflowAnnotations = exports.workloadTypesWithRolloutStatus = exports.workloadTypes = exports.deploymentTypes = exports.ServiceTypes = exports.DiscoveryAndLoadBalancerResource = exports.KubernetesWorkload = void 0; +exports.getWorkflowAnnotationKeyLabel = exports.getWorkflowAnnotationsJson = exports.workloadTypesWithRolloutStatus = exports.workloadTypes = exports.deploymentTypes = exports.ServiceTypes = exports.DiscoveryAndLoadBalancerResource = exports.KubernetesWorkload = void 0; class KubernetesWorkload { } exports.KubernetesWorkload = KubernetesWorkload; @@ -25,15 +25,26 @@ 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` -]; +function getWorkflowAnnotationsJson(lastSuccessRunSha) { + return `{` + + `'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}',` + + `'lastSuccessRunCommit': '${lastSuccessRunSha}',` + + `'branch': '${process.env.GITHUB_REF}',` + + `'deployTimestamp': '${Date.now()}',` + + `'provider': 'GitHub'` + + `}`; +} +exports.getWorkflowAnnotationsJson = getWorkflowAnnotationsJson; +function getWorkflowAnnotationKeyLabel() { + const hashKey = require("crypto").createHash("MD5") + .update(`${process.env.GITHUB_REPOSITORY}/${process.env.GITHUB_WORKFLOW}`) + .digest("hex"); + return `githubWorkflow_${hashKey}`; +} +exports.getWorkflowAnnotationKeyLabel = getWorkflowAnnotationKeyLabel; diff --git a/lib/githubClient.js b/lib/githubClient.js new file mode 100644 index 00000000..a6497faf --- /dev/null +++ b/lib/githubClient.js @@ -0,0 +1,38 @@ +"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.GitHubClient = void 0; +const core = require("@actions/core"); +const httpClient_1 = require("./utilities/httpClient"); +class GitHubClient { + constructor(repository, token) { + this._repository = repository; + this._token = token; + } + getSuccessfulRunsOnBranch(branch, force) { + return __awaiter(this, void 0, void 0, function* () { + if (force || !this._successfulRunsOnBranchPromise) { + const lastSuccessfulRunUrl = `https://api.github.com/repos/${this._repository}/actions/runs?status=success&branch=${branch}`; + const webRequest = new httpClient_1.WebRequest(); + webRequest.method = "GET"; + webRequest.uri = lastSuccessfulRunUrl; + webRequest.headers = { + Authorization: `Bearer ${this._token}` + }; + core.debug(`Getting last successful run for repo: ${this._repository} on branch: ${branch}`); + const response = yield httpClient_1.sendRequest(webRequest); + this._successfulRunsOnBranchPromise = Promise.resolve(response); + } + return this._successfulRunsOnBranchPromise; + }); + } +} +exports.GitHubClient = GitHubClient; diff --git a/lib/input-parameters.js b/lib/input-parameters.js index 21744518..218917c4 100644 --- a/lib/input-parameters.js +++ b/lib/input-parameters.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.forceDeployment = exports.args = exports.baselineAndCanaryReplicas = exports.trafficSplitMethod = exports.deploymentStrategy = exports.canaryPercentage = exports.manifests = exports.imagePullSecrets = exports.containers = exports.namespace = void 0; +exports.githubToken = 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'); @@ -12,10 +12,14 @@ 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'; +exports.githubToken = core.getInput("token"); if (!exports.namespace) { core.debug('Namespace was not supplied; using "default" namespace instead.'); exports.namespace = 'default'; } +if (!exports.githubToken) { + core.error("'token' input is not supplied. Set it to a PAT/GITHUB_TOKEN"); +} try { const pe = parseInt(exports.canaryPercentage); if (pe < 0 || pe > 100) { diff --git a/lib/kubectl-object-model.js b/lib/kubectl-object-model.js index 502a8c83..b835b7e9 100644 --- a/lib/kubectl-object-model.js +++ b/lib/kubectl-object-model.js @@ -54,6 +54,15 @@ class Kubectl { } return this.execute(args); } + labelFiles(files, labels, overwrite) { + let args = ['label']; + args = args.concat(['-f', this.createInlineArray(files)]); + args = args.concat(labels); + if (!!overwrite) { + args.push(`--overwrite`); + } + return this.execute(args); + } getAllPods() { return this.execute(['get', 'pods', '-o', 'json'], true); } diff --git a/lib/utilities/httpClient.js b/lib/utilities/httpClient.js new file mode 100644 index 00000000..0e7941fc --- /dev/null +++ b/lib/utilities/httpClient.js @@ -0,0 +1,111 @@ +"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.sleepFor = exports.sendRequest = exports.WebRequestOptions = exports.WebResponse = exports.WebRequest = exports.StatusCodes = void 0; +// Taken from https://github.com/Azure/aks-set-context/blob/master/src/client.ts +const util = require("util"); +const fs = require("fs"); +const httpClient = require("typed-rest-client/HttpClient"); +const core = require("@actions/core"); +var httpCallbackClient = new httpClient.HttpClient('GITHUB_RUNNER', null, {}); +var StatusCodes; +(function (StatusCodes) { + StatusCodes[StatusCodes["OK"] = 200] = "OK"; + StatusCodes[StatusCodes["CREATED"] = 201] = "CREATED"; + StatusCodes[StatusCodes["ACCEPTED"] = 202] = "ACCEPTED"; + StatusCodes[StatusCodes["UNAUTHORIZED"] = 401] = "UNAUTHORIZED"; + StatusCodes[StatusCodes["NOT_FOUND"] = 404] = "NOT_FOUND"; + StatusCodes[StatusCodes["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR"; + StatusCodes[StatusCodes["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE"; +})(StatusCodes = exports.StatusCodes || (exports.StatusCodes = {})); +class WebRequest { +} +exports.WebRequest = WebRequest; +class WebResponse { +} +exports.WebResponse = WebResponse; +class WebRequestOptions { +} +exports.WebRequestOptions = WebRequestOptions; +function sendRequest(request, options) { + return __awaiter(this, void 0, void 0, function* () { + let i = 0; + let retryCount = options && options.retryCount ? options.retryCount : 5; + let retryIntervalInSeconds = options && options.retryIntervalInSeconds ? options.retryIntervalInSeconds : 2; + let retriableErrorCodes = options && options.retriableErrorCodes ? options.retriableErrorCodes : ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ESOCKETTIMEDOUT", "ECONNREFUSED", "EHOSTUNREACH", "EPIPE", "EA_AGAIN"]; + let retriableStatusCodes = options && options.retriableStatusCodes ? options.retriableStatusCodes : [408, 409, 500, 502, 503, 504]; + let timeToWait = retryIntervalInSeconds; + while (true) { + try { + if (request.body && typeof (request.body) !== 'string' && !request.body["readable"]) { + request.body = fs.createReadStream(request.body["path"]); + } + let response = yield sendRequestInternal(request); + if (retriableStatusCodes.indexOf(response.statusCode) != -1 && ++i < retryCount) { + core.debug(util.format("Encountered a retriable status code: %s. Message: '%s'.", response.statusCode, response.statusMessage)); + yield sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + continue; + } + return response; + } + catch (error) { + if (retriableErrorCodes.indexOf(error.code) != -1 && ++i < retryCount) { + core.debug(util.format("Encountered a retriable error:%s. Message: %s.", error.code, error.message)); + yield sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + } + else { + if (error.code) { + core.debug("error code =" + error.code); + } + throw error; + } + } + } + }); +} +exports.sendRequest = sendRequest; +function sleepFor(sleepDurationInSeconds) { + return new Promise((resolve, reject) => { + setTimeout(resolve, sleepDurationInSeconds * 1000); + }); +} +exports.sleepFor = sleepFor; +function sendRequestInternal(request) { + return __awaiter(this, void 0, void 0, function* () { + core.debug(util.format("[%s]%s", request.method, request.uri)); + var response = yield httpCallbackClient.request(request.method, request.uri, request.body, request.headers); + return yield toWebResponse(response); + }); +} +function toWebResponse(response) { + return __awaiter(this, void 0, void 0, function* () { + var res = new WebResponse(); + if (response) { + res.statusCode = response.message.statusCode; + res.statusMessage = response.message.statusMessage; + res.headers = response.message.headers; + var body = yield response.readBody(); + if (body) { + try { + res.body = JSON.parse(body); + } + catch (error) { + core.debug("Could not parse response: " + JSON.stringify(error)); + core.debug("Response: " + JSON.stringify(res.body)); + res.body = body; + } + } + } + return res; + }); +} diff --git a/lib/utilities/strategy-helpers/deployment-helper.js b/lib/utilities/strategy-helpers/deployment-helper.js index 0cf34602..cf9880d6 100644 --- a/lib/utilities/strategy-helpers/deployment-helper.js +++ b/lib/utilities/strategy-helpers/deployment-helper.js @@ -49,7 +49,7 @@ function deploy(kubectl, manifestFilePaths, deploymentStrategy) { catch (e) { core.debug("Unable to parse pods; Error: " + e); } - annotateResources(deployedManifestFiles, kubectl, resourceTypes, allPods); + annotateAndLabelResources(deployedManifestFiles, kubectl, resourceTypes, allPods); }); } exports.deploy = deploy; @@ -110,17 +110,29 @@ 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)); - } +function annotateAndLabelResources(files, kubectl, resourceTypes, allPods) { + const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(); + annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel); + labelResources(files, kubectl, annotationKeyLabel); +} +function annotateResources(files, kubectl, resourceTypes, allPods, annotationKey) { + return __awaiter(this, void 0, void 0, function* () { + const annotateResults = []; + const lastSuccessSha = yield utility_1.getLastSuccessfulRunSha(TaskInputParameters.githubToken); + let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); + annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, [annotationKeyValStr], true)); + annotateResults.push(kubectl.annotateFiles(files, [annotationKeyValStr], true)); + resourceTypes.forEach(resource => { + if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { + utility_1.annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods) + .forEach(execResult => annotateResults.push(execResult)); + } + }); + utility_1.checkForErrors(annotateResults, true); }); - utility_1.checkForErrors(annotateResults, true); +} +function labelResources(files, kubectl, label) { + utility_1.checkForErrors([kubectl.labelFiles(files, [`workflow=${label}`], true)], true); } function updateResourceObjects(filePaths, imagePullSecrets, containers) { const newObjectsList = []; diff --git a/lib/utilities/utility.js b/lib/utilities/utility.js index 58f160a3..11f31cbe 100644 --- a/lib/utilities/utility.js +++ b/lib/utilities/utility.js @@ -1,9 +1,19 @@ "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.getCurrentTime = exports.getRandomInt = exports.sleep = exports.annotateNamespace = exports.annotateChildPods = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0; +exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.annotateChildPods = exports.getLastSuccessfulRunSha = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0; const os = require("os"); const core = require("@actions/core"); -const constants_1 = require("../constants"); +const githubClient_1 = require("../githubClient"); +const httpClient_1 = require("./httpClient"); function getExecutableExtension() { if (os.type().match(/^Win/)) { return '.exe'; @@ -18,7 +28,7 @@ function isEqual(str1, str2, ignoreCase) { if (str1 == null || str2 == null) { return false; } - if (ignoreCase) { + if (!!ignoreCase) { return str1.toUpperCase() === str2.toUpperCase(); } else { @@ -30,7 +40,7 @@ function checkForErrors(execResults, warnIfError) { if (execResults.length !== 0) { let stderr = ''; execResults.forEach(result => { - if (result && result.stderr) { + if (!!result && !!result.stderr) { if (result.code !== 0) { stderr += result.stderr + '\n'; } @@ -40,7 +50,7 @@ function checkForErrors(execResults, warnIfError) { } }); if (stderr.length > 0) { - if (warnIfError) { + if (!!warnIfError) { core.warning(stderr.trim()); } else { @@ -50,19 +60,42 @@ function checkForErrors(execResults, warnIfError) { } } exports.checkForErrors = checkForErrors; -function annotateChildPods(kubectl, resourceType, resourceName, allPods) { +function getLastSuccessfulRunSha(githubToken) { + return __awaiter(this, void 0, void 0, function* () { + let lastSuccessRunSha = ''; + const gitHubClient = new githubClient_1.GitHubClient(process.env.GITHUB_REPOSITORY, githubToken); + const branch = process.env.GITHUB_REF.replace("refs/heads/", ""); + const response = yield gitHubClient.getSuccessfulRunsOnBranch(branch); + if (response.statusCode == httpClient_1.StatusCodes.OK + && response.body + && response.body.total_count) { + if (response.body.total_count > 0) { + lastSuccessRunSha = response.body.workflow_runs[0].head_sha; + } + else { + lastSuccessRunSha = 'NA'; + } + } + else if (response.statusCode != httpClient_1.StatusCodes.OK) { + core.debug(`An error occured while getting succeessful run results. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); + } + return lastSuccessRunSha; + }); +} +exports.getLastSuccessfulRunSha = getLastSuccessfulRunSha; +function annotateChildPods(kubectl, resourceType, resourceName, annotationKeyValStr, allPods) { const commandExecutionResults = []; let owner = resourceName; if (resourceType.toLowerCase().indexOf('deployment') > -1) { owner = kubectl.getNewReplicaSet(resourceName); } - if (allPods && allPods.items && allPods.items.length > 0) { + if (!!allPods && !!allPods.items && allPods.items.length > 0) { allPods.items.forEach((pod) => { const owners = pod.metadata.ownerReferences; - if (owners) { + if (!!owners) { owners.forEach(ownerRef => { if (ownerRef.name === owner) { - commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, constants_1.workflowAnnotations, true)); + commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, [annotationKeyValStr], true)); } }); } @@ -71,26 +104,6 @@ function annotateChildPods(kubectl, resourceType, resourceName, allPods) { 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 && result.stderr) { - return result; - } - if (result && 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 1add6086..c9f2d3fc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,15 +25,24 @@ 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 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 +export function getWorkflowAnnotationsJson(lastSuccessRunSha: string): string { + return `{` + + `'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}',` + + `'lastSuccessRunCommit': '${lastSuccessRunSha}',` + + `'branch': '${process.env.GITHUB_REF}',` + + `'deployTimestamp': '${Date.now()}',` + + `'provider': 'GitHub'` + + `}`; +} +export function getWorkflowAnnotationKeyLabel(): string { + const hashKey = require("crypto").createHash("MD5") + .update(`${process.env.GITHUB_REPOSITORY}/${process.env.GITHUB_WORKFLOW}`) + .digest("hex"); + return `githubWorkflow_${hashKey}`; +} \ No newline at end of file diff --git a/src/githubClient.ts b/src/githubClient.ts new file mode 100644 index 00000000..442592b6 --- /dev/null +++ b/src/githubClient.ts @@ -0,0 +1,30 @@ +import * as core from '@actions/core'; +import { WebRequest, WebResponse, sendRequest } from "./utilities/httpClient"; + +export class GitHubClient { + constructor(repository: string, token: string) { + this._repository = repository; + this._token = token; + } + + public async getSuccessfulRunsOnBranch(branch: string, force?: boolean): Promise { + if (force || !this._successfulRunsOnBranchPromise) { + const lastSuccessfulRunUrl = `https://api.github.com/repos/${this._repository}/actions/runs?status=success&branch=${branch}`; + const webRequest = new WebRequest(); + webRequest.method = "GET"; + webRequest.uri = lastSuccessfulRunUrl; + webRequest.headers = { + Authorization: `Bearer ${this._token}` + }; + + core.debug(`Getting last successful run for repo: ${this._repository} on branch: ${branch}`); + const response: WebResponse = await sendRequest(webRequest); + this._successfulRunsOnBranchPromise = Promise.resolve(response); + } + return this._successfulRunsOnBranchPromise; + } + + private _repository: string; + private _token: string; + private _successfulRunsOnBranchPromise: Promise; +} \ No newline at end of file diff --git a/src/input-parameters.ts b/src/input-parameters.ts index 0bec64ab..d1b7a768 100644 --- a/src/input-parameters.ts +++ b/src/input-parameters.ts @@ -12,12 +12,17 @@ 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'; +export const githubToken = core.getInput("token"); if (!namespace) { core.debug('Namespace was not supplied; using "default" namespace instead.'); namespace = 'default'; } +if (!githubToken) { + core.error("'token' input is not supplied. Set it to a PAT/GITHUB_TOKEN"); +} + try { const pe = parseInt(canaryPercentage); if (pe < 0 || pe > 100) { diff --git a/src/kubectl-object-model.ts b/src/kubectl-object-model.ts index 23d1494d..65d354ea 100644 --- a/src/kubectl-object-model.ts +++ b/src/kubectl-object-model.ts @@ -65,6 +65,14 @@ export class Kubectl { return this.execute(args); } + public labelFiles(files: string | string[], labels: string[], overwrite?: boolean): IExecSyncResult { + let args = ['label']; + args = args.concat(['-f', this.createInlineArray(files)]); + args = args.concat(labels); + if (!!overwrite) { args.push(`--overwrite`); } + return this.execute(args); + } + public getAllPods(): IExecSyncResult { return this.execute(['get', 'pods', '-o', 'json'], true); } diff --git a/src/utilities/httpClient.ts b/src/utilities/httpClient.ts new file mode 100644 index 00000000..fe5d4805 --- /dev/null +++ b/src/utilities/httpClient.ts @@ -0,0 +1,114 @@ +// Taken from https://github.com/Azure/aks-set-context/blob/master/src/client.ts +import util = require("util"); +import fs = require('fs'); +import httpClient = require("typed-rest-client/HttpClient"); +import * as core from '@actions/core'; + +var httpCallbackClient = new httpClient.HttpClient('GITHUB_RUNNER', null, {}); + +export enum StatusCodes { + OK = 200, + CREATED = 201, + ACCEPTED = 202, + UNAUTHORIZED = 401, + NOT_FOUND = 404, + INTERNAL_SERVER_ERROR = 500, + SERVICE_UNAVAILABLE = 503 +} + +export class WebRequest { + public method: string; + public uri: string; + // body can be string or ReadableStream + public body: string | NodeJS.ReadableStream; + public headers: any; +} + +export class WebResponse { + public statusCode: number; + public statusMessage: string; + public headers: any; + public body: any; +} + +export class WebRequestOptions { + public retriableErrorCodes?: string[]; + public retryCount?: number; + public retryIntervalInSeconds?: number; + public retriableStatusCodes?: number[]; + public retryRequestTimedout?: boolean; +} + +export async function sendRequest(request: WebRequest, options?: WebRequestOptions): Promise { + let i = 0; + let retryCount = options && options.retryCount ? options.retryCount : 5; + let retryIntervalInSeconds = options && options.retryIntervalInSeconds ? options.retryIntervalInSeconds : 2; + let retriableErrorCodes = options && options.retriableErrorCodes ? options.retriableErrorCodes : ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ESOCKETTIMEDOUT", "ECONNREFUSED", "EHOSTUNREACH", "EPIPE", "EA_AGAIN"]; + let retriableStatusCodes = options && options.retriableStatusCodes ? options.retriableStatusCodes : [408, 409, 500, 502, 503, 504]; + let timeToWait: number = retryIntervalInSeconds; + while (true) { + try { + if (request.body && typeof (request.body) !== 'string' && !request.body["readable"]) { + request.body = fs.createReadStream(request.body["path"]); + } + + let response: WebResponse = await sendRequestInternal(request); + if (retriableStatusCodes.indexOf(response.statusCode) != -1 && ++i < retryCount) { + core.debug(util.format("Encountered a retriable status code: %s. Message: '%s'.", response.statusCode, response.statusMessage)); + await sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + continue; + } + + return response; + } + catch (error) { + if (retriableErrorCodes.indexOf(error.code) != -1 && ++i < retryCount) { + core.debug(util.format("Encountered a retriable error:%s. Message: %s.", error.code, error.message)); + await sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + } + else { + if (error.code) { + core.debug("error code =" + error.code); + } + + throw error; + } + } + } +} + +export function sleepFor(sleepDurationInSeconds: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(resolve, sleepDurationInSeconds * 1000); + }); +} + +async function sendRequestInternal(request: WebRequest): Promise { + core.debug(util.format("[%s]%s", request.method, request.uri)); + var response: httpClient.HttpClientResponse = await httpCallbackClient.request(request.method, request.uri, request.body, request.headers); + return await toWebResponse(response); +} + +async function toWebResponse(response: httpClient.HttpClientResponse): Promise { + var res = new WebResponse(); + if (response) { + res.statusCode = response.message.statusCode; + res.statusMessage = response.message.statusMessage; + res.headers = response.message.headers; + var body = await response.readBody(); + if (body) { + try { + res.body = JSON.parse(body); + } + catch (error) { + core.debug("Could not parse response: " + JSON.stringify(error)); + core.debug("Response: " + JSON.stringify(res.body)); + res.body = body; + } + } + } + + return res; +} \ No newline at end of file diff --git a/src/utilities/strategy-helpers/deployment-helper.ts b/src/utilities/strategy-helpers/deployment-helper.ts index 8e6f1a39..04613cfc 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, annotateNamespace } from "../utility"; +import { checkForErrors, annotateChildPods, getLastSuccessfulRunSha } from "../utility"; export async function deploy(kubectl: Kubectl, manifestFilePaths: string[], deploymentStrategy: string) { @@ -49,7 +49,7 @@ export async function deploy(kubectl: Kubectl, manifestFilePaths: string[], depl core.debug("Unable to parse pods; Error: " + e); } - annotateResources(deployedManifestFiles, kubectl, resourceTypes, allPods); + annotateAndLabelResources(deployedManifestFiles, kubectl, resourceTypes, allPods); } function getManifestFiles(manifestFilePaths: string[]): string[] { @@ -112,19 +112,31 @@ async function checkManifestStability(kubectl: Kubectl, resources: Resource[]): await KubernetesManifestUtility.checkManifestStability(kubectl, resources); } -function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any) { +function annotateAndLabelResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any) { + const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(); + annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel); + labelResources(files, kubectl, annotationKeyLabel); +} + +async function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any, annotationKey: string) { const annotateResults: IExecSyncResult[] = []; - annotateResults.push(annotateNamespace(kubectl, TaskInputParameters.namespace)); - annotateResults.push(kubectl.annotateFiles(files, models.workflowAnnotations, true)); + const lastSuccessSha = await getLastSuccessfulRunSha(TaskInputParameters.githubToken); + let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); + annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, [annotationKeyValStr], true)); + annotateResults.push(kubectl.annotateFiles(files, [annotationKeyValStr], true)); resourceTypes.forEach(resource => { if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { - annotateChildPods(kubectl, resource.type, resource.name, allPods) + annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods) .forEach(execResult => annotateResults.push(execResult)); } }); checkForErrors(annotateResults, true); } +function labelResources(files: string[], kubectl: Kubectl, label: string) { + checkForErrors([kubectl.labelFiles(files, [`workflow=${label}`], true)], true); +} + function updateResourceObjects(filePaths: string[], imagePullSecrets: string[], containers: string[]): string[] { const newObjectsList = []; const updateResourceObject = (inputObject) => { diff --git a/src/utilities/utility.ts b/src/utilities/utility.ts index 16e5018b..641c8aee 100644 --- a/src/utilities/utility.ts +++ b/src/utilities/utility.ts @@ -2,7 +2,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'; +import { GitHubClient } from '../githubClient'; +import { StatusCodes } from "./httpClient"; export function getExecutableExtension(): string { if (os.type().match(/^Win/)) { @@ -21,7 +22,7 @@ export function isEqual(str1: string, str2: string, ignoreCase?: boolean): boole return false; } - if (ignoreCase) { + if (!!ignoreCase) { return str1.toUpperCase() === str2.toUpperCase(); } else { return str1 === str2; @@ -32,7 +33,7 @@ export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boo if (execResults.length !== 0) { let stderr = ''; execResults.forEach(result => { - if (result && result.stderr) { + if (!!result && !!result.stderr) { if (result.code !== 0) { stderr += result.stderr + '\n'; } else { @@ -41,7 +42,7 @@ export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boo } }); if (stderr.length > 0) { - if (warnIfError) { + if (!!warnIfError) { core.warning(stderr.trim()); } else { throw new Error(stderr.trim()); @@ -50,50 +51,49 @@ export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boo } } -export function annotateChildPods(kubectl: Kubectl, resourceType: string, resourceName: string, allPods): IExecSyncResult[] { +export async function getLastSuccessfulRunSha(githubToken: string): Promise { + let lastSuccessRunSha = ''; + const gitHubClient = new GitHubClient(process.env.GITHUB_REPOSITORY, githubToken); + const branch = process.env.GITHUB_REF.replace("refs/heads/", ""); + const response = await gitHubClient.getSuccessfulRunsOnBranch(branch); + if (response.statusCode == StatusCodes.OK + && response.body + && response.body.total_count) { + if (response.body.total_count > 0) { + lastSuccessRunSha = response.body.workflow_runs[0].head_sha; + } + else { + lastSuccessRunSha = 'NA'; + } + } + else if (response.statusCode != StatusCodes.OK) { + core.debug(`An error occured while getting succeessful run results. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); + } + return lastSuccessRunSha; +} + +export function annotateChildPods(kubectl: Kubectl, resourceType: string, resourceName: string, annotationKeyValStr: 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) { + if (!!allPods && !!allPods.items && allPods.items.length > 0) { allPods.items.forEach((pod) => { const owners = pod.metadata.ownerReferences; - if (owners) { + if (!!owners) { owners.forEach(ownerRef => { if (ownerRef.name === owner) { - commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, workflowAnnotations, true)); + commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, [annotationKeyValStr], true)); } }); } }); } - 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 && result.stderr) { - return result; - } - - if (result && 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)); } From 3919a9ee22a3c7a1a796fce84fe2a8f54ccedb8a Mon Sep 17 00:00:00 2001 From: Koushik Dey Date: Fri, 31 Jul 2020 13:53:38 +0530 Subject: [PATCH 2/6] Added test cases for annotations and API integration --- __tests__/run.test.ts | 131 ++++++++++++++++++++++++++++----------- lib/utilities/utility.js | 6 +- src/utilities/utility.ts | 6 +- 3 files changed, 100 insertions(+), 43 deletions(-) diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index 05fea60f..db458b2f 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -11,7 +11,8 @@ import { getWorkflowAnnotationKeyLabel, getWorkflowAnnotationsJson } from '../sr import * as inputParam from '../src/input-parameters'; import { Kubectl, Resource } from '../src/kubectl-object-model'; -import { GitHubClient } from '../src/githubClient'; +import * as httpClient from '../src/utilities/httpClient'; +import * as utility from '../src/utilities/utility'; import { getkubectlDownloadURL } from "../src/utilities/kubectl-util"; import { mocked } from 'ts-jest/utils'; @@ -40,10 +41,50 @@ const getNamespaceMock = { 'stdout': '{"apiVersion": "v1","kind": "Namespace","metadata": {"annotations": { "resourceAnnotations": "[{\'run\':\'152673324\',\'repository\':\'koushdey/hello-kubernetes\',\'workflow\':\'.github/workflows/workflowNew.yml\',\'jobName\':\'build-and-deploy\',\'createdBy\':\'koushdey\',\'runUri\':\'https://github.com/koushdey/hello-kubernetes/actions/runs/152673324\',\'commit\':\'f45c9c04ed6bbd4813019ebc6f5e94f155c974a4\',\'branch\':\'refs/heads/koushdey-rename\',\'deployTimestamp\':\'1593516378601\',\'provider\':\'GitHub\'},{\'run\':\'12345\',\'repository\':\'testRepo\',\'workflow\':\'.github/workflows/workflow.yml\',\'jobName\':\'build-and-deploy\',\'createdBy\':\'koushdey\',\'runUri\':\'https://github.com/testRepo/actions/runs/12345\',\'commit\':\'testCommit\',\'branch\':\'testBranch\',\'deployTimestamp\':\'Now\',\'provider\':\'GitHub\'}]","key":"value"}},"spec": {"finalizers": ["kubernetes"]},"status": {"phase": "Active"}}' }; +const lastSuccessfulRunUrlResponse = { + 'statusCode': httpClient.StatusCodes.OK, + 'body': { + "total_count": 2, + "workflow_runs": [ + { + "id": 123456, + "node_id": "MDExOldvcmtmbG93UnVuMTc5NTU5ODQ1", + "head_branch": "test-branch", + "head_sha": "lastSuccessfulCommit1", + "run_number": 17, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 1532330, + "url": "https://api.github.com/repos/koushdey/hello-kubernetes/actions/runs/123456", + "html_url": "https://github.com/koushdey/hello-kubernetes/actions/runs/123456", + "created_at": "2020-07-23T08:21:25Z", + "updated_at": "2020-07-23T08:22:48Z", + }, + { + "id": 179559, + "node_id": "EDmxOldvcmtmbG93NyVuMTc5NTU5ODQ1", + "head_branch": "test-branch", + "head_sha": "lastSuccessfulCommit2", + "run_number": 17, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 1532330, + "url": "https://api.github.com/repos/koushdey/hello-kubernetes/actions/runs/179559", + "html_url": "https://github.com/koushdey/hello-kubernetes/actions/runs/179559", + "created_at": "2020-07-22T02:11:25Z", + "updated_at": "2020-07-22T02:14:48Z", + } + ] + } +} as httpClient.WebResponse; + const resources: Resource[] = [{ type: "Deployment", name: "AppName" }]; -beforeEach(() => { +beforeAll(() => { deploymentYaml = fs.readFileSync(path.join(__dirname, 'manifests', 'deployment.yml'), 'utf8'); + jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567); process.env["KUBECONFIG"] = 'kubeConfig'; process.env['GITHUB_RUN_ID'] = '12345'; @@ -210,7 +251,7 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => { const kubeCtl: jest.Mocked = new Kubectl("") as any; kubeCtl.apply = jest.fn().mockReturnValue(""); KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); - kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); + kubeCtl.getResource = jest.fn().mockReturnValue(""); kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock); kubeCtl.describe = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); @@ -219,6 +260,7 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => { KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml); + jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(lastSuccessfulRunUrlResponse)); //Invoke and assert await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); @@ -239,7 +281,7 @@ test("deployment - deploy() - deploy force flag on", async () => { 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.getResource = jest.fn().mockReturnValue(""); kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock); kubeCtl.describe = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); @@ -248,6 +290,7 @@ test("deployment - deploy() - deploy force flag on", async () => { KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const deploySpy = jest.spyOn(kubeCtl, 'apply').mockImplementation(() => applyResMock); + jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(lastSuccessfulRunUrlResponse)); //Invoke and assert await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); @@ -255,55 +298,30 @@ test("deployment - deploy() - deploy force flag on", async () => { deploySpy.mockRestore(); }); -test("deployment - deploy() - Annotate resources", async () => { - let annotationKeyValStr = getWorkflowAnnotationKeyLabel() + '=' + '[' + getWorkflowAnnotationsJson('lastSuccessSha') + ']'; +test("deployment - deploy() - Annotate & label resources", async () => { + let annotationKeyValStr = getWorkflowAnnotationKeyLabel() + '=' + getWorkflowAnnotationsJson('lastSuccessfulCommit1'); 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"]); + jest.spyOn(utility, 'getLastSuccessfulRunSha').mockImplementation(() => Promise.resolve('lastSuccessfulCommit1')); const kubeCtl: jest.Mocked = new Kubectl("") as any; kubeCtl.apply = jest.fn().mockReturnValue(""); - kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); + kubeCtl.getResource = jest.fn().mockReturnValue(""); kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock); kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9"); kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue(""); - kubeCtl.labelFiles = jest.fn().mockReturnValue(""); - + kubeCtl.labelFiles = jest.fn(); //Invoke and assert await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); + expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', [annotationKeyValStr], true); expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [annotationKeyValStr], true); expect(kubeCtl.annotate).toBeCalledTimes(2); -}); - -test("deployment - deploy() - Skip Annotate namespace", async () => { - process.env['GITHUB_REPOSITORY'] = 'testUser/test1Repo'; - let annotationKeyValStr = getWorkflowAnnotationKeyLabel() + '=' + '[' + getWorkflowAnnotationsJson('lastSuccessSha') + ']'; - 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(""); - kubeCtl.labelFiles = 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(["~/Deployment_testapp_currentTimestamp"], [annotationKeyValStr], true); - //expect(kubeCtl.annotate).toBeCalledTimes(1); - //expect(consoleOutputSpy).toHaveBeenNthCalledWith(2, `##[debug]Skipping 'annotate namespace' as namespace annotated by other workflow` + os.EOL) + expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [`workflow=${getWorkflowAnnotationKeyLabel()}`], true); }); test("deployment - deploy() - Annotate resources failed", async () => { @@ -331,5 +349,44 @@ test("deployment - deploy() - Annotate resources failed", async () => { 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(4, '##[warning]kubectl annotate failed' + os.EOL) + expect(consoleOutputSpy).toHaveBeenNthCalledWith(2, '##[warning]kubectl annotate failed' + os.EOL) +}); + +test("utility - getLastSuccessfulRunSha() - Get Commits under different conditions", async () => { + //Mocks + /*const existingBranchWebRequest = { + 'method': "exitingBranch" + } as httpClient.WebRequest; + const newBranchWebRequest = { + 'method': "newBranch" + } as httpClient.WebRequest; + const errorWebRequest = { + 'method': "errorRequest" + } as httpClient.WebRequest;*/ + + const newBranchWebResponse = { + 'statusCode': httpClient.StatusCodes.OK, + 'body': { + "total_count": 0, + "workflow_runs": [] + } + } as httpClient.WebResponse + const errorWebResponse = { + 'statusCode': httpClient.StatusCodes.UNAUTHORIZED, + 'body': {} + } as httpClient.WebResponse + + /*jest.spyOn(httpClient, 'sendRequest').mockImplementation((name) => { + if (name == existingBranchWebRequest) + return Promise.resolve(lastSuccessfulRunUrlResponse); + if (name == newBranchWebRequest) + return Promise.resolve(newBranchWebResponse); + if (name == errorWebRequest) + return Promise.resolve(errorWebResponse); + });*/ + + jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(lastSuccessfulRunUrlResponse)); + //Invoke and assert + await expect(utility.getLastSuccessfulRunSha(process.env.GITHUB_TOKEN)).resolves.not.toThrowError; + await expect(utility.getLastSuccessfulRunSha(process.env.GITHUB_TOKEN)).resolves.toBe('lastSuccessfulCommit1'); }); \ No newline at end of file diff --git a/lib/utilities/utility.js b/lib/utilities/utility.js index 11f31cbe..09eaee43 100644 --- a/lib/utilities/utility.js +++ b/lib/utilities/utility.js @@ -67,8 +67,8 @@ function getLastSuccessfulRunSha(githubToken) { const branch = process.env.GITHUB_REF.replace("refs/heads/", ""); const response = yield gitHubClient.getSuccessfulRunsOnBranch(branch); if (response.statusCode == httpClient_1.StatusCodes.OK - && response.body - && response.body.total_count) { + && !!response.body + && !!response.body.total_count) { if (response.body.total_count > 0) { lastSuccessRunSha = response.body.workflow_runs[0].head_sha; } @@ -79,7 +79,7 @@ function getLastSuccessfulRunSha(githubToken) { else if (response.statusCode != httpClient_1.StatusCodes.OK) { core.debug(`An error occured while getting succeessful run results. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); } - return lastSuccessRunSha; + return Promise.resolve(lastSuccessRunSha); }); } exports.getLastSuccessfulRunSha = getLastSuccessfulRunSha; diff --git a/src/utilities/utility.ts b/src/utilities/utility.ts index 641c8aee..c506e7a0 100644 --- a/src/utilities/utility.ts +++ b/src/utilities/utility.ts @@ -57,8 +57,8 @@ export async function getLastSuccessfulRunSha(githubToken: string): Promise 0) { lastSuccessRunSha = response.body.workflow_runs[0].head_sha; } @@ -69,7 +69,7 @@ export async function getLastSuccessfulRunSha(githubToken: string): Promise Date: Wed, 5 Aug 2020 10:43:46 +0530 Subject: [PATCH 3/6] Integrated workflow list and run API to get lastSuccessRunCommit --- __tests__/run.test.ts | 1 - lib/constants.js | 4 +- lib/githubClient.js | 21 ++++++- .../strategy-helpers/deployment-helper.js | 33 +++++----- lib/utilities/utility.js | 62 +++++++++++++------ src/constants.ts | 4 +- src/githubClient.ts | 22 ++++++- .../strategy-helpers/deployment-helper.ts | 11 ++-- src/utilities/utility.ts | 56 ++++++++++++----- 9 files changed, 151 insertions(+), 63 deletions(-) diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index db458b2f..26c87abb 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -384,7 +384,6 @@ test("utility - getLastSuccessfulRunSha() - Get Commits under different conditio if (name == errorWebRequest) return Promise.resolve(errorWebResponse); });*/ - jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(lastSuccessfulRunUrlResponse)); //Invoke and assert await expect(utility.getLastSuccessfulRunSha(process.env.GITHUB_TOKEN)).resolves.not.toThrowError; diff --git a/lib/constants.js b/lib/constants.js index 421d39b0..1ec3b50f 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -41,9 +41,9 @@ function getWorkflowAnnotationsJson(lastSuccessRunSha) { + `}`; } exports.getWorkflowAnnotationsJson = getWorkflowAnnotationsJson; -function getWorkflowAnnotationKeyLabel() { +function getWorkflowAnnotationKeyLabel(workflowFilePath) { const hashKey = require("crypto").createHash("MD5") - .update(`${process.env.GITHUB_REPOSITORY}/${process.env.GITHUB_WORKFLOW}`) + .update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`) .digest("hex"); return `githubWorkflow_${hashKey}`; } diff --git a/lib/githubClient.js b/lib/githubClient.js index a6497faf..9a350c2f 100644 --- a/lib/githubClient.js +++ b/lib/githubClient.js @@ -17,10 +17,10 @@ class GitHubClient { this._repository = repository; this._token = token; } - getSuccessfulRunsOnBranch(branch, force) { + getSuccessfulRunsOnBranch(branch, workflowFileName, force) { return __awaiter(this, void 0, void 0, function* () { if (force || !this._successfulRunsOnBranchPromise) { - const lastSuccessfulRunUrl = `https://api.github.com/repos/${this._repository}/actions/runs?status=success&branch=${branch}`; + const lastSuccessfulRunUrl = `https://api.github.com/repos/${this._repository}/actions/workflows/${workflowFileName}/runs?status=success&branch=${branch}`; const webRequest = new httpClient_1.WebRequest(); webRequest.method = "GET"; webRequest.uri = lastSuccessfulRunUrl; @@ -34,5 +34,22 @@ class GitHubClient { return this._successfulRunsOnBranchPromise; }); } + getWorkflows(force) { + return __awaiter(this, void 0, void 0, function* () { + if (force || !this._workflowsPromise) { + const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`; + const webRequest = new httpClient_1.WebRequest(); + webRequest.method = "GET"; + webRequest.uri = getWorkflowFileNameUrl; + webRequest.headers = { + Authorization: `Bearer ${this._token}` + }; + core.debug(`Getting workflows for repo: ${this._repository}`); + const response = yield httpClient_1.sendRequest(webRequest); + this._workflowsPromise = Promise.resolve(response); + } + return this._workflowsPromise; + }); + } } exports.GitHubClient = GitHubClient; diff --git a/lib/utilities/strategy-helpers/deployment-helper.js b/lib/utilities/strategy-helpers/deployment-helper.js index cf9880d6..32b9d3c9 100644 --- a/lib/utilities/strategy-helpers/deployment-helper.js +++ b/lib/utilities/strategy-helpers/deployment-helper.js @@ -111,25 +111,26 @@ function checkManifestStability(kubectl, resources) { }); } function annotateAndLabelResources(files, kubectl, resourceTypes, allPods) { - const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(); - annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel); - labelResources(files, kubectl, annotationKeyLabel); + return __awaiter(this, void 0, void 0, function* () { + const workflowFilePath = yield utility_1.getWorkflowFilePath(TaskInputParameters.githubToken); + const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(workflowFilePath); + annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel); + labelResources(files, kubectl, annotationKeyLabel); + }); } function annotateResources(files, kubectl, resourceTypes, allPods, annotationKey) { - return __awaiter(this, void 0, void 0, function* () { - const annotateResults = []; - const lastSuccessSha = yield utility_1.getLastSuccessfulRunSha(TaskInputParameters.githubToken); - let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); - annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, [annotationKeyValStr], true)); - annotateResults.push(kubectl.annotateFiles(files, [annotationKeyValStr], true)); - resourceTypes.forEach(resource => { - if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { - utility_1.annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods) - .forEach(execResult => annotateResults.push(execResult)); - } - }); - utility_1.checkForErrors(annotateResults, true); + const annotateResults = []; + const lastSuccessSha = utility_1.getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey); + let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); + annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, [annotationKeyValStr], true)); + annotateResults.push(kubectl.annotateFiles(files, [annotationKeyValStr], true)); + resourceTypes.forEach(resource => { + if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { + utility_1.annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods) + .forEach(execResult => annotateResults.push(execResult)); + } }); + utility_1.checkForErrors(annotateResults, true); } function labelResources(files, kubectl, label) { utility_1.checkForErrors([kubectl.labelFiles(files, [`workflow=${label}`], true)], true); diff --git a/lib/utilities/utility.js b/lib/utilities/utility.js index 09eaee43..297942f3 100644 --- a/lib/utilities/utility.js +++ b/lib/utilities/utility.js @@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.annotateChildPods = exports.getLastSuccessfulRunSha = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0; +exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.annotateChildPods = exports.getWorkflowFilePath = exports.getLastSuccessfulRunSha = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0; const os = require("os"); const core = require("@actions/core"); const githubClient_1 = require("../githubClient"); @@ -60,29 +60,55 @@ function checkForErrors(execResults, warnIfError) { } } exports.checkForErrors = checkForErrors; -function getLastSuccessfulRunSha(githubToken) { - return __awaiter(this, void 0, void 0, function* () { - let lastSuccessRunSha = ''; - const gitHubClient = new githubClient_1.GitHubClient(process.env.GITHUB_REPOSITORY, githubToken); - const branch = process.env.GITHUB_REF.replace("refs/heads/", ""); - const response = yield gitHubClient.getSuccessfulRunsOnBranch(branch); - if (response.statusCode == httpClient_1.StatusCodes.OK - && !!response.body - && !!response.body.total_count) { - if (response.body.total_count > 0) { - lastSuccessRunSha = response.body.workflow_runs[0].head_sha; +function getLastSuccessfulRunSha(kubectl, namespaceName, annotationKey) { + const result = kubectl.getResource('namespace', namespaceName); + if (!result) { + core.debug(`Failed to get commits from cluster.`); + return ''; + } + else { + if (result.stderr) { + core.debug(`${result.stderr}`); + return process.env.GITHUB_SHA; + } + else if (result.stdout) { + const annotationsSet = JSON.parse(result.stdout).metadata.annotations; + if (!!annotationsSet[annotationKey]) { + return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit; } else { - lastSuccessRunSha = 'NA'; + return 'NA'; } } - else if (response.statusCode != httpClient_1.StatusCodes.OK) { - core.debug(`An error occured while getting succeessful run results. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); - } - return Promise.resolve(lastSuccessRunSha); - }); + } } exports.getLastSuccessfulRunSha = getLastSuccessfulRunSha; +function getWorkflowFilePath(githubToken) { + return __awaiter(this, void 0, void 0, function* () { + let workflowFilePath = process.env.GITHUB_WORKFLOW; + if (!workflowFilePath.startsWith('.github/workflows/')) { + const githubClient = new githubClient_1.GitHubClient(process.env.GITHUB_REPOSITORY, githubToken); + const response = yield githubClient.getWorkflows(); + if (response.statusCode == httpClient_1.StatusCodes.OK + && !!response.body + && !!response.body.total_count) { + if (response.body.total_count > 0) { + for (let workflow of response.body.workflows) { + if (process.env.GITHUB_WORKFLOW === workflow.name) { + workflowFilePath = workflow.path; + break; + } + } + } + } + else if (response.statusCode != httpClient_1.StatusCodes.OK) { + core.debug(`An error occured while getting list of workflows on the repo. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); + } + } + return Promise.resolve(workflowFilePath); + }); +} +exports.getWorkflowFilePath = getWorkflowFilePath; function annotateChildPods(kubectl, resourceType, resourceName, annotationKeyValStr, allPods) { const commandExecutionResults = []; let owner = resourceName; diff --git a/src/constants.ts b/src/constants.ts index c9f2d3fc..b2dc7e44 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,9 +40,9 @@ export function getWorkflowAnnotationsJson(lastSuccessRunSha: string): string { + `'provider': 'GitHub'` + `}`; } -export function getWorkflowAnnotationKeyLabel(): string { +export function getWorkflowAnnotationKeyLabel(workflowFilePath: string): string { const hashKey = require("crypto").createHash("MD5") - .update(`${process.env.GITHUB_REPOSITORY}/${process.env.GITHUB_WORKFLOW}`) + .update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`) .digest("hex"); return `githubWorkflow_${hashKey}`; } \ No newline at end of file diff --git a/src/githubClient.ts b/src/githubClient.ts index 442592b6..11ad069a 100644 --- a/src/githubClient.ts +++ b/src/githubClient.ts @@ -7,9 +7,9 @@ export class GitHubClient { this._token = token; } - public async getSuccessfulRunsOnBranch(branch: string, force?: boolean): Promise { + public async getSuccessfulRunsOnBranch(branch: string, workflowFileName: string, force?: boolean): Promise { if (force || !this._successfulRunsOnBranchPromise) { - const lastSuccessfulRunUrl = `https://api.github.com/repos/${this._repository}/actions/runs?status=success&branch=${branch}`; + const lastSuccessfulRunUrl = `https://api.github.com/repos/${this._repository}/actions/workflows/${workflowFileName}/runs?status=success&branch=${branch}`; const webRequest = new WebRequest(); webRequest.method = "GET"; webRequest.uri = lastSuccessfulRunUrl; @@ -24,7 +24,25 @@ export class GitHubClient { return this._successfulRunsOnBranchPromise; } + public async getWorkflows(force?: boolean): Promise { + if (force || !this._workflowsPromise) { + const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`; + const webRequest = new WebRequest(); + webRequest.method = "GET"; + webRequest.uri = getWorkflowFileNameUrl; + webRequest.headers = { + Authorization: `Bearer ${this._token}` + }; + + core.debug(`Getting workflows for repo: ${this._repository}`); + const response: WebResponse = await sendRequest(webRequest); + this._workflowsPromise = Promise.resolve(response); + } + return this._workflowsPromise; + } + private _repository: string; private _token: string; private _successfulRunsOnBranchPromise: Promise; + private _workflowsPromise: Promise; } \ No newline at end of file diff --git a/src/utilities/strategy-helpers/deployment-helper.ts b/src/utilities/strategy-helpers/deployment-helper.ts index 04613cfc..31314d94 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, getLastSuccessfulRunSha } from "../utility"; +import { checkForErrors, annotateChildPods, getWorkflowFilePath, getLastSuccessfulRunSha } from "../utility"; export async function deploy(kubectl: Kubectl, manifestFilePaths: string[], deploymentStrategy: string) { @@ -112,15 +112,16 @@ async function checkManifestStability(kubectl: Kubectl, resources: Resource[]): await KubernetesManifestUtility.checkManifestStability(kubectl, resources); } -function annotateAndLabelResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any) { - const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(); +async function annotateAndLabelResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any) { + const workflowFilePath = await getWorkflowFilePath(TaskInputParameters.githubToken); + const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(workflowFilePath); annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel); labelResources(files, kubectl, annotationKeyLabel); } -async function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any, annotationKey: string) { +function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any, annotationKey: string) { const annotateResults: IExecSyncResult[] = []; - const lastSuccessSha = await getLastSuccessfulRunSha(TaskInputParameters.githubToken); + const lastSuccessSha = getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey); let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, [annotationKeyValStr], true)); annotateResults.push(kubectl.annotateFiles(files, [annotationKeyValStr], true)); diff --git a/src/utilities/utility.ts b/src/utilities/utility.ts index c506e7a0..c1588b23 100644 --- a/src/utilities/utility.ts +++ b/src/utilities/utility.ts @@ -51,25 +51,51 @@ export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boo } } -export async function getLastSuccessfulRunSha(githubToken: string): Promise { - let lastSuccessRunSha = ''; - const gitHubClient = new GitHubClient(process.env.GITHUB_REPOSITORY, githubToken); - const branch = process.env.GITHUB_REF.replace("refs/heads/", ""); - const response = await gitHubClient.getSuccessfulRunsOnBranch(branch); - if (response.statusCode == StatusCodes.OK - && !!response.body - && !!response.body.total_count) { - if (response.body.total_count > 0) { - lastSuccessRunSha = response.body.workflow_runs[0].head_sha; +export function getLastSuccessfulRunSha(kubectl: Kubectl, namespaceName: string, annotationKey: string): string { + const result = kubectl.getResource('namespace', namespaceName); + if (!result) { + core.debug(`Failed to get commits from cluster.`); + return ''; + } + else { + if (result.stderr) { + core.debug(`${result.stderr}`); + return process.env.GITHUB_SHA; } - else { - lastSuccessRunSha = 'NA'; + else if (result.stdout) { + const annotationsSet = JSON.parse(result.stdout).metadata.annotations; + if (!!annotationsSet[annotationKey]) { + return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit; + } + else { + return 'NA'; + } } } - else if (response.statusCode != StatusCodes.OK) { - core.debug(`An error occured while getting succeessful run results. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); +} + +export async function getWorkflowFilePath(githubToken: string): Promise { + let workflowFilePath = process.env.GITHUB_WORKFLOW; + if (!workflowFilePath.startsWith('.github/workflows/')) { + const githubClient = new GitHubClient(process.env.GITHUB_REPOSITORY, githubToken); + const response = await githubClient.getWorkflows(); + if (response.statusCode == StatusCodes.OK + && !!response.body + && !!response.body.total_count) { + if (response.body.total_count > 0) { + for (let workflow of response.body.workflows) { + if (process.env.GITHUB_WORKFLOW === workflow.name) { + workflowFilePath = workflow.path; + break; + } + } + } + } + else if (response.statusCode != StatusCodes.OK) { + core.debug(`An error occured while getting list of workflows on the repo. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); + } } - return Promise.resolve(lastSuccessRunSha); + return Promise.resolve(workflowFilePath); } export function annotateChildPods(kubectl: Kubectl, resourceType: string, resourceName: string, annotationKeyValStr: string, allPods): IExecSyncResult[] { From 3a1c0b10ebcccd3aab44166f11a2782d4eb65c99 Mon Sep 17 00:00:00 2001 From: Koushik Dey Date: Mon, 10 Aug 2020 20:07:46 +0530 Subject: [PATCH 4/6] Removed success run API and edited testcases. --- __tests__/run.test.ts | 132 +++++++++--------- lib/githubClient.js | 17 --- .../strategy-helpers/deployment-helper.js | 6 +- lib/utilities/utility.js | 6 +- src/githubClient.ts | 18 --- .../strategy-helpers/deployment-helper.ts | 6 +- src/utilities/utility.ts | 6 +- 7 files changed, 81 insertions(+), 110 deletions(-) diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index 26c87abb..6132a9fb 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -38,43 +38,37 @@ const getAllPodsMock = { const getNamespaceMock = { 'code': 0, - 'stdout': '{"apiVersion": "v1","kind": "Namespace","metadata": {"annotations": { "resourceAnnotations": "[{\'run\':\'152673324\',\'repository\':\'koushdey/hello-kubernetes\',\'workflow\':\'.github/workflows/workflowNew.yml\',\'jobName\':\'build-and-deploy\',\'createdBy\':\'koushdey\',\'runUri\':\'https://github.com/koushdey/hello-kubernetes/actions/runs/152673324\',\'commit\':\'f45c9c04ed6bbd4813019ebc6f5e94f155c974a4\',\'branch\':\'refs/heads/koushdey-rename\',\'deployTimestamp\':\'1593516378601\',\'provider\':\'GitHub\'},{\'run\':\'12345\',\'repository\':\'testRepo\',\'workflow\':\'.github/workflows/workflow.yml\',\'jobName\':\'build-and-deploy\',\'createdBy\':\'koushdey\',\'runUri\':\'https://github.com/testRepo/actions/runs/12345\',\'commit\':\'testCommit\',\'branch\':\'testBranch\',\'deployTimestamp\':\'Now\',\'provider\':\'GitHub\'}]","key":"value"}},"spec": {"finalizers": ["kubernetes"]},"status": {"phase": "Active"}}' + 'stdout': '{"apiVersion": "v1","kind": "Namespace","metadata": {"annotations": {"githubWorkflow_c11401b9d232942bac19cbc5bc32b42d": "{\'run\': \'202489005\',\'repository\': \'testUser/hello-kubernetes\',\'workflow\': \'workflow1\',\'jobName\': \'build-and-deploy\',\'createdBy\': \'testUser\',\'runUri\': \'https://github.com/testUser/hello-kubernetes/actions/runs/202489005\',\'commit\': \'currentCommit\',\'lastSuccessRunCommit\': \'lastCommit\',\'branch\': \'refs/heads/branch-rename\',\'deployTimestamp\': \'1597062957973\',\'provider\': \'GitHub\'}","githubWorkflow_21fd7a597282ca5adc05ba99018b3706": "{\'run\': \'202504411\',\'repository\': \'testUser/hello-kubernetes\',\'workflow\': \'workflowMaster\',\'jobName\': \'build-and-deploy\',\'createdBy\': \'testUser\',\'runUri\': \'https://github.com/testUser/hello-kubernetes/actions/runs/202504411\',\'commit\': \'currentCommit1\',\'lastSuccessRunCommit\': \'NA\',\'branch\': \'refs/heads/master\',\'deployTimestamp\': \'1597063919873\',\'provider\': \'GitHub\'}"}},"spec": {"finalizers": ["kubernetes"]},"status": {"phase": "Active"}}' }; -const lastSuccessfulRunUrlResponse = { +const getWorkflowsUrlResponse = { 'statusCode': httpClient.StatusCodes.OK, 'body': { "total_count": 2, - "workflow_runs": [ + "workflows": [ { - "id": 123456, - "node_id": "MDExOldvcmtmbG93UnVuMTc5NTU5ODQ1", - "head_branch": "test-branch", - "head_sha": "lastSuccessfulCommit1", - "run_number": 17, - "event": "push", - "status": "completed", - "conclusion": "success", - "workflow_id": 1532330, - "url": "https://api.github.com/repos/koushdey/hello-kubernetes/actions/runs/123456", - "html_url": "https://github.com/koushdey/hello-kubernetes/actions/runs/123456", - "created_at": "2020-07-23T08:21:25Z", - "updated_at": "2020-07-23T08:22:48Z", + "id": 1477727, + "node_id": "MDg6V29ya2Zsb3cxNDYwNzI3", + "name": ".github/workflows/workflow.yml", + "path": ".github/workflows/workflow.yml", + "state": "active", + "created_at": "2020-06-03T23:41:06.000+05:30", + "updated_at": "2020-08-07T15:46:42.000+05:30", + "url": "https://api.github.com/repos/testUser/hello-kubernetes/actions/workflows/1460727", + "html_url": "https://github.com/testUser/hello-kubernetes/blob/master/.github/workflows/workflow.yml", + "badge_url": "https://github.com/testUser/hello-kubernetes/workflows/.github/workflows/workflow.yml/badge.svg" }, { - "id": 179559, - "node_id": "EDmxOldvcmtmbG93NyVuMTc5NTU5ODQ1", - "head_branch": "test-branch", - "head_sha": "lastSuccessfulCommit2", - "run_number": 17, - "event": "push", - "status": "completed", - "conclusion": "success", - "workflow_id": 1532330, - "url": "https://api.github.com/repos/koushdey/hello-kubernetes/actions/runs/179559", - "html_url": "https://github.com/koushdey/hello-kubernetes/actions/runs/179559", - "created_at": "2020-07-22T02:11:25Z", - "updated_at": "2020-07-22T02:14:48Z", + "id": 1532230, + "node_id": "MDg6V29ya2Zsb3cxNTMyMzMw", + "name": "NewWorkflow", + "path": ".github/workflows/workflow1.yml", + "state": "active", + "created_at": "2020-06-11T16:05:23.000+05:30", + "updated_at": "2020-08-07T15:46:42.000+05:30", + "url": "https://api.github.com/repos/testUser/hello-kubernetes/actions/workflows/1532330", + "html_url": "https://github.com/testUser/hello-kubernetes/blob/master/.github/workflows/workflowNew.yml", + "badge_url": "https://github.com/testUser/hello-kubernetes/workflows/KoDeyi/badge.svg" } ] } @@ -82,7 +76,7 @@ const lastSuccessfulRunUrlResponse = { const resources: Resource[] = [{ type: "Deployment", name: "AppName" }]; -beforeAll(() => { +beforeEach(() => { deploymentYaml = fs.readFileSync(path.join(__dirname, 'manifests', 'deployment.yml'), 'utf8'); jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567); @@ -251,7 +245,7 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => { const kubeCtl: jest.Mocked = new Kubectl("") as any; kubeCtl.apply = jest.fn().mockReturnValue(""); 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(""); @@ -260,7 +254,7 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => { KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml); - jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(lastSuccessfulRunUrlResponse)); + jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(getWorkflowsUrlResponse)); //Invoke and assert await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); @@ -281,7 +275,7 @@ test("deployment - deploy() - deploy force flag on", async () => { const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); const kubeCtl: jest.Mocked = new Kubectl("") as any; 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(""); @@ -290,7 +284,7 @@ test("deployment - deploy() - deploy force flag on", async () => { KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const deploySpy = jest.spyOn(kubeCtl, 'apply').mockImplementation(() => applyResMock); - jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(lastSuccessfulRunUrlResponse)); + jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(getWorkflowsUrlResponse)); //Invoke and assert await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); @@ -299,18 +293,18 @@ test("deployment - deploy() - deploy force flag on", async () => { }); test("deployment - deploy() - Annotate & label resources", async () => { - let annotationKeyValStr = getWorkflowAnnotationKeyLabel() + '=' + getWorkflowAnnotationsJson('lastSuccessfulCommit1'); + let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('currentCommit'); 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"]); - jest.spyOn(utility, 'getLastSuccessfulRunSha').mockImplementation(() => Promise.resolve('lastSuccessfulCommit1')); + jest.spyOn(utility, 'getWorkflowFilePath').mockImplementation(() => Promise.resolve(process.env.GITHUB_WORKFLOW)); const kubeCtl: jest.Mocked = new Kubectl("") as any; kubeCtl.apply = jest.fn().mockReturnValue(""); - kubeCtl.getResource = 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(""); @@ -321,7 +315,36 @@ test("deployment - deploy() - Annotate & label resources", async () => { expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', [annotationKeyValStr], true); expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [annotationKeyValStr], true); expect(kubeCtl.annotate).toBeCalledTimes(2); - expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [`workflow=${getWorkflowAnnotationKeyLabel()}`], true); + expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], + [`workflowFriendlyName=workflow.yml`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`], true); +}); + +test("deployment - deploy() - Annotate & label resources for a new workflow", async () => { + process.env.GITHUB_WORKFLOW = 'NewWorkflow'; + let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('NA'); + 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"]); + jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(getWorkflowsUrlResponse)); + + 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(""); + kubeCtl.labelFiles = jest.fn(); + //Invoke and assert + await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); + expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', [annotationKeyValStr], true); + expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [annotationKeyValStr], true); + expect(kubeCtl.annotate).toBeCalledTimes(2); + expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], + [`workflowFriendlyName=${process.env.GITHUB_WORKFLOW}`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`], true); }); test("deployment - deploy() - Annotate resources failed", async () => { @@ -352,40 +375,15 @@ test("deployment - deploy() - Annotate resources failed", async () => { expect(consoleOutputSpy).toHaveBeenNthCalledWith(2, '##[warning]kubectl annotate failed' + os.EOL) }); -test("utility - getLastSuccessfulRunSha() - Get Commits under different conditions", async () => { +test("utility - getWorkflowFilePath() - Get workflow file path under API failure", async () => { //Mocks - /*const existingBranchWebRequest = { - 'method': "exitingBranch" - } as httpClient.WebRequest; - const newBranchWebRequest = { - 'method': "newBranch" - } as httpClient.WebRequest; - const errorWebRequest = { - 'method': "errorRequest" - } as httpClient.WebRequest;*/ - - const newBranchWebResponse = { - 'statusCode': httpClient.StatusCodes.OK, - 'body': { - "total_count": 0, - "workflow_runs": [] - } - } as httpClient.WebResponse const errorWebResponse = { 'statusCode': httpClient.StatusCodes.UNAUTHORIZED, 'body': {} } as httpClient.WebResponse + jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(errorWebResponse)); - /*jest.spyOn(httpClient, 'sendRequest').mockImplementation((name) => { - if (name == existingBranchWebRequest) - return Promise.resolve(lastSuccessfulRunUrlResponse); - if (name == newBranchWebRequest) - return Promise.resolve(newBranchWebResponse); - if (name == errorWebRequest) - return Promise.resolve(errorWebResponse); - });*/ - jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(lastSuccessfulRunUrlResponse)); //Invoke and assert - await expect(utility.getLastSuccessfulRunSha(process.env.GITHUB_TOKEN)).resolves.not.toThrowError; - await expect(utility.getLastSuccessfulRunSha(process.env.GITHUB_TOKEN)).resolves.toBe('lastSuccessfulCommit1'); + await expect(utility.getWorkflowFilePath(process.env.GITHUB_TOKEN)).resolves.not.toThrowError; + await expect(utility.getWorkflowFilePath(process.env.GITHUB_TOKEN)).resolves.toBe(process.env.GITHUB_WORKFLOW); }); \ No newline at end of file diff --git a/lib/githubClient.js b/lib/githubClient.js index 9a350c2f..7de8bf6c 100644 --- a/lib/githubClient.js +++ b/lib/githubClient.js @@ -17,23 +17,6 @@ class GitHubClient { this._repository = repository; this._token = token; } - getSuccessfulRunsOnBranch(branch, workflowFileName, force) { - return __awaiter(this, void 0, void 0, function* () { - if (force || !this._successfulRunsOnBranchPromise) { - const lastSuccessfulRunUrl = `https://api.github.com/repos/${this._repository}/actions/workflows/${workflowFileName}/runs?status=success&branch=${branch}`; - const webRequest = new httpClient_1.WebRequest(); - webRequest.method = "GET"; - webRequest.uri = lastSuccessfulRunUrl; - webRequest.headers = { - Authorization: `Bearer ${this._token}` - }; - core.debug(`Getting last successful run for repo: ${this._repository} on branch: ${branch}`); - const response = yield httpClient_1.sendRequest(webRequest); - this._successfulRunsOnBranchPromise = Promise.resolve(response); - } - return this._successfulRunsOnBranchPromise; - }); - } getWorkflows(force) { return __awaiter(this, void 0, void 0, function* () { if (force || !this._workflowsPromise) { diff --git a/lib/utilities/strategy-helpers/deployment-helper.js b/lib/utilities/strategy-helpers/deployment-helper.js index 32b9d3c9..0c2f6d8f 100644 --- a/lib/utilities/strategy-helpers/deployment-helper.js +++ b/lib/utilities/strategy-helpers/deployment-helper.js @@ -133,7 +133,11 @@ function annotateResources(files, kubectl, resourceTypes, allPods, annotationKey utility_1.checkForErrors(annotateResults, true); } function labelResources(files, kubectl, label) { - utility_1.checkForErrors([kubectl.labelFiles(files, [`workflow=${label}`], true)], true); + let workflowName = process.env.GITHUB_WORKFLOW; + workflowName = workflowName.startsWith('.github/workflows/') ? + workflowName.replace(".github/workflows/", "") : workflowName; + const labels = [`workflowFriendlyName=${workflowName}`, `workflow=${label}`]; + utility_1.checkForErrors([kubectl.labelFiles(files, labels, true)], true); } function updateResourceObjects(filePaths, imagePullSecrets, containers) { const newObjectsList = []; diff --git a/lib/utilities/utility.js b/lib/utilities/utility.js index 297942f3..fb71534c 100644 --- a/lib/utilities/utility.js +++ b/lib/utilities/utility.js @@ -67,13 +67,13 @@ function getLastSuccessfulRunSha(kubectl, namespaceName, annotationKey) { return ''; } else { - if (result.stderr) { + if (!!result.stderr) { core.debug(`${result.stderr}`); return process.env.GITHUB_SHA; } - else if (result.stdout) { + else if (!!result.stdout) { const annotationsSet = JSON.parse(result.stdout).metadata.annotations; - if (!!annotationsSet[annotationKey]) { + if (!!annotationsSet && !!annotationsSet[annotationKey]) { return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit; } else { diff --git a/src/githubClient.ts b/src/githubClient.ts index 11ad069a..b1cc4639 100644 --- a/src/githubClient.ts +++ b/src/githubClient.ts @@ -7,23 +7,6 @@ export class GitHubClient { this._token = token; } - public async getSuccessfulRunsOnBranch(branch: string, workflowFileName: string, force?: boolean): Promise { - if (force || !this._successfulRunsOnBranchPromise) { - const lastSuccessfulRunUrl = `https://api.github.com/repos/${this._repository}/actions/workflows/${workflowFileName}/runs?status=success&branch=${branch}`; - const webRequest = new WebRequest(); - webRequest.method = "GET"; - webRequest.uri = lastSuccessfulRunUrl; - webRequest.headers = { - Authorization: `Bearer ${this._token}` - }; - - core.debug(`Getting last successful run for repo: ${this._repository} on branch: ${branch}`); - const response: WebResponse = await sendRequest(webRequest); - this._successfulRunsOnBranchPromise = Promise.resolve(response); - } - return this._successfulRunsOnBranchPromise; - } - public async getWorkflows(force?: boolean): Promise { if (force || !this._workflowsPromise) { const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`; @@ -43,6 +26,5 @@ export class GitHubClient { private _repository: string; private _token: string; - private _successfulRunsOnBranchPromise: Promise; private _workflowsPromise: Promise; } \ No newline at end of file diff --git a/src/utilities/strategy-helpers/deployment-helper.ts b/src/utilities/strategy-helpers/deployment-helper.ts index 31314d94..9aaf6209 100644 --- a/src/utilities/strategy-helpers/deployment-helper.ts +++ b/src/utilities/strategy-helpers/deployment-helper.ts @@ -135,7 +135,11 @@ function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Res } function labelResources(files: string[], kubectl: Kubectl, label: string) { - checkForErrors([kubectl.labelFiles(files, [`workflow=${label}`], true)], true); + let workflowName = process.env.GITHUB_WORKFLOW; + workflowName = workflowName.startsWith('.github/workflows/') ? + workflowName.replace(".github/workflows/", "") : workflowName; + const labels = [`workflowFriendlyName=${workflowName}`, `workflow=${label}`]; + checkForErrors([kubectl.labelFiles(files, labels, true)], true); } function updateResourceObjects(filePaths: string[], imagePullSecrets: string[], containers: string[]): string[] { diff --git a/src/utilities/utility.ts b/src/utilities/utility.ts index c1588b23..fc8119f7 100644 --- a/src/utilities/utility.ts +++ b/src/utilities/utility.ts @@ -58,13 +58,13 @@ export function getLastSuccessfulRunSha(kubectl: Kubectl, namespaceName: string, return ''; } else { - if (result.stderr) { + if (!!result.stderr) { core.debug(`${result.stderr}`); return process.env.GITHUB_SHA; } - else if (result.stdout) { + else if (!!result.stdout) { const annotationsSet = JSON.parse(result.stdout).metadata.annotations; - if (!!annotationsSet[annotationKey]) { + if (!!annotationsSet && !!annotationsSet[annotationKey]) { return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit; } else { From 65e12248468f7e4fa5ea6469bb9885e7c9926686 Mon Sep 17 00:00:00 2001 From: Koushik Dey Date: Thu, 27 Aug 2020 17:14:59 +0530 Subject: [PATCH 5/6] Addressed review comments. --- __tests__/run.test.ts | 12 +-- lib/githubClient.js | 25 +++---- lib/kubectl-object-model.js | 22 ++---- .../strategy-helpers/deployment-helper.js | 6 +- lib/utilities/utility.js | 75 ++++++++++--------- src/constants.ts | 1 + src/githubClient.ts | 26 +++---- src/kubectl-object-model.ts | 16 ++-- .../strategy-helpers/deployment-helper.ts | 6 +- src/utilities/utility.ts | 75 ++++++++++--------- 10 files changed, 133 insertions(+), 131 deletions(-) diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index 6132a9fb..a130584b 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -312,11 +312,11 @@ test("deployment - deploy() - Annotate & label resources", async () => { kubeCtl.labelFiles = jest.fn(); //Invoke and assert await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); - expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', [annotationKeyValStr], true); - expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [annotationKeyValStr], true); + expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', annotationKeyValStr); + expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], annotationKeyValStr); expect(kubeCtl.annotate).toBeCalledTimes(2); expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], - [`workflowFriendlyName=workflow.yml`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`], true); + [`workflowFriendlyName=workflow.yml`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`]); }); test("deployment - deploy() - Annotate & label resources for a new workflow", async () => { @@ -340,11 +340,11 @@ test("deployment - deploy() - Annotate & label resources for a new workflow", as kubeCtl.labelFiles = jest.fn(); //Invoke and assert await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); - expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', [annotationKeyValStr], true); - expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], [annotationKeyValStr], true); + expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', annotationKeyValStr); + expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], annotationKeyValStr); expect(kubeCtl.annotate).toBeCalledTimes(2); expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], - [`workflowFriendlyName=${process.env.GITHUB_WORKFLOW}`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`], true); + [`workflowFriendlyName=${process.env.GITHUB_WORKFLOW}`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`]); }); test("deployment - deploy() - Annotate resources failed", async () => { diff --git a/lib/githubClient.js b/lib/githubClient.js index 7de8bf6c..54d3f5c7 100644 --- a/lib/githubClient.js +++ b/lib/githubClient.js @@ -17,21 +17,18 @@ class GitHubClient { this._repository = repository; this._token = token; } - getWorkflows(force) { + getWorkflows() { return __awaiter(this, void 0, void 0, function* () { - if (force || !this._workflowsPromise) { - const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`; - const webRequest = new httpClient_1.WebRequest(); - webRequest.method = "GET"; - webRequest.uri = getWorkflowFileNameUrl; - webRequest.headers = { - Authorization: `Bearer ${this._token}` - }; - core.debug(`Getting workflows for repo: ${this._repository}`); - const response = yield httpClient_1.sendRequest(webRequest); - this._workflowsPromise = Promise.resolve(response); - } - return this._workflowsPromise; + const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`; + const webRequest = new httpClient_1.WebRequest(); + webRequest.method = "GET"; + webRequest.uri = getWorkflowFileNameUrl; + webRequest.headers = { + Authorization: `Bearer ${this._token}` + }; + core.debug(`Getting workflows for repo: ${this._repository}`); + const response = yield httpClient_1.sendRequest(webRequest); + return Promise.resolve(response); }); } } diff --git a/lib/kubectl-object-model.js b/lib/kubectl-object-model.js index b835b7e9..868143e6 100644 --- a/lib/kubectl-object-model.js +++ b/lib/kubectl-object-model.js @@ -37,30 +37,24 @@ class Kubectl { } return newReplicaSet; } - annotate(resourceType, resourceName, annotations, overwrite) { + annotate(resourceType, resourceName, annotation) { let args = ['annotate', resourceType, resourceName]; - args = args.concat(annotations); - if (!!overwrite) { - args.push(`--overwrite`); - } + args.push(annotation); + args.push(`--overwrite`); return this.execute(args); } - annotateFiles(files, annotations, overwrite) { + annotateFiles(files, annotation) { let args = ['annotate']; args = args.concat(['-f', this.createInlineArray(files)]); - args = args.concat(annotations); - if (!!overwrite) { - args.push(`--overwrite`); - } + args.push(annotation); + args.push(`--overwrite`); return this.execute(args); } - labelFiles(files, labels, overwrite) { + labelFiles(files, labels) { let args = ['label']; args = args.concat(['-f', this.createInlineArray(files)]); args = args.concat(labels); - if (!!overwrite) { - args.push(`--overwrite`); - } + args.push(`--overwrite`); return this.execute(args); } getAllPods() { diff --git a/lib/utilities/strategy-helpers/deployment-helper.js b/lib/utilities/strategy-helpers/deployment-helper.js index 0c2f6d8f..d482e853 100644 --- a/lib/utilities/strategy-helpers/deployment-helper.js +++ b/lib/utilities/strategy-helpers/deployment-helper.js @@ -122,8 +122,8 @@ function annotateResources(files, kubectl, resourceTypes, allPods, annotationKey const annotateResults = []; const lastSuccessSha = utility_1.getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey); let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); - annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, [annotationKeyValStr], true)); - annotateResults.push(kubectl.annotateFiles(files, [annotationKeyValStr], true)); + annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, annotationKeyValStr)); + annotateResults.push(kubectl.annotateFiles(files, annotationKeyValStr)); resourceTypes.forEach(resource => { if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { utility_1.annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods) @@ -137,7 +137,7 @@ function labelResources(files, kubectl, label) { workflowName = workflowName.startsWith('.github/workflows/') ? workflowName.replace(".github/workflows/", "") : workflowName; const labels = [`workflowFriendlyName=${workflowName}`, `workflow=${label}`]; - utility_1.checkForErrors([kubectl.labelFiles(files, labels, true)], true); + utility_1.checkForErrors([kubectl.labelFiles(files, labels)], true); } function updateResourceObjects(filePaths, imagePullSecrets, containers) { const newObjectsList = []; diff --git a/lib/utilities/utility.js b/lib/utilities/utility.js index fb71534c..0c419d3d 100644 --- a/lib/utilities/utility.js +++ b/lib/utilities/utility.js @@ -28,7 +28,7 @@ function isEqual(str1, str2, ignoreCase) { if (str1 == null || str2 == null) { return false; } - if (!!ignoreCase) { + if (ignoreCase) { return str1.toUpperCase() === str2.toUpperCase(); } else { @@ -40,7 +40,7 @@ function checkForErrors(execResults, warnIfError) { if (execResults.length !== 0) { let stderr = ''; execResults.forEach(result => { - if (!!result && !!result.stderr) { + if (result && result.stderr) { if (result.code !== 0) { stderr += result.stderr + '\n'; } @@ -50,7 +50,7 @@ function checkForErrors(execResults, warnIfError) { } }); if (stderr.length > 0) { - if (!!warnIfError) { + if (warnIfError) { core.warning(stderr.trim()); } else { @@ -61,25 +61,27 @@ function checkForErrors(execResults, warnIfError) { } exports.checkForErrors = checkForErrors; function getLastSuccessfulRunSha(kubectl, namespaceName, annotationKey) { - const result = kubectl.getResource('namespace', namespaceName); - if (!result) { - core.debug(`Failed to get commits from cluster.`); - return ''; + try { + const result = kubectl.getResource('namespace', namespaceName); + if (result) { + if (result.stderr) { + core.warning(`${result.stderr}`); + return process.env.GITHUB_SHA; + } + else if (result.stdout) { + const annotationsSet = JSON.parse(result.stdout).metadata.annotations; + if (annotationsSet && annotationsSet[annotationKey]) { + return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit; + } + else { + return 'NA'; + } + } + } } - else { - if (!!result.stderr) { - core.debug(`${result.stderr}`); - return process.env.GITHUB_SHA; - } - else if (!!result.stdout) { - const annotationsSet = JSON.parse(result.stdout).metadata.annotations; - if (!!annotationsSet && !!annotationsSet[annotationKey]) { - return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit; - } - else { - return 'NA'; - } - } + catch (ex) { + core.warning(`Failed to get commits from cluster. ${JSON.stringify(ex)}`); + return ''; } } exports.getLastSuccessfulRunSha = getLastSuccessfulRunSha; @@ -89,20 +91,25 @@ function getWorkflowFilePath(githubToken) { if (!workflowFilePath.startsWith('.github/workflows/')) { const githubClient = new githubClient_1.GitHubClient(process.env.GITHUB_REPOSITORY, githubToken); const response = yield githubClient.getWorkflows(); - if (response.statusCode == httpClient_1.StatusCodes.OK - && !!response.body - && !!response.body.total_count) { - if (response.body.total_count > 0) { - for (let workflow of response.body.workflows) { - if (process.env.GITHUB_WORKFLOW === workflow.name) { - workflowFilePath = workflow.path; - break; + if (response) { + if (response.statusCode == httpClient_1.StatusCodes.OK + && response.body + && response.body.total_count) { + if (response.body.total_count > 0) { + for (let workflow of response.body.workflows) { + if (process.env.GITHUB_WORKFLOW === workflow.name) { + workflowFilePath = workflow.path; + break; + } } } } + else if (response.statusCode != httpClient_1.StatusCodes.OK) { + core.debug(`An error occured while getting list of workflows on the repo. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); + } } - else if (response.statusCode != httpClient_1.StatusCodes.OK) { - core.debug(`An error occured while getting list of workflows on the repo. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); + else { + core.warning(`Failed to get response from workflow list API`); } } return Promise.resolve(workflowFilePath); @@ -115,13 +122,13 @@ function annotateChildPods(kubectl, resourceType, resourceName, annotationKeyVal if (resourceType.toLowerCase().indexOf('deployment') > -1) { owner = kubectl.getNewReplicaSet(resourceName); } - if (!!allPods && !!allPods.items && allPods.items.length > 0) { + if (allPods && allPods.items && allPods.items.length > 0) { allPods.items.forEach((pod) => { const owners = pod.metadata.ownerReferences; - if (!!owners) { + if (owners) { owners.forEach(ownerRef => { if (ownerRef.name === owner) { - commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, [annotationKeyValStr], true)); + commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, annotationKeyValStr)); } }); } diff --git a/src/constants.ts b/src/constants.ts index b2dc7e44..d29d3235 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,6 +40,7 @@ export function getWorkflowAnnotationsJson(lastSuccessRunSha: string): string { + `'provider': 'GitHub'` + `}`; } + export function getWorkflowAnnotationKeyLabel(workflowFilePath: string): string { const hashKey = require("crypto").createHash("MD5") .update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`) diff --git a/src/githubClient.ts b/src/githubClient.ts index b1cc4639..26c1c811 100644 --- a/src/githubClient.ts +++ b/src/githubClient.ts @@ -7,24 +7,20 @@ export class GitHubClient { this._token = token; } - public async getWorkflows(force?: boolean): Promise { - if (force || !this._workflowsPromise) { - const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`; - const webRequest = new WebRequest(); - webRequest.method = "GET"; - webRequest.uri = getWorkflowFileNameUrl; - webRequest.headers = { - Authorization: `Bearer ${this._token}` - }; + public async getWorkflows(): Promise { + const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`; + const webRequest = new WebRequest(); + webRequest.method = "GET"; + webRequest.uri = getWorkflowFileNameUrl; + webRequest.headers = { + Authorization: `Bearer ${this._token}` + }; - core.debug(`Getting workflows for repo: ${this._repository}`); - const response: WebResponse = await sendRequest(webRequest); - this._workflowsPromise = Promise.resolve(response); - } - return this._workflowsPromise; + core.debug(`Getting workflows for repo: ${this._repository}`); + const response: WebResponse = await sendRequest(webRequest); + return Promise.resolve(response); } private _repository: string; private _token: string; - private _workflowsPromise: Promise; } \ No newline at end of file diff --git a/src/kubectl-object-model.ts b/src/kubectl-object-model.ts index 65d354ea..eb452cf7 100644 --- a/src/kubectl-object-model.ts +++ b/src/kubectl-object-model.ts @@ -50,26 +50,26 @@ export class Kubectl { return newReplicaSet; } - public annotate(resourceType: string, resourceName: string, annotations: string[], overwrite?: boolean): IExecSyncResult { + public annotate(resourceType: string, resourceName: string, annotation: string): IExecSyncResult { let args = ['annotate', resourceType, resourceName]; - args = args.concat(annotations); - if (!!overwrite) { args.push(`--overwrite`); } + args.push(annotation); + args.push(`--overwrite`); return this.execute(args); } - public annotateFiles(files: string | string[], annotations: string[], overwrite?: boolean): IExecSyncResult { + public annotateFiles(files: string | string[], annotation: string): IExecSyncResult { let args = ['annotate']; args = args.concat(['-f', this.createInlineArray(files)]); - args = args.concat(annotations); - if (!!overwrite) { args.push(`--overwrite`); } + args.push(annotation); + args.push(`--overwrite`); return this.execute(args); } - public labelFiles(files: string | string[], labels: string[], overwrite?: boolean): IExecSyncResult { + public labelFiles(files: string | string[], labels: string[]): IExecSyncResult { let args = ['label']; args = args.concat(['-f', this.createInlineArray(files)]); args = args.concat(labels); - if (!!overwrite) { args.push(`--overwrite`); } + args.push(`--overwrite`); return this.execute(args); } diff --git a/src/utilities/strategy-helpers/deployment-helper.ts b/src/utilities/strategy-helpers/deployment-helper.ts index 9aaf6209..68c746a9 100644 --- a/src/utilities/strategy-helpers/deployment-helper.ts +++ b/src/utilities/strategy-helpers/deployment-helper.ts @@ -123,8 +123,8 @@ function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Res const annotateResults: IExecSyncResult[] = []; const lastSuccessSha = getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey); let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); - annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, [annotationKeyValStr], true)); - annotateResults.push(kubectl.annotateFiles(files, [annotationKeyValStr], true)); + annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, annotationKeyValStr)); + annotateResults.push(kubectl.annotateFiles(files, annotationKeyValStr)); resourceTypes.forEach(resource => { if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods) @@ -139,7 +139,7 @@ function labelResources(files: string[], kubectl: Kubectl, label: string) { workflowName = workflowName.startsWith('.github/workflows/') ? workflowName.replace(".github/workflows/", "") : workflowName; const labels = [`workflowFriendlyName=${workflowName}`, `workflow=${label}`]; - checkForErrors([kubectl.labelFiles(files, labels, true)], true); + checkForErrors([kubectl.labelFiles(files, labels)], true); } function updateResourceObjects(filePaths: string[], imagePullSecrets: string[], containers: string[]): string[] { diff --git a/src/utilities/utility.ts b/src/utilities/utility.ts index fc8119f7..e75e1387 100644 --- a/src/utilities/utility.ts +++ b/src/utilities/utility.ts @@ -22,7 +22,7 @@ export function isEqual(str1: string, str2: string, ignoreCase?: boolean): boole return false; } - if (!!ignoreCase) { + if (ignoreCase) { return str1.toUpperCase() === str2.toUpperCase(); } else { return str1 === str2; @@ -33,7 +33,7 @@ export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boo if (execResults.length !== 0) { let stderr = ''; execResults.forEach(result => { - if (!!result && !!result.stderr) { + if (result && result.stderr) { if (result.code !== 0) { stderr += result.stderr + '\n'; } else { @@ -42,7 +42,7 @@ export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boo } }); if (stderr.length > 0) { - if (!!warnIfError) { + if (warnIfError) { core.warning(stderr.trim()); } else { throw new Error(stderr.trim()); @@ -52,25 +52,27 @@ export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boo } export function getLastSuccessfulRunSha(kubectl: Kubectl, namespaceName: string, annotationKey: string): string { - const result = kubectl.getResource('namespace', namespaceName); - if (!result) { - core.debug(`Failed to get commits from cluster.`); - return ''; + try { + const result = kubectl.getResource('namespace', namespaceName); + if (result) { + if (result.stderr) { + core.warning(`${result.stderr}`); + return process.env.GITHUB_SHA; + } + else if (result.stdout) { + const annotationsSet = JSON.parse(result.stdout).metadata.annotations; + if (annotationsSet && annotationsSet[annotationKey]) { + return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit; + } + else { + return 'NA'; + } + } + } } - else { - if (!!result.stderr) { - core.debug(`${result.stderr}`); - return process.env.GITHUB_SHA; - } - else if (!!result.stdout) { - const annotationsSet = JSON.parse(result.stdout).metadata.annotations; - if (!!annotationsSet && !!annotationsSet[annotationKey]) { - return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit; - } - else { - return 'NA'; - } - } + catch (ex) { + core.warning(`Failed to get commits from cluster. ${JSON.stringify(ex)}`); + return ''; } } @@ -79,20 +81,25 @@ export async function getWorkflowFilePath(githubToken: string): Promise if (!workflowFilePath.startsWith('.github/workflows/')) { const githubClient = new GitHubClient(process.env.GITHUB_REPOSITORY, githubToken); const response = await githubClient.getWorkflows(); - if (response.statusCode == StatusCodes.OK - && !!response.body - && !!response.body.total_count) { - if (response.body.total_count > 0) { - for (let workflow of response.body.workflows) { - if (process.env.GITHUB_WORKFLOW === workflow.name) { - workflowFilePath = workflow.path; - break; + if (response) { + if (response.statusCode == StatusCodes.OK + && response.body + && response.body.total_count) { + if (response.body.total_count > 0) { + for (let workflow of response.body.workflows) { + if (process.env.GITHUB_WORKFLOW === workflow.name) { + workflowFilePath = workflow.path; + break; + } } } } + else if (response.statusCode != StatusCodes.OK) { + core.debug(`An error occured while getting list of workflows on the repo. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); + } } - else if (response.statusCode != StatusCodes.OK) { - core.debug(`An error occured while getting list of workflows on the repo. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`); + else { + core.warning(`Failed to get response from workflow list API`); } } return Promise.resolve(workflowFilePath); @@ -105,13 +112,13 @@ export function annotateChildPods(kubectl: Kubectl, resourceType: string, resour owner = kubectl.getNewReplicaSet(resourceName); } - if (!!allPods && !!allPods.items && allPods.items.length > 0) { + if (allPods && allPods.items && allPods.items.length > 0) { allPods.items.forEach((pod) => { const owners = pod.metadata.ownerReferences; - if (!!owners) { + if (owners) { owners.forEach(ownerRef => { if (ownerRef.name === owner) { - commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, [annotationKeyValStr], true)); + commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, annotationKeyValStr)); } }); } From f9acc4f77259e4a9e0f2d07605894c7b0008a118 Mon Sep 17 00:00:00 2001 From: Koushik Dey Date: Fri, 28 Aug 2020 14:42:55 +0530 Subject: [PATCH 6/6] WokflowFileName added in annotations for View All Runs in UX --- __tests__/run.test.ts | 8 ++++---- lib/constants.js | 3 ++- lib/utilities/strategy-helpers/deployment-helper.js | 6 +++--- src/constants.ts | 3 ++- src/utilities/strategy-helpers/deployment-helper.ts | 6 +++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index a130584b..3b7437fb 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -293,7 +293,7 @@ test("deployment - deploy() - deploy force flag on", async () => { }); test("deployment - deploy() - Annotate & label resources", async () => { - let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('currentCommit'); + let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('currentCommit', '.github/workflows/workflow.yml'); const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); @@ -320,8 +320,8 @@ test("deployment - deploy() - Annotate & label resources", async () => { }); test("deployment - deploy() - Annotate & label resources for a new workflow", async () => { - process.env.GITHUB_WORKFLOW = 'NewWorkflow'; - let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('NA'); + process.env.GITHUB_WORKFLOW = '.github/workflows/NewWorkflow.yml'; + let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('NA', '.github/workflows/NewWorkflow.yml'); const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); @@ -344,7 +344,7 @@ test("deployment - deploy() - Annotate & label resources for a new workflow", as expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], annotationKeyValStr); expect(kubeCtl.annotate).toBeCalledTimes(2); expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp"], - [`workflowFriendlyName=${process.env.GITHUB_WORKFLOW}`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`]); + [`workflowFriendlyName=NewWorkflow.yml`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`]); }); test("deployment - deploy() - Annotate resources failed", async () => { diff --git a/lib/constants.js b/lib/constants.js index 1ec3b50f..9ee05150 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -25,11 +25,12 @@ ServiceTypes.clusterIP = 'ClusterIP'; exports.deploymentTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset']; exports.workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob']; exports.workloadTypesWithRolloutStatus = ['deployment', 'daemonset', 'statefulset']; -function getWorkflowAnnotationsJson(lastSuccessRunSha) { +function getWorkflowAnnotationsJson(lastSuccessRunSha, workflowFilePath) { return `{` + `'run': '${process.env.GITHUB_RUN_ID}',` + `'repository': '${process.env.GITHUB_REPOSITORY}',` + `'workflow': '${process.env.GITHUB_WORKFLOW}',` + + `'workflowFileName': '${workflowFilePath.replace(".github/workflows/", "")}',` + `'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}',` diff --git a/lib/utilities/strategy-helpers/deployment-helper.js b/lib/utilities/strategy-helpers/deployment-helper.js index d482e853..b317e2b9 100644 --- a/lib/utilities/strategy-helpers/deployment-helper.js +++ b/lib/utilities/strategy-helpers/deployment-helper.js @@ -114,14 +114,14 @@ function annotateAndLabelResources(files, kubectl, resourceTypes, allPods) { return __awaiter(this, void 0, void 0, function* () { const workflowFilePath = yield utility_1.getWorkflowFilePath(TaskInputParameters.githubToken); const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(workflowFilePath); - annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel); + annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel, workflowFilePath); labelResources(files, kubectl, annotationKeyLabel); }); } -function annotateResources(files, kubectl, resourceTypes, allPods, annotationKey) { +function annotateResources(files, kubectl, resourceTypes, allPods, annotationKey, workflowFilePath) { const annotateResults = []; const lastSuccessSha = utility_1.getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey); - let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); + let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha, workflowFilePath); annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, annotationKeyValStr)); annotateResults.push(kubectl.annotateFiles(files, annotationKeyValStr)); resourceTypes.forEach(resource => { diff --git a/src/constants.ts b/src/constants.ts index d29d3235..62d1bdf0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,11 +25,12 @@ 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 function getWorkflowAnnotationsJson(lastSuccessRunSha: string): string { +export function getWorkflowAnnotationsJson(lastSuccessRunSha: string, workflowFilePath: string): string { return `{` + `'run': '${process.env.GITHUB_RUN_ID}',` + `'repository': '${process.env.GITHUB_REPOSITORY}',` + `'workflow': '${process.env.GITHUB_WORKFLOW}',` + + `'workflowFileName': '${workflowFilePath.replace(".github/workflows/", "")}',` + `'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}',` diff --git a/src/utilities/strategy-helpers/deployment-helper.ts b/src/utilities/strategy-helpers/deployment-helper.ts index 68c746a9..b162c6b3 100644 --- a/src/utilities/strategy-helpers/deployment-helper.ts +++ b/src/utilities/strategy-helpers/deployment-helper.ts @@ -115,14 +115,14 @@ async function checkManifestStability(kubectl: Kubectl, resources: Resource[]): async function annotateAndLabelResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any) { const workflowFilePath = await getWorkflowFilePath(TaskInputParameters.githubToken); const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(workflowFilePath); - annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel); + annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel, workflowFilePath); labelResources(files, kubectl, annotationKeyLabel); } -function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any, annotationKey: string) { +function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any, annotationKey: string, workflowFilePath: string) { const annotateResults: IExecSyncResult[] = []; const lastSuccessSha = getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey); - let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha); + let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha, workflowFilePath); annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, annotationKeyValStr)); annotateResults.push(kubectl.annotateFiles(files, annotationKeyValStr)); resourceTypes.forEach(resource => {