Skip to content

Commit

Permalink
feat(enterprise): register workflow runs
Browse files Browse the repository at this point in the history
When the user is logged in to Garden Enterprise, the `run workflow`
command will now register a workflow run with the backend, and include
the workflow run's UID in streamed events and log entries.

This enables `run workflow` command invocations from e.g. a local
machine or an external CI system to be represented in Garden Enterprise.
  • Loading branch information
thsig committed Jul 23, 2020
1 parent 2c885f8 commit de072ac
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 54 deletions.
9 changes: 2 additions & 7 deletions garden-service/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,21 +341,16 @@ export class GardenCli {
garden = await Garden.factory(root, contextOpts)
}

if (garden.clientAuthToken && garden.enterpriseDomain && garden.projectId) {
if (garden.enterpriseContext) {
log.silly(`Connecting Garden instance to BufferedEventStream`)
bufferedEventStream.connect({
eventBus: garden.events,
clientAuthToken: garden.clientAuthToken,
enterpriseDomain: garden.enterpriseDomain,
projectId: garden.projectId,
enterpriseContext: garden.enterpriseContext,
environmentName: garden.environmentName,
namespace: garden.namespace,
})
} else {
log.silly(`Skip connecting Garden instance to BufferedEventStream`)
log.silly(`clientAuthToken present: ${!!garden.clientAuthToken}`)
log.silly(`enterpriseDomain: ${garden.enterpriseDomain}`)
log.silly(`projectId: ${garden.projectId}`)
}

// Register log file writers. We need to do this after the Garden class is initialised because
Expand Down
5 changes: 4 additions & 1 deletion garden-service/src/commands/get/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ export class GetConfigCommand extends Command<{}, Opts> {
.description("All workflow configs in the project."),
projectName: joi.string().description("The name of the project."),
projectRoot: joi.string().description("The local path to the project root."),
projectId: joi.string().description("The project ID (Garden Enterprise only)."),
projectId: joi
.string()
.optional()
.description("The project ID (Garden Enterprise only)."),
})

