mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-04-13 10:02:19 +08:00
parent
8d56cba217
commit
be01c3f321
70
README.md
70
README.md
@ -12,7 +12,7 @@ Rollout status is checked for the Kubernetes objects deployed. This is done to i
|
|||||||
#### Secret handling
|
#### Secret handling
|
||||||
The manifest files specfied as inputs are augmented with appropriate imagePullSecrets before deploying to the cluster.
|
The manifest files specfied as inputs are augmented with appropriate imagePullSecrets before deploying to the cluster.
|
||||||
|
|
||||||
|
#### Sample YAML to run a basic deployment
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: Azure/k8s-deploy@v1
|
- uses: Azure/k8s-deploy@v1
|
||||||
@ -25,6 +25,74 @@ Rollout status is checked for the Kubernetes objects deployed. This is done to i
|
|||||||
manifests: '/manifests/*.*'
|
manifests: '/manifests/*.*'
|
||||||
kubectl-version: 'latest' # optional
|
kubectl-version: 'latest' # optional
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Deployment Strategies
|
||||||
|
|
||||||
|
#### Pod Canary
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: Azure/k8s-deploy@v1
|
||||||
|
with:
|
||||||
|
namespace: 'myapp' # optional
|
||||||
|
images: 'contoso.azurecr.io/myapp:${{ event.run_id }} '
|
||||||
|
imagepullsecrets: |
|
||||||
|
image-pull-secret1
|
||||||
|
image-pull-secret2
|
||||||
|
manifests: '/manifests/*.*'
|
||||||
|
strategy: canary
|
||||||
|
percentage: 20
|
||||||
|
```
|
||||||
|
|
||||||
|
Inorder to promote or reject your canary deployment use the following:
|
||||||
|
```yaml
|
||||||
|
- uses: Azure/k8s-deploy@v1
|
||||||
|
with:
|
||||||
|
namespace: 'myapp' # optional
|
||||||
|
images: 'contoso.azurecr.io/myapp:${{ event.run_id }} '
|
||||||
|
imagepullsecrets: |
|
||||||
|
image-pull-secret1
|
||||||
|
image-pull-secret2
|
||||||
|
manifests: '/manifests/*.*'
|
||||||
|
strategy: canary
|
||||||
|
percentage: 20
|
||||||
|
action: promote # set to reject if you want to reject it
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SMI Canary
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: Azure/k8s-deploy@v1
|
||||||
|
with:
|
||||||
|
namespace: 'myapp' # optional
|
||||||
|
images: 'contoso.azurecr.io/myapp:${{ event.run_id }} '
|
||||||
|
imagepullsecrets: |
|
||||||
|
image-pull-secret1
|
||||||
|
image-pull-secret2
|
||||||
|
manifests: '/manifests/*.*'
|
||||||
|
strategy: canary
|
||||||
|
traffic-split-method: smi
|
||||||
|
percentage: 20
|
||||||
|
baseline-and-canary-replicas: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Inorder to promote or reject your canary deployment use the following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: Azure/k8s-deploy@v1
|
||||||
|
with:
|
||||||
|
namespace: 'myapp' # optional
|
||||||
|
images: 'contoso.azurecr.io/myapp:${{ event.run_id }} '
|
||||||
|
imagepullsecrets: |
|
||||||
|
image-pull-secret1
|
||||||
|
image-pull-secret2
|
||||||
|
manifests: '/manifests/*.*'
|
||||||
|
strategy: canary
|
||||||
|
traffic-split-method: smi
|
||||||
|
percentage: 20
|
||||||
|
baseline-and-canary-replicas: 1
|
||||||
|
action: promote # set to reject if you want to reject it
|
||||||
|
```
|
||||||
|
|
||||||
Refer to the action metadata file for details about all the inputs https://github.com/Azure/k8s-deploy/blob/master/action.yml
|
Refer to the action metadata file for details about all the inputs https://github.com/Azure/k8s-deploy/blob/master/action.yml
|
||||||
|
|
||||||
## End to end workflow for building container images and deploying to an Azure Kubernetes Service cluster
|
## End to end workflow for building container images and deploying to an Azure Kubernetes Service cluster
|
||||||
|
|||||||
27
action.yml
27
action.yml
@ -10,8 +10,7 @@ inputs:
|
|||||||
required: true
|
required: true
|
||||||
default: ''
|
default: ''
|
||||||
images:
|
images:
|
||||||
description: 'Fully qualified resource URL of the image(s) to be used for substitutions on the manifest files
|
description: 'Fully qualified resource URL of the image(s) to be used for substitutions on the manifest files Example: contosodemo.azurecr.io/helloworld:test'
|
||||||
Example: contosodemo.azurecr.io/helloworld:test'
|
|
||||||
required: false
|
required: false
|
||||||
imagepullsecrets:
|
imagepullsecrets:
|
||||||
description: 'Name of a docker-registry secret that has already been set up within the cluster. Each of these secret names are added under imagePullSecrets field for the workloads found in the input manifest files'
|
description: 'Name of a docker-registry secret that has already been set up within the cluster. Each of these secret names are added under imagePullSecrets field for the workloads found in the input manifest files'
|
||||||
@ -19,6 +18,30 @@ Example: contosodemo.azurecr.io/helloworld:test'
|
|||||||
kubectl-version:
|
kubectl-version:
|
||||||
description: 'Version of kubectl. Installs a specific version of kubectl binary'
|
description: 'Version of kubectl. Installs a specific version of kubectl binary'
|
||||||
required: false
|
required: false
|
||||||
|
strategy:
|
||||||
|
description: 'Deployment strategy to be used. Allowed values are none, canary'
|
||||||
|
required: false
|
||||||
|
default: 'none'
|
||||||
|
traffic-split-method:
|
||||||
|
description: "Traffic split method to be used. Allowed values are pod, smi"
|
||||||
|
required: false
|
||||||
|
default: 'pod'
|
||||||
|
baseline-and-canary-replicas:
|
||||||
|
description: 'Baseline and canary replicas count; valid value i.e between 0 to 100.'
|
||||||
|
required: false
|
||||||
|
default: 0
|
||||||
|
percentage:
|
||||||
|
description: 'Percentage of traffic redirect to canary deployment'
|
||||||
|
required: false
|
||||||
|
default: 0
|
||||||
|
args:
|
||||||
|
description: 'Arguments'
|
||||||
|
required: false
|
||||||
|
action:
|
||||||
|
description: 'deploy/promote/reject'
|
||||||
|
required: true
|
||||||
|
default: 'deploy'
|
||||||
|
|
||||||
branding:
|
branding:
|
||||||
color: 'green' # optional, decorates the entry in the GitHub Marketplace
|
color: 'green' # optional, decorates the entry in the GitHub Marketplace
|
||||||
runs:
|
runs:
|
||||||
|
|||||||
50
lib/actions/promote.js
Normal file
50
lib/actions/promote.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
'use strict';
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const deploymentHelper = require("../utilities/strategy-helpers/deployment-helper");
|
||||||
|
const canaryDeploymentHelper = require("../utilities/strategy-helpers/canary-deployment-helper");
|
||||||
|
const SMICanaryDeploymentHelper = require("../utilities/strategy-helpers/smi-canary-deployment-helper");
|
||||||
|
const utils = require("../utilities/manifest-utilities");
|
||||||
|
const TaskInputParameters = require("../input-parameters");
|
||||||
|
const kubectl_object_model_1 = require("../kubectl-object-model");
|
||||||
|
function promote(ignoreSslErrors) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const kubectl = new kubectl_object_model_1.Kubectl(yield utils.getKubectl(), TaskInputParameters.namespace, ignoreSslErrors);
|
||||||
|
if (!canaryDeploymentHelper.isCanaryDeploymentStrategy()) {
|
||||||
|
core.debug('Strategy is not canary deployment. Invalid request.');
|
||||||
|
throw ('InvalidPromotetActionDeploymentStrategy');
|
||||||
|
}
|
||||||
|
let includeServices = false;
|
||||||
|
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||||
|
includeServices = true;
|
||||||
|
// In case of SMI traffic split strategy when deployment is promoted, first we will redirect traffic to
|
||||||
|
// Canary deployment, then update stable deployment and then redirect traffic to stable deployment
|
||||||
|
core.debug('Redirecting traffic to canary deployment');
|
||||||
|
SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(kubectl, TaskInputParameters.manifests);
|
||||||
|
core.debug('Deploying input manifests with SMI canary strategy');
|
||||||
|
yield deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None');
|
||||||
|
core.debug('Redirecting traffic to stable deployment');
|
||||||
|
SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(kubectl, TaskInputParameters.manifests);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.debug('Deploying input manifests');
|
||||||
|
yield deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None');
|
||||||
|
}
|
||||||
|
core.debug('Deployment strategy selected is Canary. Deleting canary and baseline workloads.');
|
||||||
|
try {
|
||||||
|
canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests, includeServices);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.warning('Exception occurred while deleting canary and baseline workloads. Exception: ' + ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.promote = promote;
|
||||||
34
lib/actions/reject.js
Normal file
34
lib/actions/reject.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
'use strict';
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const canaryDeploymentHelper = require("../utilities/strategy-helpers/canary-deployment-helper");
|
||||||
|
const SMICanaryDeploymentHelper = require("../utilities/strategy-helpers/smi-canary-deployment-helper");
|
||||||
|
const kubectl_object_model_1 = require("../kubectl-object-model");
|
||||||
|
const utils = require("../utilities/manifest-utilities");
|
||||||
|
const TaskInputParameters = require("../input-parameters");
|
||||||
|
function reject(ignoreSslErrors) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const kubectl = new kubectl_object_model_1.Kubectl(yield utils.getKubectl(), TaskInputParameters.namespace, ignoreSslErrors);
|
||||||
|
if (!canaryDeploymentHelper.isCanaryDeploymentStrategy()) {
|
||||||
|
core.debug('Strategy is not canary deployment. Invalid request.');
|
||||||
|
throw ('InvalidRejectActionDeploymentStrategy');
|
||||||
|
}
|
||||||
|
let includeServices = false;
|
||||||
|
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||||
|
core.debug('Reject deployment with SMI canary strategy');
|
||||||
|
includeServices = true;
|
||||||
|
SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(kubectl, TaskInputParameters.manifests);
|
||||||
|
}
|
||||||
|
core.debug('Deployment strategy selected is Canary. Deleting baseline and canary workloads.');
|
||||||
|
canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests, includeServices);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.reject = reject;
|
||||||
26
lib/constants.js
Normal file
26
lib/constants.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
class KubernetesWorkload {
|
||||||
|
}
|
||||||
|
KubernetesWorkload.pod = 'Pod';
|
||||||
|
KubernetesWorkload.replicaset = 'Replicaset';
|
||||||
|
KubernetesWorkload.deployment = 'Deployment';
|
||||||
|
KubernetesWorkload.statefulSet = 'StatefulSet';
|
||||||
|
KubernetesWorkload.daemonSet = 'DaemonSet';
|
||||||
|
KubernetesWorkload.job = 'job';
|
||||||
|
KubernetesWorkload.cronjob = 'cronjob';
|
||||||
|
exports.KubernetesWorkload = KubernetesWorkload;
|
||||||
|
class DiscoveryAndLoadBalancerResource {
|
||||||
|
}
|
||||||
|
DiscoveryAndLoadBalancerResource.service = 'service';
|
||||||
|
DiscoveryAndLoadBalancerResource.ingress = 'ingress';
|
||||||
|
exports.DiscoveryAndLoadBalancerResource = DiscoveryAndLoadBalancerResource;
|
||||||
|
class ServiceTypes {
|
||||||
|
}
|
||||||
|
ServiceTypes.loadBalancer = 'LoadBalancer';
|
||||||
|
ServiceTypes.nodePort = 'NodePort';
|
||||||
|
ServiceTypes.clusterIP = 'ClusterIP';
|
||||||
|
exports.ServiceTypes = ServiceTypes;
|
||||||
|
exports.deploymentTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset'];
|
||||||
|
exports.workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob'];
|
||||||
|
exports.workloadTypesWithRolloutStatus = ['deployment', 'daemonset', 'statefulset'];
|
||||||
38
lib/input-parameters.js
Normal file
38
lib/input-parameters.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
'use strict';
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const core = require("@actions/core");
|
||||||
|
exports.namespace = core.getInput('namespace');
|
||||||
|
exports.containers = core.getInput('images').split('\n');
|
||||||
|
exports.imagePullSecrets = core.getInput('imagepullsecrets').split('\n');
|
||||||
|
exports.manifests = core.getInput('manifests').split('\n');
|
||||||
|
exports.canaryPercentage = core.getInput('percentage');
|
||||||
|
exports.deploymentStrategy = core.getInput('strategy');
|
||||||
|
exports.trafficSplitMethod = core.getInput('traffic-split-method');
|
||||||
|
exports.baselineAndCanaryReplicas = core.getInput('baseline-and-canary-replicas');
|
||||||
|
exports.args = core.getInput('arguments');
|
||||||
|
if (!exports.namespace) {
|
||||||
|
core.debug('Namespace was not supplied; using "default" namespace instead.');
|
||||||
|
exports.namespace = 'default';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const pe = parseInt(exports.canaryPercentage);
|
||||||
|
if (pe < 0 || pe > 100) {
|
||||||
|
core.setFailed('A valid percentage value is between 0 and 100');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.setFailed("Enter a valid 'percentage' integer value ");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const pe = parseInt(exports.baselineAndCanaryReplicas);
|
||||||
|
if (pe < 0 || pe > 100) {
|
||||||
|
core.setFailed('A valid baseline-and-canary-replicas value is between 0 and 100');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.setFailed("Enter a valid 'baseline-and-canary-replicas' integer value");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
96
lib/kubectl-object-model.js
Normal file
96
lib/kubectl-object-model.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
"use strict";
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const tool_runner_1 = require("./utilities/tool-runner");
|
||||||
|
class Kubectl {
|
||||||
|
constructor(kubectlPath, namespace, ignoreSSLErrors) {
|
||||||
|
this.kubectlPath = kubectlPath;
|
||||||
|
this.ignoreSSLErrors = !!ignoreSSLErrors;
|
||||||
|
if (!!namespace) {
|
||||||
|
this.namespace = namespace;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.namespace = 'default';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apply(configurationPaths) {
|
||||||
|
return this.execute(['apply', '-f', this.createInlineArray(configurationPaths)]);
|
||||||
|
}
|
||||||
|
describe(resourceType, resourceName, silent) {
|
||||||
|
return this.execute(['describe', resourceType, resourceName], silent);
|
||||||
|
}
|
||||||
|
getNewReplicaSet(deployment) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
let newReplicaSet = '';
|
||||||
|
const result = yield this.describe('deployment', deployment, true);
|
||||||
|
if (result && result.stdout) {
|
||||||
|
const stdout = result.stdout.split('\n');
|
||||||
|
stdout.forEach((line) => {
|
||||||
|
if (!!line && line.toLowerCase().indexOf('newreplicaset') > -1) {
|
||||||
|
newReplicaSet = line.substr(14).trim().split(' ')[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newReplicaSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getAllPods() {
|
||||||
|
return this.execute(['get', 'pods', '-o', 'json'], true);
|
||||||
|
}
|
||||||
|
getClusterInfo() {
|
||||||
|
return this.execute(['cluster-info'], true);
|
||||||
|
}
|
||||||
|
checkRolloutStatus(resourceType, name) {
|
||||||
|
return this.execute(['rollout', 'status', resourceType + '/' + name]);
|
||||||
|
}
|
||||||
|
getResource(resourceType, name) {
|
||||||
|
return this.execute(['get', resourceType + '/' + name, '-o', 'json']);
|
||||||
|
}
|
||||||
|
getResources(applyOutput, filterResourceTypes) {
|
||||||
|
const outputLines = applyOutput.split('\n');
|
||||||
|
const results = [];
|
||||||
|
outputLines.forEach(line => {
|
||||||
|
const words = line.split(' ');
|
||||||
|
if (words.length > 2) {
|
||||||
|
const resourceType = words[0].trim();
|
||||||
|
const resourceName = JSON.parse(words[1].trim());
|
||||||
|
if (filterResourceTypes.filter(type => !!type && resourceType.toLowerCase().startsWith(type.toLowerCase())).length > 0) {
|
||||||
|
results.push({
|
||||||
|
type: resourceType,
|
||||||
|
name: resourceName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
delete(args) {
|
||||||
|
if (typeof args === 'string')
|
||||||
|
return this.execute(['delete', args]);
|
||||||
|
else
|
||||||
|
return this.execute(['delete'].concat(args));
|
||||||
|
}
|
||||||
|
execute(args, silent) {
|
||||||
|
if (this.ignoreSSLErrors) {
|
||||||
|
args.push('--insecure-skip-tls-verify');
|
||||||
|
}
|
||||||
|
args = args.concat(['--namespace', this.namespace]);
|
||||||
|
const command = new tool_runner_1.ToolRunner(this.kubectlPath);
|
||||||
|
command.arg(args);
|
||||||
|
return command.execSync({ silent: !!silent });
|
||||||
|
}
|
||||||
|
createInlineArray(str) {
|
||||||
|
if (typeof str === 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.Kubectl = Kubectl;
|
||||||
120
lib/run.js
120
lib/run.js
@ -11,18 +11,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||||||
const toolCache = require("@actions/tool-cache");
|
const toolCache = require("@actions/tool-cache");
|
||||||
const core = require("@actions/core");
|
const core = require("@actions/core");
|
||||||
const io = require("@actions/io");
|
const io = require("@actions/io");
|
||||||
const toolrunner_1 = require("@actions/exec/lib/toolrunner");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const utility_1 = require("./utilities/utility");
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const utils_1 = require("./utils");
|
|
||||||
const kubernetes_utils_1 = require("./kubernetes-utils");
|
|
||||||
const kubectl_util_1 = require("./kubectl-util");
|
const kubectl_util_1 = require("./kubectl-util");
|
||||||
|
const deployment_helper_1 = require("./utilities/strategy-helpers/deployment-helper");
|
||||||
|
const promote_1 = require("./actions/promote");
|
||||||
|
const reject_1 = require("./actions/reject");
|
||||||
|
const kubectl_object_model_1 = require("./kubectl-object-model");
|
||||||
let kubectlPath = "";
|
let kubectlPath = "";
|
||||||
function setKubectlPath() {
|
function setKubectlPath() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
if (core.getInput('kubectl-version')) {
|
if (core.getInput('kubectl-version')) {
|
||||||
const version = core.getInput('kubect-version');
|
const version = core.getInput('kubectl-version');
|
||||||
kubectlPath = toolCache.find('kubectl', version);
|
kubectlPath = toolCache.find('kubectl', version);
|
||||||
if (!kubectlPath) {
|
if (!kubectlPath) {
|
||||||
kubectlPath = yield installKubectl(version);
|
kubectlPath = yield installKubectl(version);
|
||||||
@ -36,98 +36,14 @@ function setKubectlPath() {
|
|||||||
if (!kubectlPath) {
|
if (!kubectlPath) {
|
||||||
throw new Error('Kubectl is not installed, either add install-kubectl action or provide "kubectl-version" input to download kubectl');
|
throw new Error('Kubectl is not installed, either add install-kubectl action or provide "kubectl-version" input to download kubectl');
|
||||||
}
|
}
|
||||||
kubectlPath = path.join(kubectlPath, `kubectl${utils_1.getExecutableExtension()}`);
|
kubectlPath = path.join(kubectlPath, `kubectl${utility_1.getExecutableExtension()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function deploy(manifests, namespace) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
if (manifests) {
|
|
||||||
for (var i = 0; i < manifests.length; i++) {
|
|
||||||
let manifest = manifests[i];
|
|
||||||
let toolRunner = new toolrunner_1.ToolRunner(kubectlPath, ['apply', '-f', manifest, '--namespace', namespace]);
|
|
||||||
yield toolRunner.exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function checkRolloutStatus(name, kind, namespace) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
const toolrunner = new toolrunner_1.ToolRunner(kubectlPath, ['rollout', 'status', `${kind.trim()}/${name.trim()}`, `--namespace`, namespace]);
|
|
||||||
return toolrunner.exec();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function checkManifestsStability(manifests, namespace) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
manifests.forEach((manifest) => {
|
|
||||||
let content = fs.readFileSync(manifest).toString();
|
|
||||||
yaml.safeLoadAll(content, function (inputObject) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
if (!!inputObject.kind && !!inputObject.metadata && !!inputObject.metadata.name) {
|
|
||||||
let kind = inputObject.kind;
|
|
||||||
switch (kind.toLowerCase()) {
|
|
||||||
case 'deployment':
|
|
||||||
case 'daemonset':
|
|
||||||
case 'statefulset':
|
|
||||||
yield checkRolloutStatus(inputObject.metadata.name, kind, namespace);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
core.debug(`No rollout check for kind: ${inputObject.kind}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getManifestFileName(kind, name) {
|
|
||||||
const filePath = kind + '_' + name + '_' + utils_1.getCurrentTime().toString();
|
|
||||||
const tempDirectory = process.env['RUNNER_TEMP'];
|
|
||||||
const fileName = path.join(tempDirectory, path.basename(filePath));
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
function writeObjectsToFile(inputObjects) {
|
|
||||||
const newFilePaths = [];
|
|
||||||
if (!!inputObjects) {
|
|
||||||
inputObjects.forEach((inputObject) => {
|
|
||||||
try {
|
|
||||||
const inputObjectString = JSON.stringify(inputObject);
|
|
||||||
if (!!inputObject.kind && !!inputObject.metadata && !!inputObject.metadata.name) {
|
|
||||||
const fileName = getManifestFileName(inputObject.kind, inputObject.metadata.name);
|
|
||||||
fs.writeFileSync(path.join(fileName), inputObjectString);
|
|
||||||
newFilePaths.push(fileName);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
core.debug('Input object is not proper K8s resource object. Object: ' + inputObjectString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
core.debug('Exception occurred while wrting object to file : ' + inputObject + ' . Exception: ' + ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return newFilePaths;
|
|
||||||
}
|
|
||||||
function updateManifests(manifests, imagesToOverride, imagepullsecrets) {
|
|
||||||
const newObjectsList = [];
|
|
||||||
manifests.forEach((filePath) => {
|
|
||||||
let fileContents = fs.readFileSync(filePath).toString();
|
|
||||||
fileContents = kubernetes_utils_1.updateContainerImagesInManifestFiles(fileContents, imagesToOverride.split('\n'));
|
|
||||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
|
||||||
if (!!imagepullsecrets && !!inputObject && !!inputObject.kind) {
|
|
||||||
if (kubernetes_utils_1.isWorkloadEntity(inputObject.kind)) {
|
|
||||||
kubernetes_utils_1.updateImagePullSecrets(inputObject, imagepullsecrets.split('\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newObjectsList.push(inputObject);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return writeObjectsToFile(newObjectsList);
|
|
||||||
}
|
|
||||||
function installKubectl(version) {
|
function installKubectl(version) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
if (utils_1.isEqual(version, 'latest')) {
|
if (utility_1.isEqual(version, 'latest')) {
|
||||||
version = yield kubectl_util_1.getStableKubectlVersion();
|
version = yield kubectl_util_1.getStableKubectlVersion();
|
||||||
}
|
}
|
||||||
return yield kubectl_util_1.downloadKubectl(version);
|
return yield kubectl_util_1.downloadKubectl(version);
|
||||||
@ -150,14 +66,22 @@ function run() {
|
|||||||
if (!namespace) {
|
if (!namespace) {
|
||||||
namespace = 'default';
|
namespace = 'default';
|
||||||
}
|
}
|
||||||
|
let action = core.getInput('action');
|
||||||
let manifests = manifestsInput.split('\n');
|
let manifests = manifestsInput.split('\n');
|
||||||
const imagesToOverride = core.getInput('images');
|
if (action === 'deploy') {
|
||||||
const imagePullSecretsToAdd = core.getInput('imagepullsecrets');
|
let strategy = core.getInput('strategy');
|
||||||
if (!!imagePullSecretsToAdd || !!imagesToOverride) {
|
console.log("strategy: ", strategy);
|
||||||
manifests = updateManifests(manifests, imagesToOverride, imagePullSecretsToAdd);
|
yield deployment_helper_1.deploy(new kubectl_object_model_1.Kubectl(kubectlPath, namespace), manifests, strategy);
|
||||||
|
}
|
||||||
|
else if (action === 'promote') {
|
||||||
|
yield promote_1.promote(true);
|
||||||
|
}
|
||||||
|
else if (action === 'reject') {
|
||||||
|
yield reject_1.reject(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.setFailed('Not a valid action. The allowed actions are deploy, promote, reject');
|
||||||
}
|
}
|
||||||
yield deploy(manifests, namespace);
|
|
||||||
yield checkManifestsStability(manifests, namespace);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
run().catch(core.setFailed);
|
run().catch(core.setFailed);
|
||||||
|
|||||||
77
lib/utilities/files-helper.js
Normal file
77
lib/utilities/files-helper.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
'use strict';
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const os = require("os");
|
||||||
|
function getTempDirectory() {
|
||||||
|
return process.env['runner.tempDirectory'] || os.tmpdir();
|
||||||
|
}
|
||||||
|
exports.getTempDirectory = getTempDirectory;
|
||||||
|
function getNewUserDirPath() {
|
||||||
|
let userDir = path.join(getTempDirectory(), 'kubectlTask');
|
||||||
|
ensureDirExists(userDir);
|
||||||
|
userDir = path.join(userDir, getCurrentTime().toString());
|
||||||
|
ensureDirExists(userDir);
|
||||||
|
return userDir;
|
||||||
|
}
|
||||||
|
exports.getNewUserDirPath = getNewUserDirPath;
|
||||||
|
function ensureDirExists(dirPath) {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.ensureDirExists = ensureDirExists;
|
||||||
|
function assertFileExists(path) {
|
||||||
|
if (!fs.existsSync(path)) {
|
||||||
|
core.error(`FileNotFoundException : ${path}`);
|
||||||
|
throw new Error(`FileNotFoundException: ${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.assertFileExists = assertFileExists;
|
||||||
|
function writeObjectsToFile(inputObjects) {
|
||||||
|
const newFilePaths = [];
|
||||||
|
if (!!inputObjects) {
|
||||||
|
inputObjects.forEach((inputObject) => {
|
||||||
|
try {
|
||||||
|
const inputObjectString = JSON.stringify(inputObject);
|
||||||
|
if (!!inputObject.kind && !!inputObject.metadata && !!inputObject.metadata.name) {
|
||||||
|
const fileName = getManifestFileName(inputObject.kind, inputObject.metadata.name);
|
||||||
|
fs.writeFileSync(path.join(fileName), inputObjectString);
|
||||||
|
newFilePaths.push(fileName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.debug('Input object is not proper K8s resource object. Object: ' + inputObjectString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.debug('Exception occurred while writing object to file : ' + inputObject + ' . Exception: ' + ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newFilePaths;
|
||||||
|
}
|
||||||
|
exports.writeObjectsToFile = writeObjectsToFile;
|
||||||
|
function writeManifestToFile(inputObjectString, kind, name) {
|
||||||
|
if (inputObjectString) {
|
||||||
|
try {
|
||||||
|
const fileName = getManifestFileName(kind, name);
|
||||||
|
fs.writeFileSync(path.join(fileName), inputObjectString);
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.debug('Exception occurred while writing object to file : ' + inputObjectString + ' . Exception: ' + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
exports.writeManifestToFile = writeManifestToFile;
|
||||||
|
function getManifestFileName(kind, name) {
|
||||||
|
const filePath = kind + '_' + name + '_' + getCurrentTime().toString();
|
||||||
|
const tempDirectory = getTempDirectory();
|
||||||
|
const fileName = path.join(tempDirectory, path.basename(filePath));
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
function getCurrentTime() {
|
||||||
|
return new Date().getTime();
|
||||||
|
}
|
||||||
135
lib/utilities/manifest-stability-utility.js
Normal file
135
lib/utilities/manifest-stability-utility.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
'use strict';
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const utils = require("./utility");
|
||||||
|
const KubernetesConstants = require("../constants");
|
||||||
|
function checkManifestStability(kubectl, resources) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const rolloutStatusResults = [];
|
||||||
|
const numberOfResources = resources.length;
|
||||||
|
for (let i = 0; i < numberOfResources; i++) {
|
||||||
|
const resource = resources[i];
|
||||||
|
if (KubernetesConstants.workloadTypesWithRolloutStatus.indexOf(resource.type.toLowerCase()) >= 0) {
|
||||||
|
rolloutStatusResults.push(kubectl.checkRolloutStatus(resource.type, resource.name));
|
||||||
|
}
|
||||||
|
if (utils.isEqual(resource.type, KubernetesConstants.KubernetesWorkload.pod, true)) {
|
||||||
|
try {
|
||||||
|
yield checkPodStatus(kubectl, resource.name);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.warning(`CouldNotDeterminePodStatus ${JSON.stringify(ex)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (utils.isEqual(resource.type, KubernetesConstants.DiscoveryAndLoadBalancerResource.service, true)) {
|
||||||
|
try {
|
||||||
|
const service = getService(kubectl, resource.name);
|
||||||
|
const spec = service.spec;
|
||||||
|
const status = service.status;
|
||||||
|
if (utils.isEqual(spec.type, KubernetesConstants.ServiceTypes.loadBalancer, true)) {
|
||||||
|
if (!isLoadBalancerIPAssigned(status)) {
|
||||||
|
yield waitForServiceExternalIPAssignment(kubectl, resource.name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('ServiceExternalIP', resource.name, status.loadBalancer.ingress[0].ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.warning(`CouldNotDetermineServiceStatus of: ${resource.name} Error: ${JSON.stringify(ex)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
utils.checkForErrors(rolloutStatusResults);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.checkManifestStability = checkManifestStability;
|
||||||
|
function checkPodStatus(kubectl, podName) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const sleepTimeout = 10 * 1000; // 10 seconds
|
||||||
|
const iterations = 60; // 60 * 10 seconds timeout = 10 minutes max timeout
|
||||||
|
let podStatus;
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
yield utils.sleep(sleepTimeout);
|
||||||
|
core.debug(`Polling for pod status: ${podName}`);
|
||||||
|
podStatus = getPodStatus(kubectl, podName);
|
||||||
|
if (podStatus.phase && podStatus.phase !== 'Pending' && podStatus.phase !== 'Unknown') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
podStatus = getPodStatus(kubectl, podName);
|
||||||
|
switch (podStatus.phase) {
|
||||||
|
case 'Succeeded':
|
||||||
|
case 'Running':
|
||||||
|
if (isPodReady(podStatus)) {
|
||||||
|
console.log(`pod/${podName} is successfully rolled out`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Pending':
|
||||||
|
if (!isPodReady(podStatus)) {
|
||||||
|
core.warning(`pod/${podName} rollout status check timedout`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Failed':
|
||||||
|
core.error(`pod/${podName} rollout failed`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
core.warning(`pod/${podName} rollout status: ${podStatus.phase}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.checkPodStatus = checkPodStatus;
|
||||||
|
function getPodStatus(kubectl, podName) {
|
||||||
|
const podResult = kubectl.getResource('pod', podName);
|
||||||
|
utils.checkForErrors([podResult]);
|
||||||
|
const podStatus = JSON.parse(podResult.stdout).status;
|
||||||
|
core.debug(`Pod Status: ${JSON.stringify(podStatus)}`);
|
||||||
|
return podStatus;
|
||||||
|
}
|
||||||
|
function isPodReady(podStatus) {
|
||||||
|
let allContainersAreReady = true;
|
||||||
|
podStatus.containerStatuses.forEach(container => {
|
||||||
|
if (container.ready === false) {
|
||||||
|
console.log(`'${container.name}' status: ${JSON.stringify(container.state)}`);
|
||||||
|
allContainersAreReady = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!allContainersAreReady) {
|
||||||
|
core.warning('AllContainersNotInReadyState');
|
||||||
|
}
|
||||||
|
return allContainersAreReady;
|
||||||
|
}
|
||||||
|
function getService(kubectl, serviceName) {
|
||||||
|
const serviceResult = kubectl.getResource(KubernetesConstants.DiscoveryAndLoadBalancerResource.service, serviceName);
|
||||||
|
utils.checkForErrors([serviceResult]);
|
||||||
|
return JSON.parse(serviceResult.stdout);
|
||||||
|
}
|
||||||
|
function waitForServiceExternalIPAssignment(kubectl, serviceName) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const sleepTimeout = 10 * 1000; // 10 seconds
|
||||||
|
const iterations = 18; // 18 * 10 seconds timeout = 3 minutes max timeout
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
console.log(`waitForServiceIpAssignment : ${serviceName}`);
|
||||||
|
yield utils.sleep(sleepTimeout);
|
||||||
|
let status = (getService(kubectl, serviceName)).status;
|
||||||
|
if (isLoadBalancerIPAssigned(status)) {
|
||||||
|
console.log('ServiceExternalIP', serviceName, status.loadBalancer.ingress[0].ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.warning(`waitForServiceIpAssignmentTimedOut ${serviceName}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function isLoadBalancerIPAssigned(status) {
|
||||||
|
if (status && status.loadBalancer && status.loadBalancer.ingress && status.loadBalancer.ingress.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@ -1,18 +1,111 @@
|
|||||||
"use strict";
|
'use strict';
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const core = require("@actions/core");
|
const core = require("@actions/core");
|
||||||
const utils_1 = require("./utils");
|
const kubectlutility = require("../kubectl-util");
|
||||||
|
const io = require("@actions/io");
|
||||||
|
const utility_1 = require("./utility");
|
||||||
|
function getManifestFiles(manifestFilePaths) {
|
||||||
|
if (!manifestFilePaths) {
|
||||||
|
core.debug('file input is not present');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return manifestFilePaths;
|
||||||
|
}
|
||||||
|
exports.getManifestFiles = getManifestFiles;
|
||||||
|
function getKubectl() {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
try {
|
||||||
|
return Promise.resolve(io.which('kubectl', true));
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
return kubectlutility.downloadKubectl(yield kubectlutility.getStableKubectlVersion());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.getKubectl = getKubectl;
|
||||||
|
function createKubectlArgs(kinds, names) {
|
||||||
|
let args = '';
|
||||||
|
if (!!kinds && kinds.size > 0) {
|
||||||
|
args = args + createInlineArray(Array.from(kinds.values()));
|
||||||
|
}
|
||||||
|
if (!!names && names.size > 0) {
|
||||||
|
args = args + ' ' + Array.from(names.values()).join(' ');
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
exports.createKubectlArgs = createKubectlArgs;
|
||||||
|
function getDeleteCmdArgs(argsPrefix, inputArgs) {
|
||||||
|
let args = '';
|
||||||
|
if (!!argsPrefix && argsPrefix.length > 0) {
|
||||||
|
args = argsPrefix;
|
||||||
|
}
|
||||||
|
if (!!inputArgs && inputArgs.length > 0) {
|
||||||
|
if (args.length > 0) {
|
||||||
|
args = args + ' ';
|
||||||
|
}
|
||||||
|
args = args + inputArgs;
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
exports.getDeleteCmdArgs = getDeleteCmdArgs;
|
||||||
|
/*
|
||||||
|
For example,
|
||||||
|
currentString: `image: "example/example-image"`
|
||||||
|
imageName: `example/example-image`
|
||||||
|
imageNameWithNewTag: `example/example-image:identifiertag`
|
||||||
|
|
||||||
|
This substituteImageNameInSpecFile function would return
|
||||||
|
return Value: `image: "example/example-image:identifiertag"`
|
||||||
|
*/
|
||||||
|
function substituteImageNameInSpecFile(currentString, imageName, imageNameWithNewTag) {
|
||||||
|
if (currentString.indexOf(imageName) < 0) {
|
||||||
|
core.debug(`No occurence of replacement token: ${imageName} found`);
|
||||||
|
return currentString;
|
||||||
|
}
|
||||||
|
return currentString.split('\n').reduce((acc, line) => {
|
||||||
|
const imageKeyword = line.match(/^ *image:/);
|
||||||
|
if (imageKeyword) {
|
||||||
|
let [currentImageName, currentImageTag] = line
|
||||||
|
.substring(imageKeyword[0].length) // consume the line from keyword onwards
|
||||||
|
.trim()
|
||||||
|
.replace(/[',"]/g, '') // replace allowed quotes with nothing
|
||||||
|
.split(':');
|
||||||
|
if (!currentImageTag && currentImageName.indexOf(' ') > 0) {
|
||||||
|
currentImageName = currentImageName.split(' ')[0]; // Stripping off comments
|
||||||
|
}
|
||||||
|
if (currentImageName === imageName) {
|
||||||
|
return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc + line + '\n';
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
exports.substituteImageNameInSpecFile = substituteImageNameInSpecFile;
|
||||||
|
function createInlineArray(str) {
|
||||||
|
if (typeof str === 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.join(',');
|
||||||
|
}
|
||||||
function getImagePullSecrets(inputObject) {
|
function getImagePullSecrets(inputObject) {
|
||||||
if (!inputObject || !inputObject.spec) {
|
if (!inputObject || !inputObject.spec) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (utils_1.isEqual(inputObject.kind, 'pod')
|
if (utility_1.isEqual(inputObject.kind, 'pod')
|
||||||
&& inputObject
|
&& inputObject
|
||||||
&& inputObject.spec
|
&& inputObject.spec
|
||||||
&& inputObject.spec.imagePullSecrets) {
|
&& inputObject.spec.imagePullSecrets) {
|
||||||
return inputObject.spec.imagePullSecrets;
|
return inputObject.spec.imagePullSecrets;
|
||||||
}
|
}
|
||||||
else if (utils_1.isEqual(inputObject.kind, 'cronjob')
|
else if (utility_1.isEqual(inputObject.kind, 'cronjob')
|
||||||
&& inputObject
|
&& inputObject
|
||||||
&& inputObject.spec
|
&& inputObject.spec
|
||||||
&& inputObject.spec.jobTemplate
|
&& inputObject.spec.jobTemplate
|
||||||
@ -34,7 +127,7 @@ function setImagePullSecrets(inputObject, newImagePullSecrets) {
|
|||||||
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (utils_1.isEqual(inputObject.kind, 'pod')) {
|
if (utility_1.isEqual(inputObject.kind, 'pod')) {
|
||||||
if (inputObject
|
if (inputObject
|
||||||
&& inputObject.spec) {
|
&& inputObject.spec) {
|
||||||
if (newImagePullSecrets.length > 0) {
|
if (newImagePullSecrets.length > 0) {
|
||||||
@ -45,7 +138,7 @@ function setImagePullSecrets(inputObject, newImagePullSecrets) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (utils_1.isEqual(inputObject.kind, 'cronjob')) {
|
else if (utility_1.isEqual(inputObject.kind, 'cronjob')) {
|
||||||
if (inputObject
|
if (inputObject
|
||||||
&& inputObject.spec
|
&& inputObject.spec
|
||||||
&& inputObject.spec.jobTemplate
|
&& inputObject.spec.jobTemplate
|
||||||
@ -135,7 +228,7 @@ function isWorkloadEntity(kind) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return workloadTypes.some((type) => {
|
return workloadTypes.some((type) => {
|
||||||
return utils_1.isEqual(type, kind);
|
return utility_1.isEqual(type, kind);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
exports.isWorkloadEntity = isWorkloadEntity;
|
exports.isWorkloadEntity = isWorkloadEntity;
|
||||||
243
lib/utilities/resource-object-utility.js
Normal file
243
lib/utilities/resource-object-utility.js
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
'use strict';
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const fs = require("fs");
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const constants_1 = require("../constants");
|
||||||
|
const string_comparison_1 = require("./string-comparison");
|
||||||
|
function isDeploymentEntity(kind) {
|
||||||
|
if (!kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
return constants_1.deploymentTypes.some((type) => {
|
||||||
|
return string_comparison_1.isEqual(type, kind, string_comparison_1.StringComparer.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.isDeploymentEntity = isDeploymentEntity;
|
||||||
|
function isWorkloadEntity(kind) {
|
||||||
|
if (!kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
return constants_1.workloadTypes.some((type) => {
|
||||||
|
return string_comparison_1.isEqual(type, kind, string_comparison_1.StringComparer.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.isWorkloadEntity = isWorkloadEntity;
|
||||||
|
function isServiceEntity(kind) {
|
||||||
|
if (!kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
return string_comparison_1.isEqual("Service", kind, string_comparison_1.StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
exports.isServiceEntity = isServiceEntity;
|
||||||
|
function getReplicaCount(inputObject) {
|
||||||
|
if (!inputObject) {
|
||||||
|
throw ('NullInputObject');
|
||||||
|
}
|
||||||
|
if (!inputObject.kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (!string_comparison_1.isEqual(kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase) && !string_comparison_1.isEqual(kind, constants_1.KubernetesWorkload.daemonSet, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
return inputObject.spec.replicas;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
exports.getReplicaCount = getReplicaCount;
|
||||||
|
function updateObjectLabels(inputObject, newLabels, override) {
|
||||||
|
if (!inputObject) {
|
||||||
|
throw ('NullInputObject');
|
||||||
|
}
|
||||||
|
if (!inputObject.metadata) {
|
||||||
|
throw ('NullInputObjectMetadata');
|
||||||
|
}
|
||||||
|
if (!newLabels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (override) {
|
||||||
|
inputObject.metadata.labels = newLabels;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let existingLabels = inputObject.metadata.labels;
|
||||||
|
if (!existingLabels) {
|
||||||
|
existingLabels = new Map();
|
||||||
|
}
|
||||||
|
Object.keys(newLabels).forEach(function (key) {
|
||||||
|
existingLabels[key] = newLabels[key];
|
||||||
|
});
|
||||||
|
inputObject.metadata.labels = existingLabels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.updateObjectLabels = updateObjectLabels;
|
||||||
|
function updateImagePullSecrets(inputObject, newImagePullSecrets, override) {
|
||||||
|
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newImagePullSecretsObjects = Array.from(newImagePullSecrets, x => { return { 'name': x }; });
|
||||||
|
let existingImagePullSecretObjects = getImagePullSecrets(inputObject);
|
||||||
|
if (override) {
|
||||||
|
existingImagePullSecretObjects = newImagePullSecretsObjects;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!existingImagePullSecretObjects) {
|
||||||
|
existingImagePullSecretObjects = new Array();
|
||||||
|
}
|
||||||
|
existingImagePullSecretObjects = existingImagePullSecretObjects.concat(newImagePullSecretsObjects);
|
||||||
|
}
|
||||||
|
setImagePullSecrets(inputObject, existingImagePullSecretObjects);
|
||||||
|
}
|
||||||
|
exports.updateImagePullSecrets = updateImagePullSecrets;
|
||||||
|
function updateSpecLabels(inputObject, newLabels, override) {
|
||||||
|
if (!inputObject) {
|
||||||
|
throw ('NullInputObject');
|
||||||
|
}
|
||||||
|
if (!inputObject.kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
if (!newLabels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let existingLabels = getSpecLabels(inputObject);
|
||||||
|
if (override) {
|
||||||
|
existingLabels = newLabels;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!existingLabels) {
|
||||||
|
existingLabels = new Map();
|
||||||
|
}
|
||||||
|
Object.keys(newLabels).forEach(function (key) {
|
||||||
|
existingLabels[key] = newLabels[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSpecLabels(inputObject, existingLabels);
|
||||||
|
}
|
||||||
|
exports.updateSpecLabels = updateSpecLabels;
|
||||||
|
function updateSelectorLabels(inputObject, newLabels, override) {
|
||||||
|
if (!inputObject) {
|
||||||
|
throw ('NullInputObject');
|
||||||
|
}
|
||||||
|
if (!inputObject.kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
if (!newLabels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let existingLabels = getSpecSelectorLabels(inputObject);
|
||||||
|
if (override) {
|
||||||
|
existingLabels = newLabels;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!existingLabels) {
|
||||||
|
existingLabels = new Map();
|
||||||
|
}
|
||||||
|
Object.keys(newLabels).forEach(function (key) {
|
||||||
|
existingLabels[key] = newLabels[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSpecSelectorLabels(inputObject, existingLabels);
|
||||||
|
}
|
||||||
|
exports.updateSelectorLabels = updateSelectorLabels;
|
||||||
|
function getResources(filePaths, filterResourceTypes) {
|
||||||
|
if (!filePaths) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const resources = [];
|
||||||
|
filePaths.forEach((filePath) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const inputObjectKind = inputObject ? inputObject.kind : '';
|
||||||
|
if (filterResourceTypes.filter(type => string_comparison_1.isEqual(inputObjectKind, type, string_comparison_1.StringComparer.OrdinalIgnoreCase)).length > 0) {
|
||||||
|
const resource = {
|
||||||
|
type: inputObject.kind,
|
||||||
|
name: inputObject.metadata.name
|
||||||
|
};
|
||||||
|
resources.push(resource);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
exports.getResources = getResources;
|
||||||
|
function getSpecLabels(inputObject) {
|
||||||
|
if (!inputObject) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
return inputObject.metadata.labels;
|
||||||
|
}
|
||||||
|
if (!!inputObject.spec && !!inputObject.spec.template && !!inputObject.spec.template.metadata) {
|
||||||
|
return inputObject.spec.template.metadata.labels;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function getImagePullSecrets(inputObject) {
|
||||||
|
if (!inputObject || !inputObject.spec) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.cronjob, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
try {
|
||||||
|
return inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.debug(`Fetching imagePullSecrets failed due to this error: ${JSON.stringify(ex)}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
return inputObject.spec.imagePullSecrets;
|
||||||
|
}
|
||||||
|
if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
|
||||||
|
return inputObject.spec.template.spec.imagePullSecrets;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function setImagePullSecrets(inputObject, newImagePullSecrets) {
|
||||||
|
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
inputObject.spec.imagePullSecrets = newImagePullSecrets;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.cronjob, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
try {
|
||||||
|
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets = newImagePullSecrets;
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.debug(`Overriding imagePullSecrets failed due to this error: ${JSON.stringify(ex)}`);
|
||||||
|
//Do nothing
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
|
||||||
|
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function setSpecLabels(inputObject, newLabels) {
|
||||||
|
let specLabels = getSpecLabels(inputObject);
|
||||||
|
if (!!newLabels) {
|
||||||
|
specLabels = newLabels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getSpecSelectorLabels(inputObject) {
|
||||||
|
if (!!inputObject && !!inputObject.spec && !!inputObject.spec.selector) {
|
||||||
|
if (isServiceEntity(inputObject.kind)) {
|
||||||
|
return inputObject.spec.selector;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return inputObject.spec.selector.matchLabels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function setSpecSelectorLabels(inputObject, newLabels) {
|
||||||
|
let selectorLabels = getSpecSelectorLabels(inputObject);
|
||||||
|
if (!!selectorLabels) {
|
||||||
|
selectorLabels = newLabels;
|
||||||
|
}
|
||||||
|
}
|
||||||
185
lib/utilities/strategy-helpers/canary-deployment-helper.js
Normal file
185
lib/utilities/strategy-helpers/canary-deployment-helper.js
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
'use strict';
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const TaskInputParameters = require("../../input-parameters");
|
||||||
|
const helper = require("../resource-object-utility");
|
||||||
|
const constants_1 = require("../../constants");
|
||||||
|
const string_comparison_1 = require("../string-comparison");
|
||||||
|
const utility_1 = require("../utility");
|
||||||
|
const utils = require("../manifest-utilities");
|
||||||
|
exports.CANARY_DEPLOYMENT_STRATEGY = 'CANARY';
|
||||||
|
exports.TRAFFIC_SPLIT_STRATEGY = 'SMI';
|
||||||
|
exports.CANARY_VERSION_LABEL = 'workflow/version';
|
||||||
|
const BASELINE_SUFFIX = '-baseline';
|
||||||
|
exports.BASELINE_LABEL_VALUE = 'baseline';
|
||||||
|
const CANARY_SUFFIX = '-canary';
|
||||||
|
exports.CANARY_LABEL_VALUE = 'canary';
|
||||||
|
exports.STABLE_SUFFIX = '-stable';
|
||||||
|
exports.STABLE_LABEL_VALUE = 'stable';
|
||||||
|
function deleteCanaryDeployment(kubectl, manifestFilePaths, includeServices) {
|
||||||
|
// get manifest files
|
||||||
|
const inputManifestFiles = utils.getManifestFiles(manifestFilePaths);
|
||||||
|
if (inputManifestFiles == null || inputManifestFiles.length == 0) {
|
||||||
|
throw new Error('ManifestFileNotFound');
|
||||||
|
}
|
||||||
|
// create delete cmd prefix
|
||||||
|
cleanUpCanary(kubectl, inputManifestFiles, includeServices);
|
||||||
|
}
|
||||||
|
exports.deleteCanaryDeployment = deleteCanaryDeployment;
|
||||||
|
function markResourceAsStable(inputObject) {
|
||||||
|
if (isResourceMarkedAsStable(inputObject)) {
|
||||||
|
return inputObject;
|
||||||
|
}
|
||||||
|
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||||
|
// Adding labels and annotations.
|
||||||
|
addCanaryLabelsAndAnnotations(newObject, exports.STABLE_LABEL_VALUE);
|
||||||
|
core.debug("Added stable label: " + JSON.stringify(newObject));
|
||||||
|
return newObject;
|
||||||
|
}
|
||||||
|
exports.markResourceAsStable = markResourceAsStable;
|
||||||
|
function isResourceMarkedAsStable(inputObject) {
|
||||||
|
return inputObject &&
|
||||||
|
inputObject.metadata &&
|
||||||
|
inputObject.metadata.labels &&
|
||||||
|
inputObject.metadata.labels[exports.CANARY_VERSION_LABEL] == exports.STABLE_LABEL_VALUE;
|
||||||
|
}
|
||||||
|
exports.isResourceMarkedAsStable = isResourceMarkedAsStable;
|
||||||
|
function getStableResource(inputObject) {
|
||||||
|
var replicaCount = isSpecContainsReplicas(inputObject.kind) ? inputObject.metadata.replicas : 0;
|
||||||
|
return getNewCanaryObject(inputObject, replicaCount, exports.STABLE_LABEL_VALUE);
|
||||||
|
}
|
||||||
|
exports.getStableResource = getStableResource;
|
||||||
|
function getNewBaselineResource(stableObject, replicas) {
|
||||||
|
return getNewCanaryObject(stableObject, replicas, exports.BASELINE_LABEL_VALUE);
|
||||||
|
}
|
||||||
|
exports.getNewBaselineResource = getNewBaselineResource;
|
||||||
|
function getNewCanaryResource(inputObject, replicas) {
|
||||||
|
return getNewCanaryObject(inputObject, replicas, exports.CANARY_LABEL_VALUE);
|
||||||
|
}
|
||||||
|
exports.getNewCanaryResource = getNewCanaryResource;
|
||||||
|
function fetchCanaryResource(kubectl, kind, name) {
|
||||||
|
return fetchResource(kubectl, kind, getCanaryResourceName(name));
|
||||||
|
}
|
||||||
|
exports.fetchCanaryResource = fetchCanaryResource;
|
||||||
|
function fetchResource(kubectl, kind, name) {
|
||||||
|
const result = kubectl.getResource(kind, name);
|
||||||
|
if (result == null || !!result.stderr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!!result.stdout) {
|
||||||
|
const resource = JSON.parse(result.stdout);
|
||||||
|
try {
|
||||||
|
UnsetsClusterSpecficDetails(resource);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
core.debug('Exception occurred while Parsing ' + resource + ' in Json object');
|
||||||
|
core.debug(`Exception:${ex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
exports.fetchResource = fetchResource;
|
||||||
|
function isCanaryDeploymentStrategy() {
|
||||||
|
const deploymentStrategy = TaskInputParameters.deploymentStrategy;
|
||||||
|
return deploymentStrategy && deploymentStrategy.toUpperCase() === exports.CANARY_DEPLOYMENT_STRATEGY;
|
||||||
|
}
|
||||||
|
exports.isCanaryDeploymentStrategy = isCanaryDeploymentStrategy;
|
||||||
|
function isSMICanaryStrategy() {
|
||||||
|
const deploymentStrategy = TaskInputParameters.trafficSplitMethod;
|
||||||
|
return isCanaryDeploymentStrategy() && deploymentStrategy && deploymentStrategy.toUpperCase() === exports.TRAFFIC_SPLIT_STRATEGY;
|
||||||
|
}
|
||||||
|
exports.isSMICanaryStrategy = isSMICanaryStrategy;
|
||||||
|
function getCanaryResourceName(name) {
|
||||||
|
return name + CANARY_SUFFIX;
|
||||||
|
}
|
||||||
|
exports.getCanaryResourceName = getCanaryResourceName;
|
||||||
|
function getBaselineResourceName(name) {
|
||||||
|
return name + BASELINE_SUFFIX;
|
||||||
|
}
|
||||||
|
exports.getBaselineResourceName = getBaselineResourceName;
|
||||||
|
function getStableResourceName(name) {
|
||||||
|
return name + exports.STABLE_SUFFIX;
|
||||||
|
}
|
||||||
|
exports.getStableResourceName = getStableResourceName;
|
||||||
|
function UnsetsClusterSpecficDetails(resource) {
|
||||||
|
if (resource == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Unsets the cluster specific details in the object
|
||||||
|
if (!!resource) {
|
||||||
|
const metadata = resource.metadata;
|
||||||
|
const status = resource.status;
|
||||||
|
if (!!metadata) {
|
||||||
|
const newMetadata = {
|
||||||
|
'annotations': metadata.annotations,
|
||||||
|
'labels': metadata.labels,
|
||||||
|
'name': metadata.name
|
||||||
|
};
|
||||||
|
resource.metadata = newMetadata;
|
||||||
|
}
|
||||||
|
if (!!status) {
|
||||||
|
resource.status = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getNewCanaryObject(inputObject, replicas, type) {
|
||||||
|
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||||
|
// Updating name
|
||||||
|
if (type === exports.CANARY_LABEL_VALUE) {
|
||||||
|
newObject.metadata.name = getCanaryResourceName(inputObject.metadata.name);
|
||||||
|
}
|
||||||
|
else if (type === exports.STABLE_LABEL_VALUE) {
|
||||||
|
newObject.metadata.name = getStableResourceName(inputObject.metadata.name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newObject.metadata.name = getBaselineResourceName(inputObject.metadata.name);
|
||||||
|
}
|
||||||
|
// Adding labels and annotations.
|
||||||
|
addCanaryLabelsAndAnnotations(newObject, type);
|
||||||
|
// Updating no. of replicas
|
||||||
|
if (isSpecContainsReplicas(newObject.kind)) {
|
||||||
|
newObject.spec.replicas = replicas;
|
||||||
|
}
|
||||||
|
return newObject;
|
||||||
|
}
|
||||||
|
function isSpecContainsReplicas(kind) {
|
||||||
|
return !string_comparison_1.isEqual(kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase) &&
|
||||||
|
!string_comparison_1.isEqual(kind, constants_1.KubernetesWorkload.daemonSet, string_comparison_1.StringComparer.OrdinalIgnoreCase) &&
|
||||||
|
!helper.isServiceEntity(kind);
|
||||||
|
}
|
||||||
|
function addCanaryLabelsAndAnnotations(inputObject, type) {
|
||||||
|
const newLabels = new Map();
|
||||||
|
newLabels[exports.CANARY_VERSION_LABEL] = type;
|
||||||
|
helper.updateObjectLabels(inputObject, newLabels, false);
|
||||||
|
helper.updateSelectorLabels(inputObject, newLabels, false);
|
||||||
|
if (!helper.isServiceEntity(inputObject.kind)) {
|
||||||
|
helper.updateSpecLabels(inputObject, newLabels, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cleanUpCanary(kubectl, files, includeServices) {
|
||||||
|
var deleteObject = function (kind, name) {
|
||||||
|
try {
|
||||||
|
const result = kubectl.delete([kind, name]);
|
||||||
|
utility_1.checkForErrors([result]);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
// Ignore failures of delete if doesn't exist
|
||||||
|
}
|
||||||
|
};
|
||||||
|
files.forEach((filePath) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isDeploymentEntity(kind) || (includeServices && helper.isServiceEntity(kind))) {
|
||||||
|
const canaryObjectName = getCanaryResourceName(name);
|
||||||
|
const baselineObjectName = getBaselineResourceName(name);
|
||||||
|
deleteObject(kind, canaryObjectName);
|
||||||
|
deleteObject(kind, baselineObjectName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
150
lib/utilities/strategy-helpers/deployment-helper.js
Normal file
150
lib/utilities/strategy-helpers/deployment-helper.js
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
'use strict';
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const canaryDeploymentHelper = require("./canary-deployment-helper");
|
||||||
|
const KubernetesObjectUtility = require("../resource-object-utility");
|
||||||
|
const TaskInputParameters = require("../../input-parameters");
|
||||||
|
const models = require("../../constants");
|
||||||
|
const fileHelper = require("../files-helper");
|
||||||
|
const utils = require("../manifest-utilities");
|
||||||
|
const KubernetesManifestUtility = require("../manifest-stability-utility");
|
||||||
|
const KubernetesConstants = require("../../constants");
|
||||||
|
const pod_canary_deployment_helper_1 = require("./pod-canary-deployment-helper");
|
||||||
|
const smi_canary_deployment_helper_1 = require("./smi-canary-deployment-helper");
|
||||||
|
const utility_1 = require("../utility");
|
||||||
|
function deploy(kubectl, manifestFilePaths, deploymentStrategy) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
// get manifest files
|
||||||
|
let inputManifestFiles = getManifestFiles(manifestFilePaths);
|
||||||
|
// artifact substitution
|
||||||
|
inputManifestFiles = updateContainerImagesInManifestFiles(inputManifestFiles, TaskInputParameters.containers);
|
||||||
|
// imagePullSecrets addition
|
||||||
|
inputManifestFiles = updateImagePullSecretsInManifestFiles(inputManifestFiles, TaskInputParameters.imagePullSecrets);
|
||||||
|
// deployment
|
||||||
|
const deployedManifestFiles = deployManifests(inputManifestFiles, kubectl, isCanaryDeploymentStrategy(deploymentStrategy));
|
||||||
|
// check manifest stability
|
||||||
|
const resourceTypes = KubernetesObjectUtility.getResources(deployedManifestFiles, models.deploymentTypes.concat([KubernetesConstants.DiscoveryAndLoadBalancerResource.service]));
|
||||||
|
yield checkManifestStability(kubectl, resourceTypes);
|
||||||
|
// print ingress resources
|
||||||
|
const ingressResources = KubernetesObjectUtility.getResources(deployedManifestFiles, [KubernetesConstants.DiscoveryAndLoadBalancerResource.ingress]);
|
||||||
|
ingressResources.forEach(ingressResource => {
|
||||||
|
kubectl.getResource(KubernetesConstants.DiscoveryAndLoadBalancerResource.ingress, ingressResource.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.deploy = deploy;
|
||||||
|
function getManifestFiles(manifestFilePaths) {
|
||||||
|
const files = utils.getManifestFiles(manifestFilePaths);
|
||||||
|
if (files == null || files.length === 0) {
|
||||||
|
throw new Error(`ManifestFileNotFound : ${manifestFilePaths}`);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
function deployManifests(files, kubectl, isCanaryDeploymentStrategy) {
|
||||||
|
let result;
|
||||||
|
if (isCanaryDeploymentStrategy) {
|
||||||
|
let canaryDeploymentOutput;
|
||||||
|
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||||
|
canaryDeploymentOutput = smi_canary_deployment_helper_1.deploySMICanary(kubectl, files);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
canaryDeploymentOutput = pod_canary_deployment_helper_1.deployPodCanary(kubectl, files);
|
||||||
|
}
|
||||||
|
result = canaryDeploymentOutput.result;
|
||||||
|
files = canaryDeploymentOutput.newFilePaths;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||||
|
const updatedManifests = appendStableVersionLabelToResource(files, kubectl);
|
||||||
|
result = kubectl.apply(updatedManifests);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = kubectl.apply(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
utility_1.checkForErrors([result]);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
function appendStableVersionLabelToResource(files, kubectl) {
|
||||||
|
const manifestFiles = [];
|
||||||
|
const newObjectsList = [];
|
||||||
|
files.forEach((filePath) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (KubernetesObjectUtility.isDeploymentEntity(kind)) {
|
||||||
|
const updatedObject = canaryDeploymentHelper.markResourceAsStable(inputObject);
|
||||||
|
newObjectsList.push(updatedObject);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
manifestFiles.push(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const updatedManifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
manifestFiles.push(...updatedManifestFiles);
|
||||||
|
return manifestFiles;
|
||||||
|
}
|
||||||
|
function checkManifestStability(kubectl, resources) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
yield KubernetesManifestUtility.checkManifestStability(kubectl, resources);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function updateContainerImagesInManifestFiles(filePaths, containers) {
|
||||||
|
if (!!containers && containers.length > 0) {
|
||||||
|
const newFilePaths = [];
|
||||||
|
const tempDirectory = fileHelper.getTempDirectory();
|
||||||
|
filePaths.forEach((filePath) => {
|
||||||
|
let contents = fs.readFileSync(filePath).toString();
|
||||||
|
containers.forEach((container) => {
|
||||||
|
let imageName = container.split(':')[0];
|
||||||
|
if (imageName.indexOf('@') > 0) {
|
||||||
|
imageName = imageName.split('@')[0];
|
||||||
|
}
|
||||||
|
if (contents.indexOf(imageName) > 0) {
|
||||||
|
contents = utils.substituteImageNameInSpecFile(contents, imageName, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const fileName = path.join(tempDirectory, path.basename(filePath));
|
||||||
|
fs.writeFileSync(path.join(fileName), contents);
|
||||||
|
newFilePaths.push(fileName);
|
||||||
|
});
|
||||||
|
return newFilePaths;
|
||||||
|
}
|
||||||
|
return filePaths;
|
||||||
|
}
|
||||||
|
function updateImagePullSecretsInManifestFiles(filePaths, imagePullSecrets) {
|
||||||
|
if (!!imagePullSecrets && imagePullSecrets.length > 0) {
|
||||||
|
const newObjectsList = [];
|
||||||
|
filePaths.forEach((filePath) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath).toString();
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
if (!!inputObject && !!inputObject.kind) {
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (KubernetesObjectUtility.isWorkloadEntity(kind)) {
|
||||||
|
KubernetesObjectUtility.updateImagePullSecrets(inputObject, imagePullSecrets, false);
|
||||||
|
}
|
||||||
|
newObjectsList.push(inputObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
core.debug('New K8s objects after addin imagePullSecrets are :' + JSON.stringify(newObjectsList));
|
||||||
|
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
return newFilePaths;
|
||||||
|
}
|
||||||
|
return filePaths;
|
||||||
|
}
|
||||||
|
function isCanaryDeploymentStrategy(deploymentStrategy) {
|
||||||
|
return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase();
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
'use strict';
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const TaskInputParameters = require("../../input-parameters");
|
||||||
|
const fileHelper = require("../files-helper");
|
||||||
|
const helper = require("../resource-object-utility");
|
||||||
|
const canaryDeploymentHelper = require("./canary-deployment-helper");
|
||||||
|
function deployPodCanary(kubectl, filePaths) {
|
||||||
|
const newObjectsList = [];
|
||||||
|
const percentage = parseInt(TaskInputParameters.canaryPercentage);
|
||||||
|
filePaths.forEach((filePath) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isDeploymentEntity(kind)) {
|
||||||
|
core.debug('Calculating replica count for canary');
|
||||||
|
const canaryReplicaCount = calculateReplicaCountForCanary(inputObject, percentage);
|
||||||
|
core.debug('Replica count is ' + canaryReplicaCount);
|
||||||
|
// Get stable object
|
||||||
|
core.debug('Querying stable object');
|
||||||
|
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, name);
|
||||||
|
if (!stableObject) {
|
||||||
|
core.debug('Stable object not found. Creating only canary object');
|
||||||
|
// If stable object not found, create canary deployment.
|
||||||
|
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||||
|
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||||
|
newObjectsList.push(newCanaryObject);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.debug('Stable object found. Creating canary and baseline objects');
|
||||||
|
// If canary object not found, create canary and baseline object.
|
||||||
|
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||||
|
const newBaselineObject = canaryDeploymentHelper.getNewBaselineResource(stableObject, canaryReplicaCount);
|
||||||
|
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||||
|
core.debug('New baseline object is: ' + JSON.stringify(newBaselineObject));
|
||||||
|
newObjectsList.push(newCanaryObject);
|
||||||
|
newObjectsList.push(newBaselineObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Updating non deployment entity as it is.
|
||||||
|
newObjectsList.push(inputObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
const result = kubectl.apply(manifestFiles);
|
||||||
|
return { 'result': result, 'newFilePaths': manifestFiles };
|
||||||
|
}
|
||||||
|
exports.deployPodCanary = deployPodCanary;
|
||||||
|
function calculateReplicaCountForCanary(inputObject, percentage) {
|
||||||
|
const inputReplicaCount = helper.getReplicaCount(inputObject);
|
||||||
|
return Math.round((inputReplicaCount * percentage) / 100);
|
||||||
|
}
|
||||||
193
lib/utilities/strategy-helpers/smi-canary-deployment-helper.js
Normal file
193
lib/utilities/strategy-helpers/smi-canary-deployment-helper.js
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
'use strict';
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const core = require("@actions/core");
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const util = require("util");
|
||||||
|
const TaskInputParameters = require("../../input-parameters");
|
||||||
|
const fileHelper = require("../files-helper");
|
||||||
|
const helper = require("../resource-object-utility");
|
||||||
|
const utils = require("../manifest-utilities");
|
||||||
|
const canaryDeploymentHelper = require("./canary-deployment-helper");
|
||||||
|
const utility_1 = require("../utility");
|
||||||
|
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout';
|
||||||
|
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit';
|
||||||
|
function deploySMICanary(kubectl, filePaths) {
|
||||||
|
const newObjectsList = [];
|
||||||
|
const canaryReplicaCount = parseInt(TaskInputParameters.baselineAndCanaryReplicas);
|
||||||
|
core.debug('Replica count is ' + canaryReplicaCount);
|
||||||
|
filePaths.forEach((filePath) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isDeploymentEntity(kind)) {
|
||||||
|
// Get stable object
|
||||||
|
core.debug('Querying stable object');
|
||||||
|
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, name);
|
||||||
|
if (!stableObject) {
|
||||||
|
core.debug('Stable object not found. Creating only canary object');
|
||||||
|
// If stable object not found, create canary deployment.
|
||||||
|
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||||
|
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||||
|
newObjectsList.push(newCanaryObject);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!canaryDeploymentHelper.isResourceMarkedAsStable(stableObject)) {
|
||||||
|
throw (`StableSpecSelectorNotExist : ${name}`);
|
||||||
|
}
|
||||||
|
core.debug('Stable object found. Creating canary and baseline objects');
|
||||||
|
// If canary object not found, create canary and baseline object.
|
||||||
|
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||||
|
const newBaselineObject = canaryDeploymentHelper.getNewBaselineResource(stableObject, canaryReplicaCount);
|
||||||
|
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||||
|
core.debug('New baseline object is: ' + JSON.stringify(newBaselineObject));
|
||||||
|
newObjectsList.push(newCanaryObject);
|
||||||
|
newObjectsList.push(newBaselineObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Updating non deployment entity as it is.
|
||||||
|
newObjectsList.push(inputObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
const result = kubectl.apply(manifestFiles);
|
||||||
|
createCanaryService(kubectl, filePaths);
|
||||||
|
return { 'result': result, 'newFilePaths': manifestFiles };
|
||||||
|
}
|
||||||
|
exports.deploySMICanary = deploySMICanary;
|
||||||
|
function createCanaryService(kubectl, filePaths) {
|
||||||
|
const newObjectsList = [];
|
||||||
|
const trafficObjectsList = [];
|
||||||
|
filePaths.forEach((filePath) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isServiceEntity(kind)) {
|
||||||
|
const newCanaryServiceObject = canaryDeploymentHelper.getNewCanaryResource(inputObject);
|
||||||
|
core.debug('New canary service object is: ' + JSON.stringify(newCanaryServiceObject));
|
||||||
|
newObjectsList.push(newCanaryServiceObject);
|
||||||
|
const newBaselineServiceObject = canaryDeploymentHelper.getNewBaselineResource(inputObject);
|
||||||
|
core.debug('New baseline object is: ' + JSON.stringify(newBaselineServiceObject));
|
||||||
|
newObjectsList.push(newBaselineServiceObject);
|
||||||
|
core.debug('Querying for stable service object');
|
||||||
|
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, canaryDeploymentHelper.getStableResourceName(name));
|
||||||
|
if (!stableObject) {
|
||||||
|
const newStableServiceObject = canaryDeploymentHelper.getStableResource(inputObject);
|
||||||
|
core.debug('New stable service object is: ' + JSON.stringify(newStableServiceObject));
|
||||||
|
newObjectsList.push(newStableServiceObject);
|
||||||
|
core.debug('Creating the traffic object for service: ' + name);
|
||||||
|
const trafficObject = createTrafficSplitManifestFile(name, 0, 0, 1000);
|
||||||
|
core.debug('Creating the traffic object for service: ' + trafficObject);
|
||||||
|
trafficObjectsList.push(trafficObject);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let updateTrafficObject = true;
|
||||||
|
const trafficObject = canaryDeploymentHelper.fetchResource(kubectl, TRAFFIC_SPLIT_OBJECT, getTrafficSplitResourceName(name));
|
||||||
|
if (trafficObject) {
|
||||||
|
const trafficJObject = JSON.parse(JSON.stringify(trafficObject));
|
||||||
|
if (trafficJObject && trafficJObject.spec && trafficJObject.spec.backends) {
|
||||||
|
trafficJObject.spec.backends.forEach((s) => {
|
||||||
|
if (s.service === canaryDeploymentHelper.getCanaryResourceName(name) && s.weight === "1000m") {
|
||||||
|
core.debug('Update traffic objcet not required');
|
||||||
|
updateTrafficObject = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateTrafficObject) {
|
||||||
|
core.debug('Stable service object present so updating the traffic object for service: ' + name);
|
||||||
|
trafficObjectsList.push(updateTrafficSplitObject(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
manifestFiles.push(...trafficObjectsList);
|
||||||
|
const result = kubectl.apply(manifestFiles);
|
||||||
|
utility_1.checkForErrors([result]);
|
||||||
|
}
|
||||||
|
function redirectTrafficToCanaryDeployment(kubectl, manifestFilePaths) {
|
||||||
|
adjustTraffic(kubectl, manifestFilePaths, 0, 1000);
|
||||||
|
}
|
||||||
|
exports.redirectTrafficToCanaryDeployment = redirectTrafficToCanaryDeployment;
|
||||||
|
function redirectTrafficToStableDeployment(kubectl, manifestFilePaths) {
|
||||||
|
adjustTraffic(kubectl, manifestFilePaths, 1000, 0);
|
||||||
|
}
|
||||||
|
exports.redirectTrafficToStableDeployment = redirectTrafficToStableDeployment;
|
||||||
|
function adjustTraffic(kubectl, manifestFilePaths, stableWeight, canaryWeight) {
|
||||||
|
// get manifest files
|
||||||
|
const inputManifestFiles = utils.getManifestFiles(manifestFilePaths);
|
||||||
|
if (inputManifestFiles == null || inputManifestFiles.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trafficSplitManifests = [];
|
||||||
|
const serviceObjects = [];
|
||||||
|
inputManifestFiles.forEach((filePath) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isServiceEntity(kind)) {
|
||||||
|
trafficSplitManifests.push(createTrafficSplitManifestFile(name, stableWeight, 0, canaryWeight));
|
||||||
|
serviceObjects.push(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (trafficSplitManifests.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = kubectl.apply(trafficSplitManifests);
|
||||||
|
core.debug('serviceObjects:' + serviceObjects.join(',') + ' result:' + result);
|
||||||
|
utility_1.checkForErrors([result]);
|
||||||
|
}
|
||||||
|
function updateTrafficSplitObject(serviceName) {
|
||||||
|
const percentage = parseInt(TaskInputParameters.canaryPercentage) * 10;
|
||||||
|
const baselineAndCanaryWeight = percentage / 2;
|
||||||
|
const stableDeploymentWeight = 1000 - percentage;
|
||||||
|
core.debug('Creating the traffic object with canary weight: ' + baselineAndCanaryWeight + ',baseling weight: ' + baselineAndCanaryWeight + ',stable: ' + stableDeploymentWeight);
|
||||||
|
return createTrafficSplitManifestFile(serviceName, stableDeploymentWeight, baselineAndCanaryWeight, baselineAndCanaryWeight);
|
||||||
|
}
|
||||||
|
function createTrafficSplitManifestFile(serviceName, stableWeight, baselineWeight, canaryWeight) {
|
||||||
|
const smiObjectString = getTrafficSplitObject(serviceName, stableWeight, baselineWeight, canaryWeight);
|
||||||
|
const manifestFile = fileHelper.writeManifestToFile(smiObjectString, TRAFFIC_SPLIT_OBJECT, serviceName);
|
||||||
|
if (!manifestFile) {
|
||||||
|
throw new Error('UnableToCreateTrafficSplitManifestFile');
|
||||||
|
}
|
||||||
|
return manifestFile;
|
||||||
|
}
|
||||||
|
function getTrafficSplitObject(name, stableWeight, baselineWeight, canaryWeight) {
|
||||||
|
const trafficSplitObjectJson = `{
|
||||||
|
"apiVersion": "split.smi-spec.io/v1alpha1",
|
||||||
|
"kind": "TrafficSplit",
|
||||||
|
"metadata": {
|
||||||
|
"name": "%s"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"backends": [
|
||||||
|
{
|
||||||
|
"service": "%s",
|
||||||
|
"weight": "%sm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "%s",
|
||||||
|
"weight": "%sm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "%s",
|
||||||
|
"weight": "%sm"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"service": "%s"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
const trafficSplitObject = util.format(trafficSplitObjectJson, getTrafficSplitResourceName(name), canaryDeploymentHelper.getStableResourceName(name), stableWeight, canaryDeploymentHelper.getBaselineResourceName(name), baselineWeight, canaryDeploymentHelper.getCanaryResourceName(name), canaryWeight, name);
|
||||||
|
return trafficSplitObject;
|
||||||
|
}
|
||||||
|
function getTrafficSplitResourceName(name) {
|
||||||
|
return name + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX;
|
||||||
|
}
|
||||||
25
lib/utilities/string-comparison.js
Normal file
25
lib/utilities/string-comparison.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
var StringComparer;
|
||||||
|
(function (StringComparer) {
|
||||||
|
StringComparer[StringComparer["Ordinal"] = 0] = "Ordinal";
|
||||||
|
StringComparer[StringComparer["OrdinalIgnoreCase"] = 1] = "OrdinalIgnoreCase";
|
||||||
|
})(StringComparer = exports.StringComparer || (exports.StringComparer = {}));
|
||||||
|
function isEqual(str1, str2, stringComparer) {
|
||||||
|
if (str1 == null && str2 == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (str1 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (str2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (stringComparer == StringComparer.OrdinalIgnoreCase) {
|
||||||
|
return str1.toUpperCase() === str2.toUpperCase();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return str1 === str2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.isEqual = isEqual;
|
||||||
526
lib/utilities/tool-runner.js
Normal file
526
lib/utilities/tool-runner.js
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const os = require("os");
|
||||||
|
const events = require("events");
|
||||||
|
const child = require("child_process");
|
||||||
|
const core = require("@actions/core");
|
||||||
|
class ToolRunner extends events.EventEmitter {
|
||||||
|
constructor(toolPath) {
|
||||||
|
super();
|
||||||
|
if (!toolPath) {
|
||||||
|
throw new Error('Parameter \'toolPath\' cannot be null or empty.');
|
||||||
|
}
|
||||||
|
this.toolPath = toolPath;
|
||||||
|
this.args = [];
|
||||||
|
core.debug('toolRunner toolPath: ' + toolPath);
|
||||||
|
}
|
||||||
|
_debug(message) {
|
||||||
|
this.emit('debug', message);
|
||||||
|
}
|
||||||
|
_argStringToArray(argString) {
|
||||||
|
var args = [];
|
||||||
|
var inQuotes = false;
|
||||||
|
var escaped = false;
|
||||||
|
var lastCharWasSpace = true;
|
||||||
|
var arg = '';
|
||||||
|
var append = function (c) {
|
||||||
|
// we only escape double quotes.
|
||||||
|
if (escaped && c !== '"') {
|
||||||
|
arg += '\\';
|
||||||
|
}
|
||||||
|
arg += c;
|
||||||
|
escaped = false;
|
||||||
|
};
|
||||||
|
for (var i = 0; i < argString.length; i++) {
|
||||||
|
var c = argString.charAt(i);
|
||||||
|
if (c === ' ' && !inQuotes) {
|
||||||
|
if (!lastCharWasSpace) {
|
||||||
|
args.push(arg);
|
||||||
|
arg = '';
|
||||||
|
}
|
||||||
|
lastCharWasSpace = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lastCharWasSpace = false;
|
||||||
|
}
|
||||||
|
if (c === '"') {
|
||||||
|
if (!escaped) {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
append(c);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\\" && escaped) {
|
||||||
|
append(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\\" && inQuotes) {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
append(c);
|
||||||
|
lastCharWasSpace = false;
|
||||||
|
}
|
||||||
|
if (!lastCharWasSpace) {
|
||||||
|
args.push(arg.trim());
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
_getCommandString(options, noPrefix) {
|
||||||
|
let toolPath = this._getSpawnFileName();
|
||||||
|
let args = this._getSpawnArgs(options);
|
||||||
|
let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
// Windows + cmd file
|
||||||
|
if (this._isCmdFile()) {
|
||||||
|
cmd += toolPath;
|
||||||
|
args.forEach((a) => {
|
||||||
|
cmd += ` ${a}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Windows + verbatim
|
||||||
|
else if (options.windowsVerbatimArguments) {
|
||||||
|
cmd += `"${toolPath}"`;
|
||||||
|
args.forEach((a) => {
|
||||||
|
cmd += ` ${a}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Windows (regular)
|
||||||
|
else {
|
||||||
|
cmd += this._windowsQuoteCmdArg(toolPath);
|
||||||
|
args.forEach((a) => {
|
||||||
|
cmd += ` ${this._windowsQuoteCmdArg(a)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// OSX/Linux - this can likely be improved with some form of quoting.
|
||||||
|
// creating processes on Unix is fundamentally different than Windows.
|
||||||
|
// on Unix, execvp() takes an arg array.
|
||||||
|
cmd += toolPath;
|
||||||
|
args.forEach((a) => {
|
||||||
|
cmd += ` ${a}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// append second tool
|
||||||
|
if (this.pipeOutputToTool) {
|
||||||
|
cmd += ' | ' + this.pipeOutputToTool._getCommandString(options, /*noPrefix:*/ true);
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
_getSpawnFileName() {
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
if (this._isCmdFile()) {
|
||||||
|
return process.env['COMSPEC'] || 'cmd.exe';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.toolPath;
|
||||||
|
}
|
||||||
|
_getSpawnArgs(options) {
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
if (this._isCmdFile()) {
|
||||||
|
let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`;
|
||||||
|
for (let i = 0; i < this.args.length; i++) {
|
||||||
|
argline += ' ';
|
||||||
|
argline += options.windowsVerbatimArguments ? this.args[i] : this._windowsQuoteCmdArg(this.args[i]);
|
||||||
|
}
|
||||||
|
argline += '"';
|
||||||
|
return [argline];
|
||||||
|
}
|
||||||
|
if (options.windowsVerbatimArguments) {
|
||||||
|
// note, in Node 6.x options.argv0 can be used instead of overriding args.slice and args.unshift.
|
||||||
|
// for more details, refer to https://github.com/nodejs/node/blob/v6.x/lib/child_process.js
|
||||||
|
let args = this.args.slice(0); // copy the array
|
||||||
|
// override slice to prevent Node from creating a copy of the arg array.
|
||||||
|
// we need Node to use the "unshift" override below.
|
||||||
|
args.slice = function () {
|
||||||
|
if (arguments.length != 1 || arguments[0] != 0) {
|
||||||
|
throw new Error('Unexpected arguments passed to args.slice when windowsVerbatimArguments flag is set.');
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
};
|
||||||
|
// override unshift
|
||||||
|
//
|
||||||
|
// when using the windowsVerbatimArguments option, Node does not quote the tool path when building
|
||||||
|
// the cmdline parameter for the win32 function CreateProcess(). an unquoted space in the tool path
|
||||||
|
// causes problems for tools when attempting to parse their own command line args. tools typically
|
||||||
|
// assume their arguments begin after arg 0.
|
||||||
|
//
|
||||||
|
// by hijacking unshift, we can quote the tool path when it pushed onto the args array. Node builds
|
||||||
|
// the cmdline parameter from the args array.
|
||||||
|
//
|
||||||
|
// note, we can't simply pass a quoted tool path to Node for multiple reasons:
|
||||||
|
// 1) Node verifies the file exists (calls win32 function GetFileAttributesW) and the check returns
|
||||||
|
// false if the path is quoted.
|
||||||
|
// 2) Node passes the tool path as the application parameter to CreateProcess, which expects the
|
||||||
|
// path to be unquoted.
|
||||||
|
//
|
||||||
|
// also note, in addition to the tool path being embedded within the cmdline parameter, Node also
|
||||||
|
// passes the tool path to CreateProcess via the application parameter (optional parameter). when
|
||||||
|
// present, Windows uses the application parameter to determine which file to run, instead of
|
||||||
|
// interpreting the file from the cmdline parameter.
|
||||||
|
args.unshift = function () {
|
||||||
|
if (arguments.length != 1) {
|
||||||
|
throw new Error('Unexpected arguments passed to args.unshift when windowsVerbatimArguments flag is set.');
|
||||||
|
}
|
||||||
|
return Array.prototype.unshift.call(args, `"${arguments[0]}"`); // quote the file name
|
||||||
|
};
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.args;
|
||||||
|
}
|
||||||
|
_isCmdFile() {
|
||||||
|
let upperToolPath = this.toolPath.toUpperCase();
|
||||||
|
return this._endsWith(upperToolPath, '.CMD') || this._endsWith(upperToolPath, '.BAT');
|
||||||
|
}
|
||||||
|
_endsWith(str, end) {
|
||||||
|
return str.slice(-end.length) == end;
|
||||||
|
}
|
||||||
|
_windowsQuoteCmdArg(arg) {
|
||||||
|
// for .exe, apply the normal quoting rules that libuv applies
|
||||||
|
if (!this._isCmdFile()) {
|
||||||
|
return this._uv_quote_cmd_arg(arg);
|
||||||
|
}
|
||||||
|
// otherwise apply quoting rules specific to the cmd.exe command line parser.
|
||||||
|
// the libuv rules are generic and are not designed specifically for cmd.exe
|
||||||
|
// command line parser.
|
||||||
|
//
|
||||||
|
// for a detailed description of the cmd.exe command line parser, refer to
|
||||||
|
// http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912
|
||||||
|
// need quotes for empty arg
|
||||||
|
if (!arg) {
|
||||||
|
return '""';
|
||||||
|
}
|
||||||
|
// determine whether the arg needs to be quoted
|
||||||
|
const cmdSpecialChars = [' ', '\t', '&', '(', ')', '[', ']', '{', '}', '^', '=', ';', '!', '\'', '+', ',', '`', '~', '|', '<', '>', '"'];
|
||||||
|
let needsQuotes = false;
|
||||||
|
for (let char of arg) {
|
||||||
|
if (cmdSpecialChars.some(x => x == char)) {
|
||||||
|
needsQuotes = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// short-circuit if quotes not needed
|
||||||
|
if (!needsQuotes) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
// the following quoting rules are very similar to the rules that by libuv applies.
|
||||||
|
//
|
||||||
|
// 1) wrap the string in quotes
|
||||||
|
//
|
||||||
|
// 2) double-up quotes - i.e. " => ""
|
||||||
|
//
|
||||||
|
// this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately
|
||||||
|
// doesn't work well with a cmd.exe command line.
|
||||||
|
//
|
||||||
|
// note, replacing " with "" also works well if the arg is passed to a downstream .NET console app.
|
||||||
|
// for example, the command line:
|
||||||
|
// foo.exe "myarg:""my val"""
|
||||||
|
// is parsed by a .NET console app into an arg array:
|
||||||
|
// [ "myarg:\"my val\"" ]
|
||||||
|
// which is the same end result when applying libuv quoting rules. although the actual
|
||||||
|
// command line from libuv quoting rules would look like:
|
||||||
|
// foo.exe "myarg:\"my val\""
|
||||||
|
//
|
||||||
|
// 3) double-up slashes that preceed a quote,
|
||||||
|
// e.g. hello \world => "hello \world"
|
||||||
|
// hello\"world => "hello\\""world"
|
||||||
|
// hello\\"world => "hello\\\\""world"
|
||||||
|
// hello world\ => "hello world\\"
|
||||||
|
//
|
||||||
|
// technically this is not required for a cmd.exe command line, or the batch argument parser.
|
||||||
|
// the reasons for including this as a .cmd quoting rule are:
|
||||||
|
//
|
||||||
|
// a) this is optimized for the scenario where the argument is passed from the .cmd file to an
|
||||||
|
// external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule.
|
||||||
|
//
|
||||||
|
// b) it's what we've been doing previously (by deferring to node default behavior) and we
|
||||||
|
// haven't heard any complaints about that aspect.
|
||||||
|
//
|
||||||
|
// note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be
|
||||||
|
// escaped when used on the command line directly - even though within a .cmd file % can be escaped
|
||||||
|
// by using %%.
|
||||||
|
//
|
||||||
|
// the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts
|
||||||
|
// the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing.
|
||||||
|
//
|
||||||
|
// one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would
|
||||||
|
// often work, since it is unlikely that var^ would exist, and the ^ character is removed when the
|
||||||
|
// variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args
|
||||||
|
// to an external program.
|
||||||
|
//
|
||||||
|
// an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file.
|
||||||
|
// % can be escaped within a .cmd file.
|
||||||
|
let reverse = '"';
|
||||||
|
let quote_hit = true;
|
||||||
|
for (let i = arg.length; i > 0; i--) { // walk the string in reverse
|
||||||
|
reverse += arg[i - 1];
|
||||||
|
if (quote_hit && arg[i - 1] == '\\') {
|
||||||
|
reverse += '\\'; // double the slash
|
||||||
|
}
|
||||||
|
else if (arg[i - 1] == '"') {
|
||||||
|
quote_hit = true;
|
||||||
|
reverse += '"'; // double the quote
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
quote_hit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reverse += '"';
|
||||||
|
return reverse.split('').reverse().join('');
|
||||||
|
}
|
||||||
|
_uv_quote_cmd_arg(arg) {
|
||||||
|
// Tool runner wraps child_process.spawn() and needs to apply the same quoting as
|
||||||
|
// Node in certain cases where the undocumented spawn option windowsVerbatimArguments
|
||||||
|
// is used.
|
||||||
|
//
|
||||||
|
// Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV,
|
||||||
|
// see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details),
|
||||||
|
// pasting copyright notice from Node within this function:
|
||||||
|
//
|
||||||
|
// Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to
|
||||||
|
// deal in the Software without restriction, including without limitation the
|
||||||
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
// sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
// IN THE SOFTWARE.
|
||||||
|
if (!arg) {
|
||||||
|
// Need double quotation for empty argument
|
||||||
|
return '""';
|
||||||
|
}
|
||||||
|
if (arg.indexOf(' ') < 0 && arg.indexOf('\t') < 0 && arg.indexOf('"') < 0) {
|
||||||
|
// No quotation needed
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
if (arg.indexOf('"') < 0 && arg.indexOf('\\') < 0) {
|
||||||
|
// No embedded double quotes or backslashes, so I can just wrap
|
||||||
|
// quote marks around the whole thing.
|
||||||
|
return `"${arg}"`;
|
||||||
|
}
|
||||||
|
// Expected input/output:
|
||||||
|
// input : hello"world
|
||||||
|
// output: "hello\"world"
|
||||||
|
// input : hello""world
|
||||||
|
// output: "hello\"\"world"
|
||||||
|
// input : hello\world
|
||||||
|
// output: hello\world
|
||||||
|
// input : hello\\world
|
||||||
|
// output: hello\\world
|
||||||
|
// input : hello\"world
|
||||||
|
// output: "hello\\\"world"
|
||||||
|
// input : hello\\"world
|
||||||
|
// output: "hello\\\\\"world"
|
||||||
|
// input : hello world\
|
||||||
|
// output: "hello world\\" - note the comment in libuv actually reads "hello world\"
|
||||||
|
// but it appears the comment is wrong, it should be "hello world\\"
|
||||||
|
let reverse = '"';
|
||||||
|
let quote_hit = true;
|
||||||
|
for (let i = arg.length; i > 0; i--) { // walk the string in reverse
|
||||||
|
reverse += arg[i - 1];
|
||||||
|
if (quote_hit && arg[i - 1] == '\\') {
|
||||||
|
reverse += '\\';
|
||||||
|
}
|
||||||
|
else if (arg[i - 1] == '"') {
|
||||||
|
quote_hit = true;
|
||||||
|
reverse += '\\';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
quote_hit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reverse += '"';
|
||||||
|
return reverse.split('').reverse().join('');
|
||||||
|
}
|
||||||
|
_cloneExecOptions(options) {
|
||||||
|
options = options || {};
|
||||||
|
let result = {
|
||||||
|
cwd: options.cwd || process.cwd(),
|
||||||
|
env: options.env || process.env,
|
||||||
|
silent: options.silent || false,
|
||||||
|
failOnStdErr: options.failOnStdErr || false,
|
||||||
|
ignoreReturnCode: options.ignoreReturnCode || false,
|
||||||
|
windowsVerbatimArguments: options.windowsVerbatimArguments || false
|
||||||
|
};
|
||||||
|
result.outStream = options.outStream || process.stdout;
|
||||||
|
result.errStream = options.errStream || process.stderr;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
_getSpawnSyncOptions(options) {
|
||||||
|
let result = {};
|
||||||
|
result.cwd = options.cwd;
|
||||||
|
result.env = options.env;
|
||||||
|
result['windowsVerbatimArguments'] = options.windowsVerbatimArguments || this._isCmdFile();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add argument
|
||||||
|
* Append an argument or an array of arguments
|
||||||
|
* returns ToolRunner for chaining
|
||||||
|
*
|
||||||
|
* @param val string cmdline or array of strings
|
||||||
|
* @returns ToolRunner
|
||||||
|
*/
|
||||||
|
arg(val) {
|
||||||
|
if (!val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (val instanceof Array) {
|
||||||
|
core.debug(this.toolPath + ' arg: ' + JSON.stringify(val));
|
||||||
|
this.args = this.args.concat(val);
|
||||||
|
}
|
||||||
|
else if (typeof (val) === 'string') {
|
||||||
|
core.debug(this.toolPath + ' arg: ' + val);
|
||||||
|
this.args = this.args.concat(val.trim());
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parses an argument line into one or more arguments
|
||||||
|
* e.g. .line('"arg one" two -z') is equivalent to .arg(['arg one', 'two', '-z'])
|
||||||
|
* returns ToolRunner for chaining
|
||||||
|
*
|
||||||
|
* @param val string argument line
|
||||||
|
* @returns ToolRunner
|
||||||
|
*/
|
||||||
|
line(val) {
|
||||||
|
if (!val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
core.debug(this.toolPath + ' arg: ' + val);
|
||||||
|
this.args = this.args.concat(this._argStringToArray(val));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add argument(s) if a condition is met
|
||||||
|
* Wraps arg(). See arg for details
|
||||||
|
* returns ToolRunner for chaining
|
||||||
|
*
|
||||||
|
* @param condition boolean condition
|
||||||
|
* @param val string cmdline or array of strings
|
||||||
|
* @returns ToolRunner
|
||||||
|
*/
|
||||||
|
argIf(condition, val) {
|
||||||
|
if (condition) {
|
||||||
|
this.arg(val);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Pipe output of exec() to another tool
|
||||||
|
* @param tool
|
||||||
|
* @param file optional filename to additionally stream the output to.
|
||||||
|
* @returns {ToolRunner}
|
||||||
|
*/
|
||||||
|
pipeExecOutputToTool(tool, file) {
|
||||||
|
this.pipeOutputToTool = tool;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Exec a tool synchronously.
|
||||||
|
* Output will be *not* be streamed to the live console. It will be returned after execution is complete.
|
||||||
|
* Appropriate for short running tools
|
||||||
|
* Returns IExecSyncResult with output and return code
|
||||||
|
*
|
||||||
|
* @param tool path to tool to exec
|
||||||
|
* @param options optional exec options. See IExecSyncOptions
|
||||||
|
* @returns IExecSyncResult
|
||||||
|
*/
|
||||||
|
execSync(options) {
|
||||||
|
core.debug('exec tool: ' + this.toolPath);
|
||||||
|
core.debug('arguments:');
|
||||||
|
this.args.forEach((arg) => {
|
||||||
|
core.debug(' ' + arg);
|
||||||
|
});
|
||||||
|
options = this._cloneExecOptions(options);
|
||||||
|
if (!options.silent) {
|
||||||
|
options.outStream.write(this._getCommandString(options) + os.EOL);
|
||||||
|
}
|
||||||
|
var r = child.spawnSync(this._getSpawnFileName(), this._getSpawnArgs(options), this._getSpawnSyncOptions(options));
|
||||||
|
var res = { code: r.status, error: r.error };
|
||||||
|
if (!options.silent && r.stdout && r.stdout.length > 0) {
|
||||||
|
options.outStream.write(r.stdout);
|
||||||
|
}
|
||||||
|
if (!options.silent && r.stderr && r.stderr.length > 0) {
|
||||||
|
options.errStream.write(r.stderr);
|
||||||
|
}
|
||||||
|
res.stdout = (r.stdout) ? r.stdout.toString() : '';
|
||||||
|
res.stderr = (r.stderr) ? r.stderr.toString() : '';
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.ToolRunner = ToolRunner;
|
||||||
|
class ExecState extends events.EventEmitter {
|
||||||
|
constructor(options, toolPath) {
|
||||||
|
super();
|
||||||
|
this.delay = 10000; // 10 seconds
|
||||||
|
this.timeout = null;
|
||||||
|
if (!toolPath) {
|
||||||
|
throw new Error('toolPath must not be empty');
|
||||||
|
}
|
||||||
|
this.options = options;
|
||||||
|
this.toolPath = toolPath;
|
||||||
|
let delay = process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY'];
|
||||||
|
if (delay) {
|
||||||
|
this.delay = parseInt(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CheckComplete() {
|
||||||
|
if (this.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.processClosed) {
|
||||||
|
this._setResult();
|
||||||
|
}
|
||||||
|
else if (this.processExited) {
|
||||||
|
this.timeout = setTimeout(ExecState.HandleTimeout, this.delay, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_setResult() {
|
||||||
|
// determine whether there is an error
|
||||||
|
let error;
|
||||||
|
if (this.processExited) {
|
||||||
|
if (this.processError) {
|
||||||
|
error = new Error(`LIB_ProcessError: \n tool: ${this.toolPath} \n error: ${this.processError}`);
|
||||||
|
}
|
||||||
|
else if (this.processExitCode != 0 && !this.options.ignoreReturnCode) {
|
||||||
|
error = new Error(`LIB_ProcessExitCode\n tool: ${this.toolPath} \n Exit Code: ${this.processExitCode}`);
|
||||||
|
}
|
||||||
|
else if (this.processStderr && this.options.failOnStdErr) {
|
||||||
|
error = new Error(`LIB_ProcessStderr', ${this.toolPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// clear the timeout
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = null;
|
||||||
|
}
|
||||||
|
this.done = true;
|
||||||
|
this.emit('done', error, this.processExitCode);
|
||||||
|
}
|
||||||
|
static HandleTimeout(state) {
|
||||||
|
if (state.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.processClosed && state.processExited) {
|
||||||
|
core.debug(`LIB_StdioNotClosed`);
|
||||||
|
}
|
||||||
|
state._setResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
62
lib/utilities/utility.js
Normal file
62
lib/utilities/utility.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const os = require("os");
|
||||||
|
const core = require("@actions/core");
|
||||||
|
function getExecutableExtension() {
|
||||||
|
if (os.type().match(/^Win/)) {
|
||||||
|
return '.exe';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
exports.getExecutableExtension = getExecutableExtension;
|
||||||
|
function isEqual(str1, str2, ignoreCase) {
|
||||||
|
if (str1 == null && str2 == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (str1 == null || str2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ignoreCase) {
|
||||||
|
return str1.toUpperCase() === str2.toUpperCase();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return str1 === str2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.isEqual = isEqual;
|
||||||
|
function checkForErrors(execResults, warnIfError) {
|
||||||
|
if (execResults.length !== 0) {
|
||||||
|
let stderr = '';
|
||||||
|
execResults.forEach(result => {
|
||||||
|
if (result.stderr) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
stderr += result.stderr + '\n';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.warning(result.stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (stderr.length > 0) {
|
||||||
|
if (!!warnIfError) {
|
||||||
|
core.warning(stderr.trim());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(stderr.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.checkForErrors = checkForErrors;
|
||||||
|
function sleep(timeout) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||||
|
}
|
||||||
|
exports.sleep = sleep;
|
||||||
|
function getRandomInt(max) {
|
||||||
|
return Math.floor(Math.random() * Math.floor(max));
|
||||||
|
}
|
||||||
|
exports.getRandomInt = getRandomInt;
|
||||||
|
function getCurrentTime() {
|
||||||
|
return new Date().getTime();
|
||||||
|
}
|
||||||
|
exports.getCurrentTime = getCurrentTime;
|
||||||
26
lib/utils.js
26
lib/utils.js
@ -1,26 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
const os = require("os");
|
|
||||||
function isEqual(str1, str2) {
|
|
||||||
if (!str1)
|
|
||||||
str1 = "";
|
|
||||||
if (!str2)
|
|
||||||
str2 = "";
|
|
||||||
return str1.toLowerCase() === str2.toLowerCase();
|
|
||||||
}
|
|
||||||
exports.isEqual = isEqual;
|
|
||||||
function getRandomInt(max) {
|
|
||||||
return Math.floor(Math.random() * Math.floor(max));
|
|
||||||
}
|
|
||||||
exports.getRandomInt = getRandomInt;
|
|
||||||
function getExecutableExtension() {
|
|
||||||
if (os.type().match(/^Win/)) {
|
|
||||||
return '.exe';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
exports.getExecutableExtension = getExecutableExtension;
|
|
||||||
function getCurrentTime() {
|
|
||||||
return new Date().getTime();
|
|
||||||
}
|
|
||||||
exports.getCurrentTime = getCurrentTime;
|
|
||||||
44
src/actions/promote.ts
Normal file
44
src/actions/promote.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
|
import * as deploymentHelper from '../utilities/strategy-helpers/deployment-helper';
|
||||||
|
import * as canaryDeploymentHelper from '../utilities/strategy-helpers/canary-deployment-helper';
|
||||||
|
import * as SMICanaryDeploymentHelper from '../utilities/strategy-helpers/smi-canary-deployment-helper';
|
||||||
|
import * as utils from '../utilities/manifest-utilities';
|
||||||
|
import * as TaskInputParameters from '../input-parameters';
|
||||||
|
|
||||||
|
import { Kubectl } from '../kubectl-object-model';
|
||||||
|
|
||||||
|
export async function promote(ignoreSslErrors?: boolean) {
|
||||||
|
const kubectl = new Kubectl(await utils.getKubectl(), TaskInputParameters.namespace, ignoreSslErrors);
|
||||||
|
|
||||||
|
if (!canaryDeploymentHelper.isCanaryDeploymentStrategy()) {
|
||||||
|
core.debug('Strategy is not canary deployment. Invalid request.');
|
||||||
|
throw ('InvalidPromotetActionDeploymentStrategy');
|
||||||
|
}
|
||||||
|
|
||||||
|
let includeServices = false;
|
||||||
|
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||||
|
includeServices = true;
|
||||||
|
// In case of SMI traffic split strategy when deployment is promoted, first we will redirect traffic to
|
||||||
|
// Canary deployment, then update stable deployment and then redirect traffic to stable deployment
|
||||||
|
core.debug('Redirecting traffic to canary deployment');
|
||||||
|
SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(kubectl, TaskInputParameters.manifests);
|
||||||
|
|
||||||
|
core.debug('Deploying input manifests with SMI canary strategy');
|
||||||
|
await deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None');
|
||||||
|
|
||||||
|
core.debug('Redirecting traffic to stable deployment');
|
||||||
|
SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(kubectl, TaskInputParameters.manifests);
|
||||||
|
} else {
|
||||||
|
core.debug('Deploying input manifests');
|
||||||
|
await deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None');
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug('Deployment strategy selected is Canary. Deleting canary and baseline workloads.');
|
||||||
|
try {
|
||||||
|
canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests, includeServices);
|
||||||
|
} catch (ex) {
|
||||||
|
core.warning('Exception occurred while deleting canary and baseline workloads. Exception: ' + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/actions/reject.ts
Normal file
26
src/actions/reject.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as canaryDeploymentHelper from '../utilities/strategy-helpers/canary-deployment-helper';
|
||||||
|
import * as SMICanaryDeploymentHelper from '../utilities/strategy-helpers/smi-canary-deployment-helper';
|
||||||
|
import { Kubectl } from '../kubectl-object-model';
|
||||||
|
import * as utils from '../utilities/manifest-utilities';
|
||||||
|
import * as TaskInputParameters from '../input-parameters';
|
||||||
|
|
||||||
|
export async function reject(ignoreSslErrors?: boolean) {
|
||||||
|
const kubectl = new Kubectl(await utils.getKubectl(), TaskInputParameters.namespace, ignoreSslErrors);
|
||||||
|
|
||||||
|
if (!canaryDeploymentHelper.isCanaryDeploymentStrategy()) {
|
||||||
|
core.debug('Strategy is not canary deployment. Invalid request.');
|
||||||
|
throw ('InvalidRejectActionDeploymentStrategy');
|
||||||
|
}
|
||||||
|
|
||||||
|
let includeServices = false;
|
||||||
|
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||||
|
core.debug('Reject deployment with SMI canary strategy');
|
||||||
|
includeServices = true;
|
||||||
|
SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(kubectl, TaskInputParameters.manifests);
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug('Deployment strategy selected is Canary. Deleting baseline and canary workloads.');
|
||||||
|
canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests, includeServices);
|
||||||
|
}
|
||||||
26
src/constants.ts
Normal file
26
src/constants.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
export class KubernetesWorkload {
|
||||||
|
public static pod: string = 'Pod';
|
||||||
|
public static replicaset: string = 'Replicaset';
|
||||||
|
public static deployment: string = 'Deployment';
|
||||||
|
public static statefulSet: string = 'StatefulSet';
|
||||||
|
public static daemonSet: string = 'DaemonSet';
|
||||||
|
public static job: string = 'job';
|
||||||
|
public static cronjob: string = 'cronjob';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DiscoveryAndLoadBalancerResource {
|
||||||
|
public static service: string = 'service';
|
||||||
|
public static ingress: string = 'ingress';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServiceTypes {
|
||||||
|
public static loadBalancer: string = 'LoadBalancer';
|
||||||
|
public static nodePort: string = 'NodePort';
|
||||||
|
public static clusterIP: string = 'ClusterIP'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deploymentTypes: string[] = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset'];
|
||||||
|
export const workloadTypes: string[] = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob'];
|
||||||
|
export const workloadTypesWithRolloutStatus: string[] = ['deployment', 'daemonset', 'statefulset'];
|
||||||
40
src/input-parameters.ts
Normal file
40
src/input-parameters.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
|
export let namespace: string = core.getInput('namespace');
|
||||||
|
export const containers: string[] = core.getInput('images').split('\n');
|
||||||
|
export const imagePullSecrets: string[] = core.getInput('imagepullsecrets').split('\n');
|
||||||
|
export const manifests = core.getInput('manifests').split('\n');
|
||||||
|
export const canaryPercentage: string = core.getInput('percentage');
|
||||||
|
export const deploymentStrategy: string = core.getInput('strategy');
|
||||||
|
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');
|
||||||
|
|
||||||
|
if (!namespace) {
|
||||||
|
core.debug('Namespace was not supplied; using "default" namespace instead.');
|
||||||
|
namespace = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pe = parseInt(canaryPercentage);
|
||||||
|
if (pe < 0 || pe > 100) {
|
||||||
|
core.setFailed('A valid percentage value is between 0 and 100');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
core.setFailed("Enter a valid 'percentage' integer value ");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pe = parseInt(baselineAndCanaryReplicas);
|
||||||
|
if (pe < 0 || pe > 100) {
|
||||||
|
core.setFailed('A valid baseline-and-canary-replicas value is between 0 and 100');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
core.setFailed("Enter a valid 'baseline-and-canary-replicas' integer value");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
105
src/kubectl-object-model.ts
Normal file
105
src/kubectl-object-model.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { ToolRunner, IExecOptions } from "./utilities/tool-runner";
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Kubectl {
|
||||||
|
private kubectlPath: string;
|
||||||
|
private namespace: string;
|
||||||
|
private ignoreSSLErrors: boolean;
|
||||||
|
|
||||||
|
constructor(kubectlPath: string, namespace?: string, ignoreSSLErrors?: boolean) {
|
||||||
|
this.kubectlPath = kubectlPath;
|
||||||
|
this.ignoreSSLErrors = !!ignoreSSLErrors;
|
||||||
|
if (!!namespace) {
|
||||||
|
this.namespace = namespace;
|
||||||
|
} else {
|
||||||
|
this.namespace = 'default';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public apply(configurationPaths: string | string[]) {
|
||||||
|
return this.execute(['apply', '-f', this.createInlineArray(configurationPaths)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public describe(resourceType: string, resourceName: string, silent?: boolean) {
|
||||||
|
return this.execute(['describe', resourceType, resourceName], silent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNewReplicaSet(deployment: string) {
|
||||||
|
let newReplicaSet = '';
|
||||||
|
const result = await this.describe('deployment', deployment, true);
|
||||||
|
if (result && result.stdout) {
|
||||||
|
const stdout = result.stdout.split('\n');
|
||||||
|
stdout.forEach((line: string) => {
|
||||||
|
if (!!line && line.toLowerCase().indexOf('newreplicaset') > -1) {
|
||||||
|
newReplicaSet = line.substr(14).trim().split(' ')[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newReplicaSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllPods() {
|
||||||
|
return this.execute(['get', 'pods', '-o', 'json'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClusterInfo() {
|
||||||
|
return this.execute(['cluster-info'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkRolloutStatus(resourceType: string, name: string) {
|
||||||
|
return this.execute(['rollout', 'status', resourceType + '/' + name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getResource(resourceType: string, name: string) {
|
||||||
|
return this.execute(['get', resourceType + '/' + name, '-o', 'json']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getResources(applyOutput: string, filterResourceTypes: string[]): Resource[] {
|
||||||
|
const outputLines = applyOutput.split('\n');
|
||||||
|
const results = [];
|
||||||
|
outputLines.forEach(line => {
|
||||||
|
const words = line.split(' ');
|
||||||
|
if (words.length > 2) {
|
||||||
|
const resourceType = words[0].trim();
|
||||||
|
const resourceName = JSON.parse(words[1].trim());
|
||||||
|
if (filterResourceTypes.filter(type => !!type && resourceType.toLowerCase().startsWith(type.toLowerCase())).length > 0) {
|
||||||
|
results.push({
|
||||||
|
type: resourceType,
|
||||||
|
name: resourceName
|
||||||
|
} as Resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public delete(args: string | string[]) {
|
||||||
|
if (typeof args === 'string')
|
||||||
|
return this.execute(['delete', args]);
|
||||||
|
else
|
||||||
|
return this.execute(['delete'].concat(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
private execute(args: string[], silent?: boolean) {
|
||||||
|
if (this.ignoreSSLErrors) {
|
||||||
|
args.push('--insecure-skip-tls-verify');
|
||||||
|
}
|
||||||
|
args = args.concat(['--namespace', this.namespace]);
|
||||||
|
const command = new ToolRunner(this.kubectlPath);
|
||||||
|
command.arg(args);
|
||||||
|
|
||||||
|
return command.execSync({ silent: !!silent } as IExecOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createInlineArray(str: string | string[]): string {
|
||||||
|
if (typeof str === 'string') { return str; }
|
||||||
|
return str.join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/run.ts
119
src/run.ts
@ -1,21 +1,20 @@
|
|||||||
import * as toolCache from '@actions/tool-cache';
|
import * as toolCache from '@actions/tool-cache';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as io from '@actions/io';
|
import * as io from '@actions/io';
|
||||||
import { ToolRunner } from "@actions/exec/lib/toolrunner";
|
|
||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as yaml from 'js-yaml';
|
|
||||||
|
|
||||||
import { getExecutableExtension, isEqual, getCurrentTime } from "./utils";
|
import { getExecutableExtension, isEqual } from "./utilities/utility";
|
||||||
import { isWorkloadEntity, updateContainerImagesInManifestFiles, updateImagePullSecrets } from "./kubernetes-utils";
|
|
||||||
import { downloadKubectl, getStableKubectlVersion } from "./kubectl-util";
|
import { downloadKubectl, getStableKubectlVersion } from "./kubectl-util";
|
||||||
|
import { deploy } from './utilities/strategy-helpers/deployment-helper';
|
||||||
|
import { promote } from './actions/promote';
|
||||||
|
import { reject } from './actions/reject';
|
||||||
|
import { Kubectl } from './kubectl-object-model';
|
||||||
|
|
||||||
let kubectlPath = "";
|
let kubectlPath = "";
|
||||||
|
|
||||||
async function setKubectlPath() {
|
async function setKubectlPath() {
|
||||||
if (core.getInput('kubectl-version')) {
|
if (core.getInput('kubectl-version')) {
|
||||||
const version = core.getInput('kubect-version');
|
const version = core.getInput('kubectl-version');
|
||||||
kubectlPath = toolCache.find('kubectl', version);
|
kubectlPath = toolCache.find('kubectl', version);
|
||||||
if (!kubectlPath) {
|
if (!kubectlPath) {
|
||||||
kubectlPath = await installKubectl(version);
|
kubectlPath = await installKubectl(version);
|
||||||
@ -33,90 +32,6 @@ async function setKubectlPath() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deploy(manifests: string[], namespace: string) {
|
|
||||||
if (manifests) {
|
|
||||||
for (var i = 0; i < manifests.length; i++) {
|
|
||||||
let manifest = manifests[i];
|
|
||||||
let toolRunner = new ToolRunner(kubectlPath, ['apply', '-f', manifest, '--namespace', namespace]);
|
|
||||||
await toolRunner.exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkRolloutStatus(name: string, kind: string, namespace: string) {
|
|
||||||
const toolrunner = new ToolRunner(kubectlPath, ['rollout', 'status', `${kind.trim()}/${name.trim()}`, `--namespace`, namespace]);
|
|
||||||
return toolrunner.exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkManifestsStability(manifests: string[], namespace: string) {
|
|
||||||
manifests.forEach((manifest) => {
|
|
||||||
let content = fs.readFileSync(manifest).toString();
|
|
||||||
yaml.safeLoadAll(content, async function (inputObject: any) {
|
|
||||||
if (!!inputObject.kind && !!inputObject.metadata && !!inputObject.metadata.name) {
|
|
||||||
let kind: string = inputObject.kind;
|
|
||||||
switch (kind.toLowerCase()) {
|
|
||||||
case 'deployment':
|
|
||||||
case 'daemonset':
|
|
||||||
case 'statefulset':
|
|
||||||
await checkRolloutStatus(inputObject.metadata.name, kind, namespace);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
core.debug(`No rollout check for kind: ${inputObject.kind}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getManifestFileName(kind: string, name: string) {
|
|
||||||
const filePath = kind + '_' + name + '_' + getCurrentTime().toString();
|
|
||||||
const tempDirectory = process.env['RUNNER_TEMP'];
|
|
||||||
const fileName = path.join(tempDirectory, path.basename(filePath));
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeObjectsToFile(inputObjects: any[]): string[] {
|
|
||||||
const newFilePaths = [];
|
|
||||||
|
|
||||||
if (!!inputObjects) {
|
|
||||||
inputObjects.forEach((inputObject: any) => {
|
|
||||||
try {
|
|
||||||
const inputObjectString = JSON.stringify(inputObject);
|
|
||||||
if (!!inputObject.kind && !!inputObject.metadata && !!inputObject.metadata.name) {
|
|
||||||
const fileName = getManifestFileName(inputObject.kind, inputObject.metadata.name);
|
|
||||||
fs.writeFileSync(path.join(fileName), inputObjectString);
|
|
||||||
newFilePaths.push(fileName);
|
|
||||||
} else {
|
|
||||||
core.debug('Input object is not proper K8s resource object. Object: ' + inputObjectString);
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
core.debug('Exception occurred while wrting object to file : ' + inputObject + ' . Exception: ' + ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newFilePaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateManifests(manifests: string[], imagesToOverride: string, imagepullsecrets: string): string[] {
|
|
||||||
const newObjectsList = [];
|
|
||||||
manifests.forEach((filePath: string) => {
|
|
||||||
let fileContents = fs.readFileSync(filePath).toString();
|
|
||||||
fileContents = updateContainerImagesInManifestFiles(fileContents, imagesToOverride.split('\n'));
|
|
||||||
yaml.safeLoadAll(fileContents, function (inputObject: any) {
|
|
||||||
if (!!imagepullsecrets && !!inputObject && !!inputObject.kind) {
|
|
||||||
if (isWorkloadEntity(inputObject.kind)) {
|
|
||||||
updateImagePullSecrets(inputObject, imagepullsecrets.split('\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newObjectsList.push(inputObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return writeObjectsToFile(newObjectsList);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installKubectl(version: string) {
|
async function installKubectl(version: string) {
|
||||||
if (isEqual(version, 'latest')) {
|
if (isEqual(version, 'latest')) {
|
||||||
version = await getStableKubectlVersion();
|
version = await getStableKubectlVersion();
|
||||||
@ -141,15 +56,23 @@ async function run() {
|
|||||||
if (!namespace) {
|
if (!namespace) {
|
||||||
namespace = 'default';
|
namespace = 'default';
|
||||||
}
|
}
|
||||||
|
let action = core.getInput('action');
|
||||||
let manifests = manifestsInput.split('\n');
|
let manifests = manifestsInput.split('\n');
|
||||||
const imagesToOverride = core.getInput('images');
|
|
||||||
const imagePullSecretsToAdd = core.getInput('imagepullsecrets');
|
if (action === 'deploy') {
|
||||||
if (!!imagePullSecretsToAdd || !!imagesToOverride) {
|
let strategy = core.getInput('strategy');
|
||||||
manifests = updateManifests(manifests, imagesToOverride, imagePullSecretsToAdd)
|
console.log("strategy: ", strategy)
|
||||||
|
await deploy(new Kubectl(kubectlPath, namespace), manifests, strategy);
|
||||||
|
}
|
||||||
|
else if (action === 'promote') {
|
||||||
|
await promote(true);
|
||||||
|
}
|
||||||
|
else if (action === 'reject') {
|
||||||
|
await reject(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.setFailed('Not a valid action. The allowed actions are deploy, promote, reject');
|
||||||
}
|
}
|
||||||
await deploy(manifests, namespace);
|
|
||||||
await checkManifestsStability(manifests, namespace);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run().catch(core.setFailed);
|
run().catch(core.setFailed);
|
||||||
81
src/utilities/files-helper.ts
Normal file
81
src/utilities/files-helper.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as os from 'os';
|
||||||
|
|
||||||
|
export function getTempDirectory(): string {
|
||||||
|
return process.env['runner.tempDirectory'] || os.tmpdir();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewUserDirPath(): string {
|
||||||
|
let userDir = path.join(getTempDirectory(), 'kubectlTask');
|
||||||
|
ensureDirExists(userDir);
|
||||||
|
|
||||||
|
userDir = path.join(userDir, getCurrentTime().toString());
|
||||||
|
ensureDirExists(userDir);
|
||||||
|
|
||||||
|
return userDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureDirExists(dirPath: string): void {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertFileExists(path: string) {
|
||||||
|
if (!fs.existsSync(path)) {
|
||||||
|
core.error(`FileNotFoundException : ${path}`);
|
||||||
|
throw new Error(`FileNotFoundException: ${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeObjectsToFile(inputObjects: any[]): string[] {
|
||||||
|
const newFilePaths = [];
|
||||||
|
|
||||||
|
if (!!inputObjects) {
|
||||||
|
inputObjects.forEach((inputObject: any) => {
|
||||||
|
try {
|
||||||
|
const inputObjectString = JSON.stringify(inputObject);
|
||||||
|
if (!!inputObject.kind && !!inputObject.metadata && !!inputObject.metadata.name) {
|
||||||
|
const fileName = getManifestFileName(inputObject.kind, inputObject.metadata.name);
|
||||||
|
fs.writeFileSync(path.join(fileName), inputObjectString);
|
||||||
|
newFilePaths.push(fileName);
|
||||||
|
} else {
|
||||||
|
core.debug('Input object is not proper K8s resource object. Object: ' + inputObjectString);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
core.debug('Exception occurred while writing object to file : ' + inputObject + ' . Exception: ' + ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFilePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeManifestToFile(inputObjectString: string, kind: string, name: string): string {
|
||||||
|
if (inputObjectString) {
|
||||||
|
try {
|
||||||
|
const fileName = getManifestFileName(kind, name);
|
||||||
|
fs.writeFileSync(path.join(fileName), inputObjectString);
|
||||||
|
return fileName;
|
||||||
|
} catch (ex) {
|
||||||
|
core.debug('Exception occurred while writing object to file : ' + inputObjectString + ' . Exception: ' + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManifestFileName(kind: string, name: string) {
|
||||||
|
const filePath = kind + '_' + name + '_' + getCurrentTime().toString();
|
||||||
|
const tempDirectory = getTempDirectory();
|
||||||
|
const fileName = path.join(tempDirectory, path.basename(filePath));
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTime(): number {
|
||||||
|
return new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
126
src/utilities/manifest-stability-utility.ts
Normal file
126
src/utilities/manifest-stability-utility.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as utils from './utility';
|
||||||
|
import * as KubernetesConstants from '../constants';
|
||||||
|
import { Kubectl, Resource } from '../kubectl-object-model';
|
||||||
|
|
||||||
|
export async function checkManifestStability(kubectl: Kubectl, resources: Resource[]): Promise<void> {
|
||||||
|
const rolloutStatusResults = [];
|
||||||
|
const numberOfResources = resources.length;
|
||||||
|
for (let i = 0; i < numberOfResources; i++) {
|
||||||
|
const resource = resources[i];
|
||||||
|
if (KubernetesConstants.workloadTypesWithRolloutStatus.indexOf(resource.type.toLowerCase()) >= 0) {
|
||||||
|
rolloutStatusResults.push(kubectl.checkRolloutStatus(resource.type, resource.name));
|
||||||
|
}
|
||||||
|
if (utils.isEqual(resource.type, KubernetesConstants.KubernetesWorkload.pod, true)) {
|
||||||
|
try {
|
||||||
|
await checkPodStatus(kubectl, resource.name);
|
||||||
|
} catch (ex) {
|
||||||
|
core.warning(`CouldNotDeterminePodStatus ${JSON.stringify(ex)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (utils.isEqual(resource.type, KubernetesConstants.DiscoveryAndLoadBalancerResource.service, true)) {
|
||||||
|
try {
|
||||||
|
const service = getService(kubectl, resource.name);
|
||||||
|
const spec = service.spec;
|
||||||
|
const status = service.status;
|
||||||
|
if (utils.isEqual(spec.type, KubernetesConstants.ServiceTypes.loadBalancer, true)) {
|
||||||
|
if (!isLoadBalancerIPAssigned(status)) {
|
||||||
|
await waitForServiceExternalIPAssignment(kubectl, resource.name);
|
||||||
|
} else {
|
||||||
|
console.log('ServiceExternalIP', resource.name, status.loadBalancer.ingress[0].ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
core.warning(`CouldNotDetermineServiceStatus of: ${resource.name} Error: ${JSON.stringify(ex)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.checkForErrors(rolloutStatusResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkPodStatus(kubectl: Kubectl, podName: string): Promise<void> {
|
||||||
|
const sleepTimeout = 10 * 1000; // 10 seconds
|
||||||
|
const iterations = 60; // 60 * 10 seconds timeout = 10 minutes max timeout
|
||||||
|
let podStatus;
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
await utils.sleep(sleepTimeout);
|
||||||
|
core.debug(`Polling for pod status: ${podName}`);
|
||||||
|
podStatus = getPodStatus(kubectl, podName);
|
||||||
|
if (podStatus.phase && podStatus.phase !== 'Pending' && podStatus.phase !== 'Unknown') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
podStatus = getPodStatus(kubectl, podName);
|
||||||
|
switch (podStatus.phase) {
|
||||||
|
case 'Succeeded':
|
||||||
|
case 'Running':
|
||||||
|
if (isPodReady(podStatus)) {
|
||||||
|
console.log(`pod/${podName} is successfully rolled out`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Pending':
|
||||||
|
if (!isPodReady(podStatus)) {
|
||||||
|
core.warning(`pod/${podName} rollout status check timedout`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Failed':
|
||||||
|
core.error(`pod/${podName} rollout failed`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
core.warning(`pod/${podName} rollout status: ${podStatus.phase}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPodStatus(kubectl: Kubectl, podName: string) {
|
||||||
|
const podResult = kubectl.getResource('pod', podName);
|
||||||
|
utils.checkForErrors([podResult]);
|
||||||
|
const podStatus = JSON.parse(podResult.stdout).status;
|
||||||
|
core.debug(`Pod Status: ${JSON.stringify(podStatus)}`);
|
||||||
|
return podStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPodReady(podStatus: any): boolean {
|
||||||
|
let allContainersAreReady = true;
|
||||||
|
podStatus.containerStatuses.forEach(container => {
|
||||||
|
if (container.ready === false) {
|
||||||
|
console.log(`'${container.name}' status: ${JSON.stringify(container.state)}`);
|
||||||
|
allContainersAreReady = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!allContainersAreReady) {
|
||||||
|
core.warning('AllContainersNotInReadyState');
|
||||||
|
}
|
||||||
|
return allContainersAreReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getService(kubectl: Kubectl, serviceName) {
|
||||||
|
const serviceResult = kubectl.getResource(KubernetesConstants.DiscoveryAndLoadBalancerResource.service, serviceName);
|
||||||
|
utils.checkForErrors([serviceResult]);
|
||||||
|
return JSON.parse(serviceResult.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForServiceExternalIPAssignment(kubectl: Kubectl, serviceName: string): Promise<void> {
|
||||||
|
const sleepTimeout = 10 * 1000; // 10 seconds
|
||||||
|
const iterations = 18; // 18 * 10 seconds timeout = 3 minutes max timeout
|
||||||
|
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
console.log(`waitForServiceIpAssignment : ${serviceName}`);
|
||||||
|
await utils.sleep(sleepTimeout);
|
||||||
|
let status = (getService(kubectl, serviceName)).status;
|
||||||
|
if (isLoadBalancerIPAssigned(status)) {
|
||||||
|
console.log('ServiceExternalIP', serviceName, status.loadBalancer.ingress[0].ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.warning(`waitForServiceIpAssignmentTimedOut ${serviceName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoadBalancerIPAssigned(status: any) {
|
||||||
|
if (status && status.loadBalancer && status.loadBalancer.ingress && status.loadBalancer.ingress.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@ -1,5 +1,100 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import { isEqual } from "./utils";
|
import * as kubectlutility from '../kubectl-util';
|
||||||
|
import * as io from '@actions/io';
|
||||||
|
import { isEqual } from "./utility";
|
||||||
|
|
||||||
|
export function getManifestFiles(manifestFilePaths: string[]): string[] {
|
||||||
|
if (!manifestFilePaths) {
|
||||||
|
core.debug('file input is not present');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifestFilePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getKubectl(): Promise<string> {
|
||||||
|
try {
|
||||||
|
return Promise.resolve(io.which('kubectl', true));
|
||||||
|
} catch (ex) {
|
||||||
|
return kubectlutility.downloadKubectl(await kubectlutility.getStableKubectlVersion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createKubectlArgs(kinds: Set<string>, names: Set<string>): string {
|
||||||
|
let args = '';
|
||||||
|
if (!!kinds && kinds.size > 0) {
|
||||||
|
args = args + createInlineArray(Array.from(kinds.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!names && names.size > 0) {
|
||||||
|
args = args + ' ' + Array.from(names.values()).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeleteCmdArgs(argsPrefix: string, inputArgs: string): string {
|
||||||
|
let args = '';
|
||||||
|
|
||||||
|
if (!!argsPrefix && argsPrefix.length > 0) {
|
||||||
|
args = argsPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!inputArgs && inputArgs.length > 0) {
|
||||||
|
if (args.length > 0) {
|
||||||
|
args = args + ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
args = args + inputArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
For example,
|
||||||
|
currentString: `image: "example/example-image"`
|
||||||
|
imageName: `example/example-image`
|
||||||
|
imageNameWithNewTag: `example/example-image:identifiertag`
|
||||||
|
|
||||||
|
This substituteImageNameInSpecFile function would return
|
||||||
|
return Value: `image: "example/example-image:identifiertag"`
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function substituteImageNameInSpecFile(currentString: string, imageName: string, imageNameWithNewTag: string) {
|
||||||
|
if (currentString.indexOf(imageName) < 0) {
|
||||||
|
core.debug(`No occurence of replacement token: ${imageName} found`);
|
||||||
|
return currentString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentString.split('\n').reduce((acc, line) => {
|
||||||
|
const imageKeyword = line.match(/^ *image:/);
|
||||||
|
if (imageKeyword) {
|
||||||
|
let [currentImageName, currentImageTag] = line
|
||||||
|
.substring(imageKeyword[0].length) // consume the line from keyword onwards
|
||||||
|
.trim()
|
||||||
|
.replace(/[',"]/g, '') // replace allowed quotes with nothing
|
||||||
|
.split(':');
|
||||||
|
|
||||||
|
if (!currentImageTag && currentImageName.indexOf(' ') > 0) {
|
||||||
|
currentImageName = currentImageName.split(' ')[0]; // Stripping off comments
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentImageName === imageName) {
|
||||||
|
return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc + line + '\n';
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInlineArray(str: string | string[]): string {
|
||||||
|
if (typeof str === 'string') { return str; }
|
||||||
|
return str.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
function getImagePullSecrets(inputObject: any) {
|
function getImagePullSecrets(inputObject: any) {
|
||||||
if (!inputObject || !inputObject.spec) {
|
if (!inputObject || !inputObject.spec) {
|
||||||
288
src/utilities/resource-object-utility.ts
Normal file
288
src/utilities/resource-object-utility.ts
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
'use strict';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import { Resource } from '../kubectl-object-model';
|
||||||
|
import { KubernetesWorkload, deploymentTypes, workloadTypes } from '../constants';
|
||||||
|
import { StringComparer, isEqual } from './string-comparison';
|
||||||
|
|
||||||
|
export function isDeploymentEntity(kind: string): boolean {
|
||||||
|
if (!kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return deploymentTypes.some((type: string) => {
|
||||||
|
return isEqual(type, kind, StringComparer.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWorkloadEntity(kind: string): boolean {
|
||||||
|
if (!kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return workloadTypes.some((type: string) => {
|
||||||
|
return isEqual(type, kind, StringComparer.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isServiceEntity(kind: string): boolean {
|
||||||
|
if (!kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEqual("Service", kind, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReplicaCount(inputObject: any): any {
|
||||||
|
if (!inputObject) {
|
||||||
|
throw ('NullInputObject');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputObject.kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (!isEqual(kind, KubernetesWorkload.pod, StringComparer.OrdinalIgnoreCase) && !isEqual(kind, KubernetesWorkload.daemonSet, StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
return inputObject.spec.replicas;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateObjectLabels(inputObject: any, newLabels: Map<string, string>, override: boolean) {
|
||||||
|
|
||||||
|
if (!inputObject) {
|
||||||
|
throw ('NullInputObject');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputObject.metadata) {
|
||||||
|
throw ('NullInputObjectMetadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newLabels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (override) {
|
||||||
|
inputObject.metadata.labels = newLabels;
|
||||||
|
} else {
|
||||||
|
let existingLabels = inputObject.metadata.labels;
|
||||||
|
if (!existingLabels) {
|
||||||
|
existingLabels = new Map<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(newLabels).forEach(function (key) {
|
||||||
|
existingLabels[key] = newLabels[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
inputObject.metadata.labels = existingLabels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateImagePullSecrets(inputObject: any, newImagePullSecrets: string[], override: boolean) {
|
||||||
|
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newImagePullSecretsObjects = Array.from(newImagePullSecrets, x => { return { 'name': x }; });
|
||||||
|
let existingImagePullSecretObjects: any = getImagePullSecrets(inputObject);
|
||||||
|
|
||||||
|
if (override) {
|
||||||
|
existingImagePullSecretObjects = newImagePullSecretsObjects;
|
||||||
|
} else {
|
||||||
|
if (!existingImagePullSecretObjects) {
|
||||||
|
existingImagePullSecretObjects = new Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
existingImagePullSecretObjects = existingImagePullSecretObjects.concat(newImagePullSecretsObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImagePullSecrets(inputObject, existingImagePullSecretObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSpecLabels(inputObject: any, newLabels: Map<string, string>, override: boolean) {
|
||||||
|
if (!inputObject) {
|
||||||
|
throw ('NullInputObject');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputObject.kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newLabels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingLabels = getSpecLabels(inputObject);
|
||||||
|
|
||||||
|
if (override) {
|
||||||
|
existingLabels = newLabels;
|
||||||
|
} else {
|
||||||
|
if (!existingLabels) {
|
||||||
|
existingLabels = new Map<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(newLabels).forEach(function (key) {
|
||||||
|
existingLabels[key] = newLabels[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpecLabels(inputObject, existingLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSelectorLabels(inputObject: any, newLabels: Map<string, string>, override: boolean) {
|
||||||
|
if (!inputObject) {
|
||||||
|
throw ('NullInputObject');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputObject.kind) {
|
||||||
|
throw ('ResourceKindNotDefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newLabels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEqual(inputObject.kind, KubernetesWorkload.pod, StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingLabels = getSpecSelectorLabels(inputObject);
|
||||||
|
|
||||||
|
if (override) {
|
||||||
|
existingLabels = newLabels;
|
||||||
|
} else {
|
||||||
|
if (!existingLabels) {
|
||||||
|
existingLabels = new Map<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(newLabels).forEach(function (key) {
|
||||||
|
existingLabels[key] = newLabels[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpecSelectorLabels(inputObject, existingLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResources(filePaths: string[], filterResourceTypes: string[]): Resource[] {
|
||||||
|
if (!filePaths) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources: Resource[] = [];
|
||||||
|
|
||||||
|
filePaths.forEach((filePath: string) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const inputObjectKind = inputObject ? inputObject.kind : '';
|
||||||
|
if (filterResourceTypes.filter(type => isEqual(inputObjectKind, type, StringComparer.OrdinalIgnoreCase)).length > 0) {
|
||||||
|
const resource = {
|
||||||
|
type: inputObject.kind,
|
||||||
|
name: inputObject.metadata.name
|
||||||
|
};
|
||||||
|
resources.push(resource);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpecLabels(inputObject: any) {
|
||||||
|
|
||||||
|
if (!inputObject) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEqual(inputObject.kind, KubernetesWorkload.pod, StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
return inputObject.metadata.labels;
|
||||||
|
}
|
||||||
|
if (!!inputObject.spec && !!inputObject.spec.template && !!inputObject.spec.template.metadata) {
|
||||||
|
return inputObject.spec.template.metadata.labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImagePullSecrets(inputObject: any) {
|
||||||
|
|
||||||
|
if (!inputObject || !inputObject.spec) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEqual(inputObject.kind, KubernetesWorkload.cronjob, StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
try {
|
||||||
|
return inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
|
||||||
|
} catch (ex) {
|
||||||
|
core.debug(`Fetching imagePullSecrets failed due to this error: ${JSON.stringify(ex)}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEqual(inputObject.kind, KubernetesWorkload.pod, StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
return inputObject.spec.imagePullSecrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
|
||||||
|
return inputObject.spec.template.spec.imagePullSecrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImagePullSecrets(inputObject: any, newImagePullSecrets: any) {
|
||||||
|
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEqual(inputObject.kind, KubernetesWorkload.pod, StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
inputObject.spec.imagePullSecrets = newImagePullSecrets;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEqual(inputObject.kind, KubernetesWorkload.cronjob, StringComparer.OrdinalIgnoreCase)) {
|
||||||
|
try {
|
||||||
|
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets = newImagePullSecrets;
|
||||||
|
} catch (ex) {
|
||||||
|
core.debug(`Overriding imagePullSecrets failed due to this error: ${JSON.stringify(ex)}`);
|
||||||
|
//Do nothing
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
|
||||||
|
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSpecLabels(inputObject: any, newLabels: any) {
|
||||||
|
let specLabels = getSpecLabels(inputObject);
|
||||||
|
if (!!newLabels) {
|
||||||
|
specLabels = newLabels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpecSelectorLabels(inputObject: any) {
|
||||||
|
|
||||||
|
if (!!inputObject && !!inputObject.spec && !!inputObject.spec.selector) {
|
||||||
|
if (isServiceEntity(inputObject.kind)) {
|
||||||
|
return inputObject.spec.selector;
|
||||||
|
} else {
|
||||||
|
return inputObject.spec.selector.matchLabels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSpecSelectorLabels(inputObject: any, newLabels: any) {
|
||||||
|
|
||||||
|
let selectorLabels = getSpecSelectorLabels(inputObject);
|
||||||
|
if (!!selectorLabels) {
|
||||||
|
selectorLabels = newLabels;
|
||||||
|
}
|
||||||
|
}
|
||||||
210
src/utilities/strategy-helpers/canary-deployment-helper.ts
Normal file
210
src/utilities/strategy-helpers/canary-deployment-helper.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { Kubectl } from '../../kubectl-object-model';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
|
import * as TaskInputParameters from '../../input-parameters';
|
||||||
|
import * as helper from '../resource-object-utility';
|
||||||
|
import { KubernetesWorkload } from '../../constants';
|
||||||
|
import { StringComparer, isEqual } from '../string-comparison';
|
||||||
|
import { checkForErrors } from "../utility";
|
||||||
|
|
||||||
|
import * as utils from '../manifest-utilities';
|
||||||
|
|
||||||
|
export const CANARY_DEPLOYMENT_STRATEGY = 'CANARY';
|
||||||
|
export const TRAFFIC_SPLIT_STRATEGY = 'SMI';
|
||||||
|
export const CANARY_VERSION_LABEL = 'workflow/version';
|
||||||
|
const BASELINE_SUFFIX = '-baseline';
|
||||||
|
export const BASELINE_LABEL_VALUE = 'baseline';
|
||||||
|
const CANARY_SUFFIX = '-canary';
|
||||||
|
export const CANARY_LABEL_VALUE = 'canary';
|
||||||
|
export const STABLE_SUFFIX = '-stable';
|
||||||
|
export const STABLE_LABEL_VALUE = 'stable';
|
||||||
|
|
||||||
|
export function deleteCanaryDeployment(kubectl: Kubectl, manifestFilePaths: string[], includeServices: boolean) {
|
||||||
|
|
||||||
|
// get manifest files
|
||||||
|
const inputManifestFiles: string[] = utils.getManifestFiles(manifestFilePaths);
|
||||||
|
|
||||||
|
if (inputManifestFiles == null || inputManifestFiles.length == 0) {
|
||||||
|
throw new Error('ManifestFileNotFound');
|
||||||
|
}
|
||||||
|
|
||||||
|
// create delete cmd prefix
|
||||||
|
cleanUpCanary(kubectl, inputManifestFiles, includeServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markResourceAsStable(inputObject: any): object {
|
||||||
|
if (isResourceMarkedAsStable(inputObject)) {
|
||||||
|
return inputObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||||
|
|
||||||
|
// Adding labels and annotations.
|
||||||
|
addCanaryLabelsAndAnnotations(newObject, STABLE_LABEL_VALUE);
|
||||||
|
|
||||||
|
core.debug("Added stable label: " + JSON.stringify(newObject));
|
||||||
|
return newObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResourceMarkedAsStable(inputObject: any): boolean {
|
||||||
|
return inputObject &&
|
||||||
|
inputObject.metadata &&
|
||||||
|
inputObject.metadata.labels &&
|
||||||
|
inputObject.metadata.labels[CANARY_VERSION_LABEL] == STABLE_LABEL_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStableResource(inputObject: any): object {
|
||||||
|
var replicaCount = isSpecContainsReplicas(inputObject.kind) ? inputObject.metadata.replicas : 0;
|
||||||
|
return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewBaselineResource(stableObject: any, replicas?: number): object {
|
||||||
|
return getNewCanaryObject(stableObject, replicas, BASELINE_LABEL_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewCanaryResource(inputObject: any, replicas?: number): object {
|
||||||
|
return getNewCanaryObject(inputObject, replicas, CANARY_LABEL_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchCanaryResource(kubectl: Kubectl, kind: string, name: string): object {
|
||||||
|
return fetchResource(kubectl, kind, getCanaryResourceName(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchResource(kubectl: Kubectl, kind: string, name: string) {
|
||||||
|
const result = kubectl.getResource(kind, name);
|
||||||
|
|
||||||
|
if (result == null || !!result.stderr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!result.stdout) {
|
||||||
|
const resource = JSON.parse(result.stdout);
|
||||||
|
try {
|
||||||
|
UnsetsClusterSpecficDetails(resource);
|
||||||
|
return resource;
|
||||||
|
} catch (ex) {
|
||||||
|
core.debug('Exception occurred while Parsing ' + resource + ' in Json object');
|
||||||
|
core.debug(`Exception:${ex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCanaryDeploymentStrategy() {
|
||||||
|
const deploymentStrategy = TaskInputParameters.deploymentStrategy;
|
||||||
|
return deploymentStrategy && deploymentStrategy.toUpperCase() === CANARY_DEPLOYMENT_STRATEGY;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSMICanaryStrategy() {
|
||||||
|
const deploymentStrategy = TaskInputParameters.trafficSplitMethod;
|
||||||
|
return isCanaryDeploymentStrategy() && deploymentStrategy && deploymentStrategy.toUpperCase() === TRAFFIC_SPLIT_STRATEGY;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCanaryResourceName(name: string) {
|
||||||
|
return name + CANARY_SUFFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBaselineResourceName(name: string) {
|
||||||
|
return name + BASELINE_SUFFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStableResourceName(name: string) {
|
||||||
|
return name + STABLE_SUFFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnsetsClusterSpecficDetails(resource: any) {
|
||||||
|
|
||||||
|
if (resource == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsets the cluster specific details in the object
|
||||||
|
if (!!resource) {
|
||||||
|
const metadata = resource.metadata;
|
||||||
|
const status = resource.status;
|
||||||
|
|
||||||
|
if (!!metadata) {
|
||||||
|
const newMetadata = {
|
||||||
|
'annotations': metadata.annotations,
|
||||||
|
'labels': metadata.labels,
|
||||||
|
'name': metadata.name
|
||||||
|
};
|
||||||
|
|
||||||
|
resource.metadata = newMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!status) {
|
||||||
|
resource.status = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNewCanaryObject(inputObject: any, replicas: number, type: string): object {
|
||||||
|
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||||
|
|
||||||
|
// Updating name
|
||||||
|
if (type === CANARY_LABEL_VALUE) {
|
||||||
|
newObject.metadata.name = getCanaryResourceName(inputObject.metadata.name)
|
||||||
|
} else if (type === STABLE_LABEL_VALUE) {
|
||||||
|
newObject.metadata.name = getStableResourceName(inputObject.metadata.name)
|
||||||
|
} else {
|
||||||
|
newObject.metadata.name = getBaselineResourceName(inputObject.metadata.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding labels and annotations.
|
||||||
|
addCanaryLabelsAndAnnotations(newObject, type);
|
||||||
|
|
||||||
|
// Updating no. of replicas
|
||||||
|
if (isSpecContainsReplicas(newObject.kind)) {
|
||||||
|
newObject.spec.replicas = replicas;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpecContainsReplicas(kind: string) {
|
||||||
|
return !isEqual(kind, KubernetesWorkload.pod, StringComparer.OrdinalIgnoreCase) &&
|
||||||
|
!isEqual(kind, KubernetesWorkload.daemonSet, StringComparer.OrdinalIgnoreCase) &&
|
||||||
|
!helper.isServiceEntity(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCanaryLabelsAndAnnotations(inputObject: any, type: string) {
|
||||||
|
const newLabels = new Map<string, string>();
|
||||||
|
newLabels[CANARY_VERSION_LABEL] = type;
|
||||||
|
|
||||||
|
helper.updateObjectLabels(inputObject, newLabels, false);
|
||||||
|
helper.updateSelectorLabels(inputObject, newLabels, false);
|
||||||
|
|
||||||
|
if (!helper.isServiceEntity(inputObject.kind)) {
|
||||||
|
helper.updateSpecLabels(inputObject, newLabels, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanUpCanary(kubectl: Kubectl, files: string[], includeServices: boolean) {
|
||||||
|
var deleteObject = function (kind, name) {
|
||||||
|
try {
|
||||||
|
const result = kubectl.delete([kind, name]);
|
||||||
|
checkForErrors([result]);
|
||||||
|
} catch (ex) {
|
||||||
|
// Ignore failures of delete if doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach((filePath: string) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isDeploymentEntity(kind) || (includeServices && helper.isServiceEntity(kind))) {
|
||||||
|
const canaryObjectName = getCanaryResourceName(name);
|
||||||
|
const baselineObjectName = getBaselineResourceName(name);
|
||||||
|
deleteObject(kind, canaryObjectName);
|
||||||
|
deleteObject(kind, baselineObjectName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
161
src/utilities/strategy-helpers/deployment-helper.ts
Normal file
161
src/utilities/strategy-helpers/deployment-helper.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import * as canaryDeploymentHelper from './canary-deployment-helper';
|
||||||
|
import * as KubernetesObjectUtility from '../resource-object-utility';
|
||||||
|
import * as TaskInputParameters from '../../input-parameters';
|
||||||
|
import * as models from '../../constants';
|
||||||
|
import * as fileHelper from '../files-helper';
|
||||||
|
import * as utils from '../manifest-utilities';
|
||||||
|
import * as KubernetesManifestUtility from '../manifest-stability-utility';
|
||||||
|
import * as KubernetesConstants from '../../constants';
|
||||||
|
import { Kubectl, Resource } from '../../kubectl-object-model';
|
||||||
|
|
||||||
|
import { deployPodCanary } from './pod-canary-deployment-helper';
|
||||||
|
import { deploySMICanary } from './smi-canary-deployment-helper';
|
||||||
|
import { checkForErrors } from "../utility";
|
||||||
|
|
||||||
|
|
||||||
|
export async function deploy(kubectl: Kubectl, manifestFilePaths: string[], deploymentStrategy: string) {
|
||||||
|
|
||||||
|
// get manifest files
|
||||||
|
let inputManifestFiles: string[] = getManifestFiles(manifestFilePaths);
|
||||||
|
|
||||||
|
// artifact substitution
|
||||||
|
inputManifestFiles = updateContainerImagesInManifestFiles(inputManifestFiles, TaskInputParameters.containers);
|
||||||
|
|
||||||
|
// imagePullSecrets addition
|
||||||
|
inputManifestFiles = updateImagePullSecretsInManifestFiles(inputManifestFiles, TaskInputParameters.imagePullSecrets);
|
||||||
|
|
||||||
|
// deployment
|
||||||
|
const deployedManifestFiles = deployManifests(inputManifestFiles, kubectl, isCanaryDeploymentStrategy(deploymentStrategy));
|
||||||
|
|
||||||
|
// check manifest stability
|
||||||
|
const resourceTypes: Resource[] = KubernetesObjectUtility.getResources(deployedManifestFiles, models.deploymentTypes.concat([KubernetesConstants.DiscoveryAndLoadBalancerResource.service]));
|
||||||
|
await checkManifestStability(kubectl, resourceTypes);
|
||||||
|
|
||||||
|
// print ingress resources
|
||||||
|
const ingressResources: Resource[] = KubernetesObjectUtility.getResources(deployedManifestFiles, [KubernetesConstants.DiscoveryAndLoadBalancerResource.ingress]);
|
||||||
|
ingressResources.forEach(ingressResource => {
|
||||||
|
kubectl.getResource(KubernetesConstants.DiscoveryAndLoadBalancerResource.ingress, ingressResource.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManifestFiles(manifestFilePaths: string[]): string[] {
|
||||||
|
const files: string[] = utils.getManifestFiles(manifestFilePaths);
|
||||||
|
|
||||||
|
if (files == null || files.length === 0) {
|
||||||
|
throw new Error(`ManifestFileNotFound : ${manifestFilePaths}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deployManifests(files: string[], kubectl: Kubectl, isCanaryDeploymentStrategy: boolean): string[] {
|
||||||
|
let result;
|
||||||
|
if (isCanaryDeploymentStrategy) {
|
||||||
|
let canaryDeploymentOutput: any;
|
||||||
|
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||||
|
canaryDeploymentOutput = deploySMICanary(kubectl, files);
|
||||||
|
} else {
|
||||||
|
canaryDeploymentOutput = deployPodCanary(kubectl, files);
|
||||||
|
}
|
||||||
|
result = canaryDeploymentOutput.result;
|
||||||
|
files = canaryDeploymentOutput.newFilePaths;
|
||||||
|
} else {
|
||||||
|
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||||
|
const updatedManifests = appendStableVersionLabelToResource(files, kubectl);
|
||||||
|
result = kubectl.apply(updatedManifests);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = kubectl.apply(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkForErrors([result]);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendStableVersionLabelToResource(files: string[], kubectl: Kubectl): string[] {
|
||||||
|
const manifestFiles = [];
|
||||||
|
const newObjectsList = [];
|
||||||
|
|
||||||
|
files.forEach((filePath: string) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (KubernetesObjectUtility.isDeploymentEntity(kind)) {
|
||||||
|
const updatedObject = canaryDeploymentHelper.markResourceAsStable(inputObject);
|
||||||
|
newObjectsList.push(updatedObject);
|
||||||
|
} else {
|
||||||
|
manifestFiles.push(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedManifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
manifestFiles.push(...updatedManifestFiles);
|
||||||
|
return manifestFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkManifestStability(kubectl: Kubectl, resources: Resource[]): Promise<void> {
|
||||||
|
await KubernetesManifestUtility.checkManifestStability(kubectl, resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateContainerImagesInManifestFiles(filePaths: string[], containers: string[]): string[] {
|
||||||
|
if (!!containers && containers.length > 0) {
|
||||||
|
const newFilePaths = [];
|
||||||
|
const tempDirectory = fileHelper.getTempDirectory();
|
||||||
|
filePaths.forEach((filePath: string) => {
|
||||||
|
let contents = fs.readFileSync(filePath).toString();
|
||||||
|
containers.forEach((container: string) => {
|
||||||
|
let imageName = container.split(':')[0];
|
||||||
|
if (imageName.indexOf('@') > 0) {
|
||||||
|
imageName = imageName.split('@')[0];
|
||||||
|
}
|
||||||
|
if (contents.indexOf(imageName) > 0) {
|
||||||
|
contents = utils.substituteImageNameInSpecFile(contents, imageName, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileName = path.join(tempDirectory, path.basename(filePath));
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(fileName),
|
||||||
|
contents
|
||||||
|
);
|
||||||
|
newFilePaths.push(fileName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return newFilePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateImagePullSecretsInManifestFiles(filePaths: string[], imagePullSecrets: string[]): string[] {
|
||||||
|
if (!!imagePullSecrets && imagePullSecrets.length > 0) {
|
||||||
|
const newObjectsList = [];
|
||||||
|
filePaths.forEach((filePath: string) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath).toString();
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject: any) {
|
||||||
|
if (!!inputObject && !!inputObject.kind) {
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (KubernetesObjectUtility.isWorkloadEntity(kind)) {
|
||||||
|
KubernetesObjectUtility.updateImagePullSecrets(inputObject, imagePullSecrets, false);
|
||||||
|
}
|
||||||
|
newObjectsList.push(inputObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
core.debug('New K8s objects after addin imagePullSecrets are :' + JSON.stringify(newObjectsList));
|
||||||
|
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
return newFilePaths;
|
||||||
|
}
|
||||||
|
return filePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCanaryDeploymentStrategy(deploymentStrategy: string): boolean {
|
||||||
|
return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase();
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { Kubectl } from '../../kubectl-object-model';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
|
||||||
|
import * as TaskInputParameters from '../../input-parameters';
|
||||||
|
import * as fileHelper from '../files-helper';
|
||||||
|
import * as helper from '../resource-object-utility';
|
||||||
|
import * as canaryDeploymentHelper from './canary-deployment-helper';
|
||||||
|
|
||||||
|
export function deployPodCanary(kubectl: Kubectl, filePaths: string[]) {
|
||||||
|
const newObjectsList = [];
|
||||||
|
const percentage = parseInt(TaskInputParameters.canaryPercentage);
|
||||||
|
|
||||||
|
filePaths.forEach((filePath: string) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isDeploymentEntity(kind)) {
|
||||||
|
core.debug('Calculating replica count for canary');
|
||||||
|
const canaryReplicaCount = calculateReplicaCountForCanary(inputObject, percentage);
|
||||||
|
core.debug('Replica count is ' + canaryReplicaCount);
|
||||||
|
|
||||||
|
// Get stable object
|
||||||
|
core.debug('Querying stable object');
|
||||||
|
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, name);
|
||||||
|
if (!stableObject) {
|
||||||
|
core.debug('Stable object not found. Creating only canary object');
|
||||||
|
// If stable object not found, create canary deployment.
|
||||||
|
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||||
|
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||||
|
newObjectsList.push(newCanaryObject);
|
||||||
|
} else {
|
||||||
|
core.debug('Stable object found. Creating canary and baseline objects');
|
||||||
|
// If canary object not found, create canary and baseline object.
|
||||||
|
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||||
|
const newBaselineObject = canaryDeploymentHelper.getNewBaselineResource(stableObject, canaryReplicaCount);
|
||||||
|
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||||
|
core.debug('New baseline object is: ' + JSON.stringify(newBaselineObject));
|
||||||
|
newObjectsList.push(newCanaryObject);
|
||||||
|
newObjectsList.push(newBaselineObject);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Updating non deployment entity as it is.
|
||||||
|
newObjectsList.push(inputObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
const result = kubectl.apply(manifestFiles);
|
||||||
|
return { 'result': result, 'newFilePaths': manifestFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateReplicaCountForCanary(inputObject: any, percentage: number) {
|
||||||
|
const inputReplicaCount = helper.getReplicaCount(inputObject);
|
||||||
|
return Math.round((inputReplicaCount * percentage) / 100);
|
||||||
|
}
|
||||||
215
src/utilities/strategy-helpers/smi-canary-deployment-helper.ts
Normal file
215
src/utilities/strategy-helpers/smi-canary-deployment-helper.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { Kubectl } from '../../kubectl-object-model';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import * as util from 'util';
|
||||||
|
|
||||||
|
import * as TaskInputParameters from '../../input-parameters';
|
||||||
|
import * as fileHelper from '../files-helper';
|
||||||
|
import * as helper from '../resource-object-utility';
|
||||||
|
import * as utils from '../manifest-utilities';
|
||||||
|
import * as canaryDeploymentHelper from './canary-deployment-helper';
|
||||||
|
import { checkForErrors } from "../utility";
|
||||||
|
|
||||||
|
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout';
|
||||||
|
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit';
|
||||||
|
|
||||||
|
export function deploySMICanary(kubectl: Kubectl, filePaths: string[]) {
|
||||||
|
const newObjectsList = [];
|
||||||
|
const canaryReplicaCount = parseInt(TaskInputParameters.baselineAndCanaryReplicas);
|
||||||
|
core.debug('Replica count is ' + canaryReplicaCount);
|
||||||
|
|
||||||
|
filePaths.forEach((filePath: string) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isDeploymentEntity(kind)) {
|
||||||
|
// Get stable object
|
||||||
|
core.debug('Querying stable object');
|
||||||
|
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, name);
|
||||||
|
if (!stableObject) {
|
||||||
|
core.debug('Stable object not found. Creating only canary object');
|
||||||
|
// If stable object not found, create canary deployment.
|
||||||
|
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||||
|
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||||
|
newObjectsList.push(newCanaryObject);
|
||||||
|
} else {
|
||||||
|
if (!canaryDeploymentHelper.isResourceMarkedAsStable(stableObject)) {
|
||||||
|
throw (`StableSpecSelectorNotExist : ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug('Stable object found. Creating canary and baseline objects');
|
||||||
|
// If canary object not found, create canary and baseline object.
|
||||||
|
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||||
|
const newBaselineObject = canaryDeploymentHelper.getNewBaselineResource(stableObject, canaryReplicaCount);
|
||||||
|
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||||
|
core.debug('New baseline object is: ' + JSON.stringify(newBaselineObject));
|
||||||
|
newObjectsList.push(newCanaryObject);
|
||||||
|
newObjectsList.push(newBaselineObject);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Updating non deployment entity as it is.
|
||||||
|
newObjectsList.push(inputObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
const result = kubectl.apply(manifestFiles);
|
||||||
|
createCanaryService(kubectl, filePaths);
|
||||||
|
return { 'result': result, 'newFilePaths': manifestFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
|
||||||
|
const newObjectsList = [];
|
||||||
|
const trafficObjectsList = [];
|
||||||
|
|
||||||
|
filePaths.forEach((filePath: string) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isServiceEntity(kind)) {
|
||||||
|
const newCanaryServiceObject = canaryDeploymentHelper.getNewCanaryResource(inputObject);
|
||||||
|
core.debug('New canary service object is: ' + JSON.stringify(newCanaryServiceObject));
|
||||||
|
newObjectsList.push(newCanaryServiceObject);
|
||||||
|
|
||||||
|
const newBaselineServiceObject = canaryDeploymentHelper.getNewBaselineResource(inputObject);
|
||||||
|
core.debug('New baseline object is: ' + JSON.stringify(newBaselineServiceObject));
|
||||||
|
newObjectsList.push(newBaselineServiceObject);
|
||||||
|
|
||||||
|
core.debug('Querying for stable service object');
|
||||||
|
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, canaryDeploymentHelper.getStableResourceName(name));
|
||||||
|
if (!stableObject) {
|
||||||
|
const newStableServiceObject = canaryDeploymentHelper.getStableResource(inputObject);
|
||||||
|
core.debug('New stable service object is: ' + JSON.stringify(newStableServiceObject));
|
||||||
|
newObjectsList.push(newStableServiceObject);
|
||||||
|
|
||||||
|
core.debug('Creating the traffic object for service: ' + name);
|
||||||
|
const trafficObject = createTrafficSplitManifestFile(name, 0, 0, 1000);
|
||||||
|
core.debug('Creating the traffic object for service: ' + trafficObject);
|
||||||
|
trafficObjectsList.push(trafficObject);
|
||||||
|
} else {
|
||||||
|
let updateTrafficObject = true;
|
||||||
|
const trafficObject = canaryDeploymentHelper.fetchResource(kubectl, TRAFFIC_SPLIT_OBJECT, getTrafficSplitResourceName(name));
|
||||||
|
if (trafficObject) {
|
||||||
|
const trafficJObject = JSON.parse(JSON.stringify(trafficObject));
|
||||||
|
if (trafficJObject && trafficJObject.spec && trafficJObject.spec.backends) {
|
||||||
|
trafficJObject.spec.backends.forEach((s) => {
|
||||||
|
if (s.service === canaryDeploymentHelper.getCanaryResourceName(name) && s.weight === "1000m") {
|
||||||
|
core.debug('Update traffic objcet not required');
|
||||||
|
updateTrafficObject = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateTrafficObject) {
|
||||||
|
core.debug('Stable service object present so updating the traffic object for service: ' + name);
|
||||||
|
trafficObjectsList.push(updateTrafficSplitObject(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||||
|
manifestFiles.push(...trafficObjectsList);
|
||||||
|
const result = kubectl.apply(manifestFiles);
|
||||||
|
checkForErrors([result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redirectTrafficToCanaryDeployment(kubectl: Kubectl, manifestFilePaths: string[]) {
|
||||||
|
adjustTraffic(kubectl, manifestFilePaths, 0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redirectTrafficToStableDeployment(kubectl: Kubectl, manifestFilePaths: string[]) {
|
||||||
|
adjustTraffic(kubectl, manifestFilePaths, 1000, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustTraffic(kubectl: Kubectl, manifestFilePaths: string[], stableWeight: number, canaryWeight: number) {
|
||||||
|
// get manifest files
|
||||||
|
const inputManifestFiles: string[] = utils.getManifestFiles(manifestFilePaths);
|
||||||
|
|
||||||
|
if (inputManifestFiles == null || inputManifestFiles.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trafficSplitManifests = [];
|
||||||
|
const serviceObjects = [];
|
||||||
|
inputManifestFiles.forEach((filePath: string) => {
|
||||||
|
const fileContents = fs.readFileSync(filePath);
|
||||||
|
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||||
|
const name = inputObject.metadata.name;
|
||||||
|
const kind = inputObject.kind;
|
||||||
|
if (helper.isServiceEntity(kind)) {
|
||||||
|
trafficSplitManifests.push(createTrafficSplitManifestFile(name, stableWeight, 0, canaryWeight));
|
||||||
|
serviceObjects.push(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (trafficSplitManifests.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = kubectl.apply(trafficSplitManifests);
|
||||||
|
core.debug('serviceObjects:' + serviceObjects.join(',') + ' result:' + result);
|
||||||
|
checkForErrors([result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTrafficSplitObject(serviceName: string): string {
|
||||||
|
const percentage = parseInt(TaskInputParameters.canaryPercentage) * 10;
|
||||||
|
const baselineAndCanaryWeight = percentage / 2;
|
||||||
|
const stableDeploymentWeight = 1000 - percentage;
|
||||||
|
core.debug('Creating the traffic object with canary weight: ' + baselineAndCanaryWeight + ',baseling weight: ' + baselineAndCanaryWeight + ',stable: ' + stableDeploymentWeight);
|
||||||
|
return createTrafficSplitManifestFile(serviceName, stableDeploymentWeight, baselineAndCanaryWeight, baselineAndCanaryWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTrafficSplitManifestFile(serviceName: string, stableWeight: number, baselineWeight: number, canaryWeight: number): string {
|
||||||
|
const smiObjectString = getTrafficSplitObject(serviceName, stableWeight, baselineWeight, canaryWeight);
|
||||||
|
const manifestFile = fileHelper.writeManifestToFile(smiObjectString, TRAFFIC_SPLIT_OBJECT, serviceName);
|
||||||
|
if (!manifestFile) {
|
||||||
|
throw new Error('UnableToCreateTrafficSplitManifestFile');
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifestFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrafficSplitObject(name: string, stableWeight: number, baselineWeight: number, canaryWeight: number): string {
|
||||||
|
const trafficSplitObjectJson = `{
|
||||||
|
"apiVersion": "split.smi-spec.io/v1alpha1",
|
||||||
|
"kind": "TrafficSplit",
|
||||||
|
"metadata": {
|
||||||
|
"name": "%s"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"backends": [
|
||||||
|
{
|
||||||
|
"service": "%s",
|
||||||
|
"weight": "%sm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "%s",
|
||||||
|
"weight": "%sm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "%s",
|
||||||
|
"weight": "%sm"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"service": "%s"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const trafficSplitObject = util.format(trafficSplitObjectJson, getTrafficSplitResourceName(name), canaryDeploymentHelper.getStableResourceName(name), stableWeight, canaryDeploymentHelper.getBaselineResourceName(name), baselineWeight, canaryDeploymentHelper.getCanaryResourceName(name), canaryWeight, name);
|
||||||
|
return trafficSplitObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrafficSplitResourceName(name: string) {
|
||||||
|
return name + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX;
|
||||||
|
}
|
||||||
25
src/utilities/string-comparison.ts
Normal file
25
src/utilities/string-comparison.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export enum StringComparer {
|
||||||
|
Ordinal,
|
||||||
|
OrdinalIgnoreCase,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEqual(str1: string, str2: string, stringComparer: StringComparer): boolean {
|
||||||
|
|
||||||
|
if (str1 == null && str2 == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str1 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringComparer == StringComparer.OrdinalIgnoreCase) {
|
||||||
|
return str1.toUpperCase() === str2.toUpperCase();
|
||||||
|
} else {
|
||||||
|
return str1 === str2;
|
||||||
|
}
|
||||||
|
}
|
||||||
661
src/utilities/tool-runner.ts
Normal file
661
src/utilities/tool-runner.ts
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
import os = require('os');
|
||||||
|
import events = require('events');
|
||||||
|
import child = require('child_process');
|
||||||
|
import stream = require('stream');
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for exec options
|
||||||
|
*/
|
||||||
|
export interface IExecOptions extends IExecSyncOptions {
|
||||||
|
/** optional. whether to fail if output to stderr. defaults to false */
|
||||||
|
failOnStdErr?: boolean;
|
||||||
|
|
||||||
|
/** optional. defaults to failing on non zero. ignore will not fail leaving it up to the caller */
|
||||||
|
ignoreReturnCode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for execSync options
|
||||||
|
*/
|
||||||
|
export interface IExecSyncOptions {
|
||||||
|
/** optional working directory. defaults to current */
|
||||||
|
cwd?: string;
|
||||||
|
|
||||||
|
/** optional envvar dictionary. defaults to current process's env */
|
||||||
|
env?: { [key: string]: string };
|
||||||
|
|
||||||
|
/** optional. defaults to false */
|
||||||
|
silent?: boolean;
|
||||||
|
|
||||||
|
outStream: stream.Writable;
|
||||||
|
|
||||||
|
errStream: stream.Writable;
|
||||||
|
|
||||||
|
/** optional. foo.whether to skip quoting/escaping arguments if needed. defaults to false. */
|
||||||
|
windowsVerbatimArguments?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for exec results returned from synchronous exec functions
|
||||||
|
*/
|
||||||
|
export interface IExecSyncResult {
|
||||||
|
/** standard output */
|
||||||
|
stdout: string;
|
||||||
|
|
||||||
|
/** error output */
|
||||||
|
stderr: string;
|
||||||
|
|
||||||
|
/** return code */
|
||||||
|
code: number;
|
||||||
|
|
||||||
|
/** Error on failure */
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToolRunner extends events.EventEmitter {
|
||||||
|
constructor(toolPath: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (!toolPath) {
|
||||||
|
throw new Error('Parameter \'toolPath\' cannot be null or empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toolPath = toolPath;
|
||||||
|
this.args = [];
|
||||||
|
core.debug('toolRunner toolPath: ' + toolPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toolPath: string;
|
||||||
|
private args: string[];
|
||||||
|
private pipeOutputToTool: ToolRunner | undefined;
|
||||||
|
|
||||||
|
private _debug(message: string) {
|
||||||
|
this.emit('debug', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _argStringToArray(argString: string): string[] {
|
||||||
|
var args: string[] = [];
|
||||||
|
|
||||||
|
var inQuotes = false;
|
||||||
|
var escaped = false;
|
||||||
|
var lastCharWasSpace = true;
|
||||||
|
var arg = '';
|
||||||
|
|
||||||
|
var append = function (c: string) {
|
||||||
|
// we only escape double quotes.
|
||||||
|
if (escaped && c !== '"') {
|
||||||
|
arg += '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
arg += c;
|
||||||
|
escaped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < argString.length; i++) {
|
||||||
|
var c = argString.charAt(i);
|
||||||
|
|
||||||
|
if (c === ' ' && !inQuotes) {
|
||||||
|
if (!lastCharWasSpace) {
|
||||||
|
args.push(arg);
|
||||||
|
arg = '';
|
||||||
|
}
|
||||||
|
lastCharWasSpace = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lastCharWasSpace = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c === '"') {
|
||||||
|
if (!escaped) {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
append(c);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c === "\\" && escaped) {
|
||||||
|
append(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c === "\\" && inQuotes) {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(c);
|
||||||
|
lastCharWasSpace = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastCharWasSpace) {
|
||||||
|
args.push(arg.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCommandString(options: IExecOptions, noPrefix?: boolean): string {
|
||||||
|
let toolPath: string = this._getSpawnFileName();
|
||||||
|
let args: string[] = this._getSpawnArgs(options);
|
||||||
|
let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
// Windows + cmd file
|
||||||
|
if (this._isCmdFile()) {
|
||||||
|
cmd += toolPath;
|
||||||
|
args.forEach((a: string): void => {
|
||||||
|
cmd += ` ${a}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Windows + verbatim
|
||||||
|
else if (options.windowsVerbatimArguments) {
|
||||||
|
cmd += `"${toolPath}"`;
|
||||||
|
args.forEach((a: string): void => {
|
||||||
|
cmd += ` ${a}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Windows (regular)
|
||||||
|
else {
|
||||||
|
cmd += this._windowsQuoteCmdArg(toolPath);
|
||||||
|
args.forEach((a: string): void => {
|
||||||
|
cmd += ` ${this._windowsQuoteCmdArg(a)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// OSX/Linux - this can likely be improved with some form of quoting.
|
||||||
|
// creating processes on Unix is fundamentally different than Windows.
|
||||||
|
// on Unix, execvp() takes an arg array.
|
||||||
|
cmd += toolPath;
|
||||||
|
args.forEach((a: string): void => {
|
||||||
|
cmd += ` ${a}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// append second tool
|
||||||
|
if (this.pipeOutputToTool) {
|
||||||
|
cmd += ' | ' + this.pipeOutputToTool._getCommandString(options, /*noPrefix:*/true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getSpawnFileName(): string {
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
if (this._isCmdFile()) {
|
||||||
|
return process.env['COMSPEC'] || 'cmd.exe';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.toolPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getSpawnArgs(options: IExecOptions): string[] {
|
||||||
|
if (process.platform == 'win32') {
|
||||||
|
if (this._isCmdFile()) {
|
||||||
|
let argline: string = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`;
|
||||||
|
for (let i = 0; i < this.args.length; i++) {
|
||||||
|
argline += ' ';
|
||||||
|
argline += options.windowsVerbatimArguments ? this.args[i] : this._windowsQuoteCmdArg(this.args[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
argline += '"';
|
||||||
|
return [argline];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.windowsVerbatimArguments) {
|
||||||
|
// note, in Node 6.x options.argv0 can be used instead of overriding args.slice and args.unshift.
|
||||||
|
// for more details, refer to https://github.com/nodejs/node/blob/v6.x/lib/child_process.js
|
||||||
|
|
||||||
|
let args = this.args.slice(0); // copy the array
|
||||||
|
|
||||||
|
// override slice to prevent Node from creating a copy of the arg array.
|
||||||
|
// we need Node to use the "unshift" override below.
|
||||||
|
args.slice = function () {
|
||||||
|
if (arguments.length != 1 || arguments[0] != 0) {
|
||||||
|
throw new Error('Unexpected arguments passed to args.slice when windowsVerbatimArguments flag is set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
};
|
||||||
|
|
||||||
|
// override unshift
|
||||||
|
//
|
||||||
|
// when using the windowsVerbatimArguments option, Node does not quote the tool path when building
|
||||||
|
// the cmdline parameter for the win32 function CreateProcess(). an unquoted space in the tool path
|
||||||
|
// causes problems for tools when attempting to parse their own command line args. tools typically
|
||||||
|
// assume their arguments begin after arg 0.
|
||||||
|
//
|
||||||
|
// by hijacking unshift, we can quote the tool path when it pushed onto the args array. Node builds
|
||||||
|
// the cmdline parameter from the args array.
|
||||||
|
//
|
||||||
|
// note, we can't simply pass a quoted tool path to Node for multiple reasons:
|
||||||
|
// 1) Node verifies the file exists (calls win32 function GetFileAttributesW) and the check returns
|
||||||
|
// false if the path is quoted.
|
||||||
|
// 2) Node passes the tool path as the application parameter to CreateProcess, which expects the
|
||||||
|
// path to be unquoted.
|
||||||
|
//
|
||||||
|
// also note, in addition to the tool path being embedded within the cmdline parameter, Node also
|
||||||
|
// passes the tool path to CreateProcess via the application parameter (optional parameter). when
|
||||||
|
// present, Windows uses the application parameter to determine which file to run, instead of
|
||||||
|
// interpreting the file from the cmdline parameter.
|
||||||
|
args.unshift = function () {
|
||||||
|
if (arguments.length != 1) {
|
||||||
|
throw new Error('Unexpected arguments passed to args.unshift when windowsVerbatimArguments flag is set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.prototype.unshift.call(args, `"${arguments[0]}"`); // quote the file name
|
||||||
|
};
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isCmdFile(): boolean {
|
||||||
|
let upperToolPath: string = this.toolPath.toUpperCase();
|
||||||
|
return this._endsWith(upperToolPath, '.CMD') || this._endsWith(upperToolPath, '.BAT');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _endsWith(str: string, end: string): boolean {
|
||||||
|
return str.slice(-end.length) == end;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _windowsQuoteCmdArg(arg: string): string {
|
||||||
|
// for .exe, apply the normal quoting rules that libuv applies
|
||||||
|
if (!this._isCmdFile()) {
|
||||||
|
return this._uv_quote_cmd_arg(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise apply quoting rules specific to the cmd.exe command line parser.
|
||||||
|
// the libuv rules are generic and are not designed specifically for cmd.exe
|
||||||
|
// command line parser.
|
||||||
|
//
|
||||||
|
// for a detailed description of the cmd.exe command line parser, refer to
|
||||||
|
// http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912
|
||||||
|
|
||||||
|
// need quotes for empty arg
|
||||||
|
if (!arg) {
|
||||||
|
return '""';
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine whether the arg needs to be quoted
|
||||||
|
const cmdSpecialChars = [' ', '\t', '&', '(', ')', '[', ']', '{', '}', '^', '=', ';', '!', '\'', '+', ',', '`', '~', '|', '<', '>', '"'];
|
||||||
|
let needsQuotes = false;
|
||||||
|
for (let char of arg) {
|
||||||
|
if (cmdSpecialChars.some(x => x == char)) {
|
||||||
|
needsQuotes = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// short-circuit if quotes not needed
|
||||||
|
if (!needsQuotes) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the following quoting rules are very similar to the rules that by libuv applies.
|
||||||
|
//
|
||||||
|
// 1) wrap the string in quotes
|
||||||
|
//
|
||||||
|
// 2) double-up quotes - i.e. " => ""
|
||||||
|
//
|
||||||
|
// this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately
|
||||||
|
// doesn't work well with a cmd.exe command line.
|
||||||
|
//
|
||||||
|
// note, replacing " with "" also works well if the arg is passed to a downstream .NET console app.
|
||||||
|
// for example, the command line:
|
||||||
|
// foo.exe "myarg:""my val"""
|
||||||
|
// is parsed by a .NET console app into an arg array:
|
||||||
|
// [ "myarg:\"my val\"" ]
|
||||||
|
// which is the same end result when applying libuv quoting rules. although the actual
|
||||||
|
// command line from libuv quoting rules would look like:
|
||||||
|
// foo.exe "myarg:\"my val\""
|
||||||
|
//
|
||||||
|
// 3) double-up slashes that preceed a quote,
|
||||||
|
// e.g. hello \world => "hello \world"
|
||||||
|
// hello\"world => "hello\\""world"
|
||||||
|
// hello\\"world => "hello\\\\""world"
|
||||||
|
// hello world\ => "hello world\\"
|
||||||
|
//
|
||||||
|
// technically this is not required for a cmd.exe command line, or the batch argument parser.
|
||||||
|
// the reasons for including this as a .cmd quoting rule are:
|
||||||
|
//
|
||||||
|
// a) this is optimized for the scenario where the argument is passed from the .cmd file to an
|
||||||
|
// external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule.
|
||||||
|
//
|
||||||
|
// b) it's what we've been doing previously (by deferring to node default behavior) and we
|
||||||
|
// haven't heard any complaints about that aspect.
|
||||||
|
//
|
||||||
|
// note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be
|
||||||
|
// escaped when used on the command line directly - even though within a .cmd file % can be escaped
|
||||||
|
// by using %%.
|
||||||
|
//
|
||||||
|
// the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts
|
||||||
|
// the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing.
|
||||||
|
//
|
||||||
|
// one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would
|
||||||
|
// often work, since it is unlikely that var^ would exist, and the ^ character is removed when the
|
||||||
|
// variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args
|
||||||
|
// to an external program.
|
||||||
|
//
|
||||||
|
// an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file.
|
||||||
|
// % can be escaped within a .cmd file.
|
||||||
|
let reverse: string = '"';
|
||||||
|
let quote_hit = true;
|
||||||
|
for (let i = arg.length; i > 0; i--) { // walk the string in reverse
|
||||||
|
reverse += arg[i - 1];
|
||||||
|
if (quote_hit && arg[i - 1] == '\\') {
|
||||||
|
reverse += '\\'; // double the slash
|
||||||
|
}
|
||||||
|
else if (arg[i - 1] == '"') {
|
||||||
|
quote_hit = true;
|
||||||
|
reverse += '"'; // double the quote
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
quote_hit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse += '"';
|
||||||
|
return reverse.split('').reverse().join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _uv_quote_cmd_arg(arg: string): string {
|
||||||
|
// Tool runner wraps child_process.spawn() and needs to apply the same quoting as
|
||||||
|
// Node in certain cases where the undocumented spawn option windowsVerbatimArguments
|
||||||
|
// is used.
|
||||||
|
//
|
||||||
|
// Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV,
|
||||||
|
// see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details),
|
||||||
|
// pasting copyright notice from Node within this function:
|
||||||
|
//
|
||||||
|
// Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to
|
||||||
|
// deal in the Software without restriction, including without limitation the
|
||||||
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
// sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
// IN THE SOFTWARE.
|
||||||
|
|
||||||
|
if (!arg) {
|
||||||
|
// Need double quotation for empty argument
|
||||||
|
return '""';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.indexOf(' ') < 0 && arg.indexOf('\t') < 0 && arg.indexOf('"') < 0) {
|
||||||
|
// No quotation needed
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.indexOf('"') < 0 && arg.indexOf('\\') < 0) {
|
||||||
|
// No embedded double quotes or backslashes, so I can just wrap
|
||||||
|
// quote marks around the whole thing.
|
||||||
|
return `"${arg}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expected input/output:
|
||||||
|
// input : hello"world
|
||||||
|
// output: "hello\"world"
|
||||||
|
// input : hello""world
|
||||||
|
// output: "hello\"\"world"
|
||||||
|
// input : hello\world
|
||||||
|
// output: hello\world
|
||||||
|
// input : hello\\world
|
||||||
|
// output: hello\\world
|
||||||
|
// input : hello\"world
|
||||||
|
// output: "hello\\\"world"
|
||||||
|
// input : hello\\"world
|
||||||
|
// output: "hello\\\\\"world"
|
||||||
|
// input : hello world\
|
||||||
|
// output: "hello world\\" - note the comment in libuv actually reads "hello world\"
|
||||||
|
// but it appears the comment is wrong, it should be "hello world\\"
|
||||||
|
let reverse: string = '"';
|
||||||
|
let quote_hit = true;
|
||||||
|
for (let i = arg.length; i > 0; i--) { // walk the string in reverse
|
||||||
|
reverse += arg[i - 1];
|
||||||
|
if (quote_hit && arg[i - 1] == '\\') {
|
||||||
|
reverse += '\\';
|
||||||
|
}
|
||||||
|
else if (arg[i - 1] == '"') {
|
||||||
|
quote_hit = true;
|
||||||
|
reverse += '\\';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
quote_hit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse += '"';
|
||||||
|
return reverse.split('').reverse().join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cloneExecOptions(options?: IExecOptions): IExecOptions {
|
||||||
|
options = options || <IExecOptions>{};
|
||||||
|
let result: IExecOptions = <IExecOptions>{
|
||||||
|
cwd: options.cwd || process.cwd(),
|
||||||
|
env: options.env || process.env,
|
||||||
|
silent: options.silent || false,
|
||||||
|
failOnStdErr: options.failOnStdErr || false,
|
||||||
|
ignoreReturnCode: options.ignoreReturnCode || false,
|
||||||
|
windowsVerbatimArguments: options.windowsVerbatimArguments || false
|
||||||
|
};
|
||||||
|
result.outStream = options.outStream || <stream.Writable>process.stdout;
|
||||||
|
result.errStream = options.errStream || <stream.Writable>process.stderr;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getSpawnSyncOptions(options: IExecSyncOptions): child.SpawnSyncOptions {
|
||||||
|
let result = <child.SpawnSyncOptions>{};
|
||||||
|
result.cwd = options.cwd;
|
||||||
|
result.env = options.env;
|
||||||
|
result['windowsVerbatimArguments'] = options.windowsVerbatimArguments || this._isCmdFile();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add argument
|
||||||
|
* Append an argument or an array of arguments
|
||||||
|
* returns ToolRunner for chaining
|
||||||
|
*
|
||||||
|
* @param val string cmdline or array of strings
|
||||||
|
* @returns ToolRunner
|
||||||
|
*/
|
||||||
|
public arg(val: string | string[]): ToolRunner {
|
||||||
|
if (!val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val instanceof Array) {
|
||||||
|
core.debug(this.toolPath + ' arg: ' + JSON.stringify(val));
|
||||||
|
this.args = this.args.concat(val);
|
||||||
|
}
|
||||||
|
else if (typeof (val) === 'string') {
|
||||||
|
core.debug(this.toolPath + ' arg: ' + val);
|
||||||
|
this.args = this.args.concat(val.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an argument line into one or more arguments
|
||||||
|
* e.g. .line('"arg one" two -z') is equivalent to .arg(['arg one', 'two', '-z'])
|
||||||
|
* returns ToolRunner for chaining
|
||||||
|
*
|
||||||
|
* @param val string argument line
|
||||||
|
* @returns ToolRunner
|
||||||
|
*/
|
||||||
|
public line(val: string): ToolRunner {
|
||||||
|
if (!val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(this.toolPath + ' arg: ' + val);
|
||||||
|
this.args = this.args.concat(this._argStringToArray(val));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add argument(s) if a condition is met
|
||||||
|
* Wraps arg(). See arg for details
|
||||||
|
* returns ToolRunner for chaining
|
||||||
|
*
|
||||||
|
* @param condition boolean condition
|
||||||
|
* @param val string cmdline or array of strings
|
||||||
|
* @returns ToolRunner
|
||||||
|
*/
|
||||||
|
public argIf(condition: any, val: any) {
|
||||||
|
if (condition) {
|
||||||
|
this.arg(val);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipe output of exec() to another tool
|
||||||
|
* @param tool
|
||||||
|
* @param file optional filename to additionally stream the output to.
|
||||||
|
* @returns {ToolRunner}
|
||||||
|
*/
|
||||||
|
public pipeExecOutputToTool(tool: ToolRunner, file?: string): ToolRunner {
|
||||||
|
this.pipeOutputToTool = tool;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exec a tool synchronously.
|
||||||
|
* Output will be *not* be streamed to the live console. It will be returned after execution is complete.
|
||||||
|
* Appropriate for short running tools
|
||||||
|
* Returns IExecSyncResult with output and return code
|
||||||
|
*
|
||||||
|
* @param tool path to tool to exec
|
||||||
|
* @param options optional exec options. See IExecSyncOptions
|
||||||
|
* @returns IExecSyncResult
|
||||||
|
*/
|
||||||
|
public execSync(options?: IExecSyncOptions): IExecSyncResult {
|
||||||
|
core.debug('exec tool: ' + this.toolPath);
|
||||||
|
core.debug('arguments:');
|
||||||
|
this.args.forEach((arg) => {
|
||||||
|
core.debug(' ' + arg);
|
||||||
|
});
|
||||||
|
|
||||||
|
options = this._cloneExecOptions(options as IExecOptions);
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
options.outStream.write(this._getCommandString(options as IExecOptions) + os.EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
var r = child.spawnSync(this._getSpawnFileName(), this._getSpawnArgs(options as IExecOptions), this._getSpawnSyncOptions(options));
|
||||||
|
|
||||||
|
var res: IExecSyncResult = <IExecSyncResult>{ code: r.status, error: r.error };
|
||||||
|
if (!options.silent && r.stdout && r.stdout.length > 0) {
|
||||||
|
options.outStream.write(r.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.silent && r.stderr && r.stderr.length > 0) {
|
||||||
|
options.errStream.write(r.stderr);
|
||||||
|
}
|
||||||
|
res.stdout = (r.stdout) ? r.stdout.toString() : '';
|
||||||
|
res.stderr = (r.stderr) ? r.stderr.toString() : '';
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExecState extends events.EventEmitter {
|
||||||
|
constructor(
|
||||||
|
options: IExecOptions,
|
||||||
|
toolPath: string) {
|
||||||
|
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (!toolPath) {
|
||||||
|
throw new Error('toolPath must not be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options = options;
|
||||||
|
this.toolPath = toolPath;
|
||||||
|
let delay = process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY'];
|
||||||
|
if (delay) {
|
||||||
|
this.delay = parseInt(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processClosed: boolean; // tracks whether the process has exited and stdio is closed
|
||||||
|
processError: string;
|
||||||
|
processExitCode: number;
|
||||||
|
processExited: boolean; // tracks whether the process has exited
|
||||||
|
processStderr: boolean; // tracks whether stderr was written to
|
||||||
|
private delay = 10000; // 10 seconds
|
||||||
|
private done: boolean;
|
||||||
|
private options: IExecOptions;
|
||||||
|
private timeout: NodeJS.Timer | null = null;
|
||||||
|
private toolPath: string;
|
||||||
|
|
||||||
|
public CheckComplete(): void {
|
||||||
|
if (this.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.processClosed) {
|
||||||
|
this._setResult();
|
||||||
|
}
|
||||||
|
else if (this.processExited) {
|
||||||
|
this.timeout = setTimeout(ExecState.HandleTimeout, this.delay, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setResult(): void {
|
||||||
|
// determine whether there is an error
|
||||||
|
let error: Error | undefined;
|
||||||
|
if (this.processExited) {
|
||||||
|
if (this.processError) {
|
||||||
|
error = new Error(`LIB_ProcessError: \n tool: ${this.toolPath} \n error: ${this.processError}`);
|
||||||
|
}
|
||||||
|
else if (this.processExitCode != 0 && !this.options.ignoreReturnCode) {
|
||||||
|
error = new Error(`LIB_ProcessExitCode\n tool: ${this.toolPath} \n Exit Code: ${this.processExitCode}`);
|
||||||
|
}
|
||||||
|
else if (this.processStderr && this.options.failOnStdErr) {
|
||||||
|
error = new Error(`LIB_ProcessStderr', ${this.toolPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the timeout
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.done = true;
|
||||||
|
this.emit('done', error, this.processExitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HandleTimeout(state: ExecState) {
|
||||||
|
if (state.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.processClosed && state.processExited) {
|
||||||
|
core.debug(`LIB_StdioNotClosed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
state._setResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/utilities/utility.ts
Normal file
60
src/utilities/utility.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import * as os from 'os';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
|
||||||
|
export function getExecutableExtension(): string {
|
||||||
|
if (os.type().match(/^Win/)) {
|
||||||
|
return '.exe';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEqual(str1: string, str2: string, ignoreCase?: boolean): boolean {
|
||||||
|
if (str1 == null && str2 == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str1 == null || str2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreCase) {
|
||||||
|
return str1.toUpperCase() === str2.toUpperCase();
|
||||||
|
} else {
|
||||||
|
return str1 === str2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkForErrors(execResults, warnIfError?: boolean) {
|
||||||
|
if (execResults.length !== 0) {
|
||||||
|
let stderr = '';
|
||||||
|
execResults.forEach(result => {
|
||||||
|
if (result.stderr) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
stderr += result.stderr + '\n';
|
||||||
|
} else {
|
||||||
|
core.warning(result.stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (stderr.length > 0) {
|
||||||
|
if (!!warnIfError) {
|
||||||
|
core.warning(stderr.trim());
|
||||||
|
} else {
|
||||||
|
throw new Error(stderr.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sleep(timeout: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomInt(max: number) {
|
||||||
|
return Math.floor(Math.random() * Math.floor(max));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentTime(): number {
|
||||||
|
return new Date().getTime();
|
||||||
|
}
|
||||||
22
src/utils.ts
22
src/utils.ts
@ -1,22 +0,0 @@
|
|||||||
import * as os from 'os';
|
|
||||||
|
|
||||||
export function isEqual(str1: string, str2: string) {
|
|
||||||
if (!str1) str1 = "";
|
|
||||||
if (!str2) str2 = "";
|
|
||||||
return str1.toLowerCase() === str2.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRandomInt(max: number) {
|
|
||||||
return Math.floor(Math.random() * Math.floor(max));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getExecutableExtension(): string {
|
|
||||||
if (os.type().match(/^Win/)) {
|
|
||||||
return '.exe';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCurrentTime(): number {
|
|
||||||
return new Date().getTime();
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user