diff --git a/deployctl.ts b/deployctl.ts index 1e3ac027..f70418db 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 envSubcommand from "./src/subcommands/env.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 envSubcommand(args); + break; default: if (args.version) { console.log(`deployctl ${VERSION}`); diff --git a/src/subcommands/env.ts b/src/subcommands/env.ts new file mode 100644 index 00000000..be348132 --- /dev/null +++ b/src/subcommands/env.ts @@ -0,0 +1,91 @@ +import { wait } from "../../deps.ts"; +import { error } from "../error.ts"; +import { API, APIError } from "../utils/api.ts"; +import { parsePairs } 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 { + 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 parsePairs(rawArgs._).catch((e) => error(e)), + token, + project: args.project, + }; + + await env(opts); +} + +interface SecretsOpts { + envVars: Record; + token: string; + project: string; +} + +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); + 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.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.", + ); + } 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..8c1853dd 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 setEnvs( + 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..31af311c 100644 --- a/src/utils/mod.ts +++ b/src/utils/mod.ts @@ -1,4 +1,5 @@ export { parseEntrypoint } from "./entrypoint.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 new file mode 100644 index 00000000..59836a3c --- /dev/null +++ b/src/utils/pairs.ts @@ -0,0 +1,16 @@ +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`); + } + out[parts[0]] = parts[1]; + } + return res(out); + }); +}