options = getConfigOptions
Expand Down
5 changes: 3 additions & 2 deletions garden-service/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ export class LoginCommand extends Command {

async action({ garden, log, headerLog }: CommandParams): Promise<CommandResult> {
printHeader(headerLog, "Login", "cloud")
if (!garden.enterpriseDomain) {
const enterpriseDomain = garden.enterpriseContext?.enterpriseDomain
if (!enterpriseDomain) {
throw new ConfigurationError(`Error: Your project configuration does not specify a domain.`, {})
}
await login(garden.enterpriseDomain, log)
await login(enterpriseDomain, log)
return {}
}
}
18 changes: 18 additions & 0 deletions garden-service/src/commands/run/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { getDurationMsec } from "../../util/util"
import { runScript } from "../../util/util"
import { ExecaError } from "execa"
import { LogLevel } from "../../logger/log-node"
import { gardenEnv } from "../../constants"
import { registerWorkflowRun } from "../../enterprise/workflow-lifecycle"

const runWorkflowArgs = {
workflow: new StringParameter({
Expand Down Expand Up @@ -63,6 +65,8 @@ export class RunWorkflowCommand extends Command<Args, {}> {
const outerLog = log.placeholder()
// Prepare any configured files before continuing
const workflow = garden.getWorkflowConfig(args.workflow)
await registerAndSetUid(garden, log, workflow)
garden.events.emit("workflowRunning", {})
const templateContext = new WorkflowConfigContext(garden)
const files = resolveTemplateStrings(workflow.files || [], templateContext)

Expand Down Expand Up @@ -175,6 +179,7 @@ export class RunWorkflowCommand extends Command<Args, {}> {
}

printResult({ startedAt, log: outerLog, workflow, success: true })
garden.events.emit("workflowComplete", {})

return { result }
}
Expand Down Expand Up @@ -318,6 +323,19 @@ export function logErrors(
}
}

async function registerAndSetUid(garden: Garden, log: LogEntry, config: WorkflowConfig) {
if (garden.enterpriseContext && !gardenEnv.GARDEN_PLATFORM_SCHEDULED) {
const workflowRunUid = await registerWorkflowRun({
workflowConfig: config,
enterpriseContext: garden.enterpriseContext,
environment: garden.environmentName,
namespace: garden.namespace,
log,
})
garden.events.emit("_workflowRunRegistered", { workflowRunUid })
}
}

async function writeWorkflowFile(garden: Garden, file: WorkflowFileSpec) {
let data: string

Expand Down
4 changes: 4 additions & 0 deletions garden-service/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export const gardenEnv = {
.get("GARDEN_LOGGER_TYPE")
.required(false)
.asString(),
GARDEN_PLATFORM_SCHEDULED: env
.get("GARDEN_PLATFORM_SCHEDULED")
.required(false)
.asBool(),
GARDEN_SERVER_PORT: env
.get("GARDEN_SERVER_PORT")
.required(false)
Expand Down
34 changes: 26 additions & 8 deletions garden-service/src/enterprise/buffered-event-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { got } from "../util/http"
import { makeAuthHeader } from "./auth"
import { LogLevel } from "../logger/log-node"
import { gardenEnv } from "../constants"
import { GardenEnterpriseContext } from "../garden"

export type StreamEvent = {
name: EventName
Expand Down Expand Up @@ -48,13 +49,13 @@ export const MAX_BATCH_SIZE = 100

export interface ConnectBufferedEventStreamParams {
eventBus: EventBus
clientAuthToken: string
enterpriseDomain: string
projectId: string
enterpriseContext: GardenEnterpriseContext
environmentName: string
namespace: string
}

export const controlEventNames: Set<EventName> = new Set(["_workflowRunRegistered"])

/**
* Buffers events and log entries and periodically POSTs them to Garden Enterprise if the user is logged in.
*
Expand All @@ -73,6 +74,7 @@ export class BufferedEventStream {
private projectId: string
private environmentName: string
private namespace: string
private workflowRunUid: string | undefined

/**
* We maintain this map to facilitate unsubscribing from a previously connected event bus
Expand All @@ -96,9 +98,10 @@ export class BufferedEventStream {

connect(params: ConnectBufferedEventStreamParams) {
this.log.silly("BufferedEventStream: Connected")
this.clientAuthToken = params.clientAuthToken
this.enterpriseDomain = params.enterpriseDomain
this.projectId = params.projectId
const ctx = params.enterpriseContext
this.clientAuthToken = ctx.clientAuthToken
this.enterpriseDomain = ctx.enterpriseDomain
this.projectId = ctx.projectId
this.environmentName = params.environmentName
this.namespace = params.namespace

Expand Down Expand Up @@ -155,6 +158,11 @@ export class BufferedEventStream {
}

streamEvent<T extends EventName>(name: T, payload: Events[T]) {
if (controlEventNames.has(name)) {
this.handleControlEvent(name, payload)
return
}

this.bufferedEvents.push({
name,
payload,
Expand All @@ -173,7 +181,7 @@ export class BufferedEventStream {
}
const data = {
events,
workflowRunUid: gardenEnv.GARDEN_WORKFLOW_RUN_UID,
workflowRunUid: this.getWorkflowRunUid(),
sessionId: this.sessionId,
projectUid: this.projectId,
environment: this.environmentName,
Expand All @@ -196,7 +204,7 @@ export class BufferedEventStream {
}
const data = {
logEntries,
workflowRunUid: gardenEnv.GARDEN_WORKFLOW_RUN_UID,
workflowRunUid: this.getWorkflowRunUid(),
sessionId: this.sessionId,
projectUid: this.projectId,
}
Expand All @@ -220,4 +228,14 @@ export class BufferedEventStream {

return Bluebird.all([this.flushEvents(eventsToFlush), this.flushLogEntries(logEntriesToFlush)])
}

getWorkflowRunUid(): string | undefined {
return gardenEnv.GARDEN_WORKFLOW_RUN_UID || this.workflowRunUid
}

handleControlEvent<T extends EventName>(name: T, payload: Events[T]) {
if (name === "_workflowRunRegistered") {
this.workflowRunUid = payload.workflowRunUid
}
}
}
59 changes: 59 additions & 0 deletions garden-service/src/enterprise/workflow-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2018-2020 Garden Technologies, Inc. <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { got, GotResponse } from "../util/http"
import { makeAuthHeader } from "./auth"
import { WorkflowConfig } from "../config/workflow"
import { LogEntry } from "../logger/log-entry"
import { PlatformError } from "../exceptions"
import { GardenEnterpriseContext } from "../garden"

export interface RegisterWorkflowRunParams {
workflowConfig: WorkflowConfig
enterpriseContext: GardenEnterpriseContext
environment: string
namespace: string
log: LogEntry
}

/**
* Registers the workflow run with the platform, and returns the UID generated for the run.
*/
export async function registerWorkflowRun({
workflowConfig,
enterpriseContext,
environment,
namespace,
log,
}: RegisterWorkflowRunParams): Promise<string> {
const { clientAuthToken, projectId, enterpriseDomain } = enterpriseContext
log.debug(`Registering workflow run for ${workflowConfig.name}...`)
const headers = makeAuthHeader(clientAuthToken)
const requestData = {
projectUid: projectId,
environment,
namespace,
workflowName: workflowConfig.name,
}
let res
try {
res = await got.post(`${enterpriseDomain}/workflow-runs`, { json: requestData, headers }).json<GotResponse<any>>()
} catch (err) {
log.error(`An error occurred while registering workflow run: ${err.message}`)
throw err
}

if (res && res["workflowRunUid"] && res["status"] === "success") {
return res["workflowRunUid"]
} else {
throw new PlatformError(`Error while registering workflow run: Request failed with status ${res["status"]}`, {
status: res["status"],
workflowRunUid: res["workflowRunUid"],
})
}
}
8 changes: 8 additions & 0 deletions garden-service/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export interface Events extends LoggerEvents {
_exit: {}
_restart: {}
_test: any
_workflowRunRegistered: {
workflowRunUid: string
}

// Watcher events
configAdded: {
Expand Down Expand Up @@ -137,6 +140,8 @@ export interface Events extends LoggerEvents {
}

// Workflow events
workflowRunning: {}
workflowComplete: {}
workflowStepProcessing: {
index: number
}
Expand All @@ -157,6 +162,7 @@ export const eventNames: EventName[] = [
"_exit",
"_restart",
"_test",
"_workflowRunRegistered",
"configAdded",
"configRemoved",
"internalError",
Expand All @@ -175,6 +181,8 @@ export const eventNames: EventName[] = [
"taskStatus",
"testStatus",
"serviceStatus",
"workflowRunning",
"workflowComplete",
"workflowStepProcessing",
"workflowStepError",
"workflowStepComplete",
Expand Down
4 changes: 4 additions & 0 deletions garden-service/src/exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@ export class TimeoutError extends GardenBaseError {
export class NotFoundError extends GardenBaseError {
type = "not-found"
}

export class PlatformError extends GardenBaseError {
type = "platform"
}
Loading

0 comments on commit de072ac

Please sign in to comment.