From ecc6f80164fca3c0cde42b140b6d7404abe8c877 Mon Sep 17 00:00:00 2001 From: myarmolinsky Date: Wed, 5 Feb 2025 12:17:38 -0500 Subject: [PATCH] api-key generate: Add required argument `expiryDate` Change-type: major --- docs/balena-cli.md | 6 +++ src/commands/api-key/generate.ts | 75 ++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/docs/balena-cli.md b/docs/balena-cli.md index 3d5c2f49..779c1039 100644 --- a/docs/balena-cli.md +++ b/docs/balena-cli.md @@ -326,6 +326,8 @@ or to authenticate requests to the API with an 'Authorization: Bearer ' hea Examples: $ balena api-key generate "Jenkins Key" + $ balena api-key generate "Jenkins Key" 2025-10-30 + $ balena api-key generate "Jenkins Key" never ### Arguments @@ -333,6 +335,10 @@ Examples: the API key name +#### EXPIRYDATE + +the expiry date of the API key as an ISO date string, or "never" for no expiry + ## api-key list ### Aliases diff --git a/src/commands/api-key/generate.ts b/src/commands/api-key/generate.ts index fcdeba0c..2d6a4d17 100644 --- a/src/commands/api-key/generate.ts +++ b/src/commands/api-key/generate.ts @@ -17,7 +17,16 @@ import { Args, Command } from '@oclif/core'; import { ExpectedError } from '../../errors'; -import { getBalenaSdk, stripIndent } from '../../utils/lazy'; +import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy'; +import { + formatDuration, + intervalToDuration, + isValid, + parseISO, +} from 'date-fns'; + +// In days +const durations = [1, 7, 30, 90]; async function isLoggedInWithJwt() { const balena = getBalenaSdk(); @@ -41,13 +50,21 @@ export default class GenerateCmd extends Command { This key can be used to log into the CLI using 'balena login --token ', or to authenticate requests to the API with an 'Authorization: Bearer ' header. `; - public static examples = ['$ balena api-key generate "Jenkins Key"']; + public static examples = [ + '$ balena api-key generate "Jenkins Key"', + '$ balena api-key generate "Jenkins Key" 2025-10-30', + '$ balena api-key generate "Jenkins Key" never', + ]; public static args = { name: Args.string({ description: 'the API key name', required: true, }), + expiryDate: Args.string({ + description: + 'the expiry date of the API key as an ISO date string, or "never" for no expiry', + }), }; public static authenticated = true; @@ -55,9 +72,61 @@ export default class GenerateCmd extends Command { public async run() { const { args: params } = await this.parse(GenerateCmd); + let expiryDateResponse: string | number | undefined = params.expiryDate; let key; try { - key = await getBalenaSdk().models.apiKey.create(params.name); + if (!expiryDateResponse) { + expiryDateResponse = await getCliForm().ask({ + message: 'Please pick an expiry date for the API key', + type: 'list', + choices: [...durations, 'custom', 'never'].map((duration) => ({ + name: + duration === 'never' + ? 'No expiration' + : typeof duration === 'number' + ? formatDuration( + intervalToDuration({ + start: 0, + end: duration * 24 * 60 * 60 * 1000, + }), + ) + : 'Custom expiration', + value: duration, + })), + }); + } + let expiryDate: Date | null; + if (expiryDateResponse === 'never') { + expiryDate = null; + } else if (expiryDateResponse === 'custom') { + do { + expiryDate = parseISO( + await getCliForm().ask({ + message: + 'Please enter an expiry date for the API key as an ISO date string', + type: 'input', + }), + ); + if (!isValid(expiryDate)) { + console.error('Invalid date format'); + } + } while (!isValid(expiryDate)); + } else if (typeof expiryDateResponse === 'string') { + expiryDate = parseISO(expiryDateResponse); + if (!isValid(expiryDate)) { + throw new Error( + 'Invalid date format, please use a valid ISO date string', + ); + } + } else { + expiryDate = new Date( + Date.now() + expiryDateResponse * 24 * 60 * 60 * 1000, + ); + } + key = await getBalenaSdk().models.apiKey.create({ + name: params.name, + expiryDate: expiryDate === null ? null : expiryDate.toISOString(), + }); } catch (e) { if (e.name === 'BalenaNotLoggedIn') { if (await isLoggedInWithJwt()) {