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 { 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',
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
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
|
[/^(\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);
|
||||||
|
|
Loading…
Reference in New Issue