diff --git a/README.md b/README.md index f8a5d4c..e51d28f 100644 --- a/README.md +++ b/README.md @@ -151,20 +151,14 @@ interface Lock { path: string; author: string; links: Map; + // often duplicates of path, but useful for cross-project locks + source: string; // Timestamps, calculated from --duration and --until created_at: number; updated_at: number; expires_at: number; - // Env fields - // often duplicates of path, but useful for cross-project locks - env: { - cluster: string; - account: string; - target?: string; // optional - } - // CI fields, optional ci?: { project: string; @@ -183,7 +177,7 @@ If `$CI` is not set, the `ci` sub-struct will not be present. - create a new lock: `locked ${path} for ${type:friendly} until ${expires_at:datetime}` - > Locked `apps/acceptance/a` for a deploy until Sat 31 Dec, 12:00 - > Locked `gitlab/production` for an incident until Sat 31 Dec, 12:00 -- error, existing lock: `error: ${path} is locked until ${expires_at:datetime} by ${type:friendly} in ${cluster}/${env}` +- error, existing lock: `error: ${path} is locked until ${expires_at:datetime} by ${type:friendly} in ${source}` - > Error: `apps/acceptance` is locked until Sat 31 Dec, 12:00 by an automation run in `testing/staging`. #### Friendly Types @@ -254,24 +248,31 @@ Friendly strings for `type`: - mutually exclusive with `--until` - `--link` - array, strings +- `--source` + - string + - each component: + - first + - defaults to `$CLUSTER_NAME` if set + - defaults to `path.split.0` otherwise + - second + - defaults to `$DEPLOY_ENV` if set + - defaults to `path.split.1` otherwise + - third + - defaults to `$DEPLOY_TARGET` if set + - defaults to `path.split.2` otherwise + - fourth + - defaults to `--ci-project` if set + - defaults to `$CI_PROJECT_PATH` if set + - defaults to `path.split.3` otherwise + - fifth + - defaults to `--ci-ref` if set + - defaults to `$CI_COMMIT_REF_SLUG` if set + - defaults to `path.split.4` otherwise - `--until` - string, timestamp - duration of lock, absolute - may be given in epoch seconds (`\d+`) or as an ISO-8601 date (intervals are not allowed) - mutually exclusive with `--duration` -- `--env-cluster` - - string, enum - - defaults to `$CLUSTER_NAME` if set - - defaults to `path.split.0` otherwise -- `--env-account` - - string, enum - - defaults to `$DEPLOY_ENV` if set - - defaults to `path.split.1` otherwise -- `--env-target` - - optional string - - `/^[a-z]$/` - - defaults to `$DEPLOY_TARGET` if set - - defaults to `path.split.2` otherwise - `--ci-project` - optional string - project path @@ -334,6 +335,11 @@ Friendly strings for `type`: ### Features +- in-memory data store, mostly for testing +- DynamoDB data store +- lock paths and recursive checking +- infer lock data from CI variables + ### Building 1. Clone with `git clone git@github.com:ssube/deploy-lock.git` @@ -353,17 +359,18 @@ Friendly strings for `type`: ### TODOs -1. SQL data store, with history (don't need to remove old records) -2. S3 data store -3. REST API with lock endpoints -4. Kubernetes admission controller with webhook endpoint +1. Infer lock source from other arguments/CI variables +2. SQL data store, with history (don't need to remove old records) +3. S3 data store +4. REST API with lock endpoints +5. Kubernetes admission controller with webhook endpoint ### Questions 1. In the [deploy path](#deploy-path), should account come before region or region before account? 1. `aws/us-east-1/staging` vs `aws/staging/us-east-1` - 2. This is purely a recommendation in the docs, `lock.path` and `lock.source` (replacing `lock.env`) will both be - slash-delimited or array paths. + 2. This is purely a recommendation in the docs, `lock.path` and `lock.source` will both be slash-delimited or array + paths. 2. Should there be an `update` or `replace` command? 1. Probably not, at least not without lock history or multi-party locks. 2. When the data store can keep old locks, `replace` could expire an existing lock and create a new one @@ -382,7 +389,8 @@ Friendly strings for `type`: 2. for an incident: `[first-responder, incident-commander]` 4. Each author has to `unlock` before the lock is removed/released 5. Should `LockData.env` be a string/array, like `LockData.path`? - 1. Very probably yes, because otherwise it will need `env.cloud`, `env.network`, etc, and those + 1. Done + 2. Very probably yes, because otherwise it will need `env.cloud`, `env.network`, etc, and those are not always predictable/present. 6. Should there be an `--allow`/`LockData.allow` field? 1. Probably yes diff --git a/src/args.ts b/src/args.ts index d5e0067..717ea2d 100644 --- a/src/args.ts +++ b/src/args.ts @@ -31,10 +31,7 @@ export interface ParsedArgs { recursive: boolean; links: Array; now: number; - - 'env-cluster'?: string; - 'env-account'?: string; - 'env-target'?: string; + source: string; 'ci-project'?: string; 'ci-ref'?: string; @@ -116,6 +113,10 @@ export async function parseArgs(argv: Array): Promise { type: 'string', default: 'us-east-1', }, + 'source': { + type: 'string', + require: true, + }, 'storage': { type: 'string', choices: Object.keys(STORAGE_TYPES) as ReadonlyArray, @@ -134,15 +135,6 @@ export async function parseArgs(argv: Array): Promise { 'until': { type: 'string', }, - 'env-cluster': { - type: 'string', - }, - 'env-account': { - type: 'string', - }, - 'env-target': { - type: 'string', - }, 'ci-project': { type: 'string', }, diff --git a/src/lock.ts b/src/lock.ts index 45cd8c9..3fbaaec 100644 --- a/src/lock.ts +++ b/src/lock.ts @@ -16,12 +16,6 @@ export interface LockCI { job: string; } -export interface LockEnv { - cluster: string; - account: string; - target?: string; // optional -} - export interface LockData { type: LockType; @@ -44,7 +38,7 @@ export interface LockData { /** * Environment where the lock was created. Often duplicates the path, but useful for cross-project locks. */ - env: LockEnv; + source: string; /** * Attribution info when CI was the source of the lock. diff --git a/src/utils.ts b/src/utils.ts index 894b6de..5e6c74a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,7 @@ import { doesExist, InvalidArgumentError, mustDefault } from '@apextoaster/js-utils'; import { ParsedArgs } from './args.js'; -import { LOCK_TYPES, LockCI, LockData, LockEnv } from './lock.js'; +import { LOCK_TYPES, LockCI, LockData } from './lock.js'; export function matchPath(baseStr: string, otherStr: string): boolean { const base = splitPath(baseStr); @@ -53,14 +53,6 @@ export function buildCI(args: ParsedArgs, env: typeof process.env): LockCI | und return undefined; } -export function buildEnv(args: ParsedArgs, env: typeof process.env): LockEnv { - return { - cluster: mustDefault(args['env-cluster'], env.CLUSTER_NAME, ENV_UNSET), - account: mustDefault(args['env-account'], env.DEPLOY_ENV, ENV_UNSET), - target: mustDefault(args['env-target'], env.DEPLOY_TARGET, ENV_UNSET), - }; -} - export function buildAuthor(args: ParsedArgs, env: typeof process.env): string { if (doesExist(env.GITLAB_CI)) { return mustDefault(args.author, env.GITLAB_USER_EMAIL, env.USER); @@ -92,14 +84,14 @@ export function buildLock(args: ParsedArgs, env = process.env): LockData { created_at: args.now, updated_at: args.now, expires_at: calculateExpires(args), - env: buildEnv(args, env), + source: args.source, ci: buildCI(args, env), }; } export function printLock(path: string, data: LockData): string { const friendlyType = LOCK_TYPES[data.type]; - return `${path} is locked until ${data.expires_at} by ${friendlyType} in ${data.env.cluster}`; + return `${path} is locked until ${data.expires_at} by ${friendlyType} in ${data.source}`; } export function calculateExpires(args: ParsedArgs): number {