Skip to content

Commit

Permalink
feat(internal): support persistent commands in WS API
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald authored and thsig committed Sep 8, 2021
1 parent bfbfb2f commit 3976e9d
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 63 deletions.
36 changes: 36 additions & 0 deletions core/src/commands/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ interface PrepareOutput {
persistent: boolean
}

type DataCallback = (data: any) => void

export abstract class Command<T extends Parameters = {}, U extends Parameters = {}> {
abstract name: string
abstract help: string
Expand All @@ -87,7 +89,13 @@ export abstract class Command<T extends Parameters = {}, U extends Parameters =
streamLogEntries: boolean = false // Set to true to stream log entries for the command
server: GardenServer | undefined = undefined

subscribers: DataCallback[]
terminated: boolean

constructor(private parent?: CommandGroup) {
this.subscribers = []
this.terminated = false

// Make sure arguments and options don't have overlapping key names.
if (this.arguments && this.options) {
for (const key of Object.keys(this.options)) {
Expand Down Expand Up @@ -168,6 +176,34 @@ export abstract class Command<T extends Parameters = {}, U extends Parameters =
return { persistent: false }
}

/**
* Called by e.g. the WebSocket server to terminate persistent commands.
*/
terminate() {
this.terminated = true
}

/**
* Subscribe to any data emitted by commands via the .emit() method
*/
subscribe(cb: (data: string) => void) {
this.subscribers.push(cb)
}

/**
* Emit data to all subscribers
*/
emit(log: LogEntry, data: string) {
for (const subscriber of this.subscribers) {
// Ignore any errors here
try {
subscriber(data)
} catch (err) {
log.debug(`Error when calling subscriber on ${this.getFullName()} command: ${err.message}`)
}
}
}

abstract printHeader(params: PrintHeaderParams<T, U>): void

// Note: Due to a current TS limitation (apparently covered by https://github.com/Microsoft/TypeScript/issues/7011),
Expand Down
13 changes: 10 additions & 3 deletions core/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { startServer } from "../server/server"
import { flatten } from "lodash"
import { BuildTask } from "../tasks/build"
import { StringsParameter, BooleanParameter } from "../cli/params"
import { Garden } from "../garden"

const buildArgs = {
modules: new StringsParameter({
Expand Down Expand Up @@ -65,12 +66,12 @@ export class BuildCommand extends Command<Args, Opts> {
arguments = buildArgs
options = buildOpts

outputsSchema = () => processCommandResultSchema()
private garden?: Garden

private isPersistent = (opts) => !!opts.watch
outputsSchema = () => processCommandResultSchema()

async prepare({ footerLog, opts }: PrepareParams<Args, Opts>) {
const persistent = this.isPersistent(opts)
const persistent = !!opts.watch

if (persistent) {
this.server = await startServer({ log: footerLog })
Expand All @@ -79,6 +80,10 @@ export class BuildCommand extends Command<Args, Opts> {
return { persistent }
}

terminate() {
this.garden?.events.emit("_exit", {})
}

printHeader({ headerLog }) {
printHeader(headerLog, "Build", "hammer")
}
Expand All @@ -90,6 +95,8 @@ export class BuildCommand extends Command<Args, Opts> {
args,
opts,
}: CommandParams<Args, Opts>): Promise<CommandResult<ProcessCommandResult>> {
this.garden = garden

if (this.server) {
this.server.setGarden(garden)
}
Expand Down
6 changes: 4 additions & 2 deletions core/src/commands/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ export class DashboardCommand extends Command<Args, Opts> {
this.server!.setGarden(garden)

// The server doesn't block, so we need to loop indefinitely here.
while (true) {
await sleep(10000)
while (!this.terminated) {
await sleep(1000)
}

return {}
}
}
9 changes: 9 additions & 0 deletions core/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { DeployTask } from "../tasks/deploy"
import { naturalList } from "../util/string"
import chalk = require("chalk")
import { StringsParameter, BooleanParameter, ParameterValues } from "../cli/params"
import { Garden } from "../garden"

export const deployArgs = {
services: new StringsParameter({
Expand Down Expand Up @@ -100,6 +101,8 @@ export class DeployCommand extends Command<Args, Opts> {
arguments = deployArgs
options = deployOpts

private garden?: Garden

outputsSchema = () => processCommandResultSchema()

private isPersistent = (opts: ParameterValues<Opts>) => !!opts.watch || !!opts["hot-reload"] || !!opts["dev-mode"]
Expand All @@ -118,13 +121,19 @@ export class DeployCommand extends Command<Args, Opts> {
return { persistent }
}

terminate() {
this.garden?.events.emit("_exit", {})
}

async action({
garden,
log,
footerLog,
args,
opts,
}: CommandParams<Args, Opts>): Promise<CommandResult<ProcessCommandResult>> {
this.garden = garden

if (this.server) {
this.server.setGarden(garden)
}
Expand Down
7 changes: 7 additions & 0 deletions core/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export class DevCommand extends Command<DevCommandArgs, DevCommandOpts> {
arguments = devArgs
options = devOpts

private garden?: Garden

printHeader({ headerLog }) {
printHeader(headerLog, "Dev", "keyboard")
}
Expand All @@ -111,13 +113,18 @@ export class DevCommand extends Command<DevCommandArgs, DevCommandOpts> {
return { persistent: true }
}

terminate() {
this.garden?.events.emit("_exit", {})
}

async action({
garden,
log,
footerLog,
args,
opts,
}: CommandParams<DevCommandArgs, DevCommandOpts>): Promise<CommandResult> {
this.garden = garden
this.server?.setGarden(garden)

const graph = await garden.getConfigGraph(log)
Expand Down
20 changes: 16 additions & 4 deletions core/src/commands/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { Command, CommandResult, CommandParams } from "./base"
import { Command, CommandResult, CommandParams, PrepareParams } from "./base"
import chalk from "chalk"
import { sortBy } from "lodash"
import { ServiceLogEntry } from "../types/plugin/service/getServiceLogs"
Expand All @@ -20,6 +20,7 @@ import stripAnsi = require("strip-ansi")
import hasAnsi = require("has-ansi")
import { dedent } from "../util/string"
import { padSection } from "../logger/renderers"
import { PluginEventBroker } from "../plugin-context"

const logsArgs = {
services: new StringsParameter({
Expand All @@ -33,7 +34,6 @@ const logsOpts = {
"follow": new BooleanParameter({
help: "Continuously stream new logs from the service(s).",
alias: "f",
cliOnly: true,
}),
"tail": new IntegerParameter({
help: dedent`
Expand Down Expand Up @@ -99,6 +99,8 @@ export class LogsCommand extends Command<Args, Opts> {
arguments = logsArgs
options = logsOpts

private events?: PluginEventBroker

getLoggerType(): LoggerType {
return "basic"
}
Expand All @@ -107,6 +109,14 @@ export class LogsCommand extends Command<Args, Opts> {
printHeader(headerLog, "Logs", "scroll")
}

async prepare({ opts }: PrepareParams<Args, Opts>) {
return { persistent: !!opts.follow }
}

terminate() {
this.events?.emit("abort", {})
}

async action({ garden, log, args, opts }: CommandParams<Args, Opts>): Promise<CommandResult<ServiceLogEntry[]>> {
const { follow, timestamps } = opts
let tail = opts.tail as number | undefined
Expand Down Expand Up @@ -206,24 +216,26 @@ export class LogsCommand extends Command<Args, Opts> {
}

void stream.forEach((entry) => {
// Skip emtpy entries
// Skip empty entries
if (skipEntry(entry)) {
return
}

if (follow) {
const levelStr = logLevelMap[entry.level || LogLevel.info] || "info"
const msg = formatEntry(entry)
this.emit(log, JSON.stringify({ msg, timestamp: entry.timestamp?.getTime(), level: levelStr }))
log[levelStr]({ msg })
} else {
result.push(entry)
}
})

const actions = await garden.getActionRouter()
this.events = new PluginEventBroker()

await Bluebird.map(services, async (service: GardenService<any>) => {
await actions.getServiceLogs({ log, graph, service, stream, follow, tail, since })
await actions.getServiceLogs({ log, graph, service, stream, follow, tail, since, events: this.events })
})

const sorted = sortBy(result, "timestamp")
Expand Down
11 changes: 9 additions & 2 deletions core/src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { printHeader } from "../logger/util"
import { startServer } from "../server/server"
import { StringsParameter, BooleanParameter } from "../cli/params"
import { deline } from "../util/string"
import { Garden } from "../garden"

export const testArgs = {
modules: new StringsParameter({
Expand Down Expand Up @@ -96,14 +97,14 @@ export class TestCommand extends Command<Args, Opts> {

outputsSchema = () => processCommandResultSchema()

private isPersistent = (opts) => !!opts.watch
private garden?: Garden

printHeader({ headerLog }) {
printHeader(headerLog, `Running tests`, "thermometer")
}

async prepare({ footerLog, opts }: PrepareParams<Args, Opts>) {
const persistent = this.isPersistent(opts)
const persistent = !!opts.watch

if (persistent) {
this.server = await startServer({ log: footerLog })
Expand All @@ -112,13 +113,19 @@ export class TestCommand extends Command<Args, Opts> {
return { persistent }
}

terminate() {
this.garden?.events.emit("_exit", {})
}

async action({
garden,
log,
footerLog,
args,
opts,
}: CommandParams<Args, Opts>): Promise<CommandResult<ProcessCommandResult>> {
this.garden = garden

if (this.server) {
this.server.setGarden(garden)
}
Expand Down
1 change: 1 addition & 0 deletions core/src/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const pluginContextSchema = () =>
})

interface PluginEvents {
abort: { reason?: string }
log: { data: Buffer }
}

Expand Down
5 changes: 5 additions & 0 deletions core/src/plugins/kubernetes/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export async function streamK8sLogs(params: GetAllLogsParams) {

if (params.follow) {
const logsFollower = new K8sLogFollower({ ...params, k8sApi: api })

params.ctx.events.on("abort", () => {
logsFollower.close()
})

await logsFollower.followLogs({ tail: params.tail, since: params.since })
} else {
const pods = await getAllPods(api, params.defaultNamespace, params.resources)
Expand Down
26 changes: 8 additions & 18 deletions core/src/server/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { Command, CommandGroup } from "../commands/base"
import { joi } from "../config/common"
import { validateSchema } from "../config/validation"
import { extend, mapValues, omitBy } from "lodash"
import { Garden } from "../garden"
import { LogLevel } from "../logger/logger"
import { LogEntry } from "../logger/log-entry"
import { Parameters, ParameterValues, globalOptions } from "../cli/params"
Expand All @@ -37,20 +36,14 @@ const baseRequestSchema = () =>
})

/**
* Validate and map a request body to a Command, execute its action, and return its result.
* Validate and map a request body to a Command
*/
export async function resolveRequest(
ctx: Koa.ParameterizedContext,
garden: Garden,
log: LogEntry,
commands: CommandMap,
request: any
) {
export function parseRequest(ctx: Koa.ParameterizedContext, log: LogEntry, commands: CommandMap, request: any) {
// Perform basic validation and find command.
try {
request = validateSchema(request, baseRequestSchema(), { context: "API request" })
} catch {
ctx.throw(400, "Invalid request format")
} catch (err) {
ctx.throw(400, "Invalid request format: " + err.message)
}

const commandSpec = commands[request.command]
Expand All @@ -76,18 +69,15 @@ export async function resolveRequest(
const optParams = extend({ ...globalOptions, ...command.options })
const cmdOpts = mapParams(ctx, request.parameters, optParams)

// TODO: validate result schema
return command.action({
garden,
return {
command,
log: cmdLog,
headerLog: cmdLog,
footerLog: cmdLog,
args: cmdArgs,
opts: cmdOpts,
})
}
}

export async function prepareCommands(): Promise<CommandMap> {
export function prepareCommands(): CommandMap {
const commands: CommandMap = {}

function addCommand(command: Command) {
Expand Down
Loading

0 comments on commit 3976e9d

Please sign in to comment.