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
+24 -13
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
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;
+5 -1
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) {
+9
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
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;
});
}
@@ -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 = [];
+42 -29
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));
}