1
0
Fork 0

isolate check logic, use in both command and express, coerce now option

This commit is contained in:
Sean Sube 2023-01-03 11:13:51 -06:00
parent e2c81afcdb
commit ae84b864be
5 changed files with 80 additions and 39 deletions

View File

@ -4,6 +4,7 @@ import yargs from 'yargs';
import { CommandName } from './command/index.js'; import { CommandName } from './command/index.js';
import { LOCK_TYPES, LockType } from './lock.js'; import { LOCK_TYPES, LockType } from './lock.js';
import { STORAGE_TYPES, StorageType } from './storage/index.js'; import { STORAGE_TYPES, StorageType } from './storage/index.js';
import { parseTime } from './utils.js';
/** /**
* CLI options. * CLI options.
@ -98,6 +99,7 @@ export async function parseArgs(argv: Array<string>): Promise<ParsedArgs> {
}, },
'duration': { 'duration': {
type: 'string', type: 'string',
conflicts: ['until'],
}, },
'endpoint': { 'endpoint': {
type: 'string', type: 'string',
@ -117,9 +119,10 @@ export async function parseArgs(argv: Array<string>): Promise<ParsedArgs> {
default: 8000, default: 8000,
}, },
'now': { 'now': {
type: 'number', type: 'string', // because of coerce, ends up as a number
// eslint-disable-next-line @typescript-eslint/no-magic-numbers // eslint-disable-next-line @typescript-eslint/no-magic-numbers
default: Math.round(Date.now() / 1000), default: Math.round(Date.now() / 1000),
coerce: parseTime,
}, },
'recursive': { 'recursive': {
type: 'boolean', type: 'boolean',
@ -150,6 +153,7 @@ export async function parseArgs(argv: Array<string>): Promise<ParsedArgs> {
}, },
'until': { 'until': {
type: 'string', type: 'string',
conflicts: ['duration'],
}, },
'ci-project': { 'ci-project': {
type: 'string', type: 'string',

View File

@ -1,33 +1,18 @@
import { doesExist } from '@apextoaster/js-utils'; import { doesExist } from '@apextoaster/js-utils';
import { ParsedArgs } from '../args.js'; import { ParsedArgs } from '../args.js';
import { LockData } from '../lock.js';
import { printLock, walkPath } from '../utils.js'; import { printLock, walkPath } from '../utils.js';
import { CommandContext } from './index.js'; import { CommandContext } from './index.js';
export async function checkCommand(context: CommandContext) { export async function checkCommand(context: CommandContext) {
const { args, logger, storage } = context; const { args, logger } = context;
const { now } = args; const { now } = args;
logger.info({ now }, 'running check command'); logger.info({ now }, 'running check command');
const paths = getCheckPaths(args); const [allowed] = await checkArgsPath(context);
for (const path of paths) { return allowed;
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;
} }
export function getCheckPaths(args: ParsedArgs): Array<string> { export function getCheckPaths(args: ParsedArgs): Array<string> {
@ -37,3 +22,38 @@ export function getCheckPaths(args: ParsedArgs): Array<string> {
return [args.path]; 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<LockData>]> {
const { args, logger, storage } = context;
const { now, type } = args;
const locks: Array<LockData> = [];
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];
}

View File

@ -1,8 +1,8 @@
import { CommandContext } from './index.js'; import { CommandContext } from './index.js';
export async function pruneCommand(context: CommandContext) { export async function pruneCommand(context: CommandContext) {
const { logger, storage } = context; const { args, logger, storage } = context;
const now = Date.now(); const { now } = args;
logger.info({ now }, 'running prune command'); logger.info({ now }, 'running prune command');

View File

@ -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 express, { Express, Request, Response } from 'express';
import { APP_NAME } from '../args.js'; import { APP_NAME } from '../args.js';
import { checkArgsPath } from '../command/check.js';
import { LockData, LockType } from '../lock.js'; import { LockData, LockType } from '../lock.js';
import { AdmissionRequest, buildAdmissionResponse, getAdmissionPath } from './admission.js'; import { AdmissionRequest, buildAdmissionResponse, getAdmissionPath } from './admission.js';
import { ServerContext } from './index.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 const path = getAdmissionPath('kube', admissionRequest); // TODO: take admission base from context args
context.logger.info({ path }, 'express admission request'); context.logger.info({ path }, 'express admission request');
const lock = await context.storage.get(path); const [allowed] = await checkArgsPath({
const available = doesExist(lock) === false; ...context,
const admission = buildAdmissionResponse(available, admissionRequest.request.uid); args: {
context.logger.debug({ available, admission }, 'responding to admission request'); ...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); res.status(STATUS_ALLOWED).json(admission);
} }
export async function expressCheck(context: ServerContext, req: Request, res: Response): Promise<void> { export async function expressCheck(context: ServerContext, req: Request, res: Response): Promise<void> {
const path = req.params[0]; const path = req.params[0];
const type = mustDefault(req.query.type as LockType, 'deploy');
context.logger.info({ path }, 'express check request'); context.logger.info({ path }, 'express check request');
const lock = await context.storage.get(path); const [allowed, locks] = await checkArgsPath({
if (doesExist(lock)) { ...context,
if (doesExist(req.query.type)) { args: {
const allowed = lock.allow.includes(req.query.type as LockType); ...context.args,
sendLocks(res, [ lock ], allowed); // TODO: is this a hack?
} else { path,
sendLocks(res, [ lock ], false); type,
} }
} else { });
sendLocks(res, [], true); sendLocks(res, locks, allowed);
}
} }
export async function expressList(context: ServerContext, req: Request, res: Response): Promise<void> { export async function expressList(context: ServerContext, req: Request, res: Response): Promise<void> {

View File

@ -111,11 +111,19 @@ export const TIME_MULTIPLIERS: Array<[RegExp, number]> = [
[/^(\d+)d$/, 60 * 60 * 24], // human days [/^(\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. * Convert a string of the form `12345` or `15m` into seconds.
*/ */
export function parseTime(time: string): number { export function parseTime(time: number | string): number {
// TODO: handling for ISO timestamps 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) { for (const [regex, mult] of TIME_MULTIPLIERS) {
const match = time.match(regex); const match = time.match(regex);