GitHub API integration, labelling, annotating namespace

This commit is contained in:
Koushik Dey 2020-07-22 20:07:30 +05:30
parent 2c8e7a02f5
commit 48110f8b94
16 changed files with 500 additions and 110 deletions

View File

@ -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<Kubectl> = 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)
});

View File

@ -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

View File

@ -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;

38
lib/githubClient.js Normal file
View File

@ -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;

View File

@ -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) {

View File

@ -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);
}

111
lib/utilities/httpClient.js Normal file
View File

@ -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;
});
}

View File

@ -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 = [];

View File

@ -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));
}

View File

@ -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`
];
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}`;
}

30
src/githubClient.ts Normal file
View File

@ -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<any> {
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<any>;
}

View File

@ -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) {

View File

@ -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);
}

114
src/utilities/httpClient.ts Normal file
View File

@ -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<WebResponse> {
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<any> {
return new Promise((resolve, reject) => {
setTimeout(resolve, sleepDurationInSeconds * 1000);
});
}
async function sendRequestInternal(request: WebRequest): Promise<WebResponse> {
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<WebResponse> {
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;
}

View File

@ -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) => {

View File

@ -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<string> {
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));
}