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

render config add-profile, fixes to profile selection #48

Merged
merged 2 commits into from
Jan 26, 2023
Merged
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
24 changes: 22 additions & 2 deletions commands/config/_shared.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ALL_REGIONS, Region } from "../../config/types/enums.ts";
import { ProfileLatest } from "../../config/types/index.ts";
import { Cliffy } from "../../deps.ts";
import { ConfigLatest, ProfileLatest } from "../../config/types/index.ts";
import { Cliffy, Log, YAML } from "../../deps.ts";
import { RenderCLIError } from "../../errors.ts";

export async function requestProfileInfo(): Promise<ProfileLatest> {
Expand Down Expand Up @@ -34,3 +34,23 @@ export async function requestProfileInfo(): Promise<ProfileLatest> {
apiKey: resp.apiKey,
};
}

export async function writeProfile(logger: Log.Logger, configFile: string, cfg: ConfigLatest) {
logger.debug(`writing config to '${configFile}'`);
await Deno.writeTextFile(configFile, YAML.dump(cfg));
await chmodConfigIfPossible(logger, configFile);
}

export async function chmodConfigIfPossible(logger: Log.Logger, configFile: string) {
if (Deno.build.os === 'windows') {
logger.warning(`Deno does not currently support file permissions on Windows. As such,`);
logger.warning(`'${configFile}' has user-level default permissions. On single-user`);
logger.warning(`systems, this is fine. On multi-user systems, you may wish to further`);
logger.warning(`secure your Render credentials.`)
logger.warning('');
logger.warning('See https://rndr.in/windows-file-acl for potential solutions.');
} else {
logger.debug(`chmod '${configFile}' to 600`);
await Deno.chmod(configFile, 0o600);
}
}
47 changes: 47 additions & 0 deletions commands/config/add-profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ConfigLatest } from "../../config/types/index.ts";
import { Log, YAML } from "../../deps.ts";
import { ForceRequiredError, InitRequiredError } from "../../errors.ts";
import { ajv } from "../../util/ajv.ts";
import { pathExists } from "../../util/paths.ts";
import { getPaths } from "../../util/paths.ts";
import { standardAction, Subcommand } from "../_helpers.ts";
import { requestProfileInfo, writeProfile } from "./_shared.ts";

const desc =
`Adds a new profile to a Render CLI config file.`;

export const configAddProfileCommand =
new Subcommand()
.name('init')
.description(desc)
.option("-f, --force", "overwrites existing profile if found.")
.arguments("<profileName:string>")
.action((opts, profileName) => standardAction({
interactive: async (logger: Log.Logger) => {
const { configFile } = await getPaths();

if (!pathExists(configFile)) {
throw new InitRequiredError(`Render config file does not exist at '${configFile}'`);
}

logger.debug({ configFile }, "Loading config.");
const cfg = YAML.load(await Deno.readTextFile(configFile)) as ConfigLatest;

logger.debug("Validating config...");
ajv.validate(ConfigLatest, cfg);

if (cfg.profiles[profileName] && !opts.force) {
throw new ForceRequiredError(`Profile '${profileName}' already exists in '${configFile}'`);
}

logger.info(`Let's create a profile named '${profileName}'.`);
const profile = await requestProfileInfo();

cfg.profiles[profileName] = profile;

await writeProfile(logger, configFile, cfg);

return 0;
}
})
);
8 changes: 6 additions & 2 deletions commands/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Subcommand } from "../_helpers.ts";
import { configAddProfileCommand } from "./add-profile.ts";
import { configInitCommand } from "./init.ts";
import { configProfilesCommand } from "./profiles.ts";
import { configSchemaCommand } from "./schema.ts";

const desc =
const desc =
`Commands for interacting with the render-cli configuration.`;

