diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..21217c77 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: "build-test" +on: # rebuild any PRs and main branch changes + pull_request: + branches: + - master + - 'releases/*' + push: + branches: + - master + - 'releases/*' + +jobs: + build: # make sure build/ci works properly + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: | + npm install + npm build + npm test \ No newline at end of file diff --git a/.gitignore b/.gitignore index ddea9084..0d4440f3 100644 --- a/.gitignore +++ b/.gitignore @@ -326,4 +326,4 @@ ASALocalRun/ *.nvuser # MFractors (Xamarin productivity tool) working folder -.mfractor/ +.mfractor/ \ No newline at end of file diff --git a/__tests__/manifests/deployment.yml b/__tests__/manifests/deployment.yml new file mode 100644 index 00000000..791154a2 --- /dev/null +++ b/__tests__/manifests/deployment.yml @@ -0,0 +1,16 @@ +apiVersion : apps/v1beta1 +kind: Deployment +metadata: + name: testapp +spec: + replicas: 1 + template: + metadata: + labels: + app: testapp + spec: + containers: + - name: testapp + image: testcr.azurecr.io/testapp + ports: + - containerPort: 80 \ No newline at end of file diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts new file mode 100644 index 00000000..339501d2 --- /dev/null +++ b/__tests__/run.test.ts @@ -0,0 +1,196 @@ +import * as KubernetesManifestUtility from '../src/utilities/manifest-stability-utility'; +import * as KubernetesObjectUtility from '../src/utilities/resource-object-utility'; +import * as action from '../src/run'; +import * as core from '@actions/core'; +import * as deployment from '../src/utilities/strategy-helpers/deployment-helper'; +import * as fs from 'fs'; +import * as io from '@actions/io'; +import * as toolCache from '@actions/tool-cache'; + +import { Kubectl, Resource } from '../src/kubectl-object-model'; + +import { getkubectlDownloadURL } from "../src/utilities/kubectl-util"; +import { mocked } from 'ts-jest/utils'; + +var path = require('path'); + +const coreMock = mocked(core, true); +const ioMock = mocked(io, true); + +const toolCacheMock = mocked(toolCache, true); +const fileUtility = mocked(fs, true); + +const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt'; + +var deploymentYaml = ""; + +beforeAll(() => { + deploymentYaml = fs.readFileSync(path.join(__dirname, 'manifests', 'deployment.yml'), 'utf8'); + process.env["KUBECONFIG"] = 'kubeConfig'; +}) + +test("setKubectlPath() - install a particular version", async () => { + const kubectlVersion = 'v1.18.0' + //Mocks + coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion); + toolCacheMock.find = jest.fn().mockReturnValue(undefined); + toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath'); + toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath'); + fileUtility.chmodSync = jest.fn(); + + //Invoke and assert + await expect(action.run()).resolves.not.toThrow(); + expect(toolCacheMock.find).toBeCalledWith('kubectl', kubectlVersion); + expect(toolCacheMock.downloadTool).toBeCalledWith(getkubectlDownloadURL(kubectlVersion)); +}); + +test("setKubectlPath() - install a latest version", async () => { + const kubectlVersion = 'latest' + //Mocks + coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion); + jest.spyOn(fs, 'readFileSync').mockImplementation(() => ""); + toolCacheMock.find = jest.fn().mockReturnValue(undefined); + toolCacheMock.downloadTool = jest.fn().mockResolvedValue(''); + toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath'); + fileUtility.chmodSync = jest.fn(); + + //Invoke and assert + await expect(action.run()).resolves.not.toThrow(); + expect(toolCacheMock.find).toBeCalledWith('kubectl', kubectlVersion); + expect(toolCacheMock.downloadTool).toBeCalledWith(stableVersionUrl); + +}); + +test("setKubectlPath() - kubectl version already avilable", async () => { + const kubectlVersion = 'v1.18.0' + //Mock + coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion); + toolCacheMock.find = jest.fn().mockReturnValue('validPath'); + toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath'); + toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath'); + fileUtility.chmodSync = jest.fn(); + + //Invoke and assert + await expect(action.run()).resolves.not.toThrow(); + expect(toolCacheMock.find).toBeCalledWith('kubectl', kubectlVersion); + expect(toolCacheMock.downloadTool).toBeCalledTimes(0); +}); + +test("setKubectlPath() - kubectl version not provided and kubectl avilable on machine", async () => { + //Mock + coreMock.getInput = jest.fn().mockReturnValue(undefined); + ioMock.which = jest.fn().mockReturnValue('validPath'); + + //Invoke and assert + await expect(action.run()).resolves.not.toThrow(); + expect(ioMock.which).toBeCalledWith('kubectl', false); + expect(toolCacheMock.downloadTool).toBeCalledTimes(0); +}); + +test("setKubectlPath() - kubectl version not provided and kubectl not avilable on machine", async () => { + //Mock + coreMock.getInput = jest.fn().mockReturnValue(undefined); + ioMock.which = jest.fn().mockReturnValue(undefined); + toolCacheMock.findAllVersions = jest.fn().mockReturnValue(undefined); + + //Invoke and assert + await expect(action.run()).rejects.toThrowError(); + expect(ioMock.which).toBeCalledWith('kubectl', false); +}); + +test("run() - action not provided", async () => { + const kubectlVersion = 'v1.18.0' + coreMock.getInput = jest.fn().mockImplementation((name) => { + if (name == 'action') { + return undefined; + } + return kubectlVersion; + }); + coreMock.setFailed = jest.fn(); + //Mocks + toolCacheMock.find = jest.fn().mockReturnValue(undefined); + toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath'); + toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath'); + fileUtility.chmodSync = jest.fn(); + + //Invoke and assert + await expect(action.run()).resolves.not.toThrow(); + expect(coreMock.setFailed).toBeCalledWith('Not a valid action. The allowed actions are deploy, promote, reject'); +}); + +test("run() - deploy - Manifiest not provided", async () => { + //Mocks + const kubectlVersion = 'v1.18.0' + coreMock.getInput = jest.fn().mockImplementation((name) => { + if (name == 'manifests') { + return undefined; + } + if (name == 'action') { + return 'deploy'; + } + return kubectlVersion; + }); + coreMock.setFailed = jest.fn(); + toolCacheMock.find = jest.fn().mockReturnValue(undefined); + toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath'); + toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath'); + fileUtility.chmodSync = jest.fn(); + + //Invoke and assert + await expect(action.run()).resolves.not.toThrow(); + expect(coreMock.setFailed).toBeCalledWith('No manifests supplied to deploy'); +}); + +test("deployment - deploy() - Invokes with no manifestfiles", async () => { + const kubeCtl: jest.Mocked = new Kubectl("") as any; + + //Invoke and assert + await expect(deployment.deploy(kubeCtl, [], undefined)).rejects.toThrowError('ManifestFileNotFound'); +}); + +test("run() - deploy", async () => { + const kubectlVersion = 'v1.18.0' + //Mocks + coreMock.getInput = jest.fn().mockImplementation((name) => { + if (name == 'manifests') { + return 'manifests/deployment.yaml'; + } + if (name == 'action') { + return 'deploy'; + } + if (name == 'strategy') { + return undefined; + } + return kubectlVersion; + }); + + coreMock.setFailed = jest.fn(); + toolCacheMock.find = jest.fn().mockReturnValue('validPath'); + toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath'); + toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath'); + fileUtility.chmodSync = jest.fn(); + const deploySpy = jest.spyOn(deployment, 'deploy').mockImplementation(); + + //Invoke and assert + await expect(action.run()).resolves.not.toThrow(); + expect(deploySpy).toBeCalledWith({ "ignoreSSLErrors": false, "kubectlPath": 'validPath', "namespace": "v1.18.0" }, ['manifests/deployment.yaml'], undefined); + deploySpy.mockRestore(); +}); + +test("deployment - deploy() - Invokes with manifestfiles", async () => { + const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); + const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); + const kubeCtl: jest.Mocked = new Kubectl("") as any; + kubeCtl.apply = jest.fn().mockReturnValue(""); + const resources: Resource[] = [{ type: "Deployment", name: "AppName" }]; + KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); + kubeCtl.getResource = jest.fn().mockReturnValue(""); + KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); + + const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml); + + //Invoke and assert + await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); + expect(readFileSpy).toBeCalledWith("manifests/deployment.yaml"); + expect(kubeCtl.getResource).toBeCalledWith("ingress", "AppName"); +}); \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..45ff45ec --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + clearMocks: true, + moduleFileExtensions: ['js', 'ts'], + testEnvironment: 'node', + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.ts$': 'ts-jest' + }, + verbose: true + } \ No newline at end of file diff --git a/lib/constants.js b/lib/constants.js index 4d3283c2..50919389 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); class KubernetesWorkload { } +exports.KubernetesWorkload = KubernetesWorkload; KubernetesWorkload.pod = 'Pod'; KubernetesWorkload.replicaset = 'Replicaset'; KubernetesWorkload.deployment = 'Deployment'; @@ -9,18 +10,17 @@ KubernetesWorkload.statefulSet = 'StatefulSet'; KubernetesWorkload.daemonSet = 'DaemonSet'; KubernetesWorkload.job = 'job'; KubernetesWorkload.cronjob = 'cronjob'; -exports.KubernetesWorkload = KubernetesWorkload; class DiscoveryAndLoadBalancerResource { } +exports.DiscoveryAndLoadBalancerResource = DiscoveryAndLoadBalancerResource; DiscoveryAndLoadBalancerResource.service = 'service'; DiscoveryAndLoadBalancerResource.ingress = 'ingress'; -exports.DiscoveryAndLoadBalancerResource = DiscoveryAndLoadBalancerResource; class ServiceTypes { } +exports.ServiceTypes = 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']; diff --git a/lib/run.js b/lib/run.js index af12fe7a..369a0efd 100644 --- a/lib/run.js +++ b/lib/run.js @@ -9,16 +9,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -const toolCache = require("@actions/tool-cache"); const core = require("@actions/core"); const io = require("@actions/io"); const path = require("path"); -const utility_1 = require("./utilities/utility"); +const toolCache = require("@actions/tool-cache"); const kubectl_util_1 = require("./utilities/kubectl-util"); +const utility_1 = require("./utilities/utility"); +const kubectl_object_model_1 = require("./kubectl-object-model"); 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 = ""; function setKubectlPath() { return __awaiter(this, void 0, void 0, function* () { @@ -62,6 +62,7 @@ function run() { let manifestsInput = core.getInput('manifests'); if (!manifestsInput) { core.setFailed('No manifests supplied to deploy'); + return; } let namespace = core.getInput('namespace'); if (!namespace) { @@ -85,4 +86,5 @@ function run() { } }); } +exports.run = run; run().catch(core.setFailed); diff --git a/lib/utilities/kubectl-util.js b/lib/utilities/kubectl-util.js index ca9ee6a6..3015e91c 100644 --- a/lib/utilities/kubectl-util.js +++ b/lib/utilities/kubectl-util.js @@ -9,12 +9,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); +const core = require("@actions/core"); +const fs = require("fs"); 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 util = require("util"); const kubectlToolName = 'kubectl'; const stableKubectlVersion = 'v1.15.0'; const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt'; @@ -36,6 +36,7 @@ function getkubectlDownloadURL(version) { return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/amd64/kubectl.exe', version); } } +exports.getkubectlDownloadURL = getkubectlDownloadURL; function getStableKubectlVersion() { return __awaiter(this, void 0, void 0, function* () { return toolCache.downloadTool(stableVersionUrl).then((downloadPath) => { diff --git a/package.json b/package.json index 66d2abfa..66c46c66 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "author": "Deepak Sattiraju", "license": "MIT", "scripts": { - "build": "tsc --outDir ./lib --rootDir ./src" + "build": "tsc --outDir ./lib --rootDir ./src", + "test": "jest" }, "dependencies": { "@actions/tool-cache": "^1.0.0", @@ -14,6 +15,10 @@ "js-yaml": "3.13.1" }, "devDependencies": { - "@types/node": "^12.0.10" + "@types/node": "^12.0.10", + "jest": "^25.0.0", + "@types/jest": "^25.2.2", + "ts-jest": "^25.5.1", + "typescript": "^3.9.2" } } diff --git a/src/run.ts b/src/run.ts index fbf5fe67..72b4ce1d 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,14 +1,15 @@ -import * as toolCache from '@actions/tool-cache'; import * as core from '@actions/core'; import * as io from '@actions/io'; import * as path from 'path'; +import * as toolCache from '@actions/tool-cache'; -import { getExecutableExtension, isEqual } from "./utilities/utility"; import { downloadKubectl, getStableKubectlVersion } from "./utilities/kubectl-util"; +import { getExecutableExtension, isEqual } from "./utilities/utility"; + +import { Kubectl } from './kubectl-object-model'; 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 = ""; @@ -45,12 +46,13 @@ function checkClusterContext() { } } -async function run() { +export async function run() { checkClusterContext(); await setKubectlPath(); let manifestsInput = core.getInput('manifests'); if (!manifestsInput) { core.setFailed('No manifests supplied to deploy'); + return; } let namespace = core.getInput('namespace'); if (!namespace) { diff --git a/src/utilities/kubectl-util.ts b/src/utilities/kubectl-util.ts index 64a8ee71..11b202ec 100644 --- a/src/utilities/kubectl-util.ts +++ b/src/utilities/kubectl-util.ts @@ -1,10 +1,10 @@ +import * as core from '@actions/core'; +import * as fs from 'fs'; 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'; +import * as util from 'util'; + import { Kubectl } from '../kubectl-object-model'; const kubectlToolName = 'kubectl'; @@ -19,7 +19,7 @@ function getExecutableExtension(): string { return ''; } -function getkubectlDownloadURL(version: string): string { +export 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); diff --git a/tsconfig.json b/tsconfig.json index 7a5d8bef..3555b597 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "module": "commonjs" }, "exclude": [ - "node_modules" + "node_modules", + "__tests__" ] } \ No newline at end of file