This commit is contained in:
Deepak Sattiraju 2019-09-23 15:16:33 +05:30
Родитель 4085ca3bbc
Коммит 8d56cba217
14 изменённых файлов: 1061 добавлений и 2 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -261,7 +261,6 @@ FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg

119
README.md
Просмотреть файл

@ -1,3 +1,120 @@
# Deploy manifest action for Kubernetes
Use this action to bake and deploy manifests to Kubernetes clusters.
Assumes that the deployment target K8s cluster context was set earlier in the workflow by using either [`Azure/aks-set-context`](https://github.com/Azure/aks-set-context/tree/releases/v1) or [`Azure/k8s-set-context`](https://github.com/Azure/k8s-set-context/tree/releases/v1)
#### Artifact substitution
The deploy action takes as input a list of container images which can be specified along with their tags or digests. The same is substituted into the non-templatized version of manifest files before applying to the cluster to ensure that the right version of the image is pulled by the cluster nodes.
#### Manifest stability
Rollout status is checked for the Kubernetes objects deployed. This is done to incorporate stability checks while computing the task status as success/failure.
#### Secret handling
The manifest files specfied as inputs are augmented with appropriate imagePullSecrets before deploying to the cluster.
```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/*.*'
kubectl-version: 'latest' # optional
```
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
```yaml
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: Azure/docker-login@v1
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
docker build . -t contoso.azurecr.io/k8sdemo:${{ github.sha }}
docker push contoso.azurecr.io/k8sdemo:${{ github.sha }}
# Set the target AKS cluster.
- uses: Azure/aks-set-context@v1
with:
creds: '${{ secrets.AZURE_CREDENTIALS }}'
cluster-name: contoso
resource-group: contoso-rg
- uses: Azure/k8s-create-secret@v1
with:
container-registry-url: contoso.azurecr.io
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
secret-name: demo-k8s-secret
- uses: Azure/k8s-deploy@v1
with:
manifests: |
manifests/deployment.yml
manifests/service.yml
images: |
demo.azurecr.io/k8sdemo:${{ github.sha }}
imagepullsecrets: |
demo-k8s-secret
```
## End to end workflow for building container images and deploying to a Kubernetes cluster
```yaml
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: Azure/docker-login@v1
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
docker build . -t contoso.azurecr.io/k8sdemo:${{ github.sha }}
docker push contoso.azurecr.io/k8sdemo:${{ github.sha }}
- uses: Azure/k8s-set-context@v1
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- uses: Azure/k8s-create-secret@v1
with:
container-registry-url: contoso.azurecr.io
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
secret-name: demo-k8s-secret
- uses: Azure/k8s-deploy@v1
with:
manifests: |
manifests/deployment.yml
manifests/service.yml
images: |
demo.azurecr.io/k8sdemo:${{ github.sha }}
imagepullsecrets: |
demo-k8s-secret
```
# Contributing
@ -11,4 +128,4 @@ provided by the bot. You will only need to do this once across all repos using o
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

26
action.yml Normal file
Просмотреть файл

@ -0,0 +1,26 @@
name: 'Deploy to Kubernetes cluster'
description: 'Deploy to Kubernetes cluster'
inputs:
# Please ensure you have used either azure/k8s-actions/aks-set-context or azure/k8s-actions/k8s-set-context in the workflow before this action
namespace:
description: 'Choose the target Kubernetes namespace. If the namespace is not provided, the commands will run in the default namespace.'
required: false
manifests:
description: 'Path to the manifest files which will be used for deployment.'
required: true
default: ''
images:
description: 'Fully qualified resource URL of the image(s) to be used for substitutions on the manifest files
Example: contosodemo.azurecr.io/helloworld:test'
required: false
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'
required: false
kubectl-version:
description: 'Version of kubectl. Installs a specific version of kubectl binary'
required: false
branding:
color: 'green' # optional, decorates the entry in the GitHub Marketplace
runs:
using: 'node12'
main: 'lib/run.js'

71
lib/kubectl-util.js Normal file
Просмотреть файл

@ -0,0 +1,71 @@
"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 os = require("os");
const path = require("path");
const util = require("util");
const fs = require("fs");
const toolCache = require("@actions/tool-cache");
const core = require("@actions/core");
const kubectlToolName = 'kubectl';
const stableKubectlVersion = 'v1.15.0';
const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt';
function getExecutableExtension() {
if (os.type().match(/^Win/)) {
return '.exe';
}
return '';
}
function getkubectlDownloadURL(version) {
switch (os.type()) {
case 'Linux':
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/amd64/kubectl', version);
case 'Darwin':
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/darwin/amd64/kubectl', version);
case 'Windows_NT':
default:
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/amd64/kubectl.exe', version);
}
}
function getStableKubectlVersion() {
return __awaiter(this, void 0, void 0, function* () {
return toolCache.downloadTool(stableVersionUrl).then((downloadPath) => {
let version = fs.readFileSync(downloadPath, 'utf8').toString().trim();
if (!version) {
version = stableKubectlVersion;
}
return version;
}, (error) => {
core.debug(error);
core.warning('GetStableVersionFailed');
return stableKubectlVersion;
});
});
}
exports.getStableKubectlVersion = getStableKubectlVersion;
function downloadKubectl(version) {
return __awaiter(this, void 0, void 0, function* () {
let cachedToolpath = toolCache.find(kubectlToolName, version);
let kubectlDownloadPath = '';
if (!cachedToolpath) {
try {
kubectlDownloadPath = yield toolCache.downloadTool(getkubectlDownloadURL(version));
}
catch (exception) {
throw new Error('DownloadKubectlFailed');
}
cachedToolpath = yield toolCache.cacheFile(kubectlDownloadPath, kubectlToolName + getExecutableExtension(), kubectlToolName, version);
}
const kubectlPath = path.join(cachedToolpath, kubectlToolName + getExecutableExtension());
fs.chmodSync(kubectlPath, '777');
return kubectlPath;
});
}
exports.downloadKubectl = downloadKubectl;

141
lib/kubernetes-utils.js Normal file
Просмотреть файл

@ -0,0 +1,141 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core = require("@actions/core");
const utils_1 = require("./utils");
function getImagePullSecrets(inputObject) {
if (!inputObject || !inputObject.spec) {
return;
}
if (utils_1.isEqual(inputObject.kind, 'pod')
&& inputObject
&& inputObject.spec
&& inputObject.spec.imagePullSecrets) {
return inputObject.spec.imagePullSecrets;
}
else if (utils_1.isEqual(inputObject.kind, 'cronjob')
&& inputObject
&& inputObject.spec
&& inputObject.spec.jobTemplate
&& inputObject.spec.jobTemplate.spec
&& inputObject.spec.jobTemplate.spec.template
&& inputObject.spec.jobTemplate.spec.template.spec
&& inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets) {
return inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
}
else if (inputObject
&& inputObject.spec
&& inputObject.spec.template
&& inputObject.spec.template.spec
&& inputObject.spec.template.spec.imagePullSecrets) {
return inputObject.spec.template.spec.imagePullSecrets;
}
}
function setImagePullSecrets(inputObject, newImagePullSecrets) {
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
return;
}
if (utils_1.isEqual(inputObject.kind, 'pod')) {
if (inputObject
&& inputObject.spec) {
if (newImagePullSecrets.length > 0) {
inputObject.spec.imagePullSecrets = newImagePullSecrets;
}
else {
delete inputObject.spec.imagePullSecrets;
}
}
}
else if (utils_1.isEqual(inputObject.kind, 'cronjob')) {
if (inputObject
&& inputObject.spec
&& inputObject.spec.jobTemplate
&& inputObject.spec.jobTemplate.spec
&& inputObject.spec.jobTemplate.spec.template
&& inputObject.spec.jobTemplate.spec.template.spec) {
if (newImagePullSecrets.length > 0) {
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets = newImagePullSecrets;
}
else {
delete inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
}
}
}
else if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
if (inputObject
&& inputObject.spec
&& inputObject.spec.template
&& inputObject.spec.template.spec) {
if (newImagePullSecrets.length > 0) {
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets;
}
else {
delete inputObject.spec.template.spec.imagePullSecrets;
}
}
}
}
function substituteImageNameInSpecContent(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) {
const [currentImageName, currentImageTag] = line
.substring(imageKeyword[0].length) // consume the line from keyword onwards
.trim()
.replace(/[',"]/g, '') // replace allowed quotes with nothing
.split(':');
if (currentImageName === imageName) {
return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n`;
}
}
return acc + line + '\n';
}, '');
}
function updateContainerImagesInManifestFiles(contents, containers) {
if (!!containers && containers.length > 0) {
containers.forEach((container) => {
let imageName = container.split(':')[0];
if (imageName.indexOf('@') > 0) {
imageName = imageName.split('@')[0];
}
if (contents.indexOf(imageName) > 0) {
contents = substituteImageNameInSpecContent(contents, imageName, container);
}
});
}
return contents;
}
exports.updateContainerImagesInManifestFiles = updateContainerImagesInManifestFiles;
function updateImagePullSecrets(inputObject, newImagePullSecrets) {
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
return;
}
let newImagePullSecretsObjects;
if (newImagePullSecrets.length > 0) {
newImagePullSecretsObjects = Array.from(newImagePullSecrets, x => { return !!x ? { 'name': x } : null; });
}
else {
newImagePullSecretsObjects = [];
}
let existingImagePullSecretObjects = getImagePullSecrets(inputObject);
if (!existingImagePullSecretObjects) {
existingImagePullSecretObjects = new Array();
}
existingImagePullSecretObjects = existingImagePullSecretObjects.concat(newImagePullSecretsObjects);
setImagePullSecrets(inputObject, existingImagePullSecretObjects);
}
exports.updateImagePullSecrets = updateImagePullSecrets;
const workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob'];
function isWorkloadEntity(kind) {
if (!kind) {
core.debug('ResourceKindNotDefined');
return false;
}
return workloadTypes.some((type) => {
return utils_1.isEqual(type, kind);
});
}
exports.isWorkloadEntity = isWorkloadEntity;

163
lib/run.js Normal file
Просмотреть файл

@ -0,0 +1,163 @@
"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 toolCache = require("@actions/tool-cache");
const core = require("@actions/core");
const io = require("@actions/io");
const toolrunner_1 = require("@actions/exec/lib/toolrunner");
const path = require("path");
const fs = require("fs");
const yaml = require("js-yaml");
const utils_1 = require("./utils");
const kubernetes_utils_1 = require("./kubernetes-utils");
const kubectl_util_1 = require("./kubectl-util");
let kubectlPath = "";
function setKubectlPath() {
return __awaiter(this, void 0, void 0, function* () {
if (core.getInput('kubectl-version')) {
const version = core.getInput('kubect-version');
kubectlPath = toolCache.find('kubectl', version);
if (!kubectlPath) {
kubectlPath = yield installKubectl(version);
}
}
else {
kubectlPath = yield io.which('kubectl', false);
if (!kubectlPath) {
const allVersions = toolCache.findAllVersions('kubectl');
kubectlPath = allVersions.length > 0 ? toolCache.find('kubectl', allVersions[0]) : '';
if (!kubectlPath) {
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()}`);
}
}
});
}
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) {
return __awaiter(this, void 0, void 0, function* () {
if (utils_1.isEqual(version, 'latest')) {
version = yield kubectl_util_1.getStableKubectlVersion();
}
return yield kubectl_util_1.downloadKubectl(version);
});
}
function checkClusterContext() {
if (!process.env["KUBECONFIG"]) {
throw new Error('Cluster context not set. Use k8ssetcontext action to set cluster context');
}
}
function run() {
return __awaiter(this, void 0, void 0, function* () {
checkClusterContext();
yield setKubectlPath();
let manifestsInput = core.getInput('manifests');
if (!manifestsInput) {
core.setFailed('No manifests supplied to deploy');
}
let namespace = core.getInput('namespace');
if (!namespace) {
namespace = 'default';
}
let manifests = manifestsInput.split('\n');
const imagesToOverride = core.getInput('images');
const imagePullSecretsToAdd = core.getInput('imagepullsecrets');
if (!!imagePullSecretsToAdd || !!imagesToOverride) {
manifests = updateManifests(manifests, imagesToOverride, imagePullSecretsToAdd);
}
yield deploy(manifests, namespace);
yield checkManifestsStability(manifests, namespace);
});
}
run().catch(core.setFailed);