export const configCommand =
Expand All @@ -13,6 +15,8 @@ export const configCommand =
this.showHelp();
Deno.exit(1);
})
.command("schema", configSchemaCommand)
.command("init", configInitCommand)
.command("add-profile", configAddProfileCommand)
.command("profiles", configProfilesCommand)
.command("schema", configSchemaCommand)
;
16 changes: 2 additions & 14 deletions commands/config/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ForceRequiredError } from "../../errors.ts";
import { pathExists } from "../../util/paths.ts";
import { getPaths } from "../../util/paths.ts";
import { standardAction, Subcommand } from "../_helpers.ts";
import { requestProfileInfo } from "./_shared.ts";
import { chmodConfigIfPossible, requestProfileInfo, writeProfile } from "./_shared.ts";

const desc =
`Interactively creates a Render CLI config file.`;
Expand Down Expand Up @@ -46,19 +46,7 @@ export const configInitCommand =
},
};

logger.info(`Writing profile to '${configFile}'...`);
await Deno.writeTextFile(configFile, YAML.dump(cfg));
if (Deno.build.os === 'windows') {
logger.warning(`Deno does not currently support file permissions on Windows. As such,`);
logger.warning(`'${configFile}' has user-level default permissions. On single-user`);
logger.warning(`systems, this is fine. On multi-user systems, you may wish to further`);
logger.warning(`secure your Render credentials.`)
logger.warning('');
logger.warning('See https://rndr.in/windows-file-acl for potential solutions.');
} else {
logger.debug(`chmod '${configFile}' to 600`);
await Deno.chmod(configFile, 0o600);
}
await writeProfile(logger, configFile, cfg);

logger.info("Done! You're ready to use the Render CLI!");
return 0;
Expand Down
16 changes: 16 additions & 0 deletions commands/config/profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import { Subcommand } from "../_helpers.ts";
import { getConfig } from "../../config/index.ts";

const desc =
`Lists your configured profiles.`;

export const configProfilesCommand =
new Subcommand()
.name('profiles')
.description(desc)
.action(async () => {
const config = await getConfig();
Object.keys(config.fullConfig.profiles).forEach((profile) => console.log(profile));
return 0;
});
46 changes: 39 additions & 7 deletions commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Cliffy } from '../deps.ts';

import { jsonRecordPerLine, nonInteractive, prettyJson, verboseLogging } from "../util/logging.ts";
import { VERSION } from "../version.ts";
import { getLogger, jsonRecordPerLine, nonInteractive, prettyJson, verboseLogging } from "../util/logging.ts";
import { blueprintCommand } from "./blueprint/index.ts";
import { buildpackCommand } from './buildpack/index.ts';
import { commandsCommand } from "./commands.ts";
Expand All @@ -15,6 +14,7 @@ import { jobsCommand } from "./jobs/index.ts";
import { versionCommand } from './version.ts';
import { dashboardCommand } from './dashboard.ts';
import { getPaths, pathExists } from '../util/paths.ts';
import { funcError } from "../util/errors.ts";


