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 { 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<string>): Promise<ParsedArgs> {
},
'duration': {
type: 'string',
conflicts: ['until'],
},
'endpoint': {
type: 'string',
@ -117,9 +119,10 @@ export async function parseArgs(argv: Array<string>): Promise<ParsedArgs> {
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<string>): Promise<ParsedArgs> {
},
'until': {
type: 'string',
conflicts: ['duration'],
},
'ci-project': {
type: 'string',

View File

@ -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<string> {
@ -37,3 +22,38 @@ export function getCheckPaths(args: ParsedArgs): Array<string> {
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';
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');

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 { 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<void> {
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);
}
} else {
sendLocks(res, [], true);
const [allowed, locks] = await checkArgsPath({
...context,
args: {
...context.args,
// TODO: is this a hack?
path,
type,
}
});
sendLocks(res, locks, allowed);
}
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
];
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);