diff --git a/brigade-worker/node_modules/.yarn-integrity b/brigade-worker/node_modules/.yarn-integrity index 3703834bb..a00fd1f04 100644 --- a/brigade-worker/node_modules/.yarn-integrity +++ b/brigade-worker/node_modules/.yarn-integrity @@ -6,7 +6,7 @@ "flags": [], "linkedModules": [], "topLevelPatterns": [ - "@brigadecore/brigadier@^0.5.0", + "@brigadecore/brigadier@^0.6.1", "@kubernetes/client-node@^0.10.1", "@types/byline@^4.2.31", "@types/chai@^4.0.1", @@ -32,7 +32,7 @@ "ulid@^0.2.0" ], "lockfileEntries": { - "@brigadecore/brigadier@^0.5.0": "https://registry.yarnpkg.com/@brigadecore/brigadier/-/brigadier-0.5.0.tgz#5d50dbb26c78e7ba53e07d904f1cd2983db00414", + "@brigadecore/brigadier@^0.6.1": "https://registry.yarnpkg.com/@brigadecore/brigadier/-/brigadier-0.6.1.tgz#deb4174de9314c5997521a1b3e2a7c6bcfeddc68", "@kubernetes/client-node@^0.10.1": "https://registry.yarnpkg.com/@kubernetes/client-node/-/client-node-0.10.2.tgz#9ca9d605148774c7fd77346d73743e5869f9205b", "@sinonjs/commons@^1": "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.4.0.tgz#7b3ec2d96af481d7a0321252e7b1c94724ec5a78", "@sinonjs/commons@^1.0.2": "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.4.0.tgz#7b3ec2d96af481d7a0321252e7b1c94724ec5a78", diff --git a/brigade-worker/node_modules/@brigadecore/brigadier/LICENSE b/brigade-worker/node_modules/@brigadecore/brigadier/LICENSE index ca9023497..6a996e7cb 100644 --- a/brigade-worker/node_modules/@brigadecore/brigadier/LICENSE +++ b/brigade-worker/node_modules/@brigadecore/brigadier/LICENSE @@ -1,21 +1,201 @@ -The MIT License (MIT) - -Copyright (c) 2019 The Brigade Authors. 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. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019 The Brigade Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/brigade-worker/node_modules/@brigadecore/brigadier/out/job.d.ts b/brigade-worker/node_modules/@brigadecore/brigadier/out/job.d.ts index 78380fce3..564e964fe 100644 --- a/brigade-worker/node_modules/@brigadecore/brigadier/out/job.d.ts +++ b/brigade-worker/node_modules/@brigadecore/brigadier/out/job.d.ts @@ -5,7 +5,7 @@ * A JobRunner is an implementation of the runtime logic for a Job. */ /** */ -import { V1EnvVarSource } from "@kubernetes/client-node/dist/api"; +import { V1EnvVarSource, V1VolumeMount, V1Volume } from "@kubernetes/client-node/dist/api"; export declare const brigadeCachePath = "/mnt/brigade/cache"; export declare const brigadeStoragePath = "/mnt/brigade/share"; export declare const dockerSocketMountPath = "/var/run/docker.sock"; @@ -178,6 +178,26 @@ export declare abstract class Job { * storage controls this job's preferences on the build-wide storage. */ storage: JobStorage; + /** + * EXPERIMENTAL: define volumes for the job. + * The property is defined as a list of Kubernetes volumes, and supports all Kubernetes volume types. + * Use the job's volumeMounts property to mount a volume defined to a path in the job's container. + * The names for the volume and the desired volume mount must match. + * For more info, see https://kubernetes.io/docs/concepts/storage/volumes/ + * + * For a simple shared volume between all the containers of a job, use JobStorage. + */ + volumes: V1Volume[]; + /** + * EXPERIMENTAL: define volume mounts for the job. + * The property is defined as a list of Kubernetes volume mounts. + * Use the job's volumes property to define volumes. + * The names for the volume and the desired volume mount must match. + * For more info, see https://kubernetes.io/docs/concepts/storage/volumes/ + * + * For a simple shared volume between all the containers of a job, use JobStorage. + */ + volumeMounts: V1VolumeMount[]; /** * docker controls the job's preferences on mounting the host's docker daemon. */ diff --git a/brigade-worker/node_modules/@brigadecore/brigadier/out/job.js b/brigade-worker/node_modules/@brigadecore/brigadier/out/job.js index 34e26661f..c353d32e7 100644 --- a/brigade-worker/node_modules/@brigadecore/brigadier/out/job.js +++ b/brigade-worker/node_modules/@brigadecore/brigadier/out/job.js @@ -156,6 +156,8 @@ class Job { this.host = new JobHost(); this.resourceRequests = new JobResourceRequest(); this.resourceLimits = new JobResourceLimit(); + this.volumes = []; + this.volumeMounts = []; } /** podName is the generated name of the pod.*/ get podName() { diff --git a/brigade-worker/node_modules/@brigadecore/brigadier/package.json b/brigade-worker/node_modules/@brigadecore/brigadier/package.json index 3709fca0c..5861c7176 100644 --- a/brigade-worker/node_modules/@brigadecore/brigadier/package.json +++ b/brigade-worker/node_modules/@brigadecore/brigadier/package.json @@ -1,11 +1,11 @@ { "name": "@brigadecore/brigadier", - "version": "0.5.0", + "version": "0.6.1", "description": "Brigade library for pipelines and events", "main": "out/index.js", "types": "out/index.d.ts", "author": "Brigade Core Maintainers", - "license": "MIT", + "license": "Apache-2.0", "homepage": "https://github.com/brigadecore/brigadier", "bugs": { "url": "https://github.com/brigadecore/brigadier/issues" diff --git a/brigade-worker/package.json b/brigade-worker/package.json index 52a648a2d..782d624ae 100644 --- a/brigade-worker/package.json +++ b/brigade-worker/package.json @@ -38,7 +38,7 @@ "typescript": "^3.2.2" }, "dependencies": { - "@brigadecore/brigadier": "^0.5.0", + "@brigadecore/brigadier": "^0.6.1", "@kubernetes/client-node": "^0.10.1", "byline": "^5.0.0", "child-process-promise": "^2.2.1", diff --git a/brigade-worker/src/k8s.ts b/brigade-worker/src/k8s.ts index 96edea217..ce8d15e8e 100644 --- a/brigade-worker/src/k8s.ts +++ b/brigade-worker/src/k8s.ts @@ -442,6 +442,27 @@ export class JobRunner implements jobs.JobRunner { } } + // If the job defines volumes, add them to the pod's volume list. + // If the volume type is `hostPath`, first check if the project allows host mounts + // and throw an error if it it does not. + for (let v of job.volumes) { + if (v.hostPath != undefined && !project.allowHostMounts) { + throw new Error(`allowHostMounts is false in this project, not mounting ${v.hostPath.path}`); + } + this.runner.spec.volumes.push(v); + } + + // If the job defines volume mounts, add them to every container's spec. + for (let m of job.volumeMounts) { + if (!volumeExists(m, job.volumes)) { + throw new Error(`volume ${m.name} referenced in volume mount is not defined`); + } + + for (let i = 0; i < this.runner.spec.containers.length; i++) { + this.runner.spec.containers[i].volumeMounts.push(m); + } + } + if (job.args.length > 0) { this.runner.spec.containers[0].args = job.args; } @@ -1094,3 +1115,14 @@ export function secretToProject( } return p; } + +// helper function to check if the volume referenced by a volume mount is defined by the job +function volumeExists(volumeMount: kubernetes.V1VolumeMount, volumes: kubernetes.V1Volume[]): boolean { + for (let v of volumes) { + if (volumeMount.name === v.name) { + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/brigade-worker/test/k8s.ts b/brigade-worker/test/k8s.ts index 62ffd03d3..5ff846748 100644 --- a/brigade-worker/test/k8s.ts +++ b/brigade-worker/test/k8s.ts @@ -1,5 +1,5 @@ import "mocha"; -import { assert } from "chai"; +import { assert, expect } from "chai"; import * as mock from "./mock"; import * as k8s from "../src/k8s"; @@ -8,21 +8,21 @@ import { Job, Result, brigadeCachePath, brigadeStoragePath } from "@brigadecore/ import * as kubernetes from "@kubernetes/client-node"; -describe("k8s", function() { +describe("k8s", function () { describe("b64enc", () => { - it('encodes the string "hello"', function() { + it('encodes the string "hello"', function () { assert.equal(k8s.b64enc("hello"), "aGVsbG8="); }); }); describe("b64dec", () => { - it('decodes the string "aGVsbG8="', function() { + it('decodes the string "aGVsbG8="', function () { assert.equal(k8s.b64dec("aGVsbG8="), "hello"); }); }); - describe("secretToProjectVCS", function() { - it("converts secret to project - with a VCS", function() { + describe("secretToProjectVCS", function () { + it("converts secret to project - with a VCS", function () { let s = mockSecretVCS(); let p = k8s.secretToProject("default", s); assert.equal( @@ -44,8 +44,8 @@ describe("k8s", function() { assert.equal(p.kubernetes.cacheStorageClass, "tashtego"); assert.equal(p.kubernetes.buildStorageClass, "tashtego"); }); - describe("when cloneURL is missing", function() { - it("omits cloneURL", function() { + describe("when cloneURL is missing", function () { + it("omits cloneURL", function () { let s = mockSecretVCS(); s.data.cloneURL = ""; let p = k8s.secretToProject("default", s); @@ -66,8 +66,8 @@ describe("k8s", function() { }); }); - describe("secretToProjectnoVCS", function() { - it("converts secret to project - without a VCS", function() { + describe("secretToProjectnoVCS", function () { + it("converts secret to project - without a VCS", function () { let s = mockSecretnoVCS(); let p = k8s.secretToProject("default", s); assert.equal( @@ -86,17 +86,17 @@ describe("k8s", function() { }); }); - describe("JobRunner", function() { - describe("when constructed", function() { + describe("JobRunner", function () { + describe("when constructed", function () { let j: Job; let p: Project; let e: BrigadeEvent; - beforeEach(function() { + beforeEach(function () { j = new mock.MockJob("pequod", "whaler", ["echo hello"]); p = mock.mockProject(); e = mock.mockEvent(); }); - it("creates Kubernetes objects from a job, event, and project", function() { + it("creates Kubernetes objects from a job, event, and project", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.equal(jr.name, `pequod-${e.buildID}`); @@ -113,12 +113,12 @@ describe("k8s", function() { assert.isNotNull(jr.runner.spec.containers[0].command); assert.property(jr.secret.data, "main.sh"); }); - context("when env vars are specified", function() { - context("as data", function() { - beforeEach(function() { + context("when env vars are specified", function () { + context("as data", function () { + beforeEach(function () { j.env = { one: "first", two: "second" }; }); - it("sets them on the pod", function() { + it("sets them on the pod", function () { let jr = new k8s.JobRunner().init(j, e, p); let found = 0; @@ -135,8 +135,8 @@ describe("k8s", function() { assert.equal(found, 2); }); }); - context("as references", function() { - beforeEach(function() { + context("as references", function () { + beforeEach(function () { j.env = { one: { secretKeyRef: { @@ -152,7 +152,7 @@ describe("k8s", function() { } as kubernetes.V1EnvVarSource }; }); - it("sets them on the pod", function() { + it("sets them on the pod", function () { let jr = new k8s.JobRunner().init(j, e, p); let found = 0; @@ -167,8 +167,8 @@ describe("k8s", function() { assert.equal(found, 2); }); }); - context("as references with allowSecretKeyRef false", function() { - beforeEach(function() { + context("as references with allowSecretKeyRef false", function () { + beforeEach(function () { j.env = { one: { secretKeyRef: { @@ -184,7 +184,7 @@ describe("k8s", function() { } as kubernetes.V1EnvVarSource }; }); - it("sets them on the pod", function() { + it("sets them on the pod", function () { let jr = new k8s.JobRunner().init(j, e, p, false); let found = 0; @@ -200,14 +200,14 @@ describe("k8s", function() { }); }); }); - context("when resources are specified", function() { - beforeEach(function() { + context("when resources are specified", function () { + beforeEach(function () { j.resourceRequests.cpu = "250m"; j.resourceRequests.memory = "512Mi"; j.resourceLimits.cpu = "500m"; j.resourceLimits.memory = "1Gi"; }); - it("sets resource requests and limits for the container pod", function() { + it("sets resource requests and limits for the container pod", function () { let jr = new k8s.JobRunner().init(j, e, p); let expResources = new kubernetes.V1ResourceRequirements(); expResources.requests = { cpu: "250m", memory: "512Mi" }; @@ -218,63 +218,63 @@ describe("k8s", function() { ); }); }); - context("when service account is specified", function() { - beforeEach(function() { + context("when service account is specified", function () { + beforeEach(function () { j.serviceAccount = "svcAccount"; }); - it("sets a service account name for the pod", function() { + it("sets a service account name for the pod", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.equal(jr.runner.spec.serviceAccountName, "svcAccount"); }); }); - context("when no service account is specified", function() { - it("sets a service account name to 'brigade-worker'", function() { + context("when no service account is specified", function () { + it("sets a service account name to 'brigade-worker'", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.equal(jr.runner.spec.serviceAccountName, "brigade-worker"); }); }); - context("when custom service account is specified", function() { - it("sets a service account name to 'custom-worker'", function() { + context("when custom service account is specified", function () { + it("sets a service account name to 'custom-worker'", function () { k8s.options.serviceAccount = "custom-worker"; let jr = new k8s.JobRunner().init(j, e, p); assert.equal(jr.runner.spec.serviceAccountName, "custom-worker"); }); }); - context("when args are supplied", function() { - beforeEach(function() { + context("when args are supplied", function () { + beforeEach(function () { j.tasks = []; j.args = ["--aye", "-j", "kay"]; }); - it("adds container args", function() { + it("adds container args", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.equal(jr.runner.spec.containers[0].args.length, 3); assert.notProperty(jr.secret.data, "main.sh"); }); }); - context("when no args are supplied", function() { - beforeEach(function() { + context("when no args are supplied", function () { + beforeEach(function () { j.args = []; }); - it("has no container args", function() { + it("has no container args", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.notProperty(jr.runner.spec.containers[0], "args"); }); }); - context("when no tasks are supplied", function() { - beforeEach(function() { + context("when no tasks are supplied", function () { + beforeEach(function () { j.tasks = []; }); - it("omits commands", function() { + it("omits commands", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.isNull(jr.runner.spec.containers[0].command); assert.notProperty(jr.secret.data, "main.sh"); }); }); - context("when useSource is set to false", function() { - beforeEach(function() { + context("when useSource is set to false", function () { + beforeEach(function () { j.tasks = []; }); - it("omits init container", function() { + it("omits init container", function () { j.useSource = false; let jr = new k8s.JobRunner().init(j, e, p); // Currently, annotations are only created if the init container @@ -282,11 +282,11 @@ describe("k8s", function() { assert.deepEqual(jr.runner.metadata.annotations, {}); }); }); - context("when no cloneURL is set", function() { - beforeEach(function() { + context("when no cloneURL is set", function () { + beforeEach(function () { j.tasks = []; }); - it("omits init container", function() { + it("omits init container", function () { p.repo.cloneURL = null; let jr = new k8s.JobRunner().init(j, e, p); // Currently, annotations are only created if the init container @@ -294,11 +294,11 @@ describe("k8s", function() { assert.deepEqual(jr.runner.metadata.annotations, {}); }); }); - context("when SSH key is provided", function() { - beforeEach(function() { + context("when SSH key is provided", function () { + beforeEach(function () { p.repo.sshKey = "SUPER SECRET"; }); - it("attaches key to pod", function() { + it("attaches key to pod", function () { let jr = new k8s.JobRunner().init(j, e, p); let sidecar = jr.runner.spec.initContainers[0]; assert.equal(sidecar.env.length, 14); @@ -313,22 +313,22 @@ describe("k8s", function() { assert.isTrue(hasBrigadeRepoKey, "Has BRIGADE REPO KEY as param"); }); }); - context("when sidecar is disabled", function() { - beforeEach(function() { + context("when sidecar is disabled", function () { + beforeEach(function () { p.kubernetes.vcsSidecar = ""; }); - it("job runner should have no init containers", function() { + it("job runner should have no init containers", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.equal(jr.runner.spec.initContainers.length, 0); }); - it("job runner should have no sidecar volumes", function() { + it("job runner should have no sidecar volumes", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.notDeepInclude( jr.runner.spec.volumes, { name: "vcs-sidecar", emptyDir: {} } as kubernetes.V1Volume ); }); - it("job runner should have no sidecar volume mounts", function() { + it("job runner should have no sidecar volume mounts", function () { let jr = new k8s.JobRunner().init(j, e, p); assert.notDeepInclude( jr.runner.spec.containers[0].volumeMounts, @@ -336,11 +336,11 @@ describe("k8s", function() { ); }); }); - context("when mount path is supplied", function() { - beforeEach(function() { + context("when mount path is supplied", function () { + beforeEach(function () { j.mountPath = "/ahab"; }); - it("mounts the provided path", function() { + it("mounts the provided path", function () { let jr = new k8s.JobRunner().init(j, e, p); for (let v of jr.runner.spec.containers[0].volumeMounts) { if (v.name == "vcs-sidecar") { @@ -349,12 +349,12 @@ describe("k8s", function() { } }); }); - context("when cache is enabled", function() { - beforeEach(function() { + context("when cache is enabled", function () { + beforeEach(function () { j.cache.enabled = true; j.storage.enabled = true; }); - it("configures volumes", function() { + it("configures volumes", function () { // We uppercase to test that names are correctly downcased. Issue #224 j.name = j.name.toUpperCase(); let jr = new k8s.JobRunner().init(j, e, p); @@ -393,7 +393,7 @@ describe("k8s", function() { assert.isTrue(foundCache, "expected cache volume claim found"); assert.isTrue(foundStorage, "expected storage volume claim found"); }); - it("configures volumes with custom paths", function() { + it("configures volumes with custom paths", function () { j.cache.path = "/cache"; j.cache.enabled = true; j.storage.path = "/storage"; @@ -420,11 +420,11 @@ describe("k8s", function() { assert.isTrue(foundStorage, "expected storage volume mount found"); }); }); - context("when the project has enabled host mounts", function() { - beforeEach(function() { + context("when the project has enabled host mounts", function () { + beforeEach(function () { p.allowHostMounts = true; }); - it("allows jobs to mount the host's docker socket", function() { + it("allows jobs to mount the host's docker socket", function () { j.docker.enabled = true; let jr = new k8s.JobRunner().init(j, e, p); for (let c of jr.runner.spec.containers) { @@ -439,11 +439,11 @@ describe("k8s", function() { assert.equal(vol.hostPath.path, "/var/run/docker.sock"); }); }); - context("when the project has disabled host mounts", function() { - beforeEach(function() { + context("when the project has disabled host mounts", function () { + beforeEach(function () { p.allowHostMounts = false; }); - it("does not allow jobs to mount the host's docker socket", function() { + it("does not allow jobs to mount the host's docker socket", function () { j.docker.enabled = true; let jr = new k8s.JobRunner().init(j, e, p); for (let c of jr.runner.spec.containers) { @@ -452,8 +452,132 @@ describe("k8s", function() { assert.equal(jr.runner.spec.volumes.length, 2); }); }); - context("when job is privileged", function() { - it("privileges containers", function() { + context("when a hostPath type volume is set for a job", function () { + beforeEach(function () { + var v = new kubernetes.V1Volume(); + v.name = "mock-volume"; + v.hostPath = { + path: "/some/path", + type: "Directory" + }; + j.volumes.push(v); + }); + it("with allowHostMounts disabled, error is thrown", function () { + expect(() => new k8s.JobRunner().init(j, e, p)).to.throw(Error, "allowHostMounts is false in this project, not mounting /some/path"); + }); + it("with allowHostMounts enabled, no error is thrown", function () { + p.allowHostMounts = true; + expect(() => new k8s.JobRunner().init(j, e, p)).to.not.throw(Error); + }); + it("all properties are properly set", function () { + p.allowHostMounts = true; + expect(() => new k8s.JobRunner().init(j, e, p)).to.not.throw(Error); + let jr = new k8s.JobRunner().init(j, e, p); + + assert.equal(jr.runner.spec.volumes[2].name, "mock-volume"); + assert.equal(jr.runner.spec.volumes[2].hostPath.path, "/some/path"); + assert.equal(jr.runner.spec.volumes[2].hostPath.type, "Directory"); + }); + + it("and job enables Docker, all properties are properly set", function () { + p.allowHostMounts = true; + j.docker.enabled = true; + expect(() => new k8s.JobRunner().init(j, e, p)).to.not.throw(Error); + let jr = new k8s.JobRunner().init(j, e, p); + + assert.equal(jr.runner.spec.volumes[2].name, "docker-socket"); + assert.equal(jr.runner.spec.volumes[2].hostPath.path, "/var/run/docker.sock"); + + assert.equal(jr.runner.spec.volumes[3].name, "mock-volume"); + assert.equal(jr.runner.spec.volumes[3].hostPath.path, "/some/path"); + assert.equal(jr.runner.spec.volumes[3].hostPath.type, "Directory"); + }); + }); + + context("when a persistent volume type volume is set for a job", function () { + beforeEach(function () { + var v = new kubernetes.V1Volume(); + v.name = "mock-volume"; + v.persistentVolumeClaim = { + claimName: "some-claim" + }; + j.volumes.push(v); + }); + it("with allowHostMounts disabled, no error is thrown", function () { + expect(() => new k8s.JobRunner().init(j, e, p)).to.not.throw(Error); + }); + + it("with allowHostMounts enabled, no error is thrown", function () { + p.allowHostMounts = true; + expect(() => new k8s.JobRunner().init(j, e, p)).to.not.throw(Error); + }); + it("all properties are properly set", function () { + expect(() => new k8s.JobRunner().init(j, e, p)).to.not.throw(Error); + let jr = new k8s.JobRunner().init(j, e, p); + + assert.equal(jr.runner.spec.volumes[2].name, "mock-volume"); + assert.equal(jr.runner.spec.volumes[2].persistentVolumeClaim.claimName, "some-claim"); + }); + it("and job enables Docker, all properties are properly set", function () { + p.allowHostMounts = true; + j.docker.enabled = true; + expect(() => new k8s.JobRunner().init(j, e, p)).to.not.throw(Error); + let jr = new k8s.JobRunner().init(j, e, p); + + assert.equal(jr.runner.spec.volumes[2].name, "docker-socket"); + assert.equal(jr.runner.spec.volumes[2].hostPath.path, "/var/run/docker.sock"); + + assert.equal(jr.runner.spec.volumes[3].name, "mock-volume"); + assert.equal(jr.runner.spec.volumes[3].persistentVolumeClaim.claimName, "some-claim"); + }); + }); + + context("when volumeMounts is set for a job", function () { + beforeEach(function () { + var v = new kubernetes.V1Volume(); + v.name = "mock-volume"; + v.persistentVolumeClaim = { + claimName: "some-claim" + }; + j.volumes.push(v); + + var m = new kubernetes.V1VolumeMount(); + m.name = "mock-volume" + m.mountPath = "/mock/volume"; + j.volumeMounts.push(m); + }); + it("the volume mounts are set in all containers", function () { + let jr = new k8s.JobRunner().init(j, e, p); + for (let c of jr.runner.spec.containers) { + assert.equal(c.volumeMounts[2].mountPath, "/mock/volume"); + assert.equal(c.volumeMounts[2].name, "mock-volume"); + } + }); + it("and Docker is enabled for the job, the volume mounts are set in all containers", function () { + p.allowHostMounts = true; + j.docker.enabled = true; + let jr = new k8s.JobRunner().init(j, e, p); + for (let c of jr.runner.spec.containers) { + assert.equal(c.volumeMounts[2].mountPath, "/var/run/docker.sock"); + assert.equal(c.volumeMounts[2].name, "docker-socket"); + + assert.equal(c.volumeMounts[3].mountPath, "/mock/volume"); + assert.equal(c.volumeMounts[3].name, "mock-volume"); + } + }); + }); + context("when a volumeMount is set for a job, but the referenced volume does not exist", function () { + it("error is thrown", function () { + var m = new kubernetes.V1VolumeMount(); + m.name = "mock-volume" + m.mountPath = "/mock/volume"; + j.volumeMounts.push(m); + + expect(() => new k8s.JobRunner().init(j, e, p)).to.throw(Error, "volume mock-volume referenced in volume mount is not defined"); + }); + }); + context("when job is privileged", function () { + it("privileges containers", function () { j.privileged = true; let jr = new k8s.JobRunner().init(j, e, p); for (let c of jr.runner.spec.containers) { @@ -461,11 +585,11 @@ describe("k8s", function() { } }); }); - context("when the project has privileged mode disabled", function() { - beforeEach(function() { + context("when the project has privileged mode disabled", function () { + beforeEach(function () { p.allowPrivilegedJobs = false; }); - it("does not allow privileged jobs", function() { + it("does not allow privileged jobs", function () { j.privileged = true; let jr = new k8s.JobRunner().init(j, e, p); for (let c of jr.runner.spec.containers) { @@ -473,8 +597,8 @@ describe("k8s", function() { } }); }); - context("when image pull secrets are supplied", function() { - it("sets imagePullSecrets", function() { + context("when image pull secrets are supplied", function () { + it("sets imagePullSecrets", function () { j.imagePullSecrets = ["one", "two"]; let jr = new k8s.JobRunner().init(j, e, p); assert.equal(jr.runner.spec.imagePullSecrets.length, 2); @@ -484,8 +608,8 @@ describe("k8s", function() { } }); }); - context("when a host os is supplied", function() { - it("sets a node selector", function() { + context("when a host os is supplied", function () { + it("sets a node selector", function () { j.host.os = "windows"; let jr = new k8s.JobRunner().init(j, e, p); assert.equal( @@ -494,15 +618,15 @@ describe("k8s", function() { ); }); }); - context("when a host name is supplied", function() { - it("sets a node name", function() { + context("when a host name is supplied", function () { + it("sets a node name", function () { j.host.name = "aciBridge"; let jr = new k8s.JobRunner().init(j, e, p); assert.equal("aciBridge", jr.runner.spec.nodeName); }); }); - context("when host nodeSelector are supplied", function() { - it("sets a node selector", function() { + context("when host nodeSelector are supplied", function () { + it("sets a node selector", function () { j.host.nodeSelector.set("inn", "spouter"); j.host.nodeSelector.set("ship", "pequod"); let jr = new k8s.JobRunner().init(j, e, p); @@ -510,14 +634,14 @@ describe("k8s", function() { assert.equal("pequod", jr.runner.spec.nodeSelector["ship"]); }); }); - context("when vcsSidecar resources defined", function() { - beforeEach(function() { + context("when vcsSidecar resources defined", function () { + beforeEach(function () { p.kubernetes.vcsSidecarResourcesLimitsCPU = "100m"; p.kubernetes.vcsSidecarResourcesLimitsMemory = "100Mi"; p.kubernetes.vcsSidecarResourcesRequestsCPU = "50m"; p.kubernetes.vcsSidecarResourcesRequestsMemory = "50Mi"; }); - it("sets resource requests and limits for the init-container pod", function() { + it("sets resource requests and limits for the init-container pod", function () { let jr = new k8s.JobRunner().init(j, e, p); let expResources = new kubernetes.V1ResourceRequirements(); expResources.limits = { cpu: "100m", memory: "100Mi" }; @@ -528,12 +652,12 @@ describe("k8s", function() { ); }); }); - context("when vcsSidecar only cpu resources defined", function() { - beforeEach(function() { + context("when vcsSidecar only cpu resources defined", function () { + beforeEach(function () { p.kubernetes.vcsSidecarResourcesLimitsCPU = "100m"; p.kubernetes.vcsSidecarResourcesRequestsCPU = "50m"; }); - it("sets only cpu resource requests and limits for the init-container pod", function() { + it("sets only cpu resource requests and limits for the init-container pod", function () { let jr = new k8s.JobRunner().init(j, e, p); let expResources = new kubernetes.V1ResourceRequirements(); expResources.limits = { cpu: "100m" }; @@ -544,12 +668,12 @@ describe("k8s", function() { ); }); }); - context("when vcsSidecar only memory resources defined", function() { - beforeEach(function() { + context("when vcsSidecar only memory resources defined", function () { + beforeEach(function () { p.kubernetes.vcsSidecarResourcesLimitsMemory = "100Mi"; p.kubernetes.vcsSidecarResourcesRequestsMemory = "50Mi"; }); - it("sets only memory resource requests and limits for the init-container pod", function() { + it("sets only memory resource requests and limits for the init-container pod", function () { let jr = new k8s.JobRunner().init(j, e, p); let expResources = new kubernetes.V1ResourceRequirements(); expResources.limits = { memory: "100Mi" }; @@ -560,36 +684,36 @@ describe("k8s", function() { ); }); }); - context("when no job shell is specified", function() { - it("default shell is /bin/sh", function() { + context("when no job shell is specified", function () { + it("default shell is /bin/sh", function () { let jr = new k8s.JobRunner().init(j, e, p); - assert.deepEqual(jr.runner.spec.containers[0].command, [ '/bin/sh', '/hook/main.sh' ]); + assert.deepEqual(jr.runner.spec.containers[0].command, ['/bin/sh', '/hook/main.sh']); }); }); - context("when job shell is specified", function() { - beforeEach(function() { + context("when job shell is specified", function () { + beforeEach(function () { j.shell = "/bin/bash" }); - it("shell is /bin/bash", function() { + it("shell is /bin/bash", function () { let jr = new k8s.JobRunner().init(j, e, p); - assert.deepEqual(jr.runner.spec.containers[0].command, [ '/bin/bash', '/hook/main.sh' ]); + assert.deepEqual(jr.runner.spec.containers[0].command, ['/bin/bash', '/hook/main.sh']); }); }); }); describe("cachePVC", () => { let jr: k8s.JobRunner; - beforeEach(function() { + beforeEach(function () { let j = new mock.MockJob("pequod", "whaler", ["echo hello"]); let p = mock.mockProject(); let e = mock.mockEvent(); jr = new k8s.JobRunner().init(j, e, p); }); context("when global default cache storage class is specified", () => { - beforeEach(function() { + beforeEach(function () { jr.options.defaultCacheStorageClass = "foo"; }); context("when the cache storage class is overridden at the project level", () => { - beforeEach(function() { + beforeEach(function () { jr.project.kubernetes.cacheStorageClass = "bar"; }); it("it uses that", () => { @@ -598,7 +722,7 @@ describe("k8s", function() { }); }); context("when the cache storage class is not overridden at the project level", () => { - beforeEach(function() { + beforeEach(function () { jr.project.kubernetes.cacheStorageClass = ""; }); it("it falls back on the global default", () => { @@ -608,11 +732,11 @@ describe("k8s", function() { }); }); context("when global default cache storage class is not specified", () => { - beforeEach(function() { + beforeEach(function () { jr.options.defaultCacheStorageClass = ""; }); context("when the cache storage class is overridden at the project level", () => { - beforeEach(function() { + beforeEach(function () { jr.project.kubernetes.cacheStorageClass = "bar"; }); it("it uses that", () => { @@ -621,7 +745,7 @@ describe("k8s", function() { }); }); context("when the cache storage class is not overridden at the project level", () => { - beforeEach(function() { + beforeEach(function () { jr.project.kubernetes.cacheStorageClass = ""; }); it("it falls back on the cluster default", () => { @@ -637,16 +761,16 @@ describe("k8s", function() { describe("BuildStorage", () => { describe("buildPVC", () => { let bs: k8s.BuildStorage; - beforeEach(function() { + beforeEach(function () { bs = new k8s.BuildStorage(); bs.proj = mock.mockProject(); }); context("when global default build storage class is specified", () => { - beforeEach(function() { + beforeEach(function () { bs.options.defaultBuildStorageClass = "foo"; }); context("when the build storage class is overridden at the project level", () => { - beforeEach(function() { + beforeEach(function () { bs.proj.kubernetes.buildStorageClass = "bar"; }); it("it uses that", () => { @@ -655,7 +779,7 @@ describe("k8s", function() { }); }); context("when the build storage class is not overridden at the project level", () => { - beforeEach(function() { + beforeEach(function () { bs.proj.kubernetes.buildStorageClass = ""; }); it("it falls back on the global default", () => { @@ -665,11 +789,11 @@ describe("k8s", function() { }); }); context("when global default build storage class is not specified", () => { - beforeEach(function() { + beforeEach(function () { bs.options.defaultBuildStorageClass = ""; }); context("when the build storage class is overridden at the project level", () => { - beforeEach(function() { + beforeEach(function () { bs.proj.kubernetes.buildStorageClass = "bar"; }); it("it uses that", () => { @@ -678,7 +802,7 @@ describe("k8s", function() { }); }); context("when the build storage class is not overridden at the project level", () => { - beforeEach(function() { + beforeEach(function () { bs.proj.kubernetes.buildStorageClass = ""; }); it("it falls back on the cluster default", () => { @@ -742,3 +866,7 @@ function mockSecretnoVCS(): kubernetes.V1Secret { return s; } + +function checkObjWithVal(prop, val, arr: Array): boolean { + return arr.some(obj => obj[prop] === val); +} \ No newline at end of file diff --git a/brigade-worker/yarn.lock b/brigade-worker/yarn.lock index 9dcb0f793..778c0c2f2 100644 --- a/brigade-worker/yarn.lock +++ b/brigade-worker/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@brigadecore/brigadier@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@brigadecore/brigadier/-/brigadier-0.5.0.tgz#5d50dbb26c78e7ba53e07d904f1cd2983db00414" - integrity sha512-xx0vnod7Lb69Lwv3SrnBys6IIJGeeDmBl/C2MktNnN13Yu/EJhE3nFU1u0lstUXwgJNxccZv9wGV2tFVl9H9WQ== +"@brigadecore/brigadier@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@brigadecore/brigadier/-/brigadier-0.6.1.tgz#deb4174de9314c5997521a1b3e2a7c6bcfeddc68" + integrity sha512-95U96JnRhobKZUMJtTVtT0+EWQ8zgJw7MAFYdzd0K4ajkGZAahJdXGbFrey4R3JZbcJZhwCh3Ny+pLP7EjLgCQ== dependencies: "@kubernetes/client-node" "^0.10.1" child-process-promise "^2.2.1" diff --git a/docs/content/topics/javascript.md b/docs/content/topics/javascript.md index bc799097f..2add48d78 100644 --- a/docs/content/topics/javascript.md +++ b/docs/content/topics/javascript.md @@ -213,7 +213,8 @@ Properties of `Job` - `resourceRequests: JobResourceRequest`: CPU and memory request resources for the job pod container. - `resourceLimits: JobResourceLimit`: CPU and memory limit resources for the job pod container. - `streamLogs: boolean`: controls whether logs from the job Pod will be streamed to output (similar functionality to `kubectl logs PODNAME -f`). - +- `volumes: kubernetes.V1Volume[]`: list of Kubernetes volumes to be attached to the job pod specification. See the [Kubernetes type definition](https://github.com/kubernetes-client/javascript/blob/159b32d2cd96117eb19342190c6bc3fa9bc8e3eb/src/gen/model/v1Volume.ts#L44) +- `volumeMounts: kubernetes.V1VolumeMount[]`: list of Kubernetes volume mounts to be attached to all containers in the job pod specification. See the [Kubernetes type definition](https://github.com/kubernetes-client/javascript/blob/159b32d2cd96117eb19342190c6bc3fa9bc8e3eb/src/gen/model/v1VolumeMount.ts#L17) #### Setting execution resources to a job For some jobs is a good practice to set limits and guarantee some resources. In the following example job pod container resource requests and limits are set. diff --git a/docs/content/topics/scripting.md b/docs/content/topics/scripting.md index 82d2cc5f0..92773008d 100644 --- a/docs/content/topics/scripting.md +++ b/docs/content/topics/scripting.md @@ -1160,6 +1160,67 @@ docker.run() In the above, if the docker credentials are set for the project, a `docker push` is performed on the image just built. +#### Attaching volumes and volume mounts to jobs + +Build storage and job cache represent a very simple and convenient way that Brigade exposes +for attaching storage to your jobs. But if your job requires the mounting of an existing +[Kubernetes volume](https://kubernetes.io/docs/concepts/storage/volumes/), the Brigade JavaScript +API exposes two propeties on the `Job` class: + +- `volumes`: list of Kubernetes volumes to be attached to the pod specification. Supports all [Kubernetes +volume types](https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes) supported by your cluster configuration. Volumes are referenced by name in the `volumeMounts` property. +To reference a volume of type `hostPath`, the Brigade project must allow host mounts. +- `volumeMounts`: list of Kubernetes volume mounts to be attached to all containers in the job pod specification, referenced +by their names. Volumes referenced here must be defined in the `volumes` property. + +> Note: simple use cases for build storage or job caches should still use the existing Brigade properties for +> enabling the storage and cache. These properties should only be used in advanced scenarios that require mounting +> Kubernetes volumes. + +> This functionality was introduced with Brigade 1.2, and is in experimental state. + +Example: + +```javascript + var j = new Job("some-image"); + j.volumes = [ + { + name: "modules", + hostPath: { + path: "/lib/modules", + type: "Directory" + } + }, + { + name: "cgroup", + hostPath: { + path: "/sys/fs/cgroup", + type: "Directory" + } + }, + { + name: "docker-graph-storage", + emptyDir: {} + } + ]; + + j.volumeMounts = [ + { + name: "modules", + mountPath: "/lib/modules", + readOnly: true + }, + { + name: "cgroup", + mountPath: "/sys/fs/cgroup" + }, + { + name: "docker-graph-storage", + mountPath: "/var/lib/docker" + } + ]; +``` + ## Jobs and Return Values We have seen already that when we run a job, it will return a JavaScript Promise.