diff --git a/src/args.ts b/src/args.ts index 3787f48..1cac9d0 100644 --- a/src/args.ts +++ b/src/args.ts @@ -4,6 +4,7 @@ import yargs from 'yargs'; import { CommandName } from './command/index.js'; import { LOCK_TYPES, LockType } from './lock.js'; import { STORAGE_TYPES, StorageType } from './storage/index.js'; +import { parseTime } from './utils.js'; /** * CLI options. @@ -98,6 +99,7 @@ export async function parseArgs(argv: Array): Promise { }, 'duration': { type: 'string', + conflicts: ['until'], }, 'endpoint': { type: 'string', @@ -117,9 +119,10 @@ export async function parseArgs(argv: Array): Promise { default: 8000, }, 'now': { - type: 'number', + type: 'string', // because of coerce, ends up as a number // eslint-disable-next-line @typescript-eslint/no-magic-numbers default: Math.round(Date.now() / 1000), + coerce: parseTime, }, 'recursive': { type: 'boolean', @@ -150,6 +153,7 @@ export async function parseArgs(argv: Array): Promise { }, 'until': { type: 'string', + conflicts: ['duration'], }, 'ci-project': { type: 'string', diff --git a/src/command/check.ts b/src/command/check.ts index fcf6cb3..beaf2bb 100644 --- a/src/command/check.ts +++ b/src/command/check.ts @@ -1,33 +1,18 @@ import { doesExist } from '@apextoaster/js-utils'; import { ParsedArgs } from '../args.js'; +import { LockData } from '../lock.js'; import { printLock, walkPath } from '../utils.js'; import { CommandContext } from './index.js'; export async function checkCommand(context: CommandContext) { - const { args, logger, storage } = context; + const { args, logger } = context; const { now } = args; logger.info({ now }, 'running check command'); - const paths = getCheckPaths(args); - for (const path of paths) { - const lock = await storage.get(path); - - if (doesExist(lock)) { - if (lock.expires_at < now) { - logger.info({ lock, path }, 'found expired lock'); - } else { - const friendly = printLock(path, lock); - logger.info({ lock, friendly, path }, 'found active lock'); - return lock.allow.includes(args.type); - } - } - } - - logger.info({ path: args.path }, 'no locks found'); - - return true; + const [allowed] = await checkArgsPath(context); + return allowed; } export function getCheckPaths(args: ParsedArgs): Array { @@ -37,3 +22,38 @@ export function getCheckPaths(args: ParsedArgs): Array { return [args.path]; } } + +/** + * This is the core of the check logic, including the path recursion and allow type logic. + * + * @todo take a limited form of `args` to make this easier to use in the Express API/tests + */ +export async function checkArgsPath(context: CommandContext): Promise<[boolean, Array]> { + const { args, logger, storage } = context; + const { now, type } = args; + + const locks: Array = []; + const paths = getCheckPaths(args); + for (const path of paths) { + const lock = await storage.get(path); + + if (doesExist(lock)) { + if (lock.expires_at < now) { + logger.debug({ lock, path }, 'found expired lock'); + } else { + const friendly = printLock(path, lock); + logger.debug({ lock, friendly, path }, 'found active lock'); + locks.push(lock); + + if (lock.allow.includes(type)) { + logger.warn({ lock, friendly, path }, 'found existing lock, check type is allowed'); + } else { + logger.warn({ lock, friendly, path }, 'found existing lock, check type is not allowed'); + } + } + } + } + + const allowed = locks.every((lock) => lock.allow.includes(type)); + return [allowed, locks]; +} diff --git a/src/command/prune.ts b/src/command/prune.ts index 69fa8fb..e874b75 100644 --- a/src/command/prune.ts +++ b/src/command/prune.ts @@ -1,8 +1,8 @@ import { CommandContext } from './index.js'; export async function pruneCommand(context: CommandContext) { - const { logger, storage } = context; - const now = Date.now(); + const { args, logger, storage } = context; + const { now } = args; logger.info({ now }, 'running prune command'); diff --git a/src/server/express.ts b/src/server/express.ts index 6e5680c..f9ec553 100644 --- a/src/server/express.ts +++ b/src/server/express.ts @@ -1,7 +1,8 @@ -import { doesExist } from '@apextoaster/js-utils'; +import { doesExist, mustDefault } from '@apextoaster/js-utils'; import express, { Express, Request, Response } from 'express'; import { APP_NAME } from '../args.js'; +import { checkArgsPath } from '../command/check.js'; import { LockData, LockType } from '../lock.js'; import { AdmissionRequest, buildAdmissionResponse, getAdmissionPath } from './admission.js'; import { ServerContext } from './index.js'; @@ -40,28 +41,36 @@ export async function expressAdmission(context: ServerContext, req: Request, res const path = getAdmissionPath('kube', admissionRequest); // TODO: take admission base from context args context.logger.info({ path }, 'express admission request'); - const lock = await context.storage.get(path); - const available = doesExist(lock) === false; - const admission = buildAdmissionResponse(available, admissionRequest.request.uid); - context.logger.debug({ available, admission }, 'responding to admission request'); + const [allowed] = await checkArgsPath({ + ...context, + args: { + ...context.args, + path, + type: 'deploy', // TODO: add a way to define this? + } + }); + + const admission = buildAdmissionResponse(allowed, admissionRequest.request.uid); + context.logger.debug({ allowed, admission }, 'responding to admission request'); res.status(STATUS_ALLOWED).json(admission); } export async function expressCheck(context: ServerContext, req: Request, res: Response): Promise { const path = req.params[0]; + const type = mustDefault(req.query.type as LockType, 'deploy'); + context.logger.info({ path }, 'express check request'); - const lock = await context.storage.get(path); - if (doesExist(lock)) { - if (doesExist(req.query.type)) { - const allowed = lock.allow.includes(req.query.type as LockType); - sendLocks(res, [ lock ], allowed); - } else { - sendLocks(res, [ lock ], false); + const [allowed, locks] = await checkArgsPath({ + ...context, + args: { + ...context.args, + // TODO: is this a hack? + path, + type, } - } else { - sendLocks(res, [], true); - } + }); + sendLocks(res, locks, allowed); } export async function expressList(context: ServerContext, req: Request, res: Response): Promise { diff --git a/src/utils.ts b/src/utils.ts index 9b8d40e..e75ab86 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -111,11 +111,19 @@ export const TIME_MULTIPLIERS: Array<[RegExp, number]> = [ [/^(\d+)d$/, 60 * 60 * 24], // human days ]; +export const TIME_ISO_MATCH = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?$/; + /** * Convert a string of the form `12345` or `15m` into seconds. */ -export function parseTime(time: string): number { - // TODO: handling for ISO timestamps +export function parseTime(time: number | string): number { + if (typeof time === 'number') { + return time; + } + + if (TIME_ISO_MATCH.test(time)) { + return new Date(time).getTime() / 1000; + } for (const [regex, mult] of TIME_MULTIPLIERS) { const match = time.match(regex);