Skip to content

Commit

Permalink
feat(core): emit runtime status events
Browse files Browse the repository at this point in the history
We now emit `serviceStatus`, `testStatus` and `taskStatus` events in the
appropriate `ActionRouter` methods.
  • Loading branch information
thsig authored and edvald committed Jun 24, 2020
1 parent 7eefa2e commit 841285e
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 32 deletions.
2 changes: 1 addition & 1 deletion dashboard/src/containers/overview.tsx
Expand Up @@ -9,8 +9,8 @@
import React from "react"
import styled from "@emotion/styled"

import { RunState } from "garden-service/build/src/commands/get/get-status"
import { ServiceState } from "garden-service/build/src/types/service"
import { RunState } from "garden-service/build/src/types/plugin/base"

import PageError from "../components/page-error"
import { ModuleCard, Props as ModuleProps } from "../components/entity-cards/module"
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/contexts/api.tsx
Expand Up @@ -17,7 +17,7 @@ import { ServiceStatus } from "garden-service/build/src/types/service"
import { ModuleConfig } from "garden-service/build/src/config/module"
import { PickFromUnion } from "garden-service/build/src/util/util"
import { ServiceConfig } from "garden-service/build/src/config/service"
import { RunStatus } from "garden-service/build/src/commands/get/get-status"
import { RunStatus } from "garden-service/build/src/types/plugin/base"
import { TaskConfig } from "garden-service/build/src/config/task"
import { GetTaskResultCommandResult } from "garden-service/build/src/commands/get/get-task-result"
import { GetTestResultCommandResult } from "garden-service/build/src/commands/get/get-test-result"
Expand Down
30 changes: 29 additions & 1 deletion garden-service/src/actions.ts
Expand Up @@ -29,6 +29,7 @@ import {
PluginServiceActionParamsBase,
PluginTaskActionParamsBase,
RunResult,
runStatus,
} from "./types/plugin/base"
import { BuildModuleParams, BuildResult } from "./types/plugin/module/build"
import { BuildStatus, GetBuildStatusParams } from "./types/plugin/module/getBuildStatus"
Expand Down Expand Up @@ -341,6 +342,11 @@ export class ActionRouter implements TypeGuard {

try {
const result = await this.callModuleHandler({ params: { ...params, artifactsPath }, actionType: "testModule" })
this.garden.events.emit("testStatus", {
testName: params.testConfig.name,
moduleName: params.module.name,
status: runStatus(result),
})
return result
} finally {
// Copy everything from the temp directory, and then clean it up
Expand All @@ -359,11 +365,17 @@ export class ActionRouter implements TypeGuard {
async getTestResult<T extends Module>(
params: ModuleActionRouterParams<GetTestResultParams<T>>
): Promise<TestResult | null> {
return this.callModuleHandler({
const result = await this.callModuleHandler({
params,
actionType: "getTestResult",
defaultHandler: async () => null,
})
this.garden.events.emit("testStatus", {
testName: params.testName,
moduleName: params.module.name,
status: runStatus(result),
})
return result
}

//endregion
Expand All @@ -374,12 +386,20 @@ export class ActionRouter implements TypeGuard {

async getServiceStatus(params: ServiceActionRouterParams<GetServiceStatusParams>): Promise<ServiceStatus> {
const { result } = await this.callServiceHandler({ params, actionType: "getServiceStatus" })
this.garden.events.emit("serviceStatus", {
serviceName: params.service.name,
status: result,
})
this.validateServiceOutputs(params.service, result)
return result
}

async deployService(params: ServiceActionRouterParams<DeployServiceParams>): Promise<ServiceStatus> {
const { result } = await this.callServiceHandler({ params, actionType: "deployService" })
this.garden.events.emit("serviceStatus", {
serviceName: params.service.name,
status: result,
})
this.validateServiceOutputs(params.service, result)
return result
}
Expand Down Expand Up @@ -482,6 +502,10 @@ export class ActionRouter implements TypeGuard {

try {
const { result } = await this.callTaskHandler({ params: { ...params, artifactsPath }, actionType: "runTask" })
this.garden.events.emit("taskStatus", {
taskName: params.task.name,
status: runStatus(result),
})
result && this.validateTaskOutputs(params.task, result)
return result
} finally {
Expand All @@ -504,6 +528,10 @@ export class ActionRouter implements TypeGuard {
actionType: "getTaskResult",
defaultHandler: async () => undefined,
})
this.garden.events.emit("taskStatus", {
taskName: params.task.name,
status: runStatus(result),
})
result && this.validateTaskOutputs(params.task, result)
return result
}
Expand Down
23 changes: 1 addition & 22 deletions garden-service/src/commands/get/get-status.ts
Expand Up @@ -15,22 +15,14 @@ import { ConfigGraph } from "../../config-graph"
import { getTaskVersion } from "../../tasks/task"
import { LogEntry } from "../../logger/log-entry"
import { getTestVersion } from "../../tasks/test"
import { RunResult } from "../../types/plugin/base"
import { runStatus, RunStatus } from "../../types/plugin/base"
import chalk from "chalk"
import { deline } from "../../util/string"
import { EnvironmentStatusMap } from "../../types/plugin/provider/getEnvironmentStatus"
import { ServiceStatus, serviceStatusSchema } from "../../types/service"
import { joi, joiIdentifierMap, joiStringMap } from "../../config/common"
import { environmentStatusSchema } from "../../config/status"

export type RunState = "outdated" | "succeeded" | "failed" | "not-implemented"

export interface RunStatus {
state: RunState
startedAt?: Date
completedAt?: Date
}

export interface TestStatuses {
[testKey: string]: RunStatus
}
Expand Down Expand Up @@ -154,16 +146,3 @@ async function getTaskStatuses(garden: Garden, configGraph: ConfigGraph, log: Lo
})
)
}

function runStatus<R extends RunResult>(result: R | null | undefined): RunStatus {
if (result) {
const { startedAt, completedAt } = result
return {
startedAt,
completedAt,
state: result.success ? "succeeded" : "failed",
}
} else {
return { state: result === null ? "outdated" : "not-implemented" }
}
}
20 changes: 20 additions & 0 deletions garden-service/src/events.ts
Expand Up @@ -10,6 +10,8 @@ import { EventEmitter2 } from "eventemitter2"
import { ModuleVersion } from "./vcs/vcs"
import { GraphResult } from "./task-graph"
import { LogEntryEvent } from "./enterprise/buffered-event-stream"
import { ServiceStatus } from "./types/service"
import { RunStatus } from "./types/plugin/base"

/**
* This simple class serves as the central event bus for a Garden instance. Its function
Expand Down Expand Up @@ -119,6 +121,21 @@ export interface Events extends LoggerEvents {
}
watchingForChanges: {}

// Runtime status events
taskStatus: {
taskName: string
status: RunStatus
}
testStatus: {
testName: string
moduleName: string
status: RunStatus
}
serviceStatus: {
serviceName: string
status: ServiceStatus
}

// Workflow events
workflowStepProcessing: {
index: number
Expand Down Expand Up @@ -153,6 +170,9 @@ export const eventNames: EventName[] = [
"taskGraphProcessing",
"taskGraphComplete",
"watchingForChanges",
"taskStatus",
"testStatus",
"serviceStatus",
"workflowStepProcessing",
"workflowStepError",
"workflowStepComplete",
Expand Down
21 changes: 21 additions & 0 deletions garden-service/src/types/plugin/base.ts
Expand Up @@ -124,3 +124,24 @@ export const artifactsPathSchema = () =>
.string()
.required()
.description("A directory path where the handler should write any exported artifacts to.")

export type RunState = "outdated" | "succeeded" | "failed" | "not-implemented"

export interface RunStatus {
state: RunState
startedAt?: Date
completedAt?: Date
}

export function runStatus<R extends RunResult>(result: R | null | undefined): RunStatus {
if (result) {
const { startedAt, completedAt } = result
return {
startedAt,
completedAt,
state: result.success ? "succeeded" : "failed",
}
} else {
return { state: result === null ? "outdated" : "not-implemented" }
}
}
13 changes: 12 additions & 1 deletion garden-service/test/helpers.ts
Expand Up @@ -10,7 +10,7 @@ import td from "testdouble"
import tmp from "tmp-promise"
import Bluebird = require("bluebird")
import { resolve, join } from "path"
import { extend, keyBy, intersection } from "lodash"
import { extend, keyBy, intersection, pick } from "lodash"
import { remove, readdirSync, existsSync, copy, mkdirp, pathExists, truncate, realpath } from "fs-extra"
import execa = require("execa")

Expand Down Expand Up @@ -681,3 +681,14 @@ export async function enableAnalytics(garden: TestGarden) {
}
return resetConfig
}

export function getRuntimeStatusEvents(eventLog: EventLogEntry[]) {
const runtimeEventNames = ["taskStatus", "testStatus", "serviceStatus"]
return eventLog
.filter((e) => runtimeEventNames.includes(e.name))
.map((e) => {
const cloned = { ...e }
cloned.payload.status = pick(cloned.payload.status, ["state"])
return cloned
})
}
99 changes: 98 additions & 1 deletion garden-service/test/unit/src/actions.ts
Expand Up @@ -41,7 +41,7 @@ import { join } from "path"
const now = new Date()

describe("ActionRouter", () => {
let garden: Garden
let garden: TestGarden
let log: LogEntry
let actions: ActionRouter
let module: Module
Expand Down Expand Up @@ -330,6 +330,34 @@ describe("ActionRouter", () => {
})
})

it("should emit a testStatus event", async () => {
garden.events.eventLog = []
await actions.testModule({
log,
module,
interactive: true,
runtimeContext: {
envVars: { FOO: "bar" },
dependencies: [],
},
silent: false,
testConfig: {
name: "test",
dependencies: [],
disabled: false,
timeout: 1234,
spec: {},
},
testVersion: module.version,
})
const event = garden.events.eventLog[0]
expect(event).to.exist
expect(event.name).to.eql("testStatus")
expect(event.payload.testName).to.eql("test")
expect(event.payload.moduleName).to.eql("module-a")
expect(event.payload.status.state).to.eql("succeeded")
})

it("should copy artifacts exported by the handler to the artifacts directory", async () => {
await emptyDir(garden.artifactsPath)

Expand Down Expand Up @@ -408,6 +436,22 @@ describe("ActionRouter", () => {
})
})
})

it("should emit a testStatus event", async () => {
garden.events.eventLog = []
await actions.getTestResult({
log,
module,
testName: "test",
testVersion: module.version,
})
const event = garden.events.eventLog[0]
expect(event).to.exist
expect(event.name).to.eql("testStatus")
expect(event.payload.testName).to.eql("test")
expect(event.payload.moduleName).to.eql("module-a")
expect(event.payload.status.state).to.eql("succeeded")
})
})

describe("service actions", () => {
Expand All @@ -417,6 +461,16 @@ describe("ActionRouter", () => {
expect(result).to.eql({ forwardablePorts: [], state: "ready", detail: {}, outputs: { base: "ok", foo: "ok" } })
})

it("should emit a serviceStatus event", async () => {
garden.events.eventLog = []
await actions.getServiceStatus({ log, service, runtimeContext, hotReload: false })
const event = garden.events.eventLog[0]
expect(event).to.exist
expect(event.name).to.eql("serviceStatus")
expect(event.payload.serviceName).to.eql("service-a")
expect(event.payload.status.state).to.eql("ready")
})

it("should throw if the outputs don't match the service outputs schema of the plugin", async () => {
stubModuleAction(actions, service.module.type, "test-plugin", "getServiceStatus", async () => {
return { state: <ServiceState>"ready", detail: {}, outputs: { base: "ok", foo: 123 } }
Expand Down Expand Up @@ -452,6 +506,16 @@ describe("ActionRouter", () => {
expect(result).to.eql({ forwardablePorts: [], state: "ready", detail: {}, outputs: { base: "ok", foo: "ok" } })
})

it("should emit a serviceStatus event", async () => {
garden.events.eventLog = []
await actions.deployService({ log, service, runtimeContext, force: true, hotReload: false })
const event = garden.events.eventLog[0]
expect(event).to.exist
expect(event.name).to.eql("serviceStatus")
expect(event.payload.serviceName).to.eql("service-a")
expect(event.payload.status.state).to.eql("ready")
})

it("should throw if the outputs don't match the service outputs schema of the plugin", async () => {
stubModuleAction(actions, service.module.type, "test-plugin", "deployService", async () => {
return { state: <ServiceState>"ready", detail: {}, outputs: { base: "ok", foo: 123 } }
Expand Down Expand Up @@ -563,6 +627,20 @@ describe("ActionRouter", () => {
expect(result).to.eql(taskResult)
})

it("should emit a taskStatus event", async () => {
garden.events.eventLog = []
await actions.getTaskResult({
log,
task,
taskVersion: task.module.version,
})
const event = garden.events.eventLog[0]
expect(event).to.exist
expect(event.name).to.eql("taskStatus")
expect(event.payload.taskName).to.eql("task-a")
expect(event.payload.status.state).to.eql("succeeded")
})

it("should throw if the outputs don't match the task outputs schema of the plugin", async () => {
stubModuleAction(actions, service.module.type, "test-plugin", "getTaskResult", async () => {
return { ...taskResult, outputs: { base: "ok", foo: 123 } }
Expand Down Expand Up @@ -607,6 +685,25 @@ describe("ActionRouter", () => {
expect(result).to.eql(taskResult)
})

it("should emit a taskStatus event", async () => {
garden.events.eventLog = []
await actions.runTask({
log,
task,
interactive: true,
runtimeContext: {
envVars: { FOO: "bar" },
dependencies: [],
},
taskVersion: task.module.version,
})
const event = garden.events.eventLog[0]
expect(event).to.exist
expect(event.name).to.eql("taskStatus")
expect(event.payload.taskName).to.eql("task-a")
expect(event.payload.status.state).to.eql("succeeded")
})

it("should throw if the outputs don't match the task outputs schema of the plugin", async () => {
stubModuleAction(actions, task.module.type, "test-plugin", "runTask", async () => {
return { ...taskResult, outputs: { base: "ok", foo: 123 } }
Expand Down

0 comments on commit 841285e

Please sign in to comment.