isolate check logic, use in both command and express, coerce now option
This commit is contained in:
parent
e2c81afcdb
commit
ae84b864be
|
@ -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',
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
12
src/utils.ts
12
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);
|
||||
|
|
Loading…
Reference in New Issue