├── .gitattributes ├── bin ├── postInstall.d.ts ├── postInstall.d.ts.map ├── postInstall.js.map ├── postInstall.ts ├── postInstall.js └── start.ts ├── .atomist ├── config.json └── homebrew │ └── atomist-cli.rb ├── .gitignore ├── .dockerignore ├── .npmignore ├── Dockerfile ├── tsconfig.json ├── assets ├── bash_completion │ └── atomist └── kubectl │ └── cli.yaml ├── lib ├── gitHook.ts ├── gql.ts ├── print.ts ├── version.ts ├── execute.ts ├── start.ts ├── cliConfig.ts ├── gqlFetch.ts ├── kubeUtils.ts ├── kubeFetch.ts ├── command.ts ├── kubeEdit.ts ├── updateSdm.ts ├── install.ts ├── kubeCrypt.ts ├── spawn.ts ├── repositoryStart.ts ├── kubeInstall.ts └── config.ts ├── test ├── config.test.ts ├── gql.test.ts ├── spawn.test.ts ├── kubeCrypt.test.ts ├── kubeDecrypt.cli.test.ts ├── kubeEncrypt.cli.test.ts ├── kubeUtils.test.ts ├── command.test.ts └── kubeEdit.test.ts ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── package.json ├── CONTRIBUTING.md ├── README.md ├── tslint.json ├── LICENSE ├── CHANGELOG.md └── index.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | legal/THIRD_PARTY.md linguist-generated=true 2 | -------------------------------------------------------------------------------- /bin/postInstall.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export {}; 3 | //# sourceMappingURL=postInstall.d.ts.map -------------------------------------------------------------------------------- /.atomist/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "docker": { 3 | "tag": { 4 | "latest": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /bin/postInstall.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"postInstall.d.ts","sourceRoot":"","sources":["postInstall.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | .vscode/ 4 | *~ 5 | .#* 6 | .npmrc 7 | node_modules/ 8 | *.d.ts 9 | *.d.ts.map 10 | *.js 11 | *.js.map 12 | *.log 13 | *.txt 14 | /.nyc_output/ 15 | /build/ 16 | /coverage/ 17 | /doc/ 18 | /log/ 19 | git-info.json 20 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | .vscode/ 4 | **/*~ 5 | **/.#* 6 | .git* 7 | .npm* 8 | .travis.yml 9 | .atomist/ 10 | .nyc_output/ 11 | assets/kubectl/ 12 | node_modules/ 13 | build/ 14 | coverage/ 15 | doc/ 16 | log/ 17 | scripts/ 18 | src/ 19 | test/ 20 | CO*.md 21 | ts*.json 22 | **/*.log 23 | **/*.txt 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | .vscode/ 4 | *~ 5 | .#* 6 | .dockerignore 7 | .git* 8 | .npmrc* 9 | .travis.yml 10 | .atomist/ 11 | .nyc_output/ 12 | /build/ 13 | /doc/ 14 | /config/ 15 | /coverage/ 16 | /log/ 17 | /scripts/ 18 | /src/ 19 | /test/ 20 | /CO*.md 21 | /Dockerfile 22 | /assets/kubectl/ 23 | *.log 24 | *.txt 25 | *.ts 26 | !*.d.ts 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11 2 | 3 | LABEL maintainer="Atomist " 4 | 5 | RUN mkdir -p /opt/app 6 | 7 | WORKDIR /opt/app 8 | 9 | ENV NPM_CONFIG_LOGLEVEL warn 10 | 11 | ENV SUPPRESS_NO_CONFIG_WARNING true 12 | 13 | ENTRYPOINT ["node", "index.js"] 14 | 15 | RUN npm install -g npm 16 | 17 | COPY package.json package-lock.json ./ 18 | 19 | RUN npm ci --only=production 20 | 21 | COPY . . 22 | -------------------------------------------------------------------------------- /bin/postInstall.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"postInstall.js","sourceRoot":"","sources":["postInstall.ts"],"names":[],"mappings":";;AACA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;AAEH,mEAAmE;AACnE,mEAAmE;AACnE,gEAAgE;AAChE,aAAa;AAEb,gFAA8E;AAC9E,+BAA+B;AAE/B,SAAS,QAAQ,CAAC,CAAQ;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,SAAe,IAAI;;QAEf,IAAI;YAEA,IAAI,EAAE,CAAC,UAAU,CAAC,8BAAc,EAAE,CAAC,EAAE;gBACjC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;aACnB;YAED,mDAAmD;YACnD,MAAM,MAAM,GAAG;;;;;;;;;CAStB,CAAC;YACM,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;SAEhC;QAAC,OAAO,CAAC,EAAE;YACR,QAAQ,CAAC,CAAC,CAAC,CAAC;SACf;IACL,CAAC;CAAA;AAED,kEAAkE;AAClE,IAAI,EAAE;KACD,KAAK,CAAC,CAAC,CAAC,EAAE;IACP,QAAQ,CAAC,CAAC,CAAC,CAAC;AAChB,CAAC,CAAC;KACD,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "newLine": "LF", 4 | "target": "ES2016", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "jsx": "React", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "lib": [ 12 | "DOM", 13 | "ES2017", 14 | "DOM.Iterable", 15 | "ScriptHost", 16 | "esnext.asynciterable" 17 | ], 18 | "strict": true, 19 | "strictNullChecks": false, 20 | "forceConsistentCasingInFileNames": true, 21 | "noImplicitReturns": true, 22 | "noUnusedLocals": true, 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true 25 | }, 26 | "include": ["index.ts", "bin/*.ts", "lib/**/*.ts", "test/**/*.ts"], 27 | "exclude": [".#*"], 28 | "compileOnSave": true, 29 | "buildOnSave": false, 30 | "atom": { 31 | "rewriteTsconfig": false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assets/bash_completion/atomist: -------------------------------------------------------------------------------- 1 | ###-begin-atomist-completions-### 2 | # 3 | # yargs command completion script 4 | # 5 | # Source this file from your ~/.bashrc or put it under your 6 | # /etc/bash_completion.d or /usr/local/etc/bash_completion.d directory 7 | # 8 | _atomist_yargs_completions() 9 | { 10 | local cur_word args type_list 11 | 12 | cur_word="${COMP_WORDS[COMP_CWORD]}" 13 | args=("${COMP_WORDS[@]}") 14 | 15 | # ask yargs to generate completions. 16 | type_list=$(atomist --get-yargs-completions "${args[@]}") 17 | 18 | COMPREPLY=( $(compgen -W "${type_list}" -- ${cur_word}) ) 19 | 20 | # if no match was found, fall back to filename completion 21 | if [ ${#COMPREPLY[@]} -eq 0 ]; then 22 | COMPREPLY=( $(compgen -f -- "${cur_word}" ) ) 23 | fi 24 | 25 | return 0 26 | } 27 | complete -F _atomist_yargs_completions atomist 28 | complete -F _atomist_yargs_completions @atomist 29 | ###-end-atomist-completions-### 30 | -------------------------------------------------------------------------------- /lib/gitHook.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as print from "./print"; 18 | 19 | /** 20 | * Generate git-info.json for automation client. 21 | * 22 | * @param opts see GitOptions 23 | * @return integer return value 24 | */ 25 | export async function gitHook(args: string[]): Promise { 26 | try { 27 | const sdmLocal = require("@atomist/sdm-local"); 28 | await sdmLocal.runOnGitHook(args); 29 | } catch (e) { 30 | print.error(`Failed to process Git hook: ${e.message}`); 31 | return 1; 32 | } 33 | return 0; 34 | } 35 | -------------------------------------------------------------------------------- /test/config.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as assert from "power-assert"; 18 | 19 | import { maskString } from "../lib/config"; 20 | 21 | describe("config", () => { 22 | 23 | describe("maskString", () => { 24 | 25 | it("should mask the string", () => { 26 | const s = "thebottlerockets"; 27 | const m = maskString(s); 28 | const e = "t**************s"; 29 | assert(m === e); 30 | }); 31 | 32 | it("should mask the entire short string", () => { 33 | const s = "bottle"; 34 | const m = maskString(s); 35 | const e = "******"; 36 | assert(m === e); 37 | }); 38 | 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /lib/gql.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fs from "fs-extra"; 18 | import * as path from "path"; 19 | 20 | /** 21 | * Figure out whether the lib directory is named lib or src. lib is 22 | * preferred, meaning if it exists, it is returned and if neither it 23 | * nor src exists, it is returned. 24 | * 25 | * @param cwd directory to use as base for location of lib dir 26 | * @return Resolved, full path to lib directory 27 | */ 28 | export function libDir(cwd: string): string { 29 | const lib = path.resolve(cwd, "lib"); 30 | const src = path.resolve(cwd, "src"); 31 | if (fs.existsSync(lib)) { 32 | return lib; 33 | } else if (fs.existsSync(src)) { 34 | return src; 35 | } else { 36 | return lib; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.atomist/homebrew/atomist-cli.rb: -------------------------------------------------------------------------------- 1 | require "language/node" 2 | 3 | class AtomistCli < Formula 4 | desc "The Atomist CLI" 5 | homepage "https://github.com/atomist/cli#readme" 6 | url "%URL%" 7 | sha256 "%SHA256%" 8 | 9 | bottle do 10 | end 11 | 12 | depends_on "node" 13 | 14 | def install 15 | system "npm", "install", *Language::Node.std_npm_install_args(libexec) 16 | bin.install_symlink Dir["#{libexec}/bin/*"] 17 | bash_completion.install "#{libexec}/lib/node_modules/@atomist/cli/assets/bash_completion/atomist" 18 | end 19 | 20 | test do 21 | assert_predicate bin/"atomist", :exist? 22 | assert_predicate bin/"atomist", :executable? 23 | assert_predicate bin/"@atomist", :exist? 24 | assert_predicate bin/"@atomist", :executable? 25 | 26 | run_output = shell_output("#{bin}/atomist 2>&1", 1) 27 | assert_match "Not enough non-option arguments", run_output 28 | assert_match "Specify --help for available options", run_output 29 | 30 | version_output = shell_output("#{bin}/atomist --version") 31 | assert_match "@atomist/cli", version_output 32 | assert_match "@atomist/sdm ", version_output 33 | assert_match "@atomist/sdm-core", version_output 34 | assert_match "@atomist/sdm-local", version_output 35 | 36 | skill_output = shell_output("#{bin}/atomist show skills") 37 | assert_match(/\d+ commands are available from \d+ connected SDMs/, skill_output) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/print.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** CLI package */ 18 | const pkg = "atomist"; 19 | 20 | /* tslint:disable:no-console */ 21 | 22 | /** 23 | * Print error message to stderr. 24 | * @param msg message to print 25 | */ 26 | export function error(msg: string): void { 27 | console.error(`${pkg}: [ERROR] ${msg}`); 28 | } 29 | 30 | /** 31 | * Print informational message to stdout. 32 | * @param msg message to print 33 | */ 34 | export function info(msg: string): void { 35 | console.info(`${pkg}: [INFO] ${msg}`); 36 | } 37 | 38 | /** 39 | * Print message to stdout. 40 | * @param msg message to print 41 | */ 42 | export function log(msg: string): void { 43 | console.log(msg); 44 | } 45 | 46 | /** 47 | * Print warning message to stdout. 48 | * @param msg message to print 49 | */ 50 | export function warn(msg: string): void { 51 | console.warn(`${pkg}: [WARN] ${msg}`); 52 | } 53 | -------------------------------------------------------------------------------- /lib/version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as _ from "lodash"; 18 | import * as readPkgUp from "read-pkg-up"; 19 | import * as print from "./print"; 20 | 21 | /** 22 | * Read package name and version from nearest package.json and return 23 | * standard CLI package and version string. 24 | * 25 | * @return standard version string 26 | */ 27 | export function version(): string { 28 | try { 29 | // must be sync because yargs.version only accepts a string 30 | const pj = readPkgUp.sync({ cwd: __dirname }); 31 | const dependencies: string[] = []; 32 | _.forEach(pj.package.dependencies, (v, k) => { 33 | if (k.startsWith("@atomist/")) { 34 | dependencies.push(`${k} ${v}`); 35 | } 36 | }); 37 | return `${pj.package.name} ${pj.package.version} 38 | 39 | ${dependencies.sort((d1, d2) => d1.localeCompare(d2)).join("\n")}`; 40 | } catch (e) { 41 | print.error(`Failed to read package.json: ${e.message}`); 42 | } 43 | return "@atomist/cli 0.0.0"; 44 | } 45 | -------------------------------------------------------------------------------- /test/gql.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fs from "fs-extra"; 18 | import * as path from "path"; 19 | import * as assert from "power-assert"; 20 | import * as tmp from "tmp-promise"; 21 | 22 | import { libDir } from "../lib/gql"; 23 | 24 | describe("gql", () => { 25 | 26 | describe("libDir", () => { 27 | 28 | it("should find lib dir", () => { 29 | const l = libDir(path.join(__dirname, "..")); 30 | const e = path.resolve(__dirname, "..", "lib"); 31 | assert(l === e); 32 | }); 33 | 34 | it("should find src dir", async () => { 35 | const t = await tmp.dir({ unsafeCleanup: true }); 36 | const e = path.join(t.path, "src"); 37 | await fs.ensureDir(e); 38 | const l = libDir(t.path); 39 | assert(l === e); 40 | await t.cleanup(); 41 | }); 42 | 43 | it("should return lib when there is no dir", () => { 44 | const l = libDir(__dirname); 45 | const e = path.resolve(__dirname, "lib"); 46 | assert(l === e); 47 | }); 48 | 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Atomist Open Source Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the 4 | Atomist Open Source projects as found on https://github.com/atomist. 5 | 6 | * [Reporting a Vulnerability](#reporting-a-vulnerability) 7 | * [Disclosure Policy](#disclosure-policy) 8 | 9 | ## Reporting a Vulnerability 10 | 11 | The Atomist OSS team and community take all security vulnerabilities 12 | seriously. Thank you for improving the security of our open source 13 | software. We appreciate your efforts and responsible disclosure and will 14 | make every effort to acknowledge your contributions. 15 | 16 | Report security vulnerabilities by emailing the Atomist security team at: 17 | 18 | security@atomist.com 19 | 20 | The lead maintainer will acknowledge your email within 24 hours, and will 21 | send a more detailed response within 48 hours indicating the next steps in 22 | handling your report. After the initial reply to your report, the security 23 | team will endeavor to keep you informed of the progress towards a fix and 24 | full announcement, and may ask for additional information or guidance. 25 | 26 | Report security vulnerabilities in third-party modules to the person or 27 | team maintaining the module. 28 | 29 | ## Disclosure Policy 30 | 31 | When the security team receives a security bug report, they will assign it 32 | to a primary handler. This person will coordinate the fix and release 33 | process, involving the following steps: 34 | 35 | * Confirm the problem and determine the affected versions. 36 | * Audit code to find any potential similar problems. 37 | * Prepare fixes for all releases still under maintenance. These fixes 38 | will be released as fast as possible to NPM. 39 | -------------------------------------------------------------------------------- /lib/execute.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { CommandInvocation } from "@atomist/automation-client"; 18 | import * as stringify from "json-stringify-safe"; 19 | import { extractArgs } from "./command"; 20 | import { spawnBinary } from "./spawn"; 21 | 22 | /** 23 | * Command-line options and arguments for execute. 24 | */ 25 | export interface ExecuteOptions { 26 | /** Name of command to run */ 27 | name: string; 28 | /** Directory to run command in, must be an automation client directory */ 29 | cwd?: string; 30 | /** If true, run `npm run compile` before running command */ 31 | compile?: boolean; 32 | /** If true or no node_modules directory exists, run "npm install" before running command */ 33 | install?: boolean; 34 | /** Unprocessed command-line arguments, typically provided by yargs._ */ 35 | args: string[]; 36 | } 37 | 38 | /** 39 | * Execute a command handler. 40 | * 41 | * @param opts see ExecuteOptions 42 | * @return integer return value 43 | */ 44 | export async function execute(opts: ExecuteOptions): Promise { 45 | const args = extractArgs(opts.args); 46 | const ci: CommandInvocation = { 47 | name: opts.name, 48 | args, 49 | }; 50 | const spawnOpts = { 51 | ...opts, 52 | command: "atm-command", 53 | args: [stringify(ci)], 54 | }; 55 | return spawnBinary(spawnOpts); 56 | } 57 | -------------------------------------------------------------------------------- /lib/start.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as path from "path"; 18 | import { 19 | spawnBinary, 20 | spawnJs, 21 | SpawnOptions, 22 | } from "./spawn"; 23 | 24 | /** 25 | * Command-line options for start. 26 | */ 27 | export type StartOptions = Pick & { 28 | local: boolean, 29 | profile: string, 30 | watch: boolean, 31 | debug: boolean, 32 | }; 33 | 34 | /** 35 | * Start automation client server process. 36 | * 37 | * @param opts see StartOptions 38 | * @return integer return value 39 | */ 40 | export async function start(opts: StartOptions): Promise { 41 | if (!!opts.local) { 42 | process.env.ATOMIST_MODE = "local"; 43 | } 44 | if (!!opts.profile) { 45 | process.env.ATOMIST_CONFIG_PROFILE = opts.profile; 46 | } 47 | delete process.env.ATOMIST_DISABLE_LOGGING; 48 | 49 | if (!opts.debug) { 50 | const spawnOpts = { 51 | ...opts, 52 | command: opts.watch ? "atm-start-dev" : "atm-start", 53 | args: [] as string[], 54 | }; 55 | return spawnBinary(spawnOpts); 56 | } else { 57 | const spawnOpts = { 58 | ...opts, 59 | command: path.join("bin", opts.watch ? "start-dev.js" : "start.js"), 60 | nodeArgs: ["--inspect"], 61 | }; 62 | return spawnJs(spawnOpts); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/cliConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Configuration } from "@atomist/automation-client"; 18 | import { 19 | getUserConfig, 20 | resolveWorkspaceIds, 21 | UserConfig, 22 | } from "@atomist/automation-client/lib/configuration"; 23 | import * as print from "./print"; 24 | 25 | export type CliConfig = Pick; 26 | 27 | /** 28 | * Read user config and resolve workspace IDs then simplify config 29 | * down to a CliConfig. 30 | * 31 | * @return CliConfig populated from user config 32 | */ 33 | export function resolveUserConfig(): UserConfig { 34 | let userConfig: UserConfig; 35 | try { 36 | userConfig = getUserConfig() || {}; 37 | } catch (e) { 38 | print.warn(`Failed to load user configuration, ignoring: ${e.message}`); 39 | userConfig = {}; 40 | } 41 | resolveWorkspaceIds(userConfig); 42 | if (!userConfig.workspaceIds) { 43 | userConfig.workspaceIds = []; 44 | } 45 | if (userConfig.teamIds) { 46 | delete userConfig.teamIds; 47 | } 48 | return userConfig; 49 | } 50 | 51 | /** 52 | * Read user config and simplify it down to a CliConfig. 53 | * 54 | * @return CliConfig populated from user config 55 | */ 56 | export function resolveCliConfig(): CliConfig { 57 | const userConfig = resolveUserConfig(); 58 | return { 59 | apiKey: userConfig.apiKey, 60 | workspaceIds: userConfig.workspaceIds, 61 | endpoints: userConfig.endpoints, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /test/spawn.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as assert from "power-assert"; 18 | 19 | import { cleanCommandString } from "../lib/spawn"; 20 | 21 | describe("spawn", () => { 22 | 23 | describe("cleanCommandString", () => { 24 | 25 | it("should create a command string", () => { 26 | const c = "bodeans"; 27 | const a = ["black", "and", "white"]; 28 | const s = cleanCommandString(c, a); 29 | const e = "bodeans 'black' 'and' 'white'"; 30 | assert(s === e); 31 | }); 32 | 33 | it("should allow no arguments", () => { 34 | const c = "bodeans"; 35 | const s = cleanCommandString(c); 36 | assert(s === c); 37 | }); 38 | 39 | it("should clean the command string", () => { 40 | const c = "bodeans"; 41 | const a = ["black", "Authorization: token h3110f4ch4nc3", "white"]; 42 | const s = cleanCommandString(c, a); 43 | const e = "bodeans 'black' 'Authorization: ' 'white'"; 44 | assert(s === e); 45 | }); 46 | 47 | it("should clean all of the command string", () => { 48 | const c = "bodeans"; 49 | const a = ["black", "Authorization: token h3110f4ch4nc3", "2 Authorization: Bearer B4DF0RY0U", "white"]; 50 | const s = cleanCommandString(c, a); 51 | const e = "bodeans 'black' 'Authorization: ' '2 Authorization: ' 'white'"; 52 | assert(s === e); 53 | }); 54 | 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /bin/postInstall.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright © 2018 Atomist, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // If you change this file, commit both this file and the generated 19 | // .js, .d.ts., and .js.map because we cannot execute TypeScript in 20 | // the postInstall hook because the devDependencies might not be 21 | // available. 22 | 23 | import { userConfigPath } from "@atomist/automation-client/lib/configuration"; 24 | import * as fs from "fs-extra"; 25 | 26 | function printErr(e: Error): void { 27 | process.stderr.write(`@atomist/cli:postInstall [ERROR] ${e.message}\n`); 28 | } 29 | 30 | async function main(): Promise { 31 | 32 | try { 33 | 34 | if (fs.existsSync(userConfigPath())) { 35 | process.exit(0); 36 | } 37 | 38 | // show an informative and friendly welcome message 39 | const banner = ` 40 | ┌──────────────────────────────────────────────────────────────────────────┐ 41 | │ │ 42 | │ @atomist/cli is now installed. │ 43 | │ │ 44 | │ Head to the SDM repo (https://github.com/atomist/sdm) for more info. │ 45 | │ │ 46 | └──────────────────────────────────────────────────────────────────────────┘ 47 | 48 | `; 49 | process.stdout.write(banner); 50 | 51 | } catch (e) { 52 | printErr(e); 53 | } 54 | } 55 | 56 | // we do not want any postInstall failure to cause install to fail 57 | main() 58 | .catch(e => { 59 | printErr(e); 60 | }) 61 | .then(() => process.exit(0), e => process.exit(0)); 62 | -------------------------------------------------------------------------------- /test/kubeCrypt.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fs from "fs-extra"; 18 | import * as yaml from "js-yaml"; 19 | import * as assert from "power-assert"; 20 | import { withFile } from "tmp-promise"; 21 | import { 22 | handleSecretParameter, 23 | wrapLiteral, 24 | } from "../lib/kubeCrypt"; 25 | 26 | describe("kubeCrypt", () => { 27 | 28 | describe("handleSecretParameter", () => { 29 | const secret = { 30 | apiVersion: "v1", 31 | kind: "Secret", 32 | type: "Opaque", 33 | data: { 34 | username: "some username", 35 | password: "some password", 36 | }, 37 | }; 38 | 39 | it("file input", async () => { 40 | await withFile(async ({ path, fd }) => { 41 | await fs.writeFile(path, yaml.safeDump(secret)); 42 | 43 | const s = await handleSecretParameter({ 44 | action: "encrypt", 45 | file: path, 46 | }); 47 | 48 | assert.deepEqual(s, secret); 49 | }); 50 | }); 51 | 52 | it("literal input", async () => { 53 | await withFile(async ({ path, fd }) => { 54 | await fs.writeFile(path, yaml.safeDump(secret)); 55 | 56 | const s = await handleSecretParameter({ 57 | action: "decrypt", 58 | literal: "something", 59 | }); 60 | 61 | const key = Object.keys(s.data)[0]; 62 | assert.deepEqual(s.data[key], "something"); 63 | }); 64 | }); 65 | }); 66 | 67 | describe("wrapLiteral", () => { 68 | 69 | it("single line with special characters", () => { 70 | const o = wrapLiteral("thingmabob <>--!#@!", "doodad"); 71 | 72 | assert.deepEqual(o, { 73 | apiVersion: "v1", 74 | kind: "Secret", 75 | type: "Opaque", 76 | data: { 77 | "thingmabob <>--!#@!": "doodad", 78 | }, 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /lib/gqlFetch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fs from "fs-extra"; 18 | import * as path from "path"; 19 | 20 | import { resolveCliConfig } from "./cliConfig"; 21 | import { libDir } from "./gql"; 22 | import * as print from "./print"; 23 | import { spawnBinary } from "./spawn"; 24 | 25 | /** 26 | * Command-line options and arguments for gql-fetch. 27 | */ 28 | export interface GqlFetchOptions { 29 | /** Directory to run command in, must be an automation client directory */ 30 | cwd?: string; 31 | /** If true or no node_modules directory exists, run "npm install" before running command */ 32 | install?: boolean; 33 | } 34 | 35 | /** 36 | * Fetch the GraphQL schema for an Atomist workspace. 37 | * 38 | * @param opts see GqlFetchOptions 39 | * @return integer return value 40 | */ 41 | export async function gqlFetch(opts: GqlFetchOptions): Promise { 42 | const cliConfig = resolveCliConfig(); 43 | if (!cliConfig.apiKey) { 44 | print.error(`No API key set in user configuration, run 'atomist config' first`); 45 | return Promise.resolve(1); 46 | } 47 | if (!cliConfig.workspaceIds || cliConfig.workspaceIds.length < 1) { 48 | print.error(`No workspace IDs set in use configuration, run 'atomist config' first`); 49 | return Promise.resolve(1); 50 | } else if (cliConfig.workspaceIds.length > 1) { 51 | print.warn(`More than one workspace ID in user configuration, using first: ${cliConfig.workspaceIds[0]}`); 52 | } 53 | const workspaceId = cliConfig.workspaceIds[0]; 54 | const graphQL = cliConfig.endpoints && cliConfig.endpoints.graphql 55 | ? cliConfig.endpoints.graphql : "https://automation.atomist.com/graphql/team"; 56 | 57 | const outDir = path.join(libDir(opts.cwd), "graphql"); 58 | const outSchema = path.join(outDir, "schema.json"); 59 | await fs.ensureDir(outDir); 60 | const spawnOpts = { 61 | command: "apollo", 62 | args: [ 63 | "schema:download", 64 | outSchema, 65 | "--endpoint", `${graphQL}/${workspaceId}`, 66 | "--header", `Authorization: Bearer ${cliConfig.apiKey}`, 67 | ], 68 | cwd: opts.cwd, 69 | install: opts.install, 70 | compile: false, 71 | }; 72 | return spawnBinary(spawnOpts); 73 | } 74 | -------------------------------------------------------------------------------- /bin/postInstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | /* 4 | * Copyright © 2018 Atomist, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 19 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 20 | return new (P || (P = Promise))(function (resolve, reject) { 21 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 22 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 23 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 24 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 25 | }); 26 | }; 27 | Object.defineProperty(exports, "__esModule", { value: true }); 28 | // If you change this file, commit both this file and the generated 29 | // .js, .d.ts., and .js.map because we cannot execute TypeScript in 30 | // the postInstall hook because the devDependencies might not be 31 | // available. 32 | const configuration_1 = require("@atomist/automation-client/lib/configuration"); 33 | const fs = require("fs-extra"); 34 | function printErr(e) { 35 | process.stderr.write(`@atomist/cli:postInstall [ERROR] ${e.message}\n`); 36 | } 37 | function main() { 38 | return __awaiter(this, void 0, void 0, function* () { 39 | try { 40 | if (fs.existsSync(configuration_1.userConfigPath())) { 41 | process.exit(0); 42 | } 43 | // show an informative and friendly welcome message 44 | const banner = ` 45 | ┌──────────────────────────────────────────────────────────────────────────┐ 46 | │ │ 47 | │ @atomist/cli is now installed. │ 48 | │ │ 49 | │ Head to the SDM repo (https://github.com/atomist/sdm) for more info. │ 50 | │ │ 51 | └──────────────────────────────────────────────────────────────────────────┘ 52 | 53 | `; 54 | process.stdout.write(banner); 55 | } 56 | catch (e) { 57 | printErr(e); 58 | } 59 | }); 60 | } 61 | // we do not want any postInstall failure to cause install to fail 62 | main() 63 | .catch(e => { 64 | printErr(e); 65 | }) 66 | .then(() => process.exit(0), e => process.exit(0)); 67 | //# sourceMappingURL=postInstall.js.map -------------------------------------------------------------------------------- /lib/kubeUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | decryptSecret, 19 | encryptSecret, 20 | } from "@atomist/sdm-pack-k8s"; 21 | import * as k8s from "@kubernetes/client-node"; 22 | import * as fs from "fs-extra"; 23 | import * as yaml from "js-yaml"; 24 | import { DeepPartial } from "ts-essentials"; 25 | import { KubeCryptOptions } from "./kubeCrypt"; 26 | import * as print from "./print"; 27 | 28 | /** 29 | * Does the requested encryption/decryption of the provided secret 30 | * @param input the secret to encrypt/decrypt 31 | * @param key the secret key to encrypt/decrypt with 32 | * @return the encrypted/decrypted secret 33 | */ 34 | export async function crypt(input: DeepPartial, 35 | opts: Pick): Promise> { 36 | 37 | let secret: DeepPartial; 38 | if (opts.action === "encrypt") { 39 | secret = await encryptSecret(input, opts.secretKey); 40 | } else { 41 | secret = await decryptSecret(input, opts.secretKey); 42 | } 43 | 44 | return secret; 45 | } 46 | 47 | /** 48 | * Encodes or decodes the data section of a secret 49 | * @param secret The secret to encode/decode 50 | * @param action encode or decode 51 | */ 52 | export function base64(secret: DeepPartial, action: "encode" | "decode"): DeepPartial { 53 | for (const datum of Object.keys(secret.data)) { 54 | const encoding = action === "encode" ? 55 | Buffer.from(secret.data[datum]).toString("base64") : Buffer.from(secret.data[datum], "base64").toString(); 56 | secret.data[datum] = encoding; 57 | } 58 | return secret; 59 | } 60 | 61 | /** 62 | * prints the secret to the output 63 | * @param secret the secret to print 64 | * @param opts literal or file from KubeCryptOptions 65 | */ 66 | export function printSecret(secret: DeepPartial, opts: Pick): void { 67 | if (opts.literal) { 68 | print.log(secret.data[opts.literal]); 69 | } else if (/\.ya?ml$/.test(opts.file)) { 70 | print.log(yaml.safeDump(secret)); 71 | } else { 72 | print.log(JSON.stringify(secret, undefined, 2)); 73 | } 74 | } 75 | 76 | /** 77 | * writes the secret to file 78 | * @param secret the secret to write 79 | * @param opts file from KubeCryptOptions 80 | */ 81 | export async function writeSecret(secret: DeepPartial, opts: Pick): Promise { 82 | const dumpString = /\.ya?ml$/.test(opts.file) ? yaml.safeDump(secret) : JSON.stringify(secret, undefined, 2); 83 | await fs.writeFile(opts.file, dumpString); 84 | } 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | In the interest of fostering an open and welcoming environment, we as 4 | contributors and maintainers pledge to making participation in our 5 | project and our community a harassment-free experience for everyone, 6 | regardless of age, body size, disability, ethnicity, gender identity 7 | and expression, level of experience, nationality, personal appearance, 8 | race, religion, or sexual identity and orientation. 9 | 10 | Examples of behavior that contributes to creating a positive 11 | environment include: 12 | 13 | * Using welcoming and inclusive language 14 | * Being respectful of differing viewpoints and experiences 15 | * Gracefully accepting constructive criticism 16 | * Focusing on what is best for the community 17 | * Showing empathy towards other community members 18 | 19 | Examples of unacceptable behavior by participants include: 20 | 21 | * The use of sexualized language or imagery and unwelcome sexual 22 | attention or advances 23 | * Trolling, insulting/derogatory comments, and personal or political 24 | attacks 25 | * Public or private harassment 26 | * Publishing others' private information, such as a physical or 27 | electronic address, without explicit permission 28 | * Other conduct which could reasonably be considered inappropriate 29 | in a professional setting 30 | 31 | Project maintainers are responsible for clarifying the standards of 32 | acceptable behavior and are expected to take appropriate and fair 33 | corrective action in response to any instances of unacceptable 34 | behavior. 35 | 36 | Project maintainers have the right and responsibility to remove, edit, 37 | or reject comments, commits, code, wiki edits, issues, and other 38 | contributions that are not aligned to this Code of Conduct, or to ban 39 | temporarily or permanently any contributor for other behaviors that 40 | they deem inappropriate, threatening, offensive, or harmful. 41 | 42 | This Code of Conduct applies both within project spaces and in public 43 | spaces when an individual is representing the project or its 44 | community. Examples of representing a project or community include 45 | using an official project e-mail address, posting via an official 46 | social media account, or acting as an appointed representative at an 47 | online or offline event. Representation of a project may be further 48 | defined and clarified by project maintainers. 49 | 50 | Instances of abusive, harassing, or otherwise unacceptable behavior 51 | may be reported by contacting the project team 52 | at [code-of-conduct@atomist.com][email]. All complaints will be 53 | reviewed and investigated and will result in a response that is deemed 54 | necessary and appropriate to the circumstances. The project team is 55 | obligated to maintain confidentiality with regard to the reporter of 56 | an incident. Further details of specific enforcement policies may be 57 | posted separately. 58 | 59 | Project maintainers who do not follow or enforce the Code of Conduct 60 | in good faith may face temporary or permanent repercussions as 61 | determined by other members of the project's leadership. 62 | 63 | This Code of Conduct is adapted from 64 | the [Contributor Covenant][homepage], version 1.4, available 65 | at [http://contributor-covenant.org/version/1/4][version] 66 | 67 | [homepage]: http://contributor-covenant.org 68 | [version]: http://contributor-covenant.org/version/1/4/ 69 | [email]: mailto:code-of-conduct@atomist.com 70 | -------------------------------------------------------------------------------- /lib/kubeFetch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | kubernetesFetch, 19 | KubernetesFetchOptions, 20 | kubernetesSpecFileBasename, 21 | kubernetesSpecStringify, 22 | } from "@atomist/sdm-pack-k8s"; 23 | import * as fs from "fs-extra"; 24 | import * as path from "path"; 25 | import * as print from "./print"; 26 | 27 | /** 28 | * Command-line options and arguments for kube-fetch. 29 | */ 30 | export interface KubeFetchOptions { 31 | /** Path to Kubernetes fetch options file. */ 32 | optionsFile?: string; 33 | /** Path to directory to write spec files in. */ 34 | outputDir?: string; 35 | /** Format, either "json" or "yaml", of the created files, "yaml" is the default. */ 36 | outputFormat?: "json" | "yaml"; 37 | /** Encryption key to use for secrets. */ 38 | secretKey?: string; 39 | } 40 | 41 | /** 42 | * Fetch Kubernetes resources from the currently configured Kuberenetes 43 | * cluster, remove Kubernetes system-provided properties, and write the 44 | * resource specifications to files. 45 | * 46 | * @param opts see KubeFetchOptions 47 | * @return integer return value, 0 if successful, non-zero otherwise 48 | */ 49 | export async function kubeFetch(opts: KubeFetchOptions): Promise { 50 | let outDir: string; 51 | if (opts.outputDir) { 52 | try { 53 | await fs.ensureDir(opts.outputDir); 54 | } catch (e) { 55 | print.error(`Failed to create output directory '${opts.outputDir}': ${e.message}`); 56 | return 1; 57 | } 58 | outDir = opts.outputDir; 59 | } else { 60 | outDir = process.cwd(); 61 | } 62 | const fileFormat = (opts.outputFormat && opts.outputFormat.toLowerCase() === "json") ? "json" : "yaml"; 63 | let options: KubernetesFetchOptions; 64 | if (opts.optionsFile) { 65 | try { 66 | options = await fs.readJson(opts.optionsFile); 67 | } catch (e) { 68 | print.error(`Failed to read Kubernetes fetch options file '${opts.optionsFile}': ${e.message}`); 69 | return 2; 70 | } 71 | } 72 | const errs: string[] = []; 73 | print.log("Fetching Kubernetes resources..."); 74 | try { 75 | const resources = await kubernetesFetch(options); 76 | for (const spec of resources) { 77 | const specPath = path.join(outDir, kubernetesSpecFileBasename(spec) + "." + fileFormat); 78 | print.log(`Writing Kubernetes spec: ${specPath}`); 79 | try { 80 | const specString = await kubernetesSpecStringify(spec, { format: fileFormat, secretKey: opts.secretKey }); 81 | await fs.writeFile(specPath, specString); 82 | } catch (e) { 83 | errs.push(`'${specPath}': ${e.message}`); 84 | } 85 | } 86 | } catch (e) { 87 | print.error(`Failed to fetch Kubernetes resources: ${e.message}`); 88 | return 3; 89 | } 90 | if (errs.length > 0) { 91 | const s = (errs.length > 1) ? "s" : ""; 92 | print.error(`Failed to write Kubernetes spec file${s}: ${errs.join(", ")}`); 93 | return 10 + errs.length; 94 | } 95 | 96 | return 0; 97 | } 98 | -------------------------------------------------------------------------------- /lib/command.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Arg } from "@atomist/automation-client/lib/internal/invoker/Payload"; 18 | 19 | /** 20 | * Determine whether sdm-local commands should be loaded. To improve 21 | * startup times and eliminate client startup when unnecessary, we do 22 | * not load the sdm-local commands if we are just running a native CLI 23 | * command. 24 | * 25 | * @param args command-line arguments, typically process.argv 26 | * @return true if the SDM local commands should be loaded 27 | */ 28 | export function shouldAddLocalSdmCommands(args: string[]): boolean { 29 | if (args.includes("--help") || args.includes("--h") || args.includes("-h") || args.includes("-?")) { 30 | return true; 31 | } 32 | const command = args.slice(2).filter(a => !/^-/.test(a)).shift(); 33 | if (!command) { 34 | return false; 35 | } 36 | const internalCommands = [ 37 | "config", 38 | "connect", 39 | "execute", 40 | "git-hook", 41 | "gql-fetch", 42 | "install", 43 | "kube", 44 | "kube-decrypt", 45 | "kube-encrypt", 46 | "kube-fetch", 47 | "kube-install", 48 | "login", 49 | "start", 50 | "update", 51 | ]; 52 | return !internalCommands.includes(command); 53 | } 54 | 55 | /** 56 | * Does this command start up an embedded SDM? 57 | * @param args command-line arguments, typically process.argv 58 | */ 59 | export function isEmbeddedSdmCommand(args: string[]): boolean { 60 | const relevant = args.slice(2); 61 | return relevant.length > 0 && ["create sdm", "enable local"].includes(relevant.join(" ")); 62 | } 63 | 64 | /** 65 | * Call the provided function with the provided arguments and capture 66 | * any errors. When the function is complete, `process.exit` will be 67 | * called with the appropriate code, i.e., this function will never return. 68 | * 69 | * @param fn function providing the desired command and returning a 70 | * Promise of an integer exit value. 71 | * @param argv command-line arguments 72 | */ 73 | export async function cliCommand(fn: () => Promise): Promise { 74 | try { 75 | const status = await fn(); 76 | process.exit(status); 77 | } catch (e) { 78 | process.stderr.write(`Unhandled Error: ${e.message}`); 79 | process.exit(101); 80 | } 81 | throw new Error("You will not get here. You have exited."); 82 | } 83 | 84 | /** 85 | * Parse positional parameters into parameter name/value pairs. The 86 | * positional parameters should be of the form NAME[=VALUE]. If 87 | * =VALUE is omitted, the value is set to `undefined`. If the VALUE 88 | * is empty, i.e., NAME=, then the value is the empty string. 89 | * 90 | * @param args typically argv._ from yargs 91 | * @return array of CommandInvocation Arg 92 | */ 93 | export function extractArgs(args: string[]): Arg[] { 94 | return args.map(arg => { 95 | const split = arg.indexOf("="); 96 | if (split < 0) { 97 | return { name: arg, value: undefined }; 98 | } 99 | const name = arg.slice(0, split); 100 | const value = arg.slice(split + 1); 101 | return { name, value }; 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@atomist/cli", 3 | "version": "1.8.1", 4 | "description": "The Atomist CLI", 5 | "author": { 6 | "name": "Atomist", 7 | "email": "support@atomist.com", 8 | "url": "https://atomist.com/" 9 | }, 10 | "license": "Apache-2.0", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/atomist/cli.git" 14 | }, 15 | "homepage": "https://github.com/atomist/cli#readme", 16 | "bugs": { 17 | "url": "https://github.com/atomist/cli/issues" 18 | }, 19 | "keywords": [ 20 | "atomist", 21 | "automation", 22 | "cli" 23 | ], 24 | "dependencies": { 25 | "@atomist/automation-client": "2.0.0-master.20191206152722", 26 | "@atomist/sdm": "2.0.0-master.20191206153457", 27 | "@atomist/sdm-core": "2.0.0-master.20191206160429", 28 | "@atomist/sdm-local": "1.2.2-master.20191206162119", 29 | "@atomist/sdm-pack-k8s": "^1.10.0", 30 | "@kubernetes/client-node": "^0.10.2", 31 | "@types/cross-spawn": "^6.0.0", 32 | "@types/express": "^4.17.0", 33 | "@types/fs-extra": "^8.0.0", 34 | "@types/git-url-parse": "^9.0.0", 35 | "@types/inquirer": "^6.5.0", 36 | "@types/js-yaml": "^3.12.1", 37 | "@types/json-stringify-safe": "^5.0.0", 38 | "@types/lodash": "^4.14.138", 39 | "@types/read-pkg-up": "^3.0.1", 40 | "@types/request": "^2.48.2", 41 | "@types/tmp": "^0.1.0", 42 | "@types/yargs": "^13.0.2", 43 | "axios": "0.19.0", 44 | "chalk": "^2.4.2", 45 | "cli-highlight": "^2.1.1", 46 | "cli-spinner": "^0.2.10", 47 | "cross-spawn": "^6.0.5", 48 | "express": "^4.17.1", 49 | "fast-glob": "^3.1.0", 50 | "fs-extra": "^8.1.0", 51 | "git-url-parse": "^11.1.2", 52 | "inquirer": "^6.5.2", 53 | "js-sha256": "^0.9.0", 54 | "js-yaml": "^3.13.1", 55 | "json-stringify-safe": "^5.0.1", 56 | "lodash": "^4.17.15", 57 | "open": "^6.4.0", 58 | "read-pkg-up": "^6.0.0", 59 | "request": "^2.88.0", 60 | "source-map-support": "^0.5.12", 61 | "tmp-promise": "^2.0.2", 62 | "ts-essentials": "^3.0.0", 63 | "yargs": "13.3.0" 64 | }, 65 | "devDependencies": { 66 | "@types/mocha": "^5.2.7", 67 | "@types/node": "^12.12.14", 68 | "@types/power-assert": "^1.5.0", 69 | "espower-typescript": "^9.0.2", 70 | "external-editor": "^3.1.0", 71 | "mocha": "^6.2.2", 72 | "npm-run-all": "^4.1.5", 73 | "power-assert": "^1.6.1", 74 | "replace": "^1.1.1", 75 | "rimraf": "^3.0.0", 76 | "supervisor": "^0.12.0", 77 | "ts-node": "^8.5.4", 78 | "tslint": "^5.20.1", 79 | "typedoc": "^0.15.3", 80 | "typescript": "^3.7.3" 81 | }, 82 | "directories": { 83 | "test": "test" 84 | }, 85 | "scripts": { 86 | "autotest": "supervisor --watch index.ts,lib,test --extensions ts --no-restart-on exit --quiet --exec npm -- test", 87 | "build": "run-s compile test lint doc", 88 | "clean": "run-p clean:compile clean:doc clean:run", 89 | "clean:compile": "rimraf git-info.json \"index.{d.ts,js}{,.map}\" \"{lib,test}/**/*.{d.ts,js}{,.map}\" lib/typings/types.ts", 90 | "clean:dist": "run-s clean clean:npm", 91 | "clean:doc": "rimraf doc", 92 | "clean:npm": "rimraf node_modules", 93 | "clean:run": "rimraf *-v8.log profile.txt log", 94 | "compile": "tsc --project .", 95 | "doc": "typedoc --mode modules --excludeExternals --ignoreCompilerErrors --exclude \"**/*.d.ts\" --out doc index.ts lib", 96 | "lint": "tslint --format verbose --project . --exclude \"node_modules/**\" --exclude \"**/*.d.ts\" \"**/*.ts\"", 97 | "lint:fix": "npm run lint -- --fix", 98 | "postinstall": "node bin/postInstall.js", 99 | "start": "node index.js", 100 | "test": "mocha --require espower-typescript/guess --require source-map-support/register \"test/**/*.test.ts\"", 101 | "test:one": "mocha --require espower-typescript/guess \"test/**/${TEST:-*.test.ts}\"", 102 | "typedoc": "npm run doc" 103 | }, 104 | "bin": { 105 | "atomist": "./index.js", 106 | "@atomist": "./index.js", 107 | "atomist-start": "./bin/start.js" 108 | }, 109 | "engines": { 110 | "node": ">=8.2.0", 111 | "npm": ">=5.0.0" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/kubeEdit.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as k8s from "@kubernetes/client-node"; 18 | import { edit } from "external-editor"; 19 | import * as fs from "fs-extra"; 20 | import * as yaml from "js-yaml"; 21 | import * as _ from "lodash"; 22 | import { DeepPartial } from "ts-essentials"; 23 | import { 24 | KubeCryptActions, 25 | KubeCryptOptions, 26 | } from "./kubeCrypt"; 27 | import { 28 | base64, 29 | crypt, 30 | writeSecret, 31 | } from "./kubeUtils"; 32 | import * as print from "./print"; 33 | 34 | type kubeEditOptions = Pick; 35 | 36 | export async function kubeEdit(opts: kubeEditOptions): Promise { 37 | let secret: DeepPartial; 38 | try { 39 | const secretString = await fs.readFile(opts.file, "utf8"); 40 | secret = await yaml.safeLoad(secretString); 41 | } catch (e) { 42 | print.error(`Failed to load secret spec from file '${opts.file}': ${e.message}`); 43 | return 2; 44 | } 45 | 46 | try { 47 | if (opts.secretKey) { 48 | secret = await crypt(secret, crypAction(opts, "decrypt")); 49 | } 50 | } catch (e) { 51 | print.error(`Failed to decrypt secret: ${e.message}`); 52 | return 3; 53 | } 54 | secret = base64(secret, "decode"); 55 | 56 | try { 57 | secret = await editSecret(secret); 58 | } catch (e) { 59 | print.error(`Edit cancelled, ${e.message}`); 60 | return 4; 61 | } 62 | 63 | secret = base64(secret, "encode"); 64 | try { 65 | if (opts.secretKey) { 66 | secret = await crypt(secret, crypAction(opts, "encrypt")); 67 | } 68 | } catch (e) { 69 | print.error(`Failed to encrpyt secret: ${e.message}`); 70 | return 3; 71 | } 72 | 73 | try { 74 | await writeSecret(secret, opts); 75 | } catch (e) { 76 | print.error(`Failed to write secret to file: ${e.message}`); 77 | return 5; 78 | } 79 | 80 | return 0; 81 | } 82 | 83 | /** 84 | * Opens the secret in the editor and validates that the resulting secret is valid yaml. 85 | * If the secret is not valid the editor is reopened. 86 | * @param inputSecret the secret to be edited 87 | * @returns the secret after the user has edited it in the editor 88 | */ 89 | async function editSecret(inputSecret: DeepPartial): Promise> { 90 | const comment = 91 | `# Please edit the secret below. Lines beginning with a '#' will be ignored. 92 | # An empty file will abort the edit. 93 | # If an error occurs while saving the editor will be reopened with the relevant failures. 94 | #\n`; 95 | 96 | let errorMessage = ""; 97 | let outputSecret = _.cloneDeep(inputSecret); 98 | let secretText = yaml.safeDump(outputSecret); 99 | do { 100 | // join everything together to present it to the user 101 | secretText = comment + errorMessage + secretText; 102 | 103 | secretText = edit(secretText); 104 | // remove all lines that start with comments 105 | secretText = secretText.replace(/^#.*\n?/gm, ""); 106 | if (!secretText.trim()) { 107 | throw new Error("file is empty"); 108 | } 109 | 110 | try { 111 | outputSecret = await yaml.safeLoad(secretText); 112 | errorMessage = ""; 113 | } catch (e) { 114 | errorMessage = `# ${e.message.replace(/\n/gm, "\n# ")} \n`; 115 | } 116 | } while (errorMessage); 117 | 118 | return outputSecret; 119 | } 120 | 121 | function crypAction(opts: kubeEditOptions, KubeCryptAction: KubeCryptActions): KubeCryptOptions { 122 | return { ...opts, action: KubeCryptAction }; 123 | } 124 | -------------------------------------------------------------------------------- /lib/updateSdm.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | execPromise, 19 | spawnLog, 20 | StringCapturingProgressLog, 21 | } from "@atomist/sdm"; 22 | import chalk from "chalk"; 23 | import * as fs from "fs-extra"; 24 | import * as path from "path"; 25 | import { createSpinner } from "./config"; 26 | import * as print from "./print"; 27 | 28 | /** 29 | * Command-line options and arguments for install. 30 | */ 31 | export interface InstallOptions { 32 | /** Name or keywords to install for */ 33 | versionTag: string; 34 | /** Directory to run command in, must be an SDM directory */ 35 | cwd?: string; 36 | } 37 | 38 | function getAtomistDependencies(dependencies: any): string[] { 39 | return Object.keys(dependencies).filter(key => key.startsWith("@atomist/")); 40 | } 41 | 42 | function hasPackageJson(directory: string): boolean { 43 | return fs.existsSync(path.join(directory, "package.json")); 44 | } 45 | 46 | async function updateDependenciesToTag(dependencies: string[], versionTag: string, spinner: any): Promise { 47 | const dependenciesToUpdate = dependencies 48 | .filter(async d => { 49 | const moduleVersion = await getModuleVersion(`${d}@${versionTag}`); 50 | return moduleVersion !== undefined; 51 | }) 52 | .map(d => `${d}@${versionTag}`); 53 | spinner.setSpinnerTitle(`Updating ${dependenciesToUpdate.length} dependencies to ${versionTag} ${chalk.yellow("%s")}`); 54 | await execPromise( 55 | "npm", 56 | [ 57 | "install", 58 | "--save", 59 | ...dependenciesToUpdate, 60 | ], 61 | ); 62 | } 63 | 64 | async function getModuleVersion(module: string): Promise { 65 | const log = new StringCapturingProgressLog(); 66 | const result = await spawnLog( 67 | "npm", 68 | ["show", module, "version"], 69 | { 70 | logCommand: false, 71 | log, 72 | }); 73 | 74 | if (result.code === 0) { 75 | return log.log.trim(); 76 | } 77 | 78 | return undefined; 79 | } 80 | 81 | async function updateDevDependenciesToTag(dependencies: string[], versionTag: string, spinner: any): Promise { 82 | const dependenciesToUpdate = dependencies.map(d => `${d}@${versionTag}`); 83 | spinner.setSpinnerTitle(`Updating dev dependencies to ${versionTag} ${chalk.yellow("%s")}`); 84 | await execPromise( 85 | "npm", 86 | [ 87 | "install", 88 | "--save-dev", 89 | ...dependenciesToUpdate, 90 | ], 91 | ); 92 | } 93 | 94 | /** 95 | * Search for an SDM extension pack. 96 | * 97 | * @param opts see InstallOptions 98 | * @return integer return value 99 | */ 100 | export async function updateSdm(opts: InstallOptions): Promise { 101 | if (hasPackageJson(opts.cwd)) { 102 | const spinner = createSpinner(`Updating dependencies in package.json`); 103 | try { 104 | const packageJson = await fs.readJson(path.join(opts.cwd, "package.json")); 105 | const extractedDependencies = getAtomistDependencies(packageJson.dependencies); 106 | const extractedDevDependencies = getAtomistDependencies(packageJson.devDependencies); 107 | await updateDependenciesToTag(extractedDependencies, opts.versionTag, spinner); 108 | await updateDevDependenciesToTag(extractedDevDependencies, opts.versionTag, spinner); 109 | spinner.stop(true); 110 | return 0; 111 | } catch (e) { 112 | print.error(`Error while updating package.json`); 113 | return 1; 114 | } 115 | } else { 116 | print.error(`Current directory does not contain a package.json`); 117 | return 1; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /assets/kubectl/cli.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | app.kubernetes.io/managed-by: atomist 6 | app.kubernetes.io/name: sdm 7 | app.kubernetes.io/part-of: sdm 8 | name: sdm 9 | --- 10 | apiVersion: v1 11 | kind: ServiceAccount 12 | metadata: 13 | labels: 14 | app.kubernetes.io/managed-by: atomist 15 | app.kubernetes.io/name: cli 16 | app.kubernetes.io/part-of: cli 17 | atomist.com/workspaceId: T29E48P34 18 | name: cli 19 | namespace: sdm 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: ClusterRole 23 | metadata: 24 | labels: 25 | app.kubernetes.io/managed-by: atomist 26 | app.kubernetes.io/name: cli 27 | app.kubernetes.io/part-of: cli 28 | atomist.com/workspaceId: T29E48P34 29 | name: cli 30 | rules: 31 | - apiGroups: [""] 32 | resources: ["namespaces", "pods", "secrets", "serviceaccounts", "services", "pods/log"] 33 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete""] 34 | - apiGroups: ["apps", "extensions"] 35 | resources: ["deployments"] 36 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 37 | - apiGroups: ["extensions"] 38 | resources: ["ingresses"] 39 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 40 | - apiGroups: ["rbac.authorization.k8s.io"] 41 | resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"] 42 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 43 | - apiGroups: ["batch"] 44 | resources: ["jobs"] 45 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 46 | --- 47 | apiVersion: rbac.authorization.k8s.io/v1 48 | kind: ClusterRoleBinding 49 | metadata: 50 | labels: 51 | app.kubernetes.io/managed-by: atomist 52 | app.kubernetes.io/name: cli 53 | app.kubernetes.io/part-of: cli 54 | atomist.com/workspaceId: T29E48P34 55 | name: cli 56 | roleRef: 57 | apiGroup: rbac.authorization.k8s.io 58 | kind: ClusterRole 59 | name: cli 60 | subjects: 61 | - kind: ServiceAccount 62 | name: cli 63 | namespace: sdm 64 | --- 65 | kind: Deployment 66 | apiVersion: apps/v1 67 | metadata: 68 | labels: 69 | app.kubernetes.io/managed-by: atomist 70 | app.kubernetes.io/name: cli 71 | app.kubernetes.io/part-of: cli 72 | atomist.com/workspaceId: T29E48P34 73 | name: cli 74 | namespace: sdm 75 | spec: 76 | replicas: 1 77 | selector: 78 | matchLabels: 79 | app.kubernetes.io/name: cli 80 | atomist.com/workspaceId: T29E48P34 81 | strategy: 82 | type: RollingUpdate 83 | rollingUpdate: 84 | maxSurge: 1 85 | maxUnavailable: 0 86 | template: 87 | metadata: 88 | labels: 89 | app.kubernetes.io/managed-by: atomist 90 | app.kubernetes.io/name: cli 91 | app.kubernetes.io/part-of: cli 92 | app.kubernetes.io/version: "1" 93 | atomist.com/workspaceId: T29E48P34 94 | spec: 95 | containers: 96 | - env: 97 | - name: ATOMIST_CONFIG_PATH 98 | value: /opt/atm/client.config.json 99 | - name: TMPDIR 100 | value: /tmp 101 | image: atomist/cli:latest 102 | livenessProbe: 103 | failureThreshold: 3 104 | httpGet: 105 | path: /health 106 | port: http 107 | scheme: HTTP 108 | initialDelaySeconds: 120 109 | periodSeconds: 20 110 | successThreshold: 1 111 | timeoutSeconds: 3 112 | name: cli 113 | ports: 114 | - name: http 115 | containerPort: 2866 116 | protocol: TCP 117 | readinessProbe: 118 | failureThreshold: 3 119 | httpGet: 120 | path: /health 121 | port: http 122 | scheme: HTTP 123 | initialDelaySeconds: 20 124 | periodSeconds: 20 125 | successThreshold: 1 126 | timeoutSeconds: 3 127 | resources: 128 | limits: 129 | cpu: 1000m 130 | memory: 1024Mi 131 | requests: 132 | cpu: 100m 133 | memory: 320Mi 134 | volumeMounts: 135 | - mountPath: /opt/atm 136 | name: cli 137 | readOnly: true 138 | serviceAccountName: cli 139 | volumes: 140 | - name: cli 141 | secret: 142 | defaultMode: 288 143 | secretName: cli 144 | -------------------------------------------------------------------------------- /lib/install.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { execPromise } from "@atomist/sdm"; 18 | import chalk from "chalk"; 19 | import * as fs from "fs-extra"; 20 | import * as inquirer from "inquirer"; 21 | import * as path from "path"; 22 | import { createSpinner } from "./config"; 23 | import * as print from "./print"; 24 | 25 | /** 26 | * Command-line options and arguments for install. 27 | */ 28 | export interface InstallOptions { 29 | /** Name or keywords to install for */ 30 | keywords: string[]; 31 | /** Directory to run command in, must be an SDM directory */ 32 | cwd?: string; 33 | /** NPM Registry to install */ 34 | registry?: string; 35 | } 36 | 37 | interface ExtensionPack { 38 | name: string; 39 | description: string; 40 | version: string; 41 | maintainers: [{ 42 | username: string; 43 | email: string; 44 | }]; 45 | } 46 | 47 | /** 48 | * Search for an SDM extension pack. 49 | * 50 | * @param opts see InstallOptions 51 | * @return integer return value 52 | */ 53 | export async function install(opts: InstallOptions): Promise { 54 | const keywords = opts.keywords.filter(k => !!k); 55 | let spinner = createSpinner( 56 | `Searching extension packs${keywords.length > 0 ? " for " : ""}${chalk.cyan(keywords.join(", "))}`); 57 | let packs: ExtensionPack[]; 58 | 59 | try { 60 | const registryArgs = opts.registry ? ["--registry", opts.registry] : []; 61 | const result = await execPromise( 62 | "npm", 63 | [ 64 | "search", 65 | "--json", 66 | ...registryArgs, 67 | "atomist", 68 | "SDM", 69 | "extension", 70 | "pack", 71 | ...opts.keywords.filter(k => !!k)], 72 | {}); 73 | packs = JSON.parse(result.stdout); 74 | spinner.stop(true); 75 | } catch (e) { 76 | spinner.stop(true); 77 | print.error(`Failed to search registry: ${e.message}`); 78 | return 1; 79 | } 80 | 81 | if (packs.length > 0) { 82 | const questions: inquirer.QuestionCollection = [ 83 | { 84 | type: "list", 85 | name: "package", 86 | message: "Extension Packs", 87 | choices: packs 88 | .map(p => ({ 89 | name: `${p.name} ${chalk.yellow(p.description)} ${chalk.reset("by")} ${ 90 | chalk.gray(p.maintainers.map(m => m.username).join(", "))}`, 91 | value: p, 92 | short: p.name, 93 | })), 94 | 95 | }, 96 | ]; 97 | const answers: any = await inquirer.prompt(questions); 98 | spinner = createSpinner(`Installing extension pack ${chalk.cyan(answers.package.name)} in ${opts.cwd}`); 99 | try { 100 | await execPromise("npm", ["install", answers.package.name], { cwd: opts.cwd }); 101 | spinner.stop(true); 102 | print.log(`Successfully installed extension pack ${chalk.cyan(answers.package.name)} by ${ 103 | chalk.gray(answers.package.maintainers.map((m: any) => m.username).join(", "))}`); 104 | const p = path.join(opts.cwd, "node_modules", answers.package.name, "package.json"); 105 | const pj = JSON.parse((await fs.readFile(p)).toString()); 106 | print.log(`Visit ${chalk.yellow(pj.homepage)} for install and usage instructions`); 107 | } catch (e) { 108 | spinner.stop(true); 109 | print.error(`Failed to install extension pack '${answers.package.name}': ${e.message}`); 110 | return 1; 111 | } 112 | 113 | } else { 114 | print.log(`No SDM extension packs found for: ${opts.keywords.join(", ")}`); 115 | return 1; 116 | } 117 | return 0; 118 | } 119 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Atomist open source projects 2 | 3 | Have something you would like to contribute to this project? Awesome, 4 | and thanks for taking time to contribute! Here's what you need to 5 | know. 6 | 7 | ## Contributing code 8 | 9 | Is there an improvement to existing functionality or an entirely new 10 | feature you would like to see? Before creating enhancement 11 | suggestions, please check the issue list as you might find out that 12 | you don't need to create one. 13 | 14 | Did you know we have a [Slack community][slack]? This might be a 15 | great place to talk through your idea before starting. It allows you 16 | to see if anyone else is already working on something similar, having 17 | the same issue or to get feedback on your enhancement idea. 18 | Discussing things with the community first is likely to make the 19 | contribution process a better experience for yourself and those that 20 | are maintaining the projects. 21 | 22 | [slack]: https://join.atomist.com/ 23 | 24 | If you do not find an open issue related to your contribution and 25 | discussions in the Slack community are positive, the next thing to do 26 | is to create an issue in the appropriate GitHub repository. 27 | 28 | * Before we can accept any code changes into the Atomist codebase, 29 | we need to get some of the legal stuff covered. This is pretty 30 | standard for open-source projects. We are using 31 | [cla-assisant.io][cla-assistant] to track our Contributor License 32 | Agreement (CLA) signatures. If you have not signed a CLA for the 33 | repository to which you are contributing, you will be prompted to 34 | when you create a pull request (PR). 35 | * Be sure there is an open issue related to the contribution. 36 | * Code contributions should successfully build and pass tests. 37 | * Commit messages should follow the [standard format][commit] and 38 | should include a [reference][ref] to the open issue they are 39 | addressing. 40 | * All code contributions should be submitted via 41 | a [pull request (PR) from a forked GitHub repository][pr]. 42 | * Your PR will be reviewed by an Atomist developer. 43 | 44 | [cla-assistant]: https://cla-assistant.io/ 45 | [commit]: http://chris.beams.io/posts/git-commit/ 46 | [ref]: https://github.com/blog/957-introducing-issue-mentions 47 | [pr]: https://guides.github.com/activities/contributing-to-open-source/ 48 | 49 | ## Reporting problems 50 | 51 | Please go through the checklist below before reporting a 52 | problem. There's a chance it may have already been reported, or 53 | resolved. 54 | 55 | * Check if you can reproduce the problem in the latest version of 56 | the project. 57 | * Search the [atomist-community Slack][slack] community for common 58 | questions and problems. 59 | * Understand which repo the bug should be reported in. 60 | * Scan the list of issues to see if the problem has previously been 61 | reported. If so, you may add a comment to the existing issue 62 | rather than creating a new one. 63 | 64 | You went through the list above and it is still something you would 65 | like to report? Then, please provide us with as much of the context, 66 | by explaininig the problem and including any additional details that 67 | would help maintainers reproduce the problem. The more details you 68 | provide in the bug report, the better. 69 | 70 | Bugs are tracked as GitHub issues. After you've determined which 71 | repository your bug is related to, create an issue on that repository 72 | and provide as much information as possible. Feel free to use 73 | the bug report template below if you like. 74 | 75 | At a minimum include the following: 76 | 77 | * Where did you find the bug? For example, did you encounter the bug 78 | in chat, the CLI, somewhere else? 79 | * What version are you using? 80 | * What command were you using when it happened? (including 81 | parameters where applicable) 82 | 83 | ``` 84 | [Description of the problem] 85 | 86 | **How to Reproduce:** 87 | 88 | 1. [First Step] 89 | 2. [Second Step] 90 | 3. [n Step] 91 | 92 | **Expected behavior:** 93 | 94 | [Describe expected behavior here] 95 | 96 | **Observed behavior:** 97 | 98 | [Describe observed behavior here] 99 | 100 | **Screenshots and GIFs** 101 | 102 | ![Screenshots and GIFs which follow reproduction steps to demonstrate the problem](url) 103 | 104 | **Project version:** [Enter project version] 105 | **Atomist CLI version:** [Enter CLI version] 106 | ``` 107 | 108 | This project adheres to the Contributor Covenant [code of 109 | conduct][conduct]. By participating, you are expected to uphold this 110 | code. Please report unacceptable behavior to 111 | [code-of-conduct@atomist.com][email]. 112 | 113 | [conduct]: CODE_OF_CONDUCT.md 114 | [email]: mailto:code-of-conduct@atomist.com 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atomist CLI - `@atomist/cli` 2 | 3 | [![atomist sdm goals](https://badge.atomist.com/T29E48P34/atomist/cli/8b6783d7-2658-4ca0-b9a7-e684ad1dbcc3)](https://app.atomist.com/workspace/T29E48P34) 4 | [![npm version](https://badge.fury.io/js/%40atomist%2Fcli.svg)](https://badge.fury.io/js/%40atomist%2Fcli) 5 | 6 | The Atomist CLI, a unified command-line tool for interacting with 7 | [Atomist][atomist] services. 8 | 9 | ## Installation 10 | 11 | ### Homebrew 12 | 13 | If you are running [Homebrew][brew] on macOS, you can use it to 14 | install the Atomist CLI. 15 | 16 | ``` 17 | $ brew install atomist-cli 18 | ``` 19 | 20 | [brew]: https://brew.sh/ (Homebrew - The missing package manager for macOS) 21 | 22 | ### Manually 23 | 24 | You will need [Node.js][node] installed to run the Atomist CLI. Once 25 | Node.js is installed, you can use `npm` to install the Atomist CLI. 26 | 27 | ``` 28 | $ npm install -g @atomist/cli 29 | ``` 30 | 31 | [node]: https://nodejs.org/ (Node.js) 32 | 33 | ## Using 34 | 35 | To use local software delivery machine (SDM), you will need [Git][git] 36 | installed. See the [Local SDM][sdm-local] documentation for more 37 | information. 38 | 39 | To interact with the Atomist API, you will need an Atomist workspace. 40 | See the [Atomist Getting Started Guide][atomist-start] for 41 | instructions on how to get an Atomist workspace and connect it to your 42 | source code repositories, continuous integration, chat platform, etc. 43 | See the [Atomist Developer Guide][atomist-dev] for more complete 44 | instructions on setting up your development environment. 45 | 46 | You can run `atomist --help` to see the standard help message. See 47 | the [Atomist developer quick start][atomist-quick-start] for more 48 | information. 49 | 50 | [git]: https://git-scm.com/ (Git) 51 | [sdm-local]: https://github.com/atomist/sdm-local#readme (Atomist - Local Software Delivery Machine SDM) 52 | [atomist-start]: https://docs.atomist.com/user/ (Atomist - Getting Started) 53 | [atomist-dev]: https://docs.atomist.com/developer/prerequisites/ (Atomist - Developer Prerequisites) 54 | [atomist-quick-start]: https://docs.atomist.com/quick-start/ (Atomist Developer Quick Start) 55 | 56 | ### Configuration 57 | 58 | You can use the Atomist CLI to configure your local environment to run 59 | [software delivery machines (SDMs)][sdm] and other Atomist API 60 | clients. 61 | 62 | ``` 63 | $ atomist config 64 | ``` 65 | 66 | See the [Atomist developer prerequisites][atomist-dev] for more 67 | information. 68 | 69 | [sdm]: https://docs.atomist.com/ (Atomist Documentation) 70 | 71 | ### Kubernetes 72 | 73 | You can use the Atomist CLI to install the Atomist Kubernetes 74 | utilities in your Kubernetes cluster: 75 | 76 | ``` 77 | $ atomist kube --environment=MY_CLUSTER 78 | ``` 79 | 80 | replacing `MY_CLUSTER` with a meaningful name for the Kubernetes 81 | cluster your `kubectl` utility is configured to communicate with. See 82 | the [Atomist Kubernetes documentation][atomist-k8] for more 83 | information. 84 | 85 | [atomist-k8]: https://docs.atomist.com/user/kubernetes/ (Atomist Kubernetes) 86 | 87 | ### Fetch schema 88 | 89 | You can fetch the current version of the GraphQL schema for your 90 | Atomist workspace using the following command. 91 | 92 | ``` 93 | $ atomist gql-fetch 94 | ``` 95 | 96 | If you are defining custom types via registering ingestors in an SDM 97 | or other API client, you should download the schema in each of your 98 | SDM/API client projects prior to building them. 99 | 100 | ## Support 101 | 102 | General support questions should be discussed in the `#help` 103 | channel in the [Atomist community Slack workspace][slack]. 104 | 105 | If you find a problem, please create an [issue][]. 106 | 107 | [issue]: https://github.com/atomist/cli/issues 108 | 109 | ## Development 110 | 111 | You will need to install [node][] to build and test this project. 112 | 113 | [node]: https://nodejs.org/ (Node.js) 114 | 115 | ### Build and Test 116 | 117 | Use the following package scripts to build, test, and perform other 118 | development tasks. 119 | 120 | Command | Reason 121 | ------- | ------ 122 | `npm install` | install project dependencies 123 | `npm run build` | compile, test, lint, and generate docs 124 | `npm start` | start the Atomist CLI 125 | `npm run lint` | run TSLint against the TypeScript 126 | `npm run compile` | compile TypeScript 127 | `npm test` | run tests 128 | `npm run autotest` | run tests every time a file changes 129 | `npm run clean` | remove files generated during the build 130 | 131 | ### Release 132 | 133 | Releases are managed by the [Atomist SDM][atomist-sdm]. Press the 134 | release button in the Atomist dashboard or Slack. 135 | 136 | [atomist-sdm]: https://github.com/atomist/atomist-sdm (Atomist Software Delivery Machine) 137 | 138 | --- 139 | 140 | Created by [Atomist][atomist]. 141 | Need Help? [Join our Slack workspace][slack]. 142 | 143 | [atomist]: https://atomist.com/ (Atomist - How Teams Deliver Software) 144 | [slack]: https://join.atomist.com/ (Atomist Community Slack Workspace) 145 | -------------------------------------------------------------------------------- /test/kubeDecrypt.cli.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { execPromise } from "@atomist/sdm"; 18 | import * as fs from "fs-extra"; 19 | import * as yaml from "js-yaml"; 20 | import * as assert from "power-assert"; 21 | import { withFile } from "tmp-promise"; 22 | 23 | // tslint:disable-next-line: typedef 24 | describe("kube-decrypt cli", function() { 25 | 26 | // tslint:disable-next-line: no-invalid-this 27 | this.timeout("5s"); 28 | 29 | const encryptedSecret = { 30 | apiVersion: "v1", 31 | kind: "Secret", 32 | type: "Opaque", 33 | data: { 34 | remember: "KsSz7+4qYPKJFgAiHM9Dn7q7Of5hqKHIpm2SNQF7wWEAEwY8TftCBxfz2xeVLRKN", 35 | }, 36 | }; 37 | 38 | it("show help message", async () => { 39 | const result = await execPromise( 40 | "node", 41 | [ 42 | "index", 43 | "kube-decrypt", 44 | "--help", 45 | ]); 46 | 47 | assert(result.stdout.includes("Decrypt encrypted Kubernetes secret data values")); 48 | }); 49 | 50 | it("file and literal provided", async () => { 51 | try { 52 | await execPromise( 53 | "node", 54 | [ 55 | "index", 56 | "kube-decrypt", 57 | "--file=blah", 58 | "--literal=blahblah", 59 | ]); 60 | assert.fail("command should fail"); 61 | } catch (e) { 62 | assert(e.stderr.includes("Arguments file and literal are mutually exclusive")); 63 | } 64 | }); 65 | 66 | it("literal and key provided", async () => { 67 | const execResult = await execPromise( 68 | "node", 69 | [ 70 | "index", 71 | "kube-decrypt", 72 | "--secret-key=key", 73 | "--literal=PAk9Y8m7GR8KJULQo+g63HmZUmsLIg1VgiVAnBVjmFQhcHly+lM0i0U/BbZP8f0e", 74 | ]); 75 | 76 | const encryptedString = execResult.stdout.trim(); 77 | assert.equal(encryptedString, "Y29ycmVjdCB6ZWJyYSBiYXR0ZXJ5IHN0YXBsZQ=="); 78 | }); 79 | 80 | it("literal, base64 and key provided", async () => { 81 | const execResult = await execPromise( 82 | "node", 83 | [ 84 | "index", 85 | "kube-decrypt", 86 | "--secret-key=key", 87 | `--literal="PAk9Y8m7GR8KJULQo+g63HmZUmsLIg1VgiVAnBVjmFQhcHly+lM0i0U/BbZP8f0e"`, 88 | "--base64", 89 | ]); 90 | 91 | const encryptedString = execResult.stdout.trim(); 92 | assert.equal(encryptedString, "correct zebra battery staple"); 93 | }); 94 | 95 | it("file and key provided", async () => { 96 | await withFile(async fr => { 97 | await fs.writeFile(fr.path, yaml.safeDump(encryptedSecret)); 98 | 99 | const execResult = await execPromise( 100 | "node", 101 | [ 102 | "index", 103 | "kube-decrypt", 104 | "--secret-key=key", 105 | `--file=${fr.path}`, 106 | ]); 107 | 108 | const output = yaml.safeLoad(execResult.stdout); 109 | assert.deepEqual(output, { 110 | apiVersion: "v1", 111 | kind: "Secret", 112 | type: "Opaque", 113 | data: { 114 | remember: "Y29ycmVjdCBob3JzZSBzdGFwbGUgYmF0dGVyeQ==", 115 | }, 116 | }); 117 | }); 118 | }); 119 | 120 | it("file, base64 and key provided", async () => { 121 | await withFile(async fr => { 122 | await fs.writeFile(fr.path, yaml.safeDump(encryptedSecret)); 123 | 124 | const execResult = await execPromise( 125 | "node", 126 | [ 127 | "index", 128 | "kube-decrypt", 129 | "--secret-key=key", 130 | `--file=${fr.path}`, 131 | "--base64", 132 | ]); 133 | 134 | const output = yaml.safeLoad(execResult.stdout); 135 | assert.deepEqual(output, { 136 | apiVersion: "v1", 137 | kind: "Secret", 138 | type: "Opaque", 139 | data: { 140 | remember: "correct horse staple battery", 141 | }, 142 | }); 143 | }); 144 | }); 145 | }).timeout("5s"); 146 | -------------------------------------------------------------------------------- /bin/start.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright © 2019 Atomist, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // tslint:disable-next-line:no-import-side-effect 19 | import "source-map-support/register"; 20 | import * as yargs from "yargs"; 21 | import { cliCommand } from "../lib/command"; 22 | import * as print from "../lib/print"; 23 | import { repositoryStart } from "../lib/repositoryStart"; 24 | import { version } from "../lib/version"; 25 | 26 | async function start(): Promise { 27 | return yargs.command("*", "Start an SDM or automation client", argv => { 28 | argv.options({ 29 | "change-dir": { 30 | alias: "C", 31 | default: process.cwd(), 32 | describe: "Path to automation client project", 33 | type: "string", 34 | }, 35 | "compile": { 36 | default: true, 37 | describe: "Run 'npm run compile' before running", 38 | type: "boolean", 39 | }, 40 | "install": { 41 | describe: "Run 'npm install' before running/compiling, default is to install if no " + 42 | "'node_modules' directory exists", 43 | type: "boolean", 44 | }, 45 | "local": { 46 | default: false, 47 | describe: "Start SDM in local mode", 48 | type: "boolean", 49 | }, 50 | "profile": { 51 | describe: "Name of configuration profiles to include", 52 | type: "string", 53 | required: false, 54 | alias: "profiles", 55 | }, 56 | "watch": { 57 | describe: "Enable watch mode", 58 | type: "boolean", 59 | default: false, 60 | required: false, 61 | }, 62 | "debug": { 63 | describe: "Enable Node.js debugger", 64 | type: "boolean", 65 | default: false, 66 | required: false, 67 | }, 68 | "repository-url": { 69 | describe: "Git URL to clone", 70 | type: "string", 71 | required: false, 72 | }, 73 | "index": { 74 | describe: "Name of the file that exports the configuration", 75 | type: "string", 76 | required: false, 77 | implies: "repository-url", 78 | conflicts: "yaml", 79 | }, 80 | "yaml": { 81 | describe: "Glob patters for yaml files to import", 82 | type: "string", 83 | required: false, 84 | implies: "repository-url", 85 | conflicts: "index", 86 | }, 87 | "sha": { 88 | describe: "Git sha to checkout", 89 | type: "string", 90 | required: false, 91 | implies: "repository-url", 92 | alias: "branch", 93 | }, 94 | "seed-url": { 95 | describe: "Git URL to clone the seed to overlay with SDM repository", 96 | type: "string", 97 | required: false, 98 | implies: "repository-url", 99 | }, 100 | }); 101 | return yargs; 102 | }, async (argv: any) => { 103 | await cliCommand(() => { 104 | return repositoryStart({ 105 | cwd: argv["change-dir"], 106 | cloneUrl: argv["repository-url"], 107 | index: argv.index, 108 | yaml: argv.yaml, 109 | sha: argv.sha, 110 | local: argv.local, 111 | profile: argv.profile, 112 | seedUrl: argv["seed-url"], 113 | install: argv.install, 114 | compile: argv.compile, 115 | watch: argv.watch, 116 | debug: argv.debug, 117 | }); 118 | }); 119 | }) 120 | .completion("completion", false as any) 121 | .showHelpOnFail(true, "Specify --help for available options") 122 | .alias("help", ["h", "?"]) 123 | .version(version()) 124 | .alias("version", "v") 125 | .describe("version", "Show version information") 126 | .strict() 127 | .wrap(Math.min(100, yargs.terminalWidth())) 128 | .argv; 129 | } 130 | 131 | start() 132 | .catch((err: Error) => { 133 | print.error(`Unhandled error: ${err.message}`); 134 | print.error(err.stack); 135 | process.exit(102); 136 | }); 137 | -------------------------------------------------------------------------------- /lib/kubeCrypt.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as k8s from "@kubernetes/client-node"; 18 | import * as fs from "fs-extra"; 19 | import * as inquirer from "inquirer"; 20 | import * as yaml from "js-yaml"; 21 | import * as _ from "lodash"; 22 | import { DeepPartial } from "ts-essentials"; 23 | import { maskString } from "./config"; 24 | import { 25 | base64, 26 | crypt, 27 | printSecret, 28 | } from "./kubeUtils"; 29 | import * as print from "./print"; 30 | 31 | /** 32 | * Command-line options and arguments for kube-decrypt and kube-encrypt. 33 | */ 34 | export interface KubeCryptOptions { 35 | /** To encrypt or decrypt? */ 36 | action: KubeCryptActions; 37 | /** Path to Kubernetes secret spec file. */ 38 | file?: string; 39 | /** Literal string to encrypt/decrypt. */ 40 | literal?: string; 41 | /** Encryption key to use for encryption/decryption. */ 42 | secretKey?: string; 43 | /** Option to Base64 encode/decode data */ 44 | base64?: boolean; 45 | } 46 | 47 | /** Action to perform */ 48 | export type KubeCryptActions = "decrypt" | "encrypt"; 49 | 50 | /** 51 | * Encrypt or decrypt secret data values. 52 | * 53 | * @param opts see KubeCryptOptions 54 | * @return integer return value, 0 if successful, non-zero otherwise 55 | */ 56 | export async function kubeCrypt(opts: KubeCryptOptions): Promise { 57 | let secret: DeepPartial; 58 | 59 | secret = await handleSecretParameter(opts); 60 | if (!secret) { 61 | return 2; 62 | } 63 | opts.secretKey = await handleSecretKeyParameter(opts); 64 | 65 | const base64Encode = (s: DeepPartial) => opts.base64 ? base64(s, "encode") : s; 66 | const base64Decode = (s: DeepPartial) => opts.base64 ? base64(s, "decode") : s; 67 | 68 | try { 69 | let transformed: DeepPartial; 70 | if (opts.action === "encrypt") { 71 | transformed = await crypt(base64Encode(secret), opts); 72 | } else { 73 | transformed = base64Decode(await crypt(secret, opts)); 74 | } 75 | printSecret(transformed, opts); 76 | } catch (e) { 77 | print.error(`Failed to ${opts.action} secret: ${e.message}`); 78 | return 3; 79 | } 80 | 81 | return 0; 82 | } 83 | 84 | /** 85 | * Handle the literal or file secret parameter from the cli 86 | * @param opts file or literal of KubeCryptOptions 87 | * @returns secret or undefined if the file cannot be loaded 88 | */ 89 | export async function handleSecretParameter(opts: Pick): Promise> { 90 | let secret: DeepPartial; 91 | if (opts.file) { 92 | try { 93 | const secretString = await fs.readFile(opts.file, "utf8"); 94 | secret = await yaml.safeLoad(secretString); 95 | } catch (e) { 96 | print.error(`Failed to load secret spec from '${opts.file}'`); 97 | return undefined; 98 | } 99 | } else { 100 | if (!opts.literal) { 101 | const answers = await inquirer.prompt>([{ 102 | type: "input", 103 | name: "literal", 104 | message: `Enter literal string to be ${opts.action}ed:`, 105 | }]); 106 | opts.literal = answers.literal; 107 | } 108 | secret = wrapLiteral(opts.literal, opts.literal); 109 | } 110 | return secret; 111 | } 112 | 113 | /** 114 | * Creates a k8s.V1Secret with the input in the data section. 115 | * @param prop property name 116 | * @param literal String to wrap in k8s.V1Secret 117 | * @returns the k8s.V1Secret 118 | */ 119 | export function wrapLiteral(prop: string, literal: string): DeepPartial { 120 | const secret: DeepPartial = { 121 | apiVersion: "v1", 122 | data: {}, 123 | kind: "Secret", 124 | type: "Opaque", 125 | }; 126 | secret.data[prop] = literal; 127 | return secret; 128 | } 129 | 130 | /** 131 | * Handle the secret key parameter from the cli 132 | * @param opts secretKey from KubeCryptOptions 133 | * @returns the secret 134 | */ 135 | export async function handleSecretKeyParameter(opts: Pick): Promise { 136 | if (!opts.secretKey) { 137 | const answers = await inquirer.prompt>([{ 138 | type: "input", 139 | name: "secretKey", 140 | message: `Enter encryption key:`, 141 | transformer: maskString, 142 | validate: v => v.length < 1 ? "Secret key must have non-zero length" : true, 143 | }]); 144 | opts.secretKey = answers.secretKey; 145 | } 146 | return opts.secretKey; 147 | } 148 | -------------------------------------------------------------------------------- /test/kubeEncrypt.cli.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { execPromise } from "@atomist/sdm"; 18 | import * as fs from "fs-extra"; 19 | import * as yaml from "js-yaml"; 20 | import * as assert from "power-assert"; 21 | import { withFile } from "tmp-promise"; 22 | 23 | // tslint:disable-next-line: typedef 24 | describe("kube-encrypt cli", function() { 25 | 26 | // tslint:disable-next-line: no-invalid-this 27 | this.timeout("5s"); 28 | 29 | const plainTextSecret = { 30 | apiVersion: "v1", 31 | kind: "Secret", 32 | type: "Opaque", 33 | data: { 34 | remember: "correct kwagga staple battery", 35 | }, 36 | }; 37 | 38 | const encodedSecret = { 39 | apiVersion: "v1", 40 | kind: "Secret", 41 | type: "Opaque", 42 | data: { 43 | remember: "Y29ycmVjdCBob3JzZSBzdGFwbGUgYmF0dGVyeQ==", 44 | }, 45 | }; 46 | 47 | it("show help message", async () => { 48 | const result = await execPromise( 49 | "node", 50 | [ 51 | "index", 52 | "kube-encrypt", 53 | "--help", 54 | ]); 55 | 56 | assert(result.stdout.includes("Encrypt Base64 encoded Kubernetes secret data values")); 57 | }); 58 | 59 | it("file and literal provided", async () => { 60 | try { 61 | await execPromise( 62 | "node", 63 | [ 64 | "index", 65 | "kube-encrypt", 66 | "--file=blah", 67 | "--literal=blahblah", 68 | ]); 69 | assert.fail("command should fail"); 70 | } catch (e) { 71 | assert(e.stderr.includes("Arguments file and literal are mutually exclusive")); 72 | } 73 | }); 74 | 75 | it("literal and key provided", async () => { 76 | const execResult = await execPromise( 77 | "node", 78 | [ 79 | "index", 80 | "kube-encrypt", 81 | "--secret-key=key", 82 | "--literal=Y29ycmVjdCB6ZWJyYSBiYXR0ZXJ5IHN0YXBsZQ==", 83 | ]); 84 | 85 | const encryptedString = execResult.stdout.trim(); 86 | assert.equal(encryptedString, "PAk9Y8m7GR8KJULQo+g63HmZUmsLIg1VgiVAnBVjmFQhcHly+lM0i0U/BbZP8f0e"); 87 | }); 88 | 89 | it("literal, base64 and key provided", async () => { 90 | const execResult = await execPromise( 91 | "node", 92 | [ 93 | "index", 94 | "kube-encrypt", 95 | "--secret-key=key", 96 | `--literal="correct zebra battery staple"`, 97 | "--base64", 98 | ]); 99 | 100 | const encryptedString = execResult.stdout.trim(); 101 | assert.equal(encryptedString, "PAk9Y8m7GR8KJULQo+g63HmZUmsLIg1VgiVAnBVjmFQhcHly+lM0i0U/BbZP8f0e"); 102 | }); 103 | 104 | it("file and key provided", async () => { 105 | await withFile(async fr => { 106 | await fs.writeFile(fr.path, yaml.safeDump(encodedSecret)); 107 | 108 | const execResult = await execPromise( 109 | "node", 110 | [ 111 | "index", 112 | "kube-encrypt", 113 | "--secret-key=key", 114 | `--file=${fr.path}`, 115 | ]); 116 | 117 | const output = yaml.safeLoad(execResult.stdout); 118 | assert.deepEqual(output, { 119 | apiVersion: "v1", 120 | kind: "Secret", 121 | type: "Opaque", 122 | data: { 123 | remember: "KsSz7+4qYPKJFgAiHM9Dn7q7Of5hqKHIpm2SNQF7wWEAEwY8TftCBxfz2xeVLRKN", 124 | }, 125 | }); 126 | }); 127 | }); 128 | 129 | it("file, base64 and key provided", async () => { 130 | await withFile(async fr => { 131 | await fs.writeFile(fr.path, yaml.safeDump(plainTextSecret)); 132 | 133 | const execResult = await execPromise( 134 | "node", 135 | [ 136 | "index", 137 | "kube-encrypt", 138 | "--secret-key=key", 139 | `--file=${fr.path}`, 140 | "--base64", 141 | ]); 142 | 143 | const output = yaml.safeLoad(execResult.stdout); 144 | assert.deepEqual(output, { 145 | apiVersion: "v1", 146 | kind: "Secret", 147 | type: "Opaque", 148 | data: { 149 | remember: "UwpScTa59rFyIdAHOP60eGHbYzrvm/npND+To6fsE9IQ6+cXyTLWT6dLgeRz+ouk", 150 | }, 151 | }); 152 | }); 153 | }); 154 | 155 | it("literal, base64 and key provided", async () => { 156 | const execResult = await execPromise( 157 | "node", 158 | [ 159 | "index", 160 | "kube-encrypt", 161 | "--secret-key=key", 162 | `--literal="correct zebra battery staple"`, 163 | "--base64", 164 | ]); 165 | 166 | const encryptedString = execResult.stdout.trim(); 167 | assert.equal(encryptedString, "PAk9Y8m7GR8KJULQo+g63HmZUmsLIg1VgiVAnBVjmFQhcHly+lM0i0U/BbZP8f0e"); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /test/kubeUtils.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as k8s from "@kubernetes/client-node"; 18 | import * as assert from "power-assert"; 19 | import { DeepPartial } from "ts-essentials"; 20 | import { 21 | base64, 22 | crypt, 23 | } from "../lib/kubeUtils"; 24 | 25 | describe("kubeCrypt", () => { 26 | 27 | describe("base64", () => { 28 | 29 | it("encode", () => { 30 | const secret: DeepPartial = { 31 | apiVersion: "v1", 32 | kind: "Secret", 33 | type: "Opaque", 34 | data: { 35 | jane: "doe", 36 | john: "smith", 37 | }, 38 | }; 39 | 40 | const encodedSecret = base64(secret, "encode"); 41 | 42 | assert.deepStrictEqual(encodedSecret, { 43 | apiVersion: "v1", 44 | kind: "Secret", 45 | type: "Opaque", 46 | data: { 47 | jane: "ZG9l", 48 | john: "c21pdGg=", 49 | }, 50 | }); 51 | }); 52 | 53 | it("decode", () => { 54 | const secret: DeepPartial = { 55 | apiVersion: "v1", 56 | kind: "Secret", 57 | type: "Opaque", 58 | data: { 59 | music: "Um9jayAmIFJvbGw=", 60 | }, 61 | }; 62 | 63 | const encodedSecret = base64(secret, "decode"); 64 | 65 | assert.deepStrictEqual(encodedSecret, { 66 | apiVersion: "v1", 67 | kind: "Secret", 68 | type: "Opaque", 69 | data: { 70 | music: "Rock & Roll", 71 | }, 72 | }); 73 | }); 74 | }); 75 | 76 | describe("doKubeCrypt", () => { 77 | 78 | it("encrypt without base64", async () => { 79 | const secret: DeepPartial = { 80 | apiVersion: "v1", 81 | kind: "Secret", 82 | type: "Opaque", 83 | data: { 84 | jane: "doe", 85 | john: "smith", 86 | }, 87 | }; 88 | 89 | const encodedSecret = await crypt(secret, { action: "encrypt", secretKey: "super-secret-key" }); 90 | 91 | assert.deepStrictEqual(encodedSecret, { 92 | apiVersion: "v1", 93 | kind: "Secret", 94 | type: "Opaque", 95 | data: { 96 | jane: "UiYhAtTurHKXQ2rWnmOs/Q==", 97 | john: "VNqXYEwNsLpS7TQ4hJmSFw==", 98 | }, 99 | }); 100 | }); 101 | 102 | it("encrypt with base64", async () => { 103 | const secret: DeepPartial = { 104 | apiVersion: "v1", 105 | kind: "Secret", 106 | type: "Opaque", 107 | data: { 108 | jane: "doe", 109 | john: "smith", 110 | }, 111 | }; 112 | 113 | const encryptedSecret = await crypt( 114 | base64(secret, "encode"), 115 | { action: "encrypt", secretKey: "super-secret-key" }, 116 | ); 117 | 118 | assert.deepStrictEqual(encryptedSecret, { 119 | apiVersion: "v1", 120 | kind: "Secret", 121 | type: "Opaque", 122 | data: { 123 | jane: "ZTqxNwPNH8m87L2D6nzYaQ==", 124 | john: "dwM3clgniklNL8yBW67xMA==", 125 | }, 126 | }); 127 | }); 128 | 129 | it("decrypt without base64", async () => { 130 | const secret: DeepPartial = { 131 | apiVersion: "v1", 132 | kind: "Secret", 133 | type: "Opaque", 134 | data: { 135 | jane: "UiYhAtTurHKXQ2rWnmOs/Q==", 136 | john: "VNqXYEwNsLpS7TQ4hJmSFw==", 137 | }, 138 | }; 139 | 140 | const encodedSecret = await crypt(secret, { action: "decrypt", secretKey: "super-secret-key" }); 141 | 142 | assert.deepStrictEqual(encodedSecret, { 143 | apiVersion: "v1", 144 | kind: "Secret", 145 | type: "Opaque", 146 | data: { 147 | jane: "doe", 148 | john: "smith", 149 | }, 150 | }); 151 | }); 152 | 153 | it("decrypt with base64", async () => { 154 | const secret: DeepPartial = { 155 | apiVersion: "v1", 156 | kind: "Secret", 157 | type: "Opaque", 158 | data: { 159 | jane: "ZTqxNwPNH8m87L2D6nzYaQ==", 160 | john: "dwM3clgniklNL8yBW67xMA==", 161 | }, 162 | }; 163 | 164 | const decryptedSecret = await crypt(secret, { action: "decrypt", secretKey: "super-secret-key" }); 165 | const decodedSecret = base64(decryptedSecret, "decode"); 166 | 167 | assert.deepStrictEqual(decodedSecret, { 168 | apiVersion: "v1", 169 | kind: "Secret", 170 | type: "Opaque", 171 | data: { 172 | jane: "doe", 173 | john: "smith", 174 | }, 175 | }); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/command.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as assert from "power-assert"; 18 | 19 | import { 20 | extractArgs, 21 | shouldAddLocalSdmCommands, 22 | } from "../lib/command"; 23 | 24 | describe("command", () => { 25 | 26 | describe("shouldAddLocalSdmCommands", () => { 27 | 28 | const knownCommands = [ 29 | "config", 30 | "execute", 31 | "git-hook", 32 | "gql-fetch", 33 | "kube-install", 34 | "start", 35 | ]; 36 | 37 | it("should return false for empty args", () => { 38 | const args: string[] = []; 39 | assert(!shouldAddLocalSdmCommands(args)); 40 | }); 41 | 42 | it("should return false for no commands", () => { 43 | const args = ["/usr/local/Cellar/node/10.8.0/bin/node", "/Users/rihanna/develop/atomist/cli/index.js"]; 44 | assert(!shouldAddLocalSdmCommands(args)); 45 | }); 46 | 47 | it("should return true for --help", () => { 48 | const args = ["node", "index.js", "--help"]; 49 | assert(shouldAddLocalSdmCommands(args)); 50 | }); 51 | 52 | it("should return false for --version", () => { 53 | const args = ["node", "index.js", "--version"]; 54 | assert(!shouldAddLocalSdmCommands(args)); 55 | }); 56 | 57 | it("should return false for known commands", () => { 58 | const firstArgs = ["node", "index.js"]; 59 | knownCommands.forEach(c => { 60 | assert(!shouldAddLocalSdmCommands([...firstArgs, c])); 61 | }); 62 | }); 63 | 64 | it("should return false for known commands with more arguments", () => { 65 | const firstArgs = ["node", "index.js"]; 66 | const lastArgs = ["some", "--command", "line", "-o", "ptions"]; 67 | knownCommands.forEach(c => { 68 | assert(!shouldAddLocalSdmCommands([...firstArgs, c, ...lastArgs])); 69 | }); 70 | }); 71 | 72 | it("should return true for unknown commands", () => { 73 | const firstArgs = ["node", "index.js"]; 74 | ["conf", "gt", "gql-fletch", "gql-generate", "kubernetes", "stop"].forEach(c => { 75 | assert(shouldAddLocalSdmCommands([...firstArgs, c])); 76 | }); 77 | }); 78 | 79 | it("should return true for unknown commands with more arguments", () => { 80 | const firstArgs = ["node", "index.js"]; 81 | const lastArgs = ["some", "--command", "line", "-o", "ptions"]; 82 | ["umbrella", "betta", "have", "my", "money"].forEach(c => { 83 | assert(shouldAddLocalSdmCommands([...firstArgs, c, ...lastArgs])); 84 | }); 85 | }); 86 | 87 | it("should ignore known commands later in the arguments", () => { 88 | const firstArgs = ["node", "index.js"]; 89 | const lastArgs = ["config", "--git", "gql-fetch", "-k", "start"]; 90 | ["umbrella", "betta", "have", "my", "money"].forEach(c => { 91 | assert(shouldAddLocalSdmCommands([...firstArgs, c, ...lastArgs])); 92 | }); 93 | }); 94 | 95 | }); 96 | 97 | describe("extractArgs", () => { 98 | 99 | it("should parse parameter value pairs", () => { 100 | const pvs = ["mavis=staples", "cleotha=staples", "pervis=staples", "roebuck=pops"]; 101 | const args = extractArgs(pvs); 102 | assert(args.length === pvs.length); 103 | assert(args[0].name === "mavis"); 104 | assert(args[0].value === "staples"); 105 | assert(args[1].name === "cleotha"); 106 | assert(args[1].value === "staples"); 107 | assert(args[2].name === "pervis"); 108 | assert(args[2].value === "staples"); 109 | assert(args[3].name === "roebuck"); 110 | assert(args[3].value === "pops"); 111 | }); 112 | 113 | it("should parse values with equal signs", () => { 114 | const pvs = ["staples=cleotha=mavis=pervis=pops"]; 115 | const args = extractArgs(pvs); 116 | assert(args.length === pvs.length); 117 | assert(args[0].name === "staples"); 118 | assert(args[0].value === "cleotha=mavis=pervis=pops"); 119 | }); 120 | 121 | it("should parse parameters without value to undefined", () => { 122 | const pvs = ["cleotha", "mavis", "pervis", "pops"]; 123 | const args = extractArgs(pvs); 124 | assert(args.length === pvs.length); 125 | assert(args[0].name === "cleotha"); 126 | assert(args[0].value === undefined); 127 | assert(args[1].name === "mavis"); 128 | assert(args[1].value === undefined); 129 | assert(args[2].name === "pervis"); 130 | assert(args[2].value === undefined); 131 | assert(args[3].name === "pops"); 132 | assert(args[3].value === undefined); 133 | }); 134 | 135 | it("should parse parameters with empty values to empty strings", () => { 136 | const pvs = ["cleotha=", "mavis=", "pervis=", "pops="]; 137 | const args = extractArgs(pvs); 138 | assert(args.length === pvs.length); 139 | assert(args[0].name === "cleotha"); 140 | assert(args[0].value === ""); 141 | assert(args[1].name === "mavis"); 142 | assert(args[1].value === ""); 143 | assert(args[2].name === "pervis"); 144 | assert(args[2].value === ""); 145 | assert(args[3].name === "pops"); 146 | assert(args[3].value === ""); 147 | }); 148 | 149 | }); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [], 4 | "jsRules": {}, 5 | "rules": { 6 | "adjacent-overload-signatures": true, 7 | "align": { 8 | "options": ["parameters", "statements"] 9 | }, 10 | "array-type": { 11 | "options": ["array-simple"] 12 | }, 13 | "arrow-parens": { 14 | "options": ["ban-single-arg-parens"] 15 | }, 16 | "arrow-return-shorthand": true, 17 | "await-promise": true, 18 | "ban-types": { 19 | "options": [ 20 | ["Object", "Avoid using the `Object` type. Did you mean `object`?"], 21 | [ 22 | "Function", 23 | "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." 24 | ], 25 | ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"], 26 | ["Number", "Avoid using the `Number` type. Did you mean `number`?"], 27 | ["String", "Avoid using the `String` type. Did you mean `string`?"], 28 | ["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"] 29 | ] 30 | }, 31 | "callable-types": true, 32 | "class-name": true, 33 | "comment-format": { 34 | "options": ["check-space"] 35 | }, 36 | "curly": true, 37 | "cyclomatic-complexity": { 38 | "severity": "warning" 39 | }, 40 | "deprecation": true, 41 | "eofline": true, 42 | "forin": true, 43 | "import-blacklist": [true, "axios"], 44 | "import-spacing": true, 45 | "indent": { 46 | "options": ["spaces"] 47 | }, 48 | "interface-name": { 49 | "options": ["never-prefix"] 50 | }, 51 | "interface-over-type-literal": true, 52 | "jsdoc-format": true, 53 | "label-position": true, 54 | "max-classes-per-file": { 55 | "severity": "warning", 56 | "options": [7] 57 | }, 58 | "max-file-line-count": { 59 | "severity": "warning", 60 | "options": [500] 61 | }, 62 | "max-line-length": { 63 | "severity": "warning", 64 | "options": [150] 65 | }, 66 | "member-access": true, 67 | "member-ordering": false, 68 | "new-parens": true, 69 | "no-angle-bracket-type-assertion": true, 70 | "no-any": false, 71 | "no-arg": true, 72 | "no-bitwise": true, 73 | "no-conditional-assignment": true, 74 | "no-consecutive-blank-lines": true, 75 | "no-console": { 76 | "severity": "warning", 77 | "options": ["debug", "error", "info", "log", "warn"] 78 | }, 79 | "no-construct": true, 80 | "no-debugger": true, 81 | "no-duplicate-imports": true, 82 | "no-duplicate-super": true, 83 | "no-duplicate-switch-case": true, 84 | "no-empty": { 85 | "options": ["allow-empty-catch", "allow-empty-functions"] 86 | }, 87 | "no-empty-interface": true, 88 | "no-eval": true, 89 | "no-floating-promises": true, 90 | "no-implicit-dependencies": { 91 | "options": ["dev"] 92 | }, 93 | "no-import-side-effect": true, 94 | "no-inferred-empty-object-type": true, 95 | "no-internal-module": true, 96 | "no-invalid-template-strings": { 97 | "severity": "warning" 98 | }, 99 | "no-invalid-this": true, 100 | "no-magic-numbers": false, 101 | "no-misused-new": true, 102 | "no-namespace": true, 103 | "no-null-keyword": { 104 | "severity": "warning" 105 | }, 106 | "no-object-literal-type-assertion": true, 107 | "no-parameter-properties": false, 108 | "no-parameter-reassignment": true, 109 | "no-reference": true, 110 | "no-reference-import": true, 111 | "no-return-await": true, 112 | "no-shadowed-variable": true, 113 | "no-string-literal": true, 114 | "no-string-throw": true, 115 | "no-switch-case-fall-through": true, 116 | "no-trailing-whitespace": true, 117 | "no-unnecessary-callback-wrapper": true, 118 | "no-unnecessary-class": true, 119 | "no-unnecessary-initializer": true, 120 | "no-unnecessary-type-assertion": { 121 | "severity": "warning" 122 | }, 123 | "no-unsafe-finally": true, 124 | "no-unused-expression": true, 125 | "no-use-before-declare": false, 126 | "no-var-keyword": true, 127 | "no-var-requires": true, 128 | "object-literal-key-quotes": { 129 | "options": ["consistent-as-needed"] 130 | }, 131 | "object-literal-shorthand": true, 132 | "object-literal-sort-keys": false, 133 | "one-line": { 134 | "options": [ 135 | "check-catch", 136 | "check-else", 137 | "check-finally", 138 | "check-open-brace", 139 | "check-whitespace" 140 | ] 141 | }, 142 | "one-variable-per-declaration": { 143 | "options": ["ignore-for-loop"] 144 | }, 145 | "only-arrow-functions": { 146 | "options": ["allow-declarations", "allow-named-functions"] 147 | }, 148 | "ordered-imports": { 149 | "options": { 150 | "import-sources-order": "case-insensitive", 151 | "module-source-path": "full", 152 | "named-imports-order": "case-insensitive" 153 | } 154 | }, 155 | "prefer-const": true, 156 | "prefer-for-of": true, 157 | "prefer-readonly": true, 158 | "quotemark": { 159 | "options": ["double", "avoid-escape"] 160 | }, 161 | "radix": true, 162 | "semicolon": { 163 | "options": ["always"] 164 | }, 165 | "space-before-function-paren": { 166 | "options": { 167 | "anonymous": "never", 168 | "asyncArrow": "always", 169 | "constructor": "never", 170 | "method": "never", 171 | "named": "never" 172 | } 173 | }, 174 | "trailing-comma": { 175 | "options": { 176 | "esSpecCompliant": true, 177 | "multiline": "always", 178 | "singleline": "never" 179 | } 180 | }, 181 | "triple-equals": { 182 | "options": ["allow-null-check"] 183 | }, 184 | "typedef": { 185 | "severity": "warning", 186 | "options": [ 187 | true, 188 | "call-signature", 189 | "parameter", 190 | "property-declaration", 191 | "member-variable-declaration" 192 | ] 193 | }, 194 | "typedef-whitespace": { 195 | "options": [ 196 | { 197 | "call-signature": "nospace", 198 | "index-signature": "nospace", 199 | "parameter": "nospace", 200 | "property-declaration": "nospace", 201 | "variable-declaration": "nospace" 202 | }, 203 | { 204 | "call-signature": "onespace", 205 | "index-signature": "onespace", 206 | "parameter": "onespace", 207 | "property-declaration": "onespace", 208 | "variable-declaration": "onespace" 209 | } 210 | ] 211 | }, 212 | "typeof-compare": false, 213 | "unified-signatures": true, 214 | "use-isnan": true, 215 | "variable-name": { 216 | "options": ["ban-keywords", "check-format", "allow-pascal-case"] 217 | }, 218 | "whitespace": { 219 | "options": [ 220 | "check-branch", 221 | "check-decl", 222 | "check-operator", 223 | "check-separator", 224 | "check-type", 225 | "check-typecast" 226 | ] 227 | } 228 | }, 229 | "rulesDirectory": [] 230 | } 231 | -------------------------------------------------------------------------------- /test/kubeEdit.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fs from "fs-extra"; 18 | import * as yaml from "js-yaml"; 19 | import * as assert from "power-assert"; 20 | import { 21 | file, 22 | withFile, 23 | } from "tmp-promise"; 24 | import { kubeEdit } from "../lib/kubeEdit"; 25 | 26 | describe("kubeEdit", () => { 27 | 28 | const encodedSecret = { 29 | apiVersion: "v1", 30 | kind: "Secret", 31 | type: "Opaque", 32 | metadata: { 33 | name: "mysecret", 34 | }, 35 | data: { 36 | username: "dGhlIHJvb3RtaW5pc3RyYXRvcg==", 37 | password: "Y29ycmVjdCBob3JzZSBiYXR0ZXJ5IHN0YXBsZQ==", 38 | }, 39 | }; 40 | 41 | const encryptedSecret = { 42 | apiVersion: "v1", 43 | kind: "Secret", 44 | type: "Opaque", 45 | metadata: { 46 | name: "mysecret", 47 | }, 48 | data: { 49 | username: "2RtW5dUnrmmgDPmGExnYt1nHizOm+CUkMsNG9+xOnSU=", 50 | password: "jkc8la4XtG9KZSQzTXByBYdmx80rVtHtEMKbNOIQj13t11KWH9iR4Pnzn7IuHFbP", 51 | }, 52 | }; 53 | 54 | describe("exit handling", () => { 55 | it("abort edit", async () => { 56 | const editorCommand = ` 57 | let args = process.argv.slice(2); 58 | let fileName = args[0]; 59 | require('fs').writeFile(fileName, "", () => {});`; 60 | 61 | let commandErrorCode; 62 | let output; 63 | 64 | const mockEditor = await file(); 65 | const inputfile = await file(); 66 | 67 | await fs.writeFile(mockEditor.path, editorCommand); 68 | await fs.writeFile(inputfile.path, yaml.safeDump(encodedSecret)); 69 | 70 | process.env.EDITOR = `node ${mockEditor.path}`; 71 | 72 | commandErrorCode = await kubeEdit({file: inputfile.path}); 73 | output = await yaml.safeLoad(await fs.readFile(inputfile.path, "utf8")); 74 | 75 | await mockEditor.cleanup(); 76 | await inputfile.cleanup(); 77 | 78 | assert.equal(4, commandErrorCode); 79 | assert.deepStrictEqual(output, { 80 | apiVersion: "v1", 81 | kind: "Secret", 82 | type: "Opaque", 83 | metadata: { 84 | name: "mysecret", 85 | }, 86 | data: { 87 | username: "dGhlIHJvb3RtaW5pc3RyYXRvcg==", 88 | password: "Y29ycmVjdCBob3JzZSBiYXR0ZXJ5IHN0YXBsZQ==", 89 | }, 90 | }); 91 | }); 92 | 93 | it("wrong secret provided", async () => { 94 | let output; 95 | let commandErrorCode; 96 | await withFile(async ({path, fd}) => { 97 | await fs.writeFile(path, yaml.safeDump(encryptedSecret)); 98 | 99 | commandErrorCode = await kubeEdit({file: path, secretKey: "this is not the secret"}); 100 | 101 | output = await yaml.safeLoad(await fs.readFile(path, "utf8")); 102 | }); 103 | 104 | assert.equal(3, commandErrorCode); 105 | assert.deepStrictEqual(output, encryptedSecret); 106 | }); 107 | 108 | it("missing file provided", async () => { 109 | let commandErrorCode; 110 | commandErrorCode = await kubeEdit({file: "a-file-that-doesnt-exist.yaml"}); 111 | assert.equal(2, commandErrorCode); 112 | }); 113 | 114 | it("invalid file provided", async () => { 115 | const invalidFileContents = `apiVersion: v1 116 | kind: Secret 117 | metadata: 118 | name: mysecret 119 | type: Opaque 120 | data:`; 121 | 122 | let commandErrorCode; 123 | await withFile(async ({path, fd}) => { 124 | await fs.writeFile(path, invalidFileContents); 125 | commandErrorCode = await kubeEdit({file: path}); 126 | }); 127 | assert.equal(2, commandErrorCode); 128 | }); 129 | }); 130 | 131 | describe("edit functionality", () => { 132 | it("edit nothing", async () => { 133 | process.env.EDITOR = "npx replace nothing yoursecret"; 134 | 135 | let output; 136 | await withFile(async ({path, fd}) => { 137 | await fs.writeFile(path, yaml.safeDump(encodedSecret)); 138 | 139 | await kubeEdit({file: path}); 140 | 141 | output = await yaml.safeLoad(await fs.readFile(path, "utf8")); 142 | }); 143 | 144 | assert.deepStrictEqual(output, { 145 | apiVersion: "v1", 146 | kind: "Secret", 147 | type: "Opaque", 148 | metadata: { 149 | name: "mysecret", 150 | }, 151 | data: { 152 | username: "dGhlIHJvb3RtaW5pc3RyYXRvcg==", 153 | password: "Y29ycmVjdCBob3JzZSBiYXR0ZXJ5IHN0YXBsZQ==", 154 | }, 155 | }); 156 | }); 157 | 158 | it("edit metadata", async () => { 159 | process.env.EDITOR = "npx replace --silent mysecret yoursecret"; 160 | 161 | let output; 162 | await withFile(async ({path, fd}) => { 163 | await fs.writeFile(path, yaml.safeDump(encodedSecret)); 164 | 165 | await kubeEdit({file: path}); 166 | 167 | output = await yaml.safeLoad(await fs.readFile(path, "utf8")); 168 | }); 169 | 170 | assert.deepStrictEqual(output, { 171 | apiVersion: "v1", 172 | kind: "Secret", 173 | type: "Opaque", 174 | metadata: { 175 | name: "yoursecret", 176 | }, 177 | data: { 178 | username: "dGhlIHJvb3RtaW5pc3RyYXRvcg==", 179 | password: "Y29ycmVjdCBob3JzZSBiYXR0ZXJ5IHN0YXBsZQ==", 180 | }, 181 | }); 182 | }); 183 | 184 | it("edit encoded", async () => { 185 | process.env.EDITOR = "npx replace --silent horse zebra"; 186 | 187 | let output; 188 | await withFile(async ({path, fd}) => { 189 | await fs.writeFile(path, yaml.safeDump(encodedSecret)); 190 | 191 | await kubeEdit({file: path}); 192 | 193 | output = await yaml.safeLoad(await fs.readFile(path, "utf8")); 194 | }); 195 | 196 | assert.deepStrictEqual(output, { 197 | apiVersion: "v1", 198 | kind: "Secret", 199 | type: "Opaque", 200 | metadata: { 201 | name: "mysecret", 202 | }, 203 | data: { 204 | username: "dGhlIHJvb3RtaW5pc3RyYXRvcg==", 205 | password: "Y29ycmVjdCB6ZWJyYSBiYXR0ZXJ5IHN0YXBsZQ==", 206 | }, 207 | }); 208 | }); 209 | 210 | it("edit encrypted", async () => { 211 | process.env.EDITOR = "npx replace --silent horse zebra"; 212 | 213 | let output; 214 | await withFile(async ({path, fd}) => { 215 | await fs.writeFile(path, yaml.safeDump(encryptedSecret)); 216 | 217 | await kubeEdit({file: path, secretKey: "you-would-never-guess"}); 218 | 219 | output = await yaml.safeLoad(await fs.readFile(path, "utf8")); 220 | }); 221 | 222 | assert.deepStrictEqual(output, { 223 | apiVersion: "v1", 224 | kind: "Secret", 225 | type: "Opaque", 226 | metadata: { 227 | name: "mysecret", 228 | }, 229 | data: { 230 | username: "2RtW5dUnrmmgDPmGExnYt1nHizOm+CUkMsNG9+xOnSU=", 231 | password: "n04B+JR7vlmMG/Kri0XFjVWqx2q2QvskF5xeaNNgi7S+o1s0eXlCoY7yKd2LXYC1", 232 | }, 233 | }); 234 | }); 235 | }); 236 | 237 | }); 238 | -------------------------------------------------------------------------------- /lib/spawn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as child_process from "child_process"; 18 | import * as spawn from "cross-spawn"; 19 | import * as fs from "fs-extra"; 20 | import * as path from "path"; 21 | 22 | import * as print from "./print"; 23 | 24 | /** 25 | * Options when executing a command. 26 | */ 27 | export interface SpawnOptions { 28 | /** Command to execute */ 29 | command: string; 30 | /** Command-line arguments of command */ 31 | args?: string[]; 32 | /** Directory to run command in, must be an automation client directory */ 33 | cwd?: string; 34 | /** If true or no node_modules directory exists, run "npm install" before running command */ 35 | install?: boolean; 36 | /** If true, run `npm run compile` before running command */ 37 | compile?: boolean; 38 | /** Checks to run after the optional install and compile */ 39 | checks?: Array<() => number>; 40 | } 41 | 42 | /** 43 | * Run a binary automation-client dependency in a platform-independent-ish way. 44 | */ 45 | export async function spawnBinary(opts: SpawnOptions): Promise { 46 | opts.cwd = path.resolve(opts.cwd); 47 | opts.command = path.join(opts.cwd, "node_modules", ".bin", opts.command); 48 | opts.checks = [ 49 | () => { 50 | if (!fs.existsSync(opts.command)) { 51 | print.error(`Project at '${opts.cwd}' is not a valid automation client: missing ${opts.command}`); 52 | print.error(`Make sure the @atomist/automation-client dependency in the project is up to date`); 53 | return 1; 54 | } 55 | return 0; 56 | }, 57 | ]; 58 | return spawnCommand(opts); 59 | } 60 | 61 | /** 62 | * Run an @atomist/automation-client Node.js script. 63 | */ 64 | export async function spawnJs(opts: SpawnOptions & { nodeArgs?: string[] }): Promise { 65 | opts.cwd = path.resolve(opts.cwd); 66 | const script = path.join(opts.cwd, "node_modules", "@atomist", "automation-client", opts.command); 67 | opts.args = [...(opts.nodeArgs || []), script, ...(opts.args || [])]; 68 | opts.command = process.argv0; 69 | opts.checks = [ 70 | () => { 71 | if (!fs.existsSync(script)) { 72 | print.error(`Project at '${opts.cwd}' is not a valid automation client project: missing ${script}`); 73 | print.error(`Make sure the @atomist/automation-client dependency in the project is up to date`); 74 | return 1; 75 | } 76 | return 0; 77 | }, 78 | ]; 79 | return spawnCommand(opts); 80 | } 81 | 82 | /** 83 | * Run the given command with the given arguments, optionally 84 | * installing and compiling first and return a Promise of the command 85 | * exit value. 86 | * 87 | * @param opts see SpawnOptions 88 | * @return Promise of integer return value of command, Promise.resolve(0) if command is successful 89 | */ 90 | async function spawnCommand(opts: SpawnOptions): Promise { 91 | // if --install or --no-install is provided, do that 92 | if (opts.install || 93 | // otherwise run install if --no-install was not given & the node_modules directory does not exist 94 | // tslint:disable-next-line:no-boolean-literal-compare 95 | (opts.install !== false && !fs.existsSync(path.join(opts.cwd, "node_modules")))) { 96 | const installStatus = await npmInstall(opts.cwd); 97 | if (installStatus !== 0) { 98 | return installStatus; 99 | } 100 | } 101 | 102 | if (opts.compile) { 103 | const compileStatus = await npmCompile(opts.cwd); 104 | if (compileStatus !== 0) { 105 | return compileStatus; 106 | } 107 | } 108 | 109 | if (opts.checks) { 110 | for (const check of opts.checks) { 111 | const checkStatus = check(); 112 | if (checkStatus !== 0) { 113 | return checkStatus; 114 | } 115 | } 116 | } 117 | 118 | return spawnPromise(opts); 119 | } 120 | 121 | /** 122 | * Run `npm ci` if a package-lock.json exists in the project; runs `npm install` otherwise. 123 | * It ensures a package.json exist in cwd. 124 | * 125 | * @param cwd directory to run install in 126 | * @return return value of the `npm install` command 127 | */ 128 | async function npmInstall(cwd: string): Promise { 129 | const packageJsonLock = path.join(cwd, "package-lock.json"); 130 | if (await fs.pathExists(packageJsonLock)) { 131 | return spawnPromise({ 132 | command: "npm", 133 | args: ["ci", "--no-progress", "--quiet"], 134 | cwd, 135 | checkPackageJson: true, 136 | }); 137 | } else { 138 | return spawnPromise({ 139 | command: "npm", 140 | args: ["install", "--no-progress", "--quiet"], 141 | cwd, 142 | checkPackageJson: true, 143 | }); 144 | } 145 | } 146 | 147 | /** 148 | * Run `npm run compile`. It ensures a package.json exist in cwd. 149 | * 150 | * @param cwd directory to run the compilation in 151 | * @return return value of the `npm run compile` command 152 | */ 153 | async function npmCompile(cwd: string): Promise { 154 | return spawnPromise({ 155 | command: "npm", 156 | args: ["run", "compile"], 157 | cwd, 158 | checkPackageJson: true, 159 | }); 160 | } 161 | 162 | /** 163 | * Options used by spawnPromise to execute a command. 164 | */ 165 | export interface SpawnPromiseOptions { 166 | /** Executable to spawn */ 167 | command: string; 168 | /** Arguments to pass to executable */ 169 | args?: string[]; 170 | /** Directory to launch executable process in, default is process process.cwd */ 171 | cwd?: string; 172 | /** If true, ensure package.json exist in cwd before running command */ 173 | checkPackageJson?: boolean; 174 | } 175 | 176 | /** 177 | * Run a command and return a promise supplying the exit value of 178 | * the command. 179 | * 180 | * @param options see SpawnPromiseoptions 181 | * @return a Promise of the return value of the spawned command 182 | */ 183 | export async function spawnPromise(options: SpawnPromiseOptions): Promise { 184 | const signals: { [key: string]: number } = { 185 | SIGHUP: 1, 186 | SIGINT: 2, 187 | SIGQUIT: 3, 188 | SIGILL: 4, 189 | SIGTRAP: 5, 190 | SIGABRT: 6, 191 | SIGBUS: 7, 192 | SIGFPE: 8, 193 | SIGKILL: 9, 194 | SIGUSR1: 10, 195 | SIGSEGV: 11, 196 | SIGUSR2: 12, 197 | SIGPIPE: 13, 198 | SIGALRM: 14, 199 | SIGTERM: 15, 200 | SIGCHLD: 17, 201 | SIGCONT: 18, 202 | SIGSTOP: 19, 203 | SIGTSTP: 20, 204 | SIGTTIN: 21, 205 | SIGTTOU: 22, 206 | SIGURG: 23, 207 | SIGXCPU: 24, 208 | SIGXFSZ: 25, 209 | SIGVTALRM: 26, 210 | SIGPROF: 27, 211 | SIGSYS: 31, 212 | }; 213 | const cwd = (options.cwd) ? options.cwd : process.cwd(); 214 | const spawnOptions: child_process.SpawnOptions = { 215 | cwd, 216 | env: process.env, 217 | stdio: "inherit", 218 | }; 219 | const cmdString = cleanCommandString(options.command, options.args); 220 | try { 221 | if (options.checkPackageJson && !checkPackageJson(cwd)) { 222 | return 1; 223 | } 224 | print.info(`Running "${cmdString}" in '${cwd}'`); 225 | const cp = spawn(options.command, options.args, spawnOptions); 226 | return new Promise((resolve, reject) => { 227 | cp.on("exit", (code, signal) => { 228 | if (code === 0) { 229 | resolve(code); 230 | } else if (code) { 231 | print.error(`Command "${cmdString}" failed with non-zero status: ${code}`); 232 | resolve(code); 233 | } else { 234 | print.error(`Command "${cmdString}" exited due to signal: ${signal}`); 235 | if (signals[signal]) { 236 | resolve(128 + signals[signal]); 237 | } 238 | resolve(128 + 32); 239 | } 240 | }); 241 | cp.on("error", err => { 242 | const msg = `Command "${cmdString}" errored when spawning: ${err.message}`; 243 | print.error(msg); 244 | reject(new Error(msg)); 245 | }); 246 | }); 247 | } catch (e) { 248 | print.error(`Failed to spawn command "${cmdString}": ${e.message}`); 249 | return 100; 250 | } 251 | } 252 | 253 | /** 254 | * Check if a package.json file exists in cwd. 255 | * 256 | * @param cwd directory to check for package.json 257 | * @return true if package.json exists in cwd, false otherwise 258 | */ 259 | function checkPackageJson(cwd: string): boolean { 260 | const pkgPath = path.join(cwd, "package.json"); 261 | if (!fs.existsSync(pkgPath)) { 262 | print.error(`No 'package.json' in '${cwd}'`); 263 | return false; 264 | } 265 | return true; 266 | } 267 | 268 | /** 269 | * Create string representation of command and arguments, removing 270 | * sensitive information. 271 | * 272 | * @param cmd command 273 | * @param args command arguments 274 | * @return sanitized string representing command 275 | */ 276 | export function cleanCommandString(cmd: string, args?: string[]): string { 277 | const cmdString = cmd + ((args && args.length > 0) ? ` '${args.join("' '")}'` : ""); 278 | return cmdString.replace(/Authorization:\s+\w+\s+\w+/g, "Authorization: "); 279 | } 280 | -------------------------------------------------------------------------------- /lib/repositoryStart.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { guid } from "@atomist/automation-client"; 18 | import { obtainGitInfo } from "@atomist/automation-client/lib/internal/env/gitInfo"; 19 | import { execPromise } from "@atomist/automation-client/lib/util/child_process"; 20 | import * as fg from "fast-glob"; 21 | import * as fs from "fs-extra"; 22 | import gitUrlParse = require("git-url-parse"); 23 | import * as _ from "lodash"; 24 | import * as os from "os"; 25 | import * as path from "path"; 26 | import * as print from "./print"; 27 | import { 28 | start, 29 | StartOptions, 30 | } from "./start"; 31 | 32 | /** 33 | * Configuration options for repository start command 34 | */ 35 | export interface RepositoryStartOptions extends StartOptions { 36 | cloneUrl: string; 37 | index: string; 38 | yaml: string; 39 | sha: string; 40 | seedUrl: string; 41 | } 42 | 43 | /** 44 | * Files to check and copy from the seed into the cloned client/SDM repo 45 | */ 46 | const FilesToCopy = [ 47 | "package.json", 48 | "package-lock.json", 49 | "tsconfig.json", 50 | "tslint.json", 51 | ".gitignore", 52 | ]; 53 | 54 | /** 55 | * Clone, prepare and start an SDM/client project from a remote Git or Gist location 56 | */ 57 | export async function repositoryStart(opts: { cloneUrl: string } & Partial): Promise { 58 | 59 | const optsToUse: RepositoryStartOptions = _.merge({ 60 | index: "index.ts", 61 | yaml: undefined, 62 | sha: "master", 63 | local: false, 64 | profile: undefined, 65 | watch: false, 66 | debug: false, 67 | seedUrl: process.env.ATOMIST_SEED_URL || 68 | (!!opts.yaml ? "https://github.com/atomist-seeds/yaml-sdm.git" : "https://github.com/atomist-seeds/empty-sdm.git"), 69 | }, opts); 70 | 71 | let cwd = optsToUse.cwd; 72 | let seed = path.join(os.homedir(), ".atomist", "cache", `sdm-${guid().slice(0, 7)}`); 73 | 74 | if (!!optsToUse.cloneUrl || optsToUse.yaml) { 75 | 76 | if (!!optsToUse.cloneUrl) { 77 | const gitUrl = gitUrlParse(optsToUse.cloneUrl); 78 | cwd = path.join(os.homedir(), ".atomist", "sdm", gitUrl.owner, gitUrl.name); 79 | 80 | // Git clone 81 | try { 82 | await clone(optsToUse, cwd); 83 | } catch (e) { 84 | print.error(`Failed to clone/checkout repository: ${e.message}`); 85 | return 5; 86 | } 87 | } else { 88 | cwd = path.join(os.homedir(), ".atomist", "sdm", process.cwd().split(path.sep).slice(-1)[0]); 89 | 90 | // Copy project 91 | try { 92 | print.info("Copying repository..."); 93 | await fs.emptyDir(cwd); 94 | await fs.copy(process.cwd(), cwd); 95 | } catch (e) { 96 | print.error(`Failed to copy repository: ${e.message}`); 97 | return 5; 98 | } 99 | } 100 | 101 | if (!!optsToUse.yaml) { 102 | // If yaml is specified use that 103 | const patterns = optsToUse.yaml.split(",").map(p => p.trim()); 104 | await copyYamlIndexJs(patterns, optsToUse, cwd); 105 | } else if (!!optsToUse.index && optsToUse.index !== "index.ts") { 106 | // Move the provided file into the index.ts 107 | await copyIndexTs(optsToUse, cwd); 108 | } 109 | 110 | // Clone the seed if there are some needed files missing in the repository 111 | try { 112 | if (FilesToCopy.some(f => !fs.pathExistsSync(path.join(cwd, f)))) { 113 | if (isRemoteSeed(optsToUse)) { 114 | await cloneSeed(optsToUse, seed); 115 | } else { 116 | seed = optsToUse.seedUrl; 117 | optsToUse.install = false; 118 | } 119 | } 120 | } catch (e) { 121 | print.error(`Failed to checkout seed: ${e.message}`); 122 | return 10; 123 | } 124 | 125 | // Copy over package.json, tsconfig.json in case they are missing 126 | try { 127 | await copyFiles(optsToUse, cwd, seed); 128 | } catch (e) { 129 | print.error(`Failed to copy seed files into clone: ${e.message}`); 130 | return 15; 131 | } 132 | 133 | // Delete the seed clone as we don't need it any more 134 | try { 135 | if (isRemoteSeed(optsToUse)) { 136 | await fs.remove(seed); 137 | } 138 | } catch (e) { 139 | print.warn(`Failed to remove seed: ${e.message}`); 140 | } 141 | 142 | // Move the cloned and prepared content over the seed to start it 143 | try { 144 | if (!isRemoteSeed(optsToUse)) { 145 | // Prepare the git-info.json file that would otherwise be missing 146 | const gitInfoName = "git-info.json"; 147 | print.info(`Creating '${gitInfoName}'`); 148 | const gitInfoPath = path.join(cwd, gitInfoName); 149 | const gitInfo = await obtainGitInfo(cwd); 150 | await fs.writeJson(gitInfoPath, gitInfo, { spaces: 2, encoding: "utf8" }); 151 | // Copy content over the seed 152 | await fs.copy(cwd, seed); 153 | cwd = seed; 154 | } 155 | } catch (e) { 156 | print.error(`Failed to copy seed files into clone: ${e.message}`); 157 | return 15; 158 | } 159 | 160 | // Find out if we need to compile 161 | const files = await fg(`${cwd}/**/*.ts`, 162 | { ignore: [`${cwd}/**/{.git,node_modules,test}/**`] }); 163 | if (files.length > 0) { 164 | optsToUse.compile = true; 165 | } else { 166 | optsToUse.compile = false; 167 | } 168 | } 169 | 170 | // Finally call start on the SDM/client 171 | return start({ 172 | ...optsToUse, 173 | cwd, 174 | }); 175 | } 176 | 177 | function isRemoteSeed(opts: RepositoryStartOptions): boolean { 178 | return opts.seedUrl.startsWith("git@") || opts.seedUrl.startsWith("https://"); 179 | } 180 | 181 | async function clone(optsToUse: RepositoryStartOptions, cwd: string): Promise { 182 | if (!(await fs.pathExists(cwd))) { 183 | print.info("Cloning repository..."); 184 | await execPromise( 185 | "git", 186 | ["clone", optsToUse.cloneUrl, cwd]); 187 | print.info("Finished"); 188 | } else { 189 | print.info("Updating repository..."); 190 | for (const f of FilesToCopy) { 191 | await fs.remove(path.join(cwd, f)); 192 | } 193 | await execPromise( 194 | "git", 195 | ["reset", "--hard"], 196 | { cwd }); 197 | await execPromise( 198 | "git", 199 | ["pull"], 200 | { cwd }); 201 | print.info("Finished"); 202 | } 203 | if (!!optsToUse.sha) { 204 | print.info(`Checking out '${optsToUse.sha}'...`); 205 | await execPromise( 206 | "git", 207 | ["checkout", optsToUse.sha], 208 | { cwd }); 209 | print.info("Finished"); 210 | } 211 | } 212 | 213 | async function cloneSeed(optsToUse: RepositoryStartOptions, seed: string): Promise { 214 | print.info("Cloning seed..."); 215 | await execPromise( 216 | "git", 217 | ["clone", optsToUse.seedUrl, seed]); 218 | print.info("Finished"); 219 | } 220 | 221 | async function copyIndexTs(optsToUse: RepositoryStartOptions, cwd: string): Promise { 222 | print.info(`Preparing '${optsToUse.index}'...`); 223 | const from = path.join(cwd, optsToUse.index.replace(".ts", "").replace(".js", "")); 224 | // Rewrite the index.ts to export the configuration from provided file to not break relative imports 225 | const indexTs = `import { configuration as cfg } from "${from}"; 226 | 227 | export const configuration = cfg; 228 | `; 229 | await fs.writeFile(path.join(cwd, "index.ts"), indexTs); 230 | print.info("Finished"); 231 | } 232 | 233 | async function copyYamlIndexJs(pattern: string[], optsToUse: RepositoryStartOptions, cwd: string): Promise { 234 | print.info(`Preparing 'index.js'...`); 235 | // Rewrite the index.js to export the configuration from provided file to not break relative imports 236 | const indexJs = `const sdm_core = require("@atomist/sdm-core/lib/machine/yaml/configureYaml"); 237 | exports.configuration = sdm_core.configureYaml(${pattern.map(p => `"${p}"`).join(", ")}); 238 | `; 239 | await fs.writeFile(path.join(cwd, "index.js"), indexJs); 240 | print.info("Finished"); 241 | } 242 | 243 | async function copyFiles(optsToUse: RepositoryStartOptions, cwd: string, seed: string): Promise { 244 | for (const f of FilesToCopy) { 245 | const fp = path.join(cwd, f); 246 | const sp = path.join(seed, f); 247 | if (!(await fs.pathExists(fp)) && (await fs.pathExists(sp))) { 248 | print.info(`Creating '${f}'`); 249 | 250 | await fs.ensureDir(path.dirname(fp)); 251 | await fs.copyFile(sp, fp); 252 | 253 | if (f === "package.json") { 254 | const pj = await fs.readJson(fp); 255 | 256 | if (!!optsToUse.cloneUrl) { 257 | const gitUrl = gitUrlParse(optsToUse.cloneUrl); 258 | // This is special handling required to support gist clone urls 259 | if ((gitUrl.owner.length === 0 || gitUrl.owner === "git") 260 | && optsToUse.cloneUrl.includes("gist")) { 261 | pj.name = `@gist/${gitUrl.name.slice(0, 7)}`; 262 | } else { 263 | pj.name = `@${gitUrl.owner}/${gitUrl.name}`; 264 | } 265 | } else { 266 | pj.name = process.cwd().split(path.sep).slice(-1)[0]; 267 | } 268 | 269 | const sha = await execPromise( 270 | "git", 271 | ["log", "--pretty=format:%h", "-n", "1"], 272 | { cwd }); 273 | pj.version = `0.1.0-${sha.stdout.trim()}`; 274 | 275 | await fs.writeJson(fp, pj, { replacer: undefined, spaces: 2 }); 276 | } 277 | } else if (f === "package.json") { 278 | optsToUse.install = true; 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /lib/kubeInstall.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Configuration, 19 | webhookBaseUrl, 20 | } from "@atomist/automation-client"; 21 | import { execPromise } from "@atomist/sdm"; 22 | import { 23 | K8sObject, 24 | K8sObjectApi, 25 | } from "@atomist/sdm-pack-k8s/lib/kubernetes/api"; 26 | import { loadKubeConfig } from "@atomist/sdm-pack-k8s/lib/kubernetes/config"; 27 | import chalk from "chalk"; 28 | import { highlight } from "cli-highlight"; 29 | import * as fs from "fs-extra"; 30 | import * as inquirer from "inquirer"; 31 | import * as yaml from "js-yaml"; 32 | import * as stringify from "json-stringify-safe"; 33 | import * as os from "os"; 34 | import * as path from "path"; 35 | import * as request from "request"; 36 | import { resolveCliConfig } from "./cliConfig"; 37 | import * as print from "./print"; 38 | 39 | /** 40 | * Command-line options and arguments for kube. 41 | */ 42 | export interface KubeInstallOptions { 43 | /** Name of Kubernetes cluster */ 44 | env?: string; 45 | /** Namespace to deploy Atomist Kubernetes utilities into */ 46 | ns?: string; 47 | /** Only dry-run the command */ 48 | dryRun?: boolean; 49 | /** Confirm all questions */ 50 | yes?: boolean; 51 | /** URL of the public ingress */ 52 | url?: string; 53 | } 54 | 55 | /** 56 | * Deploy Atomist Kubernetes utilities into a Kubernetes cluster. 57 | * 58 | * @param opts see [[KubeInstallOptions]] 59 | * @return integer return value, 0 if successful, non-zero otherwise 60 | */ 61 | // tslint:disable-next-line:cyclomatic-complexity 62 | export async function kubeInstall(opts: KubeInstallOptions): Promise { 63 | const ns: string = opts.ns; 64 | const yes = opts.yes; 65 | let dryRun = opts.dryRun; 66 | 67 | const cliConfig = resolveCliConfig(); 68 | const apiKey = cliConfig.apiKey; 69 | if (!apiKey) { 70 | print.error(`No API key set in user configuration, run 'atomist config' first`); 71 | return Promise.resolve(1); 72 | } 73 | const workspaceIds = cliConfig.workspaceIds; 74 | if (!workspaceIds || workspaceIds.length < 1) { 75 | print.error(`No workspace IDs set in user configuration, run 'atomist config' first`); 76 | return Promise.resolve(1); 77 | } 78 | 79 | let context: string; 80 | try { 81 | const kubeConfigPath = path.join(os.homedir(), ".kube", "config"); 82 | const kubeConfigString = await fs.readFile(kubeConfigPath, "utf8"); 83 | const kubeConfig = yaml.safeLoad(kubeConfigString); 84 | context = kubeConfig["current-context"]; 85 | } catch (e) { 86 | print.warn(`Failed to get current context from Kubernetes config: ${e.message}`); 87 | } 88 | 89 | let url = opts.url; 90 | if (context === "minikube" && !url) { 91 | try { 92 | const ipResult = await execPromise("minikube", ["ip"]); 93 | url = `http://${ipResult.stdout.trim()}`; 94 | } catch (e) { 95 | print.warn(`Failed to get minikube IP address, not setting URL: ${e.message}`); 96 | } 97 | } 98 | 99 | const environment = opts.env || context || "kubernetes"; 100 | 101 | if (dryRun === undefined && yes === undefined) { 102 | const target = context ? `context ${chalk.cyan(context)}` : `cluster ${chalk.cyan(environment)}`; 103 | const questions: inquirer.QuestionCollection = [ 104 | { 105 | type: "list", 106 | name: "dryRun", 107 | message: `Deploy Atomist Kubernetes utilities to ${target}:`, 108 | default: "yes", 109 | choices: [ 110 | { 111 | name: "Yes", 112 | value: "yes", 113 | short: "Yes", 114 | }, 115 | { 116 | name: "Dry-run (print Kubernetes specs but do not apply them)", 117 | value: "dry-run", 118 | short: "Dry-run", 119 | }, 120 | { 121 | name: "No (exits immediately)", 122 | value: "no", 123 | short: "No", 124 | }, 125 | ], 126 | }, 127 | ]; 128 | const answers = await inquirer.prompt(questions); 129 | if (answers.dryRun === "no") { 130 | return 0; 131 | } else if (answers.dryRun === "dry-run") { 132 | dryRun = true; 133 | } else { 134 | dryRun = false; 135 | } 136 | } 137 | 138 | let specs: K8sObject[]; 139 | try { 140 | specs = await fetchSpecs(ns); 141 | } catch (e) { 142 | print.error(e.message); 143 | return 1; 144 | } 145 | 146 | const webhooks = kubeWebhookUrls(workspaceIds); 147 | const k8sConfig = k8sSdmConfig({ apiKey, environment, url, workspaceIds }); 148 | specs.push(encodeSecret("k8vent", ns || "k8vent", { environment, webhooks })); 149 | specs.push(encodeSecret("k8s-sdm", ns || "sdm", { "client.config.json": stringify(k8sConfig) })); 150 | 151 | if (dryRun) { 152 | specs.forEach(spec => { 153 | print.log("---"); 154 | print.log(highlight(yaml.safeDump(spec), { language: "yaml" }).replace(/\n$/, "")); 155 | }); 156 | return 0; 157 | } 158 | 159 | let client: K8sObjectApi; 160 | try { 161 | const kc = loadKubeConfig(); 162 | client = kc.makeApiClient(K8sObjectApi); 163 | } catch (e) { 164 | print.error(`Failed to create Kubernetes client: ${errMsg(e)}`); 165 | return 2; 166 | } 167 | for (const spec of specs) { 168 | try { 169 | await applySpec(client, spec); 170 | } catch (e) { 171 | print.error(`Failed to apply ${specSlug(spec)} spec: ${errMsg(e)}`); 172 | return 3; 173 | } 174 | } 175 | 176 | print.log(""); 177 | print.log(`Successfully installed Atomist Kubernetes utilities into your cluster`); 178 | print.log(`Please confirm correct startup of k8s-sdm by running:`); 179 | print.log(` $ ${chalk.yellow("kubectl get pod -n " + (ns || "sdm"))}`); 180 | return 0; 181 | } 182 | 183 | /** 184 | * Fetch the appropriate Kubernetes specs from GitHub and parse them. 185 | * 186 | * @param ns Namespace resources are to be deployed to, if not cluster-wide 187 | * @return Resource specs that need to be upserted 188 | */ 189 | export async function fetchSpecs(ns?: string): Promise { 190 | const specTails: Record = { 191 | "k8s-sdm": { 192 | ns: "/assets/kubectl/namespace-scoped.yaml", 193 | cluster: "/assets/kubectl/cluster-wide.yaml", 194 | }, 195 | "k8vent": { 196 | ns: "/kube/namespace-scoped.yaml", 197 | cluster: "/kube/cluster-wide.yaml", 198 | }, 199 | }; 200 | const specs: K8sObject[] = []; 201 | for (const specSrc of Object.keys(specTails)) { 202 | const specBaseUrl = ghBaseRawUrl(specSrc); 203 | const specUrl = specBaseUrl + ((ns) ? specTails[specSrc].ns : specTails[specSrc].cluster); 204 | try { 205 | print.log(`Fetching ${chalk.cyan(specSrc)} specs: ${specUrl}`); 206 | const result = await requestPromise(specUrl); 207 | specs.push(...processSpecs(result.body, ns)); 208 | } catch (e) { 209 | e.message = `Failed to download and parse spec '${specUrl}': ${errMsg(e)}`; 210 | throw e; 211 | } 212 | } 213 | return specs; 214 | } 215 | 216 | /** 217 | * Convert workspace IDs to Atomist Kubernetes webhook URLs. 218 | * 219 | * @param workspaceIds array of Atomist workspace/team IDs 220 | * @return comma-delimited list of webhook URLs 221 | */ 222 | export function kubeWebhookUrls(workspaceIds: string[]): string { 223 | const base = `${webhookBaseUrl()}/atomist/kube/teams`; 224 | return workspaceIds.map(id => `${base}/${id}`).join(","); 225 | } 226 | 227 | /** 228 | * Return base raw content GitHub.com URL for atomist repos. 229 | */ 230 | function ghBaseRawUrl(repo: string): string { 231 | return `https://raw.githubusercontent.com/atomist/${repo}/main`; 232 | } 233 | 234 | /** Options informing the k8s-sdm configuration. */ 235 | export interface K8sSdmConfigOptions { 236 | apiKey: string; 237 | environment: string; 238 | workspaceIds: string[]; 239 | url?: string; 240 | } 241 | 242 | /** 243 | * Create appropriate configuration for a k8s-sdm. 244 | */ 245 | export function k8sSdmConfig(opts: K8sSdmConfigOptions): Configuration { 246 | const cfg: Configuration = { 247 | apiKey: opts.apiKey, 248 | environment: opts.environment, 249 | logging: { level: "debug" }, 250 | name: `@atomist/k8s-sdm_${opts.environment}`, 251 | sdm: { 252 | k8s: { 253 | options: { 254 | addCommands: true, 255 | registerCluster: true, 256 | }, 257 | }, 258 | kubernetes: { 259 | provider: { 260 | url: opts.url, 261 | }, 262 | }, 263 | }, 264 | workspaceIds: opts.workspaceIds, 265 | }; 266 | return cfg; 267 | } 268 | 269 | function requestPromise(uri: string): Promise<{ body: any, response: request.Response }> { 270 | return new Promise((resolve, reject) => { 271 | request(uri, (error, response, body) => { 272 | if (error) { 273 | reject(error); 274 | } else { 275 | if (response.statusCode >= 200 && response.statusCode <= 299) { 276 | resolve({ response, body }); 277 | } else { 278 | reject({ response, body }); 279 | } 280 | } 281 | }); 282 | }); 283 | } 284 | 285 | function errMsg(e: any): string { 286 | if (e.message) { 287 | return e.message; 288 | } else if (e.body && e.body.message) { 289 | return e.body.message; 290 | } else if (e.response && e.response.body && e.response.body.message) { 291 | return e.response.body.message; 292 | } else if (e.response && e.response.statusMessage) { 293 | return e.response.body.message; 294 | } else { 295 | return stringify(e); 296 | } 297 | } 298 | 299 | export function processSpecs(raw: string, ns?: string): K8sObject[] { 300 | const specs: K8sObject[] = yaml.safeLoadAll(raw); 301 | if (ns) { 302 | specs.forEach(s => s.metadata.namespace = ns); 303 | } 304 | return specs; 305 | } 306 | 307 | /** 308 | * Simple Kubernetes v1.Secret interface. 309 | */ 310 | export interface KubeSecret { 311 | apiVersion: "v1"; 312 | kind: "Secret"; 313 | type: "Opaque"; 314 | metadata: { 315 | name: string; 316 | namespace?: string; 317 | }; 318 | data: { 319 | [key: string]: string; 320 | }; 321 | } 322 | 323 | /** 324 | * Create encoded secret object from key-value pairs. 325 | * 326 | * @param secrets key-value pairs of secrets, the values are base64 encoded 327 | * @return Kubernetes secret object 328 | */ 329 | export function encodeSecret(name: string, ns: string, secrets: { [key: string]: string }): KubeSecret { 330 | const kubeSecret: KubeSecret = { 331 | apiVersion: "v1", 332 | kind: "Secret", 333 | type: "Opaque", 334 | metadata: { 335 | name, 336 | namespace: ns, 337 | }, 338 | data: {}, 339 | }; 340 | for (const secret of Object.keys(secrets)) { 341 | kubeSecret.data[secret] = Buffer.from(secrets[secret]).toString("base64"); 342 | } 343 | return kubeSecret; 344 | } 345 | 346 | /** 347 | * Return informative string for spec. 348 | */ 349 | export function specSlug(spec: K8sObject): string { 350 | return [spec.kind, spec.metadata.namespace, spec.metadata.name].join("/").replace("//", "/"); 351 | } 352 | 353 | /** 354 | * Create or update Kubernetes resource. 355 | */ 356 | export async function applySpec(client: K8sObjectApi, spec: K8sObject): Promise { 357 | const slug = specSlug(spec); 358 | try { 359 | await client.read(spec); 360 | } catch (e) { 361 | print.log(`Creating ${slug}`); 362 | await client.create(spec); 363 | return; 364 | } 365 | print.log(`Updating ${slug}`); 366 | await client.patch(spec); 367 | return; 368 | } 369 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased](https://github.com/atomist/cli/compare/1.8.0...HEAD) 9 | 10 | ### Added 11 | 12 | - Base64 support in kube commands. [#120](https://github.com/atomist/cli/issues/120) 13 | - Issue/#114 cli editor support. [#121](https://github.com/atomist/cli/issues/121) 14 | - Add --yaml option to start. [#124](https://github.com/atomist/cli/issues/124) 15 | 16 | ## [1.8.0](https://github.com/atomist/cli/compare/1.7.0...1.8.0) - 2019-09-15 17 | 18 | ### Added 19 | 20 | - Add `--dev` and `--debug` flag to start command. [#116](https://github.com/atomist/cli/issues/116) 21 | 22 | ### Changed 23 | 24 | - Add kube-install command, deprecate kube. [#106](https://github.com/atomist/cli/issues/106) 25 | 26 | ### Deprecated 27 | 28 | - Remove broken, deprecate outdated commands. [#110](https://github.com/atomist/cli/issues/110) 29 | 30 | ### Removed 31 | 32 | - **BREAKING** Remove provider config command. [#103](https://github.com/atomist/cli/issues/103) 33 | - Remove broken, deprecate outdated commands. [#110](https://github.com/atomist/cli/issues/110) 34 | 35 | ## [1.7.0](https://github.com/atomist/cli/compare/1.6.1...1.7.0) - 2019-08-02 36 | 37 | ### Added 38 | 39 | - Create command to update atomist dependencies. [#95](https://github.com/atomist/cli/issues/95) 40 | - Add kube-fetch command. [#98](https://github.com/atomist/cli/issues/98) 41 | - Add kube-encrypt and kube-decrypt commands. [#101](https://github.com/atomist/cli/issues/101) 42 | 43 | ### Deprecated 44 | 45 | - Provider configuration using the CLI is deprecated. [#103](https://github.com/atomist/cli/issues/103) 46 | 47 | ## [1.6.1](https://github.com/atomist/cli/compare/1.6.0...1.6.1) - 2019-07-11 48 | 49 | ### Added 50 | 51 | - Add support for passing configuration profiles to start. [c5ffce8](https://github.com/atomist/cli/commit/c5ffce8bfa53fd66c01990638dd3cbeb3d9cc374) 52 | 53 | ### Changed 54 | 55 | - Update automation-client, sdm, sdm-core and sdm-local. [75a48a0](https://github.com/atomist/cli/commit/75a48a0eb61ae470149da1f7b381cac72c01ba30) 56 | 57 | ## [1.6.0](https://github.com/atomist/cli/compare/1.5.1...1.6.0) - 2019-07-09 58 | 59 | ### Added 60 | 61 | - Tell people not to run git-hook manually. [#96](https://github.com/atomist/cli/pull/96) 62 | 63 | ### Changed 64 | 65 | - Update Atomist and TypeScript deps. [5f93892](https://github.com/atomist/cli/commit/5f938925534df984b117e6dfa3d49efa2a6a5699) 66 | 67 | ### Fixed 68 | 69 | - Fix install behavior. [866638a](https://github.com/atomist/cli/commit/866638a0a8b506baa6cb2f23b8c6b430e93e9400) 70 | 71 | ## [1.5.1](https://github.com/atomist/cli/compare/1.5.0...1.5.1) - 2019-05-31 72 | 73 | ### Changed 74 | 75 | - Update sdm-local. [805371b](https://github.com/atomist/cli/commit/805371b0d134cbbe7f714eebdfdec5cdde9023da) 76 | 77 | ### Fixed 78 | 79 | - Update Homebrew shell_output test. [f2256dd](https://github.com/atomist/cli/commit/f2256dd6a2f5260ca19afb53c7176e3ae31dd328) 80 | 81 | ## [1.5.0](https://github.com/atomist/cli/compare/1.4.0...1.5.0) - 2019-05-27 82 | 83 | ### Added 84 | 85 | - Add url to atomist kube command. [1b1a175](https://github.com/atomist/cli/commit/1b1a1754150243fcaf5a66894d607a4f95456901) 86 | - Publish latest tag for docker image on release. [#70](https://github.com/atomist/cli/issues/70) 87 | - Add support for starting an SDM from a remote repo. [#87](https://github.com/atomist/cli/issues/87) 88 | 89 | ### Changed 90 | 91 | - We should 'npm ci' not 'npm install'. [#91](https://github.com/atomist/cli/issues/91) 92 | 93 | ## [1.4.0](https://github.com/atomist/cli/compare/1.3.0...1.4.0) - 2019-04-15 94 | 95 | ### Changed 96 | 97 | - When the APIkey is invalid, please ask for another one. [#69](https://github.com/atomist/cli/issues/69) 98 | - Spell out Kubernetes instead of k8s. [#78](https://github.com/atomist/cli/issues/78) 99 | - Make a more clear prompt than '? (mapped parameter) target-owner'. [#47](https://github.com/atomist/cli/issues/47) 100 | - If there is only one option, do not provide selector. [#80](https://github.com/atomist/cli/issues/80) 101 | 102 | ### Fixed 103 | 104 | - Fix create sdm undefined bug. [d1a04be](https://github.com/atomist/cli/commit/d1a04be2c135bda5697a497a179dfa4983aab758) 105 | 106 | ## [1.3.0](https://github.com/atomist/cli/compare/1.2.0...1.3.0) - 2019-04-01 107 | 108 | ### Added 109 | 110 | - Workspace selecter should validate that at least one is selected. [#62](https://github.com/atomist/cli/issues/62) 111 | - Add repository configuration to SCM provider command. [#75](https://github.com/atomist/cli/issues/75) 112 | 113 | ## [1.2.0](https://github.com/atomist/cli/compare/1.1.0...1.2.0) - 2019-03-15 114 | 115 | ### Added 116 | 117 | - Add `install` command to search and install an SDM extension pack from an NPM registry. [#b706d70](https://github.com/atomist/cli/commit/b706d70831e98cfcb7738d537bfc89b6af10198d) 118 | - Add provider and workspace create commands. [#61](https://github.com/atomist/cli/issues/61) 119 | - Add `provider config` command. [#64](https://github.com/atomist/cli/issues/64) 120 | 121 | ### Changed 122 | 123 | - Deploy k8s-sdm as part of `atomist kube`. [#65](https://github.com/atomist/cli/issues/65) 124 | - Add dry run and print current context to kube command. [#67](https://github.com/atomist/cli/issues/67) 125 | 126 | ## [1.1.0](https://github.com/atomist/cli/compare/1.0.3...1.1.0) - 2018-12-27 127 | 128 | ### Added 129 | 130 | - Add login, config command to connect to a workspace. [#57](https://github.com/atomist/cli/issues/57) 131 | 132 | ## [1.0.3](https://github.com/atomist/cli/compare/1.0.2...1.0.3) - 2018-12-08 133 | 134 | ### Changed 135 | 136 | - Update to apollo CLI for gql-fetch. [#52](https://github.com/atomist/cli/issues/52) 137 | 138 | ### Fixed 139 | 140 | - Fix some typos in the text intro for config. [#50](https://github.com/atomist/cli/issues/50) 141 | 142 | ## [1.0.2](https://github.com/atomist/cli/compare/1.0.1...1.0.2) - 2018-11-09 143 | 144 | ## [1.0.1](https://github.com/atomist/cli/compare/1.0.0-RC.2...1.0.1) - 2018-11-09 145 | 146 | ## [1.0.0-RC.2](https://github.com/atomist/cli/compare/1.0.0-RC.1...1.0.0-RC.2) - 2018-10-30 147 | 148 | ### Added 149 | 150 | - Add homebrew formula template and bash completion. [#46](https://github.com/atomist/cli/issues/46) 151 | 152 | ### Removed 153 | 154 | - **BREAKING** Remove deprecated git-info & gql-gen commands. [#45](https://github.com/atomist/cli/issues/45) 155 | 156 | ## [1.0.0-RC.1](https://github.com/atomist/cli/compare/1.0.0-M.5a...1.0.0-RC.1) - 2018-10-15 157 | 158 | ## [1.0.0-M.5a](https://github.com/atomist/cli/compare/1.0.0-M.5...1.0.0-M.5a) - 2018-09-28 159 | 160 | ## [1.0.0-M.5](https://github.com/atomist/cli/compare/1.0.0-M.4...1.0.0-M.5) - 2018-09-26 161 | 162 | ### Changed 163 | 164 | - Simplify postInstall message. [#35](https://github.com/atomist/cli/pull/35) 165 | 166 | ## [1.0.0-M.4](https://github.com/atomist/cli/compare/1.0.0-M.3...1.0.0-M.4) - 2018-09-16 167 | 168 | ## [1.0.0-M.3](https://github.com/atomist/cli/compare/1.0.0-M.2...1.0.0-M.3) - 2018-09-04 169 | 170 | ### Changed 171 | 172 | - Provided masked API key default and input. [#23](https://github.com/atomist/cli/issues/23) 173 | - Print more version information in --version. [#26](https://github.com/atomist/cli/issues/26) 174 | 175 | ### Removed 176 | 177 | - Remove deprecated scripts. [#24](https://github.com/atomist/cli/issues/24) 178 | 179 | ## [1.0.0-M.2](https://github.com/atomist/cli/compare/1.0.0-M.1...1.0.0-M.2) 180 | 181 | ### Changed 182 | 183 | - Update sdm-local and automation-client. 184 | 185 | ### Fixed 186 | 187 | - `atomist help` fails to show output. [#21](https://github.com/atomist/cli/issues/21) 188 | - Fix postInstall banner resolution. 189 | 190 | ## [1.0.0-M.1](https://github.com/atomist/cli/compare/0.6.7...1.0.0-M.1) - 2018-08-27 191 | 192 | ### Changed 193 | 194 | - Provide postInstall script as JavaScript. 195 | - Update Atomist dependencies to 1.0.0 Milestone 1 versions. 196 | 197 | ## [0.6.7](https://github.com/atomist/cli/compare/0.6.6...0.6.7) - 2018-08-25 198 | 199 | ### Added 200 | 201 | - Add postInstall message. [#11a7e91](https://github.com/atomist/cli/commit/11a7e9105582232e5f22c9a6bd9122472338972d) 202 | 203 | ## [0.6.6](https://github.com/atomist/cli/compare/0.6.5...0.6.6) - 2018-08-24 204 | 205 | ### Added 206 | 207 | - Add sourcemap support. [#12](https://github.com/atomist/cli/issues/12) 208 | 209 | ## [0.6.5](https://github.com/atomist/cli/compare/0.6.4...0.6.5) - 2018-08-22 210 | 211 | ### Changed 212 | 213 | - Use @atomist/automation-client bin scripts. 214 | 215 | ## [0.6.4](https://github.com/atomist/cli/compare/0.6.3...0.6.4) - 2018-08-21 216 | 217 | ### Fixed 218 | 219 | - Commands not working on MS Windows. [#2](https://github.com/atomist/cli/issues/2) 220 | - Get rid of deprecation warnings when installing cli. [#11](https://github.com/atomist/cli/issues/11) 221 | 222 | ## [0.6.3](https://github.com/atomist/cli/compare/0.6.2...0.6.3) - 2018-08-20 223 | 224 | ### Changed 225 | 226 | - Update to @atomist/sdm-local@0.1.8, @atomist/automation-client@0.21.1. [#ee928bc](https://github.com/atomist/cli/commit/ee928bcc578409117b78a2980c54ce3e7078ce97) 227 | 228 | ## [0.6.2](https://github.com/atomist/cli/compare/0.6.1...0.6.2) - 2018-08-20 229 | 230 | ### Changed 231 | 232 | - Update to @atomist/sdm-local@0.1.7. 233 | 234 | ## [0.6.1](https://github.com/atomist/cli/compare/0.6.0...0.6.1) - 2018-08-19 235 | 236 | ### Changed 237 | 238 | - Delay loading sdm-local in gitHook. 239 | 240 | ## [0.6.0](https://github.com/atomist/cli/compare/0.5.2...0.6.0) - 2018-08-14 241 | 242 | ### Added 243 | 244 | - Add `git-hook` subcommand. 245 | 246 | ### Changed 247 | 248 | - Update automation-client dependency. 249 | - Use automation-client scripts in package scripts. 250 | - Use cross-spawn to make running commands more cross-platform. 251 | 252 | ### Deprecated 253 | 254 | - The `git` and `gql-gen` subcommands have been moved to automation-client. 255 | - Deprecate `githook` script in favor of `git-hook` subcommand. 256 | 257 | ### Removed 258 | 259 | - **BREAKING** Remove `cmd` and `exec` aliases for `execute`. 260 | 261 | ### Fixed 262 | 263 | - Recognize `execute` as a reserved command. 264 | - Show SDM local commands in `--help` output. [#9](https://github.com/atomist/cli/issues/9) 265 | 266 | ## [0.5.2](https://github.com/atomist/cli/compare/0.5.1...0.5.2) - 2018-08-09 267 | 268 | ### Fixed 269 | 270 | - Update automation-client to make running "new sdm" outside a 271 | 272 | ## [0.5.1](https://github.com/atomist/cli/compare/0.5.0...0.5.1) - 2018-08-09 273 | 274 | ### Fixed 275 | 276 | - Move Atomist dependencies back from devDependencies. 277 | 278 | ## [0.5.0](https://github.com/atomist/cli/compare/0.4.0...0.5.0) - 2018-08-09 279 | 280 | ### Added 281 | 282 | - Add SDM local mode. 283 | 284 | ### Changed 285 | 286 | - Update dependencies. 287 | 288 | ### Fixed 289 | 290 | - Improve argument processing to avoid loading SDM local commands 291 | 292 | ## [0.4.0](https://github.com/atomist/cli/compare/0.3.0...0.4.0) - 2018-08-04 293 | 294 | ### Changed 295 | 296 | - Improve config handling of workspace/team IDs. 297 | - Always have config prompt for API key. 298 | 299 | ### Fixed 300 | 301 | - Properly sanitize command line before printing 302 | 303 | ## [0.3.0](https://github.com/atomist/cli/compare/0.2.1...0.3.0) - 2018-08-01 304 | 305 | ### Added 306 | 307 | - Add Command-line option for API key, `--api-key`. 308 | 309 | ### Removed 310 | 311 | - **BREAKING** Remove `--atomist-token` command-line option and its 312 | - **BREAKING** Remove config GitHub-related command-line options, 313 | 314 | ## [0.2.1](https://github.com/atomist/cli/compare/0.2.0...0.2.1) - 2018-07-31 315 | 316 | ### Changed 317 | 318 | - Update TypeScript and supporting packages. 319 | 320 | ### Fixed 321 | 322 | - Support both `src` and `lib` in GraphQL commands. 323 | 324 | ## [0.2.0](https://github.com/atomist/cli/compare/0.1.0...0.2.0) - 2018-07-31 325 | 326 | ### Added 327 | 328 | - Provide `--atomist-token` command-line option for config. 329 | 330 | ### Changed 331 | 332 | - **BREAKING** Updated command-line options and arguments to use 333 | - Reorganize package structure to be more standard Node.js. 334 | - Standardize command line processing. 335 | - **BREAKING** Workspace ID argument to gql-fetch is now an option 336 | - Use async functions where possible. 337 | - Improve the tslint configuration. 338 | 339 | ### Removed 340 | 341 | - SDM configuration helpers no longer necessary. 342 | - **BREAKING** Remove gql alias for gql-gen. 343 | 344 | ### Fixed 345 | 346 | - The kube command can be run repeatedly without error. 347 | - Improve config error handling. 348 | - The --version command-line option should always report the right 349 | 350 | ## [0.1.0](https://github.com/atomist/cli/tree/0.1.0) - 2018-07-06 351 | 352 | ### Added 353 | 354 | - Atomist CLI, migrated from @atomist/automation-client. 355 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Configuration, 19 | QueryNoCacheOptions, 20 | toStringArray, 21 | } from "@atomist/automation-client"; 22 | import { 23 | defaultConfiguration, 24 | mergeConfigs, 25 | userConfigPath, 26 | writeUserConfig, 27 | } from "@atomist/automation-client/lib/configuration"; 28 | import { ApolloGraphClient } from "@atomist/automation-client/lib/graph/ApolloGraphClient"; 29 | import { Deferred } from "@atomist/automation-client/lib/internal/util/Deferred"; 30 | import { scanFreePort } from "@atomist/automation-client/lib/util/port"; 31 | // tslint:disable-next-line:import-blacklist 32 | import axios from "axios"; 33 | import chalk from "chalk"; 34 | import * as express from "express"; 35 | import * as inquirer from "inquirer"; 36 | import { sha256 } from "js-sha256"; 37 | import * as _ from "lodash"; 38 | import opn = require("open"); 39 | import * as os from "os"; 40 | import { resolveUserConfig } from "./cliConfig"; 41 | import * as print from "./print"; 42 | 43 | /** 44 | * Command-line options and arguments for config 45 | */ 46 | export interface ConfigOptions { 47 | 48 | /** Atomist API key */ 49 | apiKey?: string; 50 | 51 | /** Atomist workspace ID */ 52 | workspaceId?: string; 53 | 54 | /** Create new API key regardless of a configured one */ 55 | createApiKey?: boolean; 56 | } 57 | 58 | const UserQuery = `query User { 59 | user { 60 | principal { 61 | sub 62 | pid 63 | } 64 | } 65 | }`; 66 | 67 | interface User { 68 | user: { 69 | person: Array<{ email: string }>; 70 | }; 71 | } 72 | 73 | const PersonByIdentityQuery = `query PersonByIdentity { 74 | personByIdentity { 75 | team { 76 | id 77 | name 78 | } 79 | } 80 | }`; 81 | 82 | interface PersonByIdentity { 83 | personByIdentity: Array<{ 84 | email: string, 85 | team: { 86 | id: string, 87 | name: string, 88 | }, 89 | user: { 90 | principal: { 91 | pid: string, 92 | sub: string, 93 | }, 94 | }, 95 | }>; 96 | } 97 | 98 | const CreateApiKeyMutation = `mutation createKey($description: String!){ 99 | createApiKey(description: $description) { 100 | id 101 | key 102 | description 103 | createdAt 104 | lastUsed 105 | owner { 106 | id 107 | } 108 | } 109 | }`; 110 | 111 | interface CreateApiKeyVariables { 112 | description: string; 113 | } 114 | 115 | interface CreateApiKey { 116 | createApiKey: { 117 | key: string; 118 | }; 119 | } 120 | 121 | /** 122 | * Set up local configuration with Atomist api key and workspaces 123 | * @param opts 124 | */ 125 | export async function config(opts: ConfigOptions): Promise { 126 | const cfgPath = userConfigPath(); 127 | const userCfg = resolveUserConfig(); 128 | const defaultCfg = defaultConfiguration(); 129 | const cfg = mergeConfigs(defaultCfg, userCfg); 130 | 131 | let apiKey = opts.createApiKey !== true ? (opts.apiKey || cfg.apiKey) : undefined; 132 | 133 | // No api key; config and create a new key 134 | if (!apiKey) { 135 | try { 136 | apiKey = await createApiKey(cfg); 137 | } catch (e) { 138 | print.error(`Failed to create API key: ${e.message}`); 139 | return 1; 140 | } 141 | } 142 | 143 | try { 144 | // Validate api key 145 | await validateApiKey(apiKey, cfg); 146 | } catch (e) { 147 | print.error(`Failed to validate API key: ${e.message}`); 148 | return 1; 149 | } 150 | 151 | userCfg.apiKey = apiKey; 152 | await writeUserConfig(userCfg); 153 | 154 | let workspaceIds; 155 | if (!opts.workspaceId) { 156 | try { 157 | // Retrieve list and config workspaces 158 | workspaceIds = await configureWorkspaces(apiKey, cfg); 159 | } catch (e) { 160 | print.error(`Failed to configure workspaces: ${e.message}`); 161 | return 1; 162 | } 163 | } else { 164 | workspaceIds = opts.workspaceId.split(/\s+/); 165 | } 166 | 167 | userCfg.workspaceIds = workspaceIds; 168 | await writeUserConfig(userCfg); 169 | 170 | print.log(`Successfully wrote configuration ${chalk.green(cfgPath)}`); 171 | print.log(`You are ready to connect a software delivery machine. 172 | To create one, try 'atomist create sdm'. 173 | More info: https://docs.atomist.com/quick-start/#create-a-software-delivery-machine`); 174 | return 0; 175 | } 176 | 177 | /** 178 | * Initiate a login flow using a selected auth provider to create a new api key 179 | * @param cfg 180 | */ 181 | async function createApiKey(cfg: Configuration): Promise { 182 | 183 | let questions: inquirer.QuestionCollection = [ 184 | { 185 | type: "input", 186 | name: "apiKey", 187 | transformer: maskString, 188 | message: `Enter your ${chalk.cyan("api key")} from ${chalk.yellow("https://app.atomist.com/apikeys")} 189 | or hit ${chalk.cyan("")} to login with Atomist:`, 190 | }, 191 | ]; 192 | 193 | let answers = await inquirer.prompt(questions); 194 | if (!answers.apiKey) { 195 | const providers = await axios.get(`${cfg.endpoints.auth}/providers`); 196 | 197 | let authUrl; 198 | if (providers.data.length > 1) { 199 | print.log(`Select one of the following authentication providers\navailable to login with Atomist:`); 200 | 201 | questions = [ 202 | { 203 | type: "list", 204 | name: "provider", 205 | message: "Authentication Provider", 206 | choices: providers.data.map((p: any) => ({ 207 | name: p.display_name, 208 | value: p.login_url, 209 | })), 210 | }, 211 | ]; 212 | 213 | answers = await inquirer.prompt(questions); 214 | authUrl = answers.provider; 215 | } else { 216 | authUrl = providers.data[0].login_url; 217 | } 218 | 219 | let spinner = createSpinner(`Waiting for login flow to finish in your browser`); 220 | 221 | const state = nonce(20); 222 | const verifier = nonce(); 223 | const code = sha256(verifier); 224 | const port = await scanFreePort(); 225 | const url = `${authUrl}?state=${state}&redirect-uri=http://127.0.0.1:${port}/callback&code-challenge=${code}`; 226 | 227 | await opn(url, { wait: false }); 228 | 229 | const app = express(); 230 | const callback = new Deferred<{ jwt: string }>(); 231 | app.get("/callback", async (req, res) => { 232 | if (state !== req.query.state) { 233 | callback.reject("State parameter not correct after authentication. Abort!"); 234 | // res.status(500).json({ message: "State parameter not correct after authentication" }); 235 | res.redirect("https://atomist.com/error-oauth.html"); 236 | return; 237 | } 238 | try { 239 | const token = await axios.post(`${cfg.endpoints.auth}/token`, { 240 | code: req.query.code, 241 | verifier, 242 | grant_type: "pkce", 243 | }); 244 | callback.resolve({ jwt: token.data.access_token }); 245 | res.redirect("https://atomist.com/success-oauth.html"); 246 | } catch (e) { 247 | callback.reject(new Error(`Authentication failed: ${e.message}`)); 248 | // res.status(500).json({ message: e.message }); 249 | res.redirect("https://atomist.com/error-oauth.html"); 250 | } 251 | }); 252 | 253 | const server = app.listen(port); 254 | let jwt; 255 | try { 256 | jwt = (await callback.promise).jwt; 257 | } finally { 258 | server.close(); 259 | spinner.stop(true); 260 | } 261 | 262 | const graphClient = new ApolloGraphClient( 263 | cfg.endpoints.graphql.replace("/team", ""), 264 | { Authorization: `Bearer ${jwt}` }); 265 | 266 | spinner = createSpinner(`Creating new API key`); 267 | 268 | const result = await graphClient.mutate({ 269 | mutation: CreateApiKeyMutation, 270 | variables: { 271 | description: `Generated by Atomist CLI on ${os.hostname()} by ${os.userInfo().username}`, 272 | }, 273 | }); 274 | 275 | spinner.stop(true); 276 | return result.createApiKey.key; 277 | } else { 278 | return answers.apiKey; 279 | } 280 | } 281 | 282 | /** 283 | * Validate a given api key by making a backend call to the GraphQL endpoint 284 | * @param apiKey 285 | * @param cfg 286 | */ 287 | export async function validateApiKey(apiKey: string, cfg: Configuration): Promise { 288 | const spinner = createSpinner("Validating API key"); 289 | const graphClient = new ApolloGraphClient( 290 | cfg.endpoints.graphql.replace("/team", ""), 291 | { Authorization: `Bearer ${apiKey}` }); 292 | try { 293 | const providers = await axios.get(`${cfg.endpoints.auth}/providers`); 294 | const result = await graphClient.query({ 295 | query: UserQuery, 296 | }); 297 | spinner.stop(true); 298 | 299 | // If there is no workspace yet, there is also no record returned 300 | const sub = _.get(result, "user.principal.sub"); 301 | const pid = _.get(result, "user.principal.pid"); 302 | const provider = providers.data.find((p: any) => p.id === pid); 303 | if (!!sub && !!pid) { 304 | print.log(`Logged in as ${chalk.green(sub)} using ${ 305 | chalk.green(_.get(provider, "display_name", "n/a"))}`); 306 | } else { 307 | print.log(`Logged in`); 308 | } 309 | } catch (e) { 310 | if (!!e.networkError && e.networkError.statusCode === 401) { 311 | print.log(` 312 | API key is invalid. Run ${chalk.cyan("atomist config --create-api-key")} to obtain a new API key. 313 | `); 314 | } 315 | throw e; 316 | } finally { 317 | spinner.stop(true); 318 | } 319 | } 320 | 321 | /** 322 | * Read the list of workspaces and let the user choose to which workspaces to connect to 323 | * @param apiKey 324 | * @param cfg 325 | */ 326 | export async function configureWorkspaces(apiKey: string, cfg: Configuration, multiple: boolean = true): Promise { 327 | const graphClient = new ApolloGraphClient( 328 | cfg.endpoints.graphql.replace("/team", ""), 329 | { Authorization: `Bearer ${apiKey}` }); 330 | const result = await graphClient.query({ 331 | query: PersonByIdentityQuery, 332 | options: QueryNoCacheOptions, 333 | }); 334 | const workspaces = _.get(result, "personByIdentity") || []; 335 | 336 | if (workspaces.length === 0) { 337 | print.log(`No workspaces available. Run ${chalk.cyan("atomist workspace create")}`); 338 | return []; 339 | } 340 | 341 | if (multiple) { 342 | print.log(`Select one or more workspaces:`); 343 | } else { 344 | print.log(`Select a workspace:`); 345 | } 346 | 347 | const questions = [ 348 | { 349 | type: multiple ? "checkbox" : "list", 350 | name: "workspaceIds", 351 | message: "Workspaces", 352 | choices: workspaces.sort((p1, p2) => p1.team.name.localeCompare(p2.team.name)) 353 | .map(p => ({ 354 | name: `${p.team.id} - ${p.team.name}`, 355 | value: p.team.id, 356 | checked: cfg.workspaceIds.includes(p.team.id), 357 | short: p.team.id, 358 | })), 359 | validate: (value: any) => { 360 | if (value.length < 1) { 361 | return chalk.red(multiple ? "Please select at least one workspace" : "Please select one workspace"); 362 | } 363 | return true; 364 | }, 365 | }, 366 | ]; 367 | 368 | const answers: any = await inquirer.prompt(questions); 369 | return toStringArray(answers.workspaceIds) || []; 370 | } 371 | 372 | export function createSpinner(text: string): any { 373 | const Spinner = require("cli-spinner").Spinner; 374 | const spinner = new Spinner(`${text} ${chalk.yellow("%s")} `); 375 | spinner.setSpinnerDelay(100); 376 | spinner.setSpinnerString("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"); 377 | spinner.start(); 378 | return spinner; 379 | } 380 | 381 | export function nonce(length: number = 40): string { 382 | const crypto = require("crypto"); 383 | return crypto 384 | .randomBytes(Math.ceil((length * 3) / 4)) 385 | .toString("base64") // convert to base64 format 386 | .slice(0, length) // return required number of characters 387 | .replace(/\+/g, "0") // replace '+' with '0' 388 | .replace(/\//g, "0"); // replace '/' with '0' 389 | } 390 | 391 | /** 392 | * Mask secret string. 393 | * @param secret string to mask 394 | * @return masked string 395 | */ 396 | export function maskString(s: string): string { 397 | if (s.length > 10) { 398 | return s.charAt(0) + "*".repeat(s.length - 2) + s.charAt(s.length - 1); 399 | } 400 | return "*".repeat(s.length); 401 | } 402 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright © 2019 Atomist, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import * as yb from "@atomist/sdm-local/lib/cli/invocation/command/support/yargBuilder"; 19 | // tslint:disable-next-line:no-import-side-effect 20 | import "source-map-support/register"; 21 | import * as yargs from "yargs"; 22 | 23 | import { 24 | cliCommand, 25 | isEmbeddedSdmCommand, 26 | shouldAddLocalSdmCommands, 27 | } from "./lib/command"; 28 | import { config } from "./lib/config"; 29 | import { execute } from "./lib/execute"; 30 | import { gitHook } from "./lib/gitHook"; 31 | import { gqlFetch } from "./lib/gqlFetch"; 32 | import { install } from "./lib/install"; 33 | import { kubeCrypt } from "./lib/kubeCrypt"; 34 | import { kubeEdit } from "./lib/kubeEdit"; 35 | import { kubeFetch } from "./lib/kubeFetch"; 36 | import { kubeInstall } from "./lib/kubeInstall"; 37 | import * as print from "./lib/print"; 38 | import { repositoryStart } from "./lib/repositoryStart"; 39 | import { updateSdm } from "./lib/updateSdm"; 40 | import { version } from "./lib/version"; 41 | 42 | process.env.SUPPRESS_NO_CONFIG_WARNING = "true"; 43 | if (!isEmbeddedSdmCommand(process.argv)) { 44 | process.env.ATOMIST_DISABLE_LOGGING = "true"; 45 | } 46 | 47 | function setupYargs(yargBuilder: yb.YargBuilder): void { 48 | const commonOptions: { [key: string]: yb.CommandLineParameter } = { 49 | changeDir: { 50 | parameterName: "change-dir", 51 | alias: "C", 52 | default: process.cwd(), 53 | describe: "Path to automation client project", 54 | type: "string", 55 | }, 56 | compile: { 57 | parameterName: "compile", 58 | default: true, 59 | describe: "Run 'npm run compile' before running", 60 | type: "boolean", 61 | }, 62 | install: { 63 | parameterName: "install", 64 | describe: "Run 'npm install' before running/compiling, default is to install if no " + 65 | "'node_modules' directory exists", 66 | type: "boolean", 67 | }, 68 | }; 69 | 70 | yargBuilder.withSubcommand({ 71 | command: "config", 72 | describe: "Configure connection to Atomist", 73 | parameters: [{ 74 | parameterName: "api-key", 75 | describe: "Atomist API key", 76 | type: "string", 77 | }, { 78 | parameterName: "workspace-id", 79 | describe: "Atomist workspace ID", 80 | type: "string", 81 | }, { 82 | parameterName: "create-api-key", 83 | describe: "Create a new API key regardless if currently one is configured", 84 | type: "boolean", 85 | default: false, 86 | }], 87 | handler: argv => cliCommand(() => config({ 88 | apiKey: argv["api-key"], 89 | workspaceId: argv["workspace-id"], 90 | createApiKey: argv["create-api-key"], 91 | })), 92 | }); 93 | yargBuilder.withSubcommand({ 94 | command: "connect", 95 | aliases: ["login"], 96 | describe: "DEPRECATED use 'config'", 97 | parameters: [{ 98 | parameterName: "api-key", 99 | describe: "Atomist API key", 100 | type: "string", 101 | }, { 102 | parameterName: "workspace-id", 103 | describe: "Atomist workspace ID", 104 | type: "string", 105 | }, { 106 | parameterName: "create-api-key", 107 | describe: "Create a new API key regardless if currently one is configured", 108 | type: "boolean", 109 | default: false, 110 | }], 111 | handler: argv => { 112 | deprecated(argv, "atomist config"); 113 | return cliCommand(() => config({ 114 | apiKey: argv["api-key"], 115 | workspaceId: argv["workspace-id"], 116 | createApiKey: argv["create-api-key"], 117 | })); 118 | }, 119 | }); 120 | yargBuilder.withSubcommand({ 121 | command: "execute ", 122 | describe: "Run a command", 123 | positional: [{ 124 | key: "name", opts: { 125 | describe: "Name of command to run, command parameters PARAM=VALUE can follow", 126 | }, 127 | }], 128 | parameters: [commonOptions.changeDir, commonOptions.install, commonOptions.changeDir], 129 | handler: argv => cliCommand(() => execute({ 130 | name: argv.name, 131 | cwd: argv["change-dir"], 132 | compile: argv.compile, 133 | install: argv.install, 134 | args: argv._.filter((a: any) => a !== "execute" && a !== "exec" && a !== "cmd"), 135 | })), 136 | }); 137 | yargBuilder.withSubcommand({ 138 | command: "install [keywords]", 139 | describe: "DEPRECATED use 'npm install'", 140 | positional: [{ 141 | key: "keywords", 142 | opts: { 143 | describe: "keywords to search for", 144 | }, 145 | }], 146 | parameters: [ 147 | commonOptions.changeDir, 148 | { 149 | parameterName: "registry", 150 | describe: "NPM registry to search", 151 | type: "string", 152 | required: false, 153 | }], 154 | handler: argv => { 155 | deprecated(argv, "npm install"); 156 | return cliCommand(() => install({ 157 | keywords: [argv.keywords, ...argv._.filter((a: any) => a !== "install")], 158 | cwd: argv["change-dir"], 159 | registry: argv.registry, 160 | })); 161 | }, 162 | }); 163 | yargBuilder.withSubcommand({ 164 | command: "update sdm", 165 | describe: "Update an SDM to the latest dependency version of Atomist of a certain branch", 166 | parameters: [ 167 | commonOptions.changeDir, 168 | { 169 | parameterName: "tag", 170 | describe: "NPM tag to update the dependencies to", 171 | type: "string", 172 | required: false, 173 | default: "latest", 174 | }], 175 | handler: argv => cliCommand(() => updateSdm({ 176 | versionTag: argv.tag, 177 | cwd: argv["change-dir"], 178 | })), 179 | }); 180 | yargBuilder.withSubcommand({ 181 | command: "git-hook", 182 | describe: "(not for human use)", 183 | handler: argv => cliCommand(() => gitHook(process.argv)), 184 | }); 185 | yargBuilder.withSubcommand({ 186 | command: "gql-fetch", describe: "Retrieve GraphQL schema", 187 | parameters: [commonOptions.changeDir, commonOptions.install], 188 | handler: argv => cliCommand(() => gqlFetch({ 189 | cwd: argv["change-dir"], 190 | install: argv.install, 191 | })), 192 | }); 193 | yargBuilder.withSubcommand({ 194 | command: "kube", 195 | aliases: ["k8s"], 196 | describe: "DEPRECATED use 'kube-install'", 197 | parameters: [{ 198 | parameterName: "environment", 199 | describe: "Informative name for your Kubernetes cluster", 200 | type: "string", 201 | }, { 202 | parameterName: "namespace", 203 | describe: "Deploy utilities in namespace mode", 204 | type: "string", 205 | }, { 206 | parameterName: "url", 207 | describe: "URL of publicly accessible hostname (e.g. http://a.atomist.io)", 208 | type: "string", 209 | }, { 210 | parameterName: "dry-run", 211 | describe: "Only print the k8s objects that would be deployed, without sending them", 212 | type: "boolean", 213 | }, { 214 | parameterName: "yes", 215 | describe: "Confirm all questions with yes", 216 | type: "boolean", 217 | }], 218 | handler: (argv: any) => { 219 | deprecated(argv, "kube-install"); 220 | return cliCommand(() => kubeInstall({ 221 | env: argv.environment, 222 | ns: argv.namespace, 223 | dryRun: argv["dry-run"], 224 | yes: argv.yes, 225 | url: argv.url, 226 | })); 227 | }, 228 | }); 229 | yargBuilder.withSubcommand({ 230 | command: "kube-decrypt", 231 | describe: "Decrypt encrypted Kubernetes secret data values", 232 | parameters: [{ 233 | parameterName: "file", 234 | describe: "Decrypt Kubernetes secret data values from secret spec file", 235 | type: "string", 236 | conflicts: "literal", 237 | }, { 238 | parameterName: "literal", 239 | describe: "Decrypt secret data value provided as a literal string", 240 | type: "string", 241 | conflicts: "file", 242 | }, { 243 | parameterName: "secret-key", 244 | describe: "Key to use to decrypt secret data values", 245 | type: "string", 246 | }, { 247 | parameterName: "base64", 248 | describe: "Base64 decode data after decrypting", 249 | type: "boolean", 250 | default: false, 251 | }], 252 | handler: (argv: any) => cliCommand(() => kubeCrypt({ 253 | action: "decrypt", 254 | base64: argv.base64, 255 | file: argv.file, 256 | literal: argv.literal, 257 | secretKey: argv["secret-key"], 258 | })), 259 | }); 260 | yargBuilder.withSubcommand({ 261 | command: "kube-encrypt", 262 | describe: "Encrypt Base64 encoded Kubernetes secret data values", 263 | parameters: [{ 264 | parameterName: "file", 265 | describe: "Encrypt Kubernetes secret data values from secret spec file", 266 | type: "string", 267 | conflicts: "literal", 268 | }, { 269 | parameterName: "literal", 270 | describe: "Encrypt secret data value provided as a literal string", 271 | type: "string", 272 | conflicts: "file", 273 | }, { 274 | parameterName: "secret-key", 275 | describe: "Key to use to encrypt secret data values", 276 | type: "string", 277 | }, { 278 | parameterName: "base64", 279 | describe: "Base64 encode data before encrypting", 280 | type: "boolean", 281 | default: false, 282 | }], 283 | handler: (argv: any) => cliCommand(() => kubeCrypt({ 284 | action: "encrypt", 285 | base64: argv.base64, 286 | file: argv.file, 287 | literal: argv.literal, 288 | secretKey: argv["secret-key"], 289 | })), 290 | }); 291 | yargBuilder.withSubcommand({ 292 | command: "kube-edit ", 293 | describe: "Decrypts a secret and opens it in an editor. Output is re-encrypted and saved back to the original file", 294 | positional: [{ 295 | key: "secret-spec-file", opts: { 296 | describe: "Kubernetes secret spec file", 297 | }, 298 | }], 299 | parameters: [{ 300 | parameterName: "secret-key", 301 | describe: "Key used to decrypt & encrypt secret data values", 302 | type: "string", 303 | }], 304 | handler: (argv: any) => cliCommand(() => kubeEdit({ 305 | file: argv["secret-spec-file"], 306 | secretKey: argv["secret-key"], 307 | })), 308 | }); 309 | yargBuilder.withSubcommand({ 310 | command: "kube-fetch", 311 | describe: "Fetch resources from a Kubernetes cluster using the currently configured Kubernetes credentials, " + 312 | "remove system-populated properties, and save each resource specification to a file", 313 | parameters: [{ 314 | parameterName: "options-file", 315 | describe: "Path to file containing a JSON object defining options selecting which resources to fetch, see " + 316 | "https://atomist.github.io/sdm-pack-k8s/interfaces/_lib_kubernetes_fetch_.kubernetesfetchoptions.html " + 317 | "for details on the structure of the object", 318 | type: "string", 319 | }, { 320 | parameterName: "output-dir", 321 | describe: "Directory to write spec files in, if not provided current directory is used", 322 | type: "string", 323 | }, { 324 | parameterName: "output-format", 325 | describe: "File format to write spec files in, supported formats are 'json' and 'yaml', 'yaml' is the default", 326 | type: "string", 327 | }, { 328 | parameterName: "secret-key", 329 | describe: "Key to use to encrypt secret data values before writing to file", 330 | type: "string", 331 | }], 332 | handler: (argv: any) => cliCommand(() => kubeFetch({ 333 | optionsFile: argv["options-file"], 334 | outputDir: argv["output-dir"], 335 | outputFormat: argv["output-format"], 336 | secretKey: argv["secret-key"], 337 | })), 338 | }); 339 | yargBuilder.withSubcommand({ 340 | command: "kube-install", 341 | describe: "Deploy Atomist utilities to Kubernetes a cluster", 342 | parameters: [{ 343 | parameterName: "environment", 344 | describe: "Informative name for your Kubernetes cluster", 345 | type: "string", 346 | }, { 347 | parameterName: "namespace", 348 | describe: "Deploy utilities in namespace mode", 349 | type: "string", 350 | }, { 351 | parameterName: "url", 352 | describe: "URL of publicly accessible hostname (e.g. http://a.atomist.io)", 353 | type: "string", 354 | }, { 355 | parameterName: "dry-run", 356 | describe: "Only print the k8s objects that would be deployed, without sending them", 357 | type: "boolean", 358 | }, { 359 | parameterName: "yes", 360 | describe: "Confirm all questions with yes", 361 | type: "boolean", 362 | }], 363 | handler: (argv: any) => cliCommand(() => kubeInstall({ 364 | env: argv.environment, 365 | ns: argv.namespace, 366 | dryRun: argv["dry-run"], 367 | yes: argv.yes, 368 | url: argv.url, 369 | })), 370 | }); 371 | yargBuilder.withSubcommand({ 372 | command: "start", 373 | describe: "Start an SDM or automation client", 374 | parameters: [ 375 | commonOptions.changeDir, 376 | commonOptions.compile, 377 | commonOptions.install, { 378 | parameterName: "local", 379 | default: false, 380 | describe: "Start SDM in local mode", 381 | type: "boolean", 382 | }, { 383 | parameterName: "profile", 384 | describe: "Name of configuration profiles to include", 385 | type: "string", 386 | required: false, 387 | }, { 388 | parameterName: "watch", 389 | describe: "Enable watch mode", 390 | type: "boolean", 391 | default: false, 392 | required: false, 393 | }, { 394 | parameterName: "debug", 395 | describe: "Enable Node.js debugger", 396 | type: "boolean", 397 | default: false, 398 | required: false, 399 | }, { 400 | parameterName: "repository-url", 401 | describe: "Git URL to clone", 402 | type: "string", 403 | required: false, 404 | }, { 405 | parameterName: "index", 406 | describe: "Name of the file that exports the configuration", 407 | type: "string", 408 | required: false, 409 | implies: "repository-url", 410 | conflicts: "yaml", 411 | }, { 412 | parameterName: "yaml", 413 | describe: "Glob patters for yaml files to import", 414 | type: "string", 415 | required: false, 416 | conflicts: "index", 417 | }, { 418 | parameterName: "sha", 419 | describe: "Git sha to checkout", 420 | type: "string", 421 | required: false, 422 | implies: "repository-url", 423 | }, { 424 | parameterName: "seed-url", 425 | describe: "Git URL to clone the seed to overlay with SDM repository", 426 | type: "string", 427 | required: false, 428 | }], 429 | handler: (argv: any) => cliCommand(() => { 430 | return repositoryStart({ 431 | cwd: argv["change-dir"], 432 | cloneUrl: argv["repository-url"], 433 | index: argv.index, 434 | yaml: argv.yaml, 435 | sha: argv.sha, 436 | local: argv.local, 437 | profile: argv.profile, 438 | seedUrl: argv["seed-url"], 439 | install: argv.install, 440 | compile: argv.compile, 441 | watch: argv.watch, 442 | debug: argv.debug, 443 | }); 444 | }), 445 | }); 446 | yargBuilder.build().save(yargs); 447 | // tslint:disable-next-line:no-unused-expression 448 | yargs.completion("completion", false as any) 449 | .showHelpOnFail(false, "Specify --help for available options") 450 | .alias("help", ["h", "?"]) 451 | .version(version()) 452 | .alias("version", "v") 453 | .describe("version", "Show version information") 454 | .strict() 455 | .wrap(Math.min(100, yargs.terminalWidth())) 456 | .argv; 457 | } 458 | 459 | function deprecated(argv: any, alt?: string): void { 460 | print.warn(`The '${argv._[0]}' command is DEPRECATED and will be removed in a future release.`); 461 | if (alt) { 462 | print.warn(`Use '${alt}' instead.`); 463 | } 464 | } 465 | 466 | async function main(): Promise { 467 | const YargBuilder = yb.freshYargBuilder({ epilogForHelpMessage: "Copyright Atomist, Inc. 2019" }); 468 | if (shouldAddLocalSdmCommands(process.argv)) { 469 | // Lazily load sdm-local to prevent early initialization 470 | const sdmLocal = require("@atomist/sdm-local"); 471 | await sdmLocal.addLocalSdmCommands(YargBuilder); 472 | } 473 | setupYargs(YargBuilder); 474 | } 475 | 476 | main() 477 | .catch((err: Error) => { 478 | print.error(`Unhandled error: ${err.message}`); 479 | print.error(err.stack); 480 | process.exit(102); 481 | }); 482 | --------------------------------------------------------------------------------