From 04e119dc812059ebba373eee400f97a659c1cf7c Mon Sep 17 00:00:00 2001 From: Kristopher Rahim Afful-Brown Date: Tue, 17 Oct 2023 19:12:50 +0100 Subject: [PATCH 1/6] update with env option --- deployctl.ts | 4 ++ src/subcommands/secrets.ts | 81 ++++++++++++++++++++++++++++++++++++++ src/utils/api.ts | 12 ++++++ src/utils/mod.ts | 1 + src/utils/pairs.ts | 18 +++++++++ 5 files changed, 116 insertions(+) create mode 100644 src/subcommands/secrets.ts create mode 100644 src/utils/pairs.ts diff --git a/deployctl.ts b/deployctl.ts index 1e3ac027..a6d8eb3a 100755 --- a/deployctl.ts +++ b/deployctl.ts @@ -8,6 +8,7 @@ import { error } from "./src/error.ts"; import deploySubcommand from "./src/subcommands/deploy.ts"; import upgradeSubcommand from "./src/subcommands/upgrade.ts"; import logsSubcommand from "./src/subcommands/logs.ts"; +import secretsSubcommand from "./src/subcommands/secrets.ts"; import { MINIMUM_DENO_VERSION, VERSION } from "./src/version.ts"; import { fetchReleases, getConfigPaths } from "./src/utils/info.ts"; @@ -87,6 +88,9 @@ switch (subcommand) { case "logs": await logsSubcommand(args); break; + case "secrets": + await secretsSubcommand(args); + break; default: if (args.version) { console.log(`deployctl ${VERSION}`); diff --git a/src/subcommands/secrets.ts b/src/subcommands/secrets.ts new file mode 100644 index 00000000..e546a832 --- /dev/null +++ b/src/subcommands/secrets.ts @@ -0,0 +1,81 @@ +import { wait } from "../../deps.ts"; +import { error } from "../error.ts"; +import { API, APIError } from "../utils/api.ts"; +import { parseKVStrings } from "../utils/pairs.ts"; + +const help = `deployctl secrets +Manage environment variables for the given project +`; + +export interface Args { + help: boolean; + project: string | null; + token: string | null; +} + +export default async function (rawArgs: Record): Promise { + const args: Args = { + help: !!rawArgs.help, + token: rawArgs.token ? String(rawArgs.token) : null, + project: rawArgs.project ? String(rawArgs.project) : null, + }; + + if (args.help) { + console.log(help); + Deno.exit(0); + } + + const token = args.token ?? Deno.env.get("DENO_DEPLOY_TOKEN") ?? null; + if (token === null) { + console.error(help); + error("Missing access token. Set via --token or DENO_DEPLOY_TOKEN."); + } + if (rawArgs._.length < 1) { + console.error(help); + error("Requires at least one SECRET=VALUE pair"); + } + if (args.project === null) { + console.error(help); + error("Missing project ID."); + } + + const opts = { + envVars: await parseKVStrings(rawArgs._).catch((e) => error(e)), + token, + project: args.project, + }; + + await secrets(opts); +} + +interface SecretsOpts { + envVars: Record; + token: string; + project: string; +} + +async function secrets(opts: SecretsOpts) { + const projectSpinner = wait("Fetching project information...").start(); + const api = API.fromToken(opts.token); + const project = await api.getProject(opts.project); + if (project === null) { + projectSpinner.fail("Project not found."); + Deno.exit(1); + } + projectSpinner.succeed(`Project: ${project!.name}`); + + const envSpinner = wait("Uploading environment variables").start(); + try { + await api.editEnvs(project!.id, opts.envVars); + envSpinner.succeed( + "Secrets have been staged. Deploy or update machines in this app for the secrets to take effect.", + ); + } catch (err) { + envSpinner.fail("Failed to update environment variables"); + if (err instanceof APIError) { + error(err.toString()); + } else { + throw err; + } + } +} diff --git a/src/utils/api.ts b/src/utils/api.ts index 843f7fa7..01f0202e 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -203,4 +203,16 @@ export class API { getMetadata(): Promise { return this.#requestJson("/meta"); } + + async editEnvs( + projectId: string, + envs: Record, + ): Promise { + const { envVars } = await this.#requestJson<{ envVars: string[] }>( + `/projects/${projectId}/env`, + { method: "PATCH", body: envs }, + ); + + return envVars; + } } diff --git a/src/utils/mod.ts b/src/utils/mod.ts index bb2a97c0..f500a65e 100644 --- a/src/utils/mod.ts +++ b/src/utils/mod.ts @@ -1,4 +1,5 @@ export { parseEntrypoint } from "./entrypoint.ts"; +export { parseKVStrings } from "./pairs.ts"; export { API, APIError } from "./api.ts"; export { walk } from "./walk.ts"; export { fromFileUrl, resolve } from "../../deps.ts"; diff --git a/src/utils/pairs.ts b/src/utils/pairs.ts new file mode 100644 index 00000000..9086a03e --- /dev/null +++ b/src/utils/pairs.ts @@ -0,0 +1,18 @@ +export function parseKVStrings( + args: string[], +): Promise> { + + + return new Promise((res, rej) => { + const out: Record = {}; + + for (const arg of args) { + const parts = arg.split("=", 2); + if (parts.length !== 2) { + return rej(`${arg} must be in the format NAME=VALUE`); + } + out[parts[0]] = parts[1]; + } + return res(out); + }); +} From 2d0fc6a47059c439efc0d26a25d0a591f6153b60 Mon Sep 17 00:00:00 2001 From: Kristopher Rahim Afful-Brown Date: Tue, 17 Oct 2023 19:13:24 +0100 Subject: [PATCH 2/6] rename subcommand inside help --- src/subcommands/secrets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommands/secrets.ts b/src/subcommands/secrets.ts index e546a832..25e591d8 100644 --- a/src/subcommands/secrets.ts +++ b/src/subcommands/secrets.ts @@ -3,7 +3,7 @@ import { error } from "../error.ts"; import { API, APIError } from "../utils/api.ts"; import { parseKVStrings } from "../utils/pairs.ts"; -const help = `deployctl secrets +const help = `deployctl env Manage environment variables for the given project `; From ff76783f119e86c4274ac35f7b7ad84cc792ab4d Mon Sep 17 00:00:00 2001 From: Kristopher Rahim Afful-Brown Date: Tue, 17 Oct 2023 19:25:02 +0100 Subject: [PATCH 3/6] update helper text --- src/subcommands/secrets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommands/secrets.ts b/src/subcommands/secrets.ts index 25e591d8..2196d7ba 100644 --- a/src/subcommands/secrets.ts +++ b/src/subcommands/secrets.ts @@ -68,7 +68,7 @@ async function secrets(opts: SecretsOpts) { try { await api.editEnvs(project!.id, opts.envVars); envSpinner.succeed( - "Secrets have been staged. Deploy or update machines in this app for the secrets to take effect.", + "A new production deployment will be created automatically with the new environment variables when you next push your code.", ); } catch (err) { envSpinner.fail("Failed to update environment variables"); From 958a105fea087f9fe0d25156eb0d4a7d3b450ed5 Mon Sep 17 00:00:00 2001 From: Kristopher Rahim Afful-Brown Date: Tue, 17 Oct 2023 19:29:10 +0100 Subject: [PATCH 4/6] update helper --- src/subcommands/secrets.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/subcommands/secrets.ts b/src/subcommands/secrets.ts index 2196d7ba..1d6d5a07 100644 --- a/src/subcommands/secrets.ts +++ b/src/subcommands/secrets.ts @@ -5,6 +5,16 @@ import { parseKVStrings } from "../utils/pairs.ts"; const help = `deployctl env Manage environment variables for the given project + +To set environment variables for a project: + deployctl env --project=helloworld ENV1=VALUE_1 ENV2=VALUE_2 + +USAGE: + deployctl env [OPTIONS] [] + +OPTIONS: + -p, --project=NAME The project you want to get the logs + --token=TOKEN The API token to use (defaults to DENO_DEPLOY_TOKEN env var) `; export interface Args { From c626a90d34b3920e59b3edfec8b30edf9d8c51d7 Mon Sep 17 00:00:00 2001 From: Kristopher Rahim Afful-Brown Date: Tue, 17 Oct 2023 19:52:27 +0100 Subject: [PATCH 5/6] rename --- deployctl.ts | 4 ++-- src/subcommands/{secrets.ts => env.ts} | 6 +++--- src/utils/api.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/subcommands/{secrets.ts => env.ts} (95%) diff --git a/deployctl.ts b/deployctl.ts index a6d8eb3a..f70418db 100755 --- a/deployctl.ts +++ b/deployctl.ts @@ -8,7 +8,7 @@ import { error } from "./src/error.ts"; import deploySubcommand from "./src/subcommands/deploy.ts"; import upgradeSubcommand from "./src/subcommands/upgrade.ts"; import logsSubcommand from "./src/subcommands/logs.ts"; -import secretsSubcommand from "./src/subcommands/secrets.ts"; +import envSubcommand from "./src/subcommands/env.ts"; import { MINIMUM_DENO_VERSION, VERSION } from "./src/version.ts"; import { fetchReleases, getConfigPaths } from "./src/utils/info.ts"; @@ -89,7 +89,7 @@ switch (subcommand) { await logsSubcommand(args); break; case "secrets": - await secretsSubcommand(args); + await envSubcommand(args); break; default: if (args.version) { diff --git a/src/subcommands/secrets.ts b/src/subcommands/env.ts similarity index 95% rename from src/subcommands/secrets.ts rename to src/subcommands/env.ts index 1d6d5a07..d4ad2874 100644 --- a/src/subcommands/secrets.ts +++ b/src/subcommands/env.ts @@ -55,7 +55,7 @@ export default async function (rawArgs: Record): Promise { project: args.project, }; - await secrets(opts); + await env(opts); } interface SecretsOpts { @@ -64,7 +64,7 @@ interface SecretsOpts { project: string; } -async function secrets(opts: SecretsOpts) { +async function env(opts: SecretsOpts) { const projectSpinner = wait("Fetching project information...").start(); const api = API.fromToken(opts.token); const project = await api.getProject(opts.project); @@ -76,7 +76,7 @@ async function secrets(opts: SecretsOpts) { const envSpinner = wait("Uploading environment variables").start(); try { - await api.editEnvs(project!.id, opts.envVars); + await api.setEnvs(project!.id, opts.envVars); envSpinner.succeed( "A new production deployment will be created automatically with the new environment variables when you next push your code.", ); diff --git a/src/utils/api.ts b/src/utils/api.ts index 01f0202e..8c1853dd 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -204,7 +204,7 @@ export class API { return this.#requestJson("/meta"); } - async editEnvs( + async setEnvs( projectId: string, envs: Record, ): Promise { From 0dc1ba202bcd3253257bcc870e76667a6912eca7 Mon Sep 17 00:00:00 2001 From: Kristopher Rahim Afful-Brown Date: Tue, 17 Oct 2023 20:07:07 +0100 Subject: [PATCH 6/6] format --- src/subcommands/env.ts | 4 ++-- src/utils/mod.ts | 2 +- src/utils/pairs.ts | 16 +++++++--------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/subcommands/env.ts b/src/subcommands/env.ts index d4ad2874..be348132 100644 --- a/src/subcommands/env.ts +++ b/src/subcommands/env.ts @@ -1,7 +1,7 @@ import { wait } from "../../deps.ts"; import { error } from "../error.ts"; import { API, APIError } from "../utils/api.ts"; -import { parseKVStrings } from "../utils/pairs.ts"; +import { parsePairs } from "../utils/pairs.ts"; const help = `deployctl env Manage environment variables for the given project @@ -50,7 +50,7 @@ export default async function (rawArgs: Record): Promise { } const opts = { - envVars: await parseKVStrings(rawArgs._).catch((e) => error(e)), + envVars: await parsePairs(rawArgs._).catch((e) => error(e)), token, project: args.project, }; diff --git a/src/utils/mod.ts b/src/utils/mod.ts index f500a65e..31af311c 100644 --- a/src/utils/mod.ts +++ b/src/utils/mod.ts @@ -1,5 +1,5 @@ export { parseEntrypoint } from "./entrypoint.ts"; -export { parseKVStrings } from "./pairs.ts"; +export { parsePairs } from "./pairs.ts"; export { API, APIError } from "./api.ts"; export { walk } from "./walk.ts"; export { fromFileUrl, resolve } from "../../deps.ts"; diff --git a/src/utils/pairs.ts b/src/utils/pairs.ts index 9086a03e..59836a3c 100644 --- a/src/utils/pairs.ts +++ b/src/utils/pairs.ts @@ -1,18 +1,16 @@ -export function parseKVStrings( +export function parsePairs( args: string[], ): Promise> { - - return new Promise((res, rej) => { const out: Record = {}; - for (const arg of args) { - const parts = arg.split("=", 2); - if (parts.length !== 2) { - return rej(`${arg} must be in the format NAME=VALUE`); + for (const arg of args) { + const parts = arg.split("=", 2); + if (parts.length !== 2) { + return rej(`${arg} must be in the format NAME=VALUE`); + } + out[parts[0]] = parts[1]; } - out[parts[0]] = parts[1]; - } return res(out); }); }