export const ROOT_COMMAND =
Expand All @@ -31,23 +31,55 @@ export const ROOT_COMMAND =
"--non-interactive",
"Forces Render to act as though it's not in a TTY.",
{
action: () => nonInteractive(),
action: async (opts) => {
(await getLogger()).debug("--non-interactive", opts);
nonInteractive();
},
})
.globalOption(
"--pretty-json",
"If in non-interactive mode, prints prettified JSON.",
{
action: () => prettyJson(),
action: async (opts) => {
(await getLogger()).debug("--pretty-json", opts);
prettyJson();
},
})
.globalOption(
"--json-record-per-line",
"if emitting JSON, prints each JSON record as a separate line of stdout.",
{
action: () => jsonRecordPerLine(),
action: async (opts) => {
(await getLogger()).debug("--json-record-per-line", opts);
jsonRecordPerLine();
},
conflicts: ["pretty-json"],
})
.globalOption("-p, --profile <profileName>", "The Render profile to use for this invocation. Overrides RENDERCLI_PROFILE.")
.globalOption("-r, --region <regionName>", "The Render region to use for this invocation; always accepted but not always relevant. Overrides RENDERCLI_REGION.")
.globalOption(
"-p, --profile <profileName>",
"The Render profile to use for this invocation. Overrides RENDERCLI_PROFILE.",
{
action: async (opts) => {
(await getLogger()).debug("--profile", opts);
Deno.env.set(
"RENDERCLI_PROFILE",
opts.profile || funcError(new Error("--profile passed but no argument received")),
);
},
}
)
.globalOption(
"-r, --region <regionName>",
"The Render region to use for this invocation; always accepted but not always relevant. Overrides RENDERCLI_REGION.",
{
action: async (opts) => {
(await getLogger()).debug("--region", opts);
Deno.env.set(
"RENDERCLI_REGION",
opts.region || funcError(new Error("--region passed but no argument received")),
);
},
})
.action(async function() {
const { configFile } = await getPaths();

Expand Down
14 changes: 10 additions & 4 deletions config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { YAML, } from "../deps.ts";
import { Log, YAML, } from "../deps.ts";
import { ajv } from "../util/ajv.ts";
import { identity } from "../util/fn.ts";
import { getPaths } from "../util/paths.ts";
Expand All @@ -7,6 +7,7 @@ import { APIKeyRequired } from '../errors.ts';
import { ALL_REGIONS, Region } from "./types/enums.ts";
import { assertValidRegion } from "./types/enums.ts";
import { ConfigAny, ConfigLatest, ProfileLatest, RuntimeConfiguration } from "./types/index.ts";
import { getLogger } from "../util/logging.ts";

let config: RuntimeConfiguration | null = null;

Expand All @@ -27,7 +28,7 @@ export async function getConfig(): Promise<RuntimeConfiguration> {
if (config === null) {
const cfg = await fetchAndParseConfig();

const runtimeProfile = buildRuntimeProfile(cfg);
const runtimeProfile = await buildRuntimeProfile(cfg);
const ret: RuntimeConfiguration = {
fullConfig: cfg,
...runtimeProfile,
Expand Down Expand Up @@ -87,8 +88,13 @@ async function fetchAndParseConfig(): Promise<ConfigLatest> {
}
}

function buildRuntimeProfile(cfg: ConfigLatest): { profile: ProfileLatest, profileName: string } {
const profileName = Deno.env.get("RENDERCLI_PROFILE") ?? 'default';
async function buildRuntimeProfile(
cfg: ConfigLatest,
): Promise<{ profile: ProfileLatest, profileName: string }> {
const logger = await getLogger();
const profileFromEnv = Deno.env.get("RENDERCLI_PROFILE");
const profileName = profileFromEnv ?? 'default';
logger.debug(`Using profile '${profileName}' (env: ${profileFromEnv})`);
const profile = cfg.profiles[profileName] ?? {};

const ret: ProfileLatest = {
Expand Down
7 changes: 6 additions & 1 deletion errors.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { RuntimeConfiguration } from "./config/types/index.ts";
import { Typebox } from "./deps.ts";
import { ajv } from './util/ajv.ts';

export class RenderCLIError extends Error {

}

export class InitRequiredError extends RenderCLIError {
constructor(msg: string) {
super(`${msg}; run 'render config init' to create a config file.`);
}
}

export class ForceRequiredError extends RenderCLIError {
constructor(msg: string) {
super(`${msg}; pass --force to do this anyway.`);
Expand Down
8 changes: 7 additions & 1 deletion util/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ let PRETTY_JSON = false;
let JSON_RECORD_PER_LINE = false;
export let NON_INTERACTIVE = !Deno.isatty(Deno.stdout.rid);

export function verboseLogging() {
export async function verboseLogging() {
if (isLoggingSetup) {
Log.getLogger().warning("`verboseLogging` called after logging setup.");
}

// Deno's log function may be already called when these methods are called
// because the CLI framework doesn't seem consistent in what order it calls
// the `action` parameter for global options, so as a compromise we blow away
// the existing logging config and re-instantiate.
LOG_VERBOSITY = 'DEBUG'
isLoggingSetup = false;
await setupLogging();
}

export function nonInteractive() {
Expand Down