26
lib/utils.js Normal file
Просмотреть файл

@ -0,0 +1,26 @@
"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;

98
package-lock.json сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,98 @@
{
"name": "k8s-deploy-action",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@actions/core": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.1.0.tgz",
"integrity": "sha512-KKpo3xzo0Zsikni9tbOsEQkxZBGDsYSJZNkTvmo0gPSXrc98TBOcdTvKwwjitjkjHkreTggWdB1ACiAFVgsuzA=="
},
"@actions/exec": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz",
"integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ=="
},
"@actions/io": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz",
"integrity": "sha512-rhq+tfZukbtaus7xyUtwKfuiCRXd1hWSfmJNEpFgBQJ4woqPEpsBw04awicjwz9tyG2/MVhAEMfVn664Cri5zA=="
},
"@actions/tool-cache": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-1.1.1.tgz",
"integrity": "sha512-AILekrrj/L4N/5z5TGtUKVie4nKjxDioCgOEymyYxzPhGfjIxfE71tN2VTTpiICEWJ883rPRj2+WinTr1b6yVA==",
"requires": {
"@actions/core": "^1.1.0",
"@actions/exec": "^1.0.1",
"@actions/io": "^1.0.1",
"semver": "^6.1.0",
"typed-rest-client": "^1.4.0",
"uuid": "^3.3.2"
}
},
"@types/node": {
"version": "12.7.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.4.tgz",
"integrity": "sha512-W0+n1Y+gK/8G2P/piTkBBN38Qc5Q1ZSO6B5H3QmPCUewaiXOo2GCAWZ4ElZCcNhjJuBSUSLGFUJnmlCn5+nxOQ==",
"dev": true
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"tunnel": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz",
"integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM="
},
"typed-rest-client": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz",
"integrity": "sha512-DVZRlmsfnTjp6ZJaatcdyvvwYwbWvR4YDNFDqb+qdTxpvaVP99YCpBkA8rxsLtAPjBVoDe4fNsnMIdZTiPuKWg==",
"requires": {
"tunnel": "0.0.4",
"underscore": "1.8.3"
}
},
"underscore": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
},
"uuid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
}
}
}

19
package.json Normal file
Просмотреть файл

@ -0,0 +1,19 @@
{
"name": "k8s-deploy-action",
"version": "0.0.0",
"author": "Deepak Sattiraju",
"license": "MIT",
"scripts": {
"build": "tsc --outDir ./lib --rootDir ./src"
},
"dependencies": {
"@actions/tool-cache": "^1.0.0",
"@actions/io": "^1.0.0",
"@actions/core": "^1.0.0",
"@actions/exec": "^1.0.0",
"js-yaml": "3.13.1"
},
"devDependencies": {
"@types/node": "^12.0.10"
}
}

65
src/kubectl-util.ts Normal file
Просмотреть файл

@ -0,0 +1,65 @@
import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import * as fs from 'fs';
import * as toolCache from '@actions/tool-cache';
import * as core from '@actions/core';
const kubectlToolName = 'kubectl';
const stableKubectlVersion = 'v1.15.0';
const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt';
function getExecutableExtension(): string {
if (os.type().match(/^Win/)) {
return '.exe';
}
return '';
}
function getkubectlDownloadURL(version: string): string {
switch (os.type()) {
case 'Linux':
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/amd64/kubectl', version);
case 'Darwin':
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/darwin/amd64/kubectl', version);
case 'Windows_NT':
default:
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/amd64/kubectl.exe', version);
}
}
export async function getStableKubectlVersion(): Promise<string> {
return toolCache.downloadTool(stableVersionUrl).then((downloadPath) => {
let version = fs.readFileSync(downloadPath, 'utf8').toString().trim();
if (!version) {
version = stableKubectlVersion;
}
return version;
}, (error) => {
core.debug(error);
core.warning('GetStableVersionFailed');
return stableKubectlVersion;
});
}
export async function downloadKubectl(version: string): Promise<string> {
let cachedToolpath = toolCache.find(kubectlToolName, version);
let kubectlDownloadPath = '';
if (!cachedToolpath) {
try {
kubectlDownloadPath = await toolCache.downloadTool(getkubectlDownloadURL(version));
} catch (exception) {
throw new Error('DownloadKubectlFailed');
}
cachedToolpath = await toolCache.cacheFile(kubectlDownloadPath, kubectlToolName + getExecutableExtension(), kubectlToolName, version);
}
const kubectlPath = path.join(cachedToolpath, kubectlToolName + getExecutableExtension());
fs.chmodSync(kubectlPath, '777');
return kubectlPath;
}

148
src/kubernetes-utils.ts Normal file
Просмотреть файл

@ -0,0 +1,148 @@
import * as core from '@actions/core';
import { isEqual } from "./utils";
function getImagePullSecrets(inputObject: any) {
if (!inputObject || !inputObject.spec) {
return;
}
if (isEqual(inputObject.kind, 'pod')
&& inputObject
&& inputObject.spec
&& inputObject.spec.imagePullSecrets) {
return inputObject.spec.imagePullSecrets;
} else if (isEqual(inputObject.kind, 'cronjob')
&& inputObject
&& inputObject.spec
&& inputObject.spec.jobTemplate
&& inputObject.spec.jobTemplate.spec
&& inputObject.spec.jobTemplate.spec.template
&& inputObject.spec.jobTemplate.spec.template.spec
&& inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets) {
return inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
} else if (inputObject
&& inputObject.spec
&& inputObject.spec.template
&& inputObject.spec.template.spec
&& inputObject.spec.template.spec.imagePullSecrets) {
return inputObject.spec.template.spec.imagePullSecrets;
}
}
function setImagePullSecrets(inputObject: any, newImagePullSecrets: any) {
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
return;
}
if (isEqual(inputObject.kind, 'pod')) {
if (inputObject
&& inputObject.spec) {
if (newImagePullSecrets.length > 0) {
inputObject.spec.imagePullSecrets = newImagePullSecrets;
} else {
delete inputObject.spec.imagePullSecrets;
}
}
} else if (isEqual(inputObject.kind, 'cronjob')) {
if (inputObject
&& inputObject.spec
&& inputObject.spec.jobTemplate
&& inputObject.spec.jobTemplate.spec
&& inputObject.spec.jobTemplate.spec.template
&& inputObject.spec.jobTemplate.spec.template.spec) {
if (newImagePullSecrets.length > 0) {
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets = newImagePullSecrets;
} else {
delete inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
}
}
} else if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
if (inputObject
&& inputObject.spec
&& inputObject.spec.template
&& inputObject.spec.template.spec) {
if (newImagePullSecrets.length > 0) {
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets;
} else {
delete inputObject.spec.template.spec.imagePullSecrets;
}
}
}
}
function substituteImageNameInSpecContent(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) {
const [currentImageName, currentImageTag] = line
.substring(imageKeyword[0].length) // consume the line from keyword onwards
.trim()
.replace(/[',"]/g, '') // replace allowed quotes with nothing
.split(':');
if (currentImageName === imageName) {
return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n`;
}
}
return acc + line + '\n';
}, '');
}
export function updateContainerImagesInManifestFiles(contents, containers: string[]): string {
if (!!containers && containers.length > 0) {
containers.forEach((container: string) => {
let imageName = container.split(':')[0];
if (imageName.indexOf('@') > 0) {
imageName = imageName.split('@')[0];
}
if (contents.indexOf(imageName) > 0) {
contents = substituteImageNameInSpecContent(contents, imageName, container);
}
});
}
return contents;
}
export function updateImagePullSecrets(inputObject: any, newImagePullSecrets: string[]) {
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
return;
}
let newImagePullSecretsObjects;
if (newImagePullSecrets.length > 0) {
newImagePullSecretsObjects = Array.from(newImagePullSecrets, x => { return !!x ? { 'name': x } : null; });
} else {
newImagePullSecretsObjects = [];
}
let existingImagePullSecretObjects: any = getImagePullSecrets(inputObject);
if (!existingImagePullSecretObjects) {
existingImagePullSecretObjects = new Array();
}
existingImagePullSecretObjects = existingImagePullSecretObjects.concat(newImagePullSecretsObjects);
setImagePullSecrets(inputObject, existingImagePullSecretObjects);
}
const workloadTypes: string[] = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob'];
export function isWorkloadEntity(kind: string): boolean {
if (!kind) {
core.debug('ResourceKindNotDefined');
return false;
}
return workloadTypes.some((type: string) => {
return isEqual(type, kind);
});
}

155
src/run.ts Normal file
Просмотреть файл

@ -0,0 +1,155 @@
import * as toolCache from '@actions/tool-cache';
import * as core from '@actions/core';
import * as io from '@actions/io';
import { ToolRunner } from "@actions/exec/lib/toolrunner";
import * as path from 'path';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import { getExecutableExtension, isEqual, getCurrentTime } from "./utils";
import { isWorkloadEntity, updateContainerImagesInManifestFiles, updateImagePullSecrets } from "./kubernetes-utils";
import { downloadKubectl, getStableKubectlVersion } from "./kubectl-util";
let kubectlPath = "";
async function setKubectlPath() {
if (core.getInput('kubectl-version')) {
const version = core.getInput('kubect-version');
kubectlPath = toolCache.find('kubectl', version);
if (!kubectlPath) {
kubectlPath = await installKubectl(version);
}
} else {
kubectlPath = await io.which('kubectl', false);
if (!kubectlPath) {
const allVersions = toolCache.findAllVersions('kubectl');
kubectlPath = allVersions.length > 0 ? toolCache.find('kubectl', allVersions[0]) : '';
if (!kubectlPath) {
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${getExecutableExtension()}`);
}
}
}
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) {
if (isEqual(version, 'latest')) {
version = await getStableKubectlVersion();
}
return await downloadKubectl(version);
}
function checkClusterContext() {
if (!process.env["KUBECONFIG"]) {
throw new Error('Cluster context not set. Use k8ssetcontext action to set cluster context');
}
}
async function run() {
checkClusterContext();
await setKubectlPath();
let manifestsInput = core.getInput('manifests');
if (!manifestsInput) {
core.setFailed('No manifests supplied to deploy');
}
let namespace = core.getInput('namespace');
if (!namespace) {
namespace = 'default';
}
let manifests = manifestsInput.split('\n');
const imagesToOverride = core.getInput('images');
const imagePullSecretsToAdd = core.getInput('imagepullsecrets');
if (!!imagePullSecretsToAdd || !!imagesToOverride) {
manifests = updateManifests(manifests, imagesToOverride, imagePullSecretsToAdd)
}
await deploy(manifests, namespace);
await checkManifestsStability(manifests, namespace);
}
run().catch(core.setFailed);

22
src/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,22 @@
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();
}

9
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs"
},
"exclude": [
"node_modules"
]
}