Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to set envvars from the cli #179

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deployctl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -87,6 +88,9 @@ switch (subcommand) {
case "logs":
await logsSubcommand(args);
break;
case "secrets":
adoublef marked this conversation as resolved.
Show resolved Hide resolved
await envSubcommand(args);
break;
default:
if (args.version) {
console.log(`deployctl ${VERSION}`);
Expand Down
91 changes: 91 additions & 0 deletions src/subcommands/env.ts
Original file line number Diff line number Diff line change
@@ -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
adoublef marked this conversation as resolved.
Show resolved Hide resolved
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] [<ENVVARS>]

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<string, any>): Promise<void> {
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)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will want to introduce other operations related to envs: list, delete, export, import from .env file, etc. I think it would be preferable if we already plan for this by having a set subcommand (ie deployctl env set key=value key2=value2)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good to know, I was unsure at first due to the only API I could see was to set (i.e making a PATCH request to the api) but this makes sense.

also with #185 looking to also add option for file would it make sense for us to use the same subcommand? some thing like env set [key=value] and env set --env-file? or better think about merging those later?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was unsure at first due to the only API I could see was to set (i.e making a PATCH request to the api)

The PATCH is also used to remove envs, by setting null as value.

also with #185 looking to also add option for file would it make sense for us to use the same subcommand? some thing like env set [key=value] and env set --env-file? or better think about merging those later?

Yes, we definitely want that feature, and I think it makes sense to have it within the env subcommand. However, I wonder if we wouldn't prefer a separate env subcommand, like deployctl env set key=value [keyn=valuen] and deployctl env import [--env-file env-file]. In any case, let's coordinate with @drollinger.

Adding also @kwhinnery to the loop.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be happy with this, will check to see if @kwhinnery has any thoughts before implementing my changes as I have time to try and complete this soon

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adoublef I would go ahead and implement deployctl env set key=value and deployctl env import [--env-file env-file].

token,
project: args.project,
};

await env(opts);
}

interface SecretsOpts {
envVars: Record<string, string>;
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.",
adoublef marked this conversation as resolved.
Show resolved Hide resolved
);
} catch (err) {
envSpinner.fail("Failed to update environment variables");
if (err instanceof APIError) {
error(err.toString());
} else {
throw err;
adoublef marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
12 changes: 12 additions & 0 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,16 @@ export class API {
getMetadata(): Promise<Metadata> {
return this.#requestJson("/meta");
}

async setEnvs(
projectId: string,
envs: Record<string, string>,
): Promise<string[]> {
const { envVars } = await this.#requestJson<{ envVars: string[] }>(
`/projects/${projectId}/env`,
{ method: "PATCH", body: envs },
);

return envVars;
}
}
1 change: 1 addition & 0 deletions src/utils/mod.ts
Original file line number Diff line number Diff line change
@@ -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";
16 changes: 16 additions & 0 deletions src/utils/pairs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function parsePairs(
args: string[],
): Promise<Record<string, string>> {
adoublef marked this conversation as resolved.
Show resolved Hide resolved
return new Promise((res, rej) => {
const out: Record<string, string> = {};

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);
});
}