Skip to content

Commit

Permalink
chore(cloudbuilder): add runtime information to build status events (#…
Browse files Browse the repository at this point in the history
…6153)

* chore(cloudbuilder): add runtime information to build status events

Add runtime information to build status events, so we can display them
in the Cloud UI

* improvement: store fallback reason in task result

* test: fix framework unit tests

* chore: send runtime information in buildStatus events

* fix: make sure that fallback properties can't be forgotten

Hack; Without a property to distinguish between union types, the type
system will treat additional properties as optional which is not what we
want, as that means that defining a fallback without fallbackReason
would not throw a type error.

* refactor: remove unnecessary schema

* refactor: remove unnecessary fallback bool flag

* refactor: better comments
  • Loading branch information
stefreak committed Jun 11, 2024
1 parent 41fbd1d commit 6bb5a7f
Show file tree
Hide file tree
Showing 28 changed files with 360 additions and 62 deletions.
7 changes: 7 additions & 0 deletions core/src/actions/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export async function getDeployStatusPayloads({
force: false,
action,
sessionId,
// TODO: Once needed, send ActionRuntime information to Cloud; See getBuildStatusPayloads
runtime: undefined,
}) as ActionStatusPayload<DeployStatusForEventPayload>

return [action.name, payload]
Expand Down Expand Up @@ -201,6 +203,7 @@ export async function getBuildStatusPayloads({
force: false,
action,
sessionId,
runtime: (result.detail ?? {}).runtime,
}) as ActionStatusPayload<BuildStatusForEventPayload>

return [action.name, payload]
Expand Down Expand Up @@ -235,6 +238,8 @@ export async function getTestStatusPayloads({
force: false,
action,
sessionId,
// TODO: Once needed, send ActionRuntime information to Cloud; See getBuildStatusPayloads
runtime: undefined,
}) as ActionStatusPayload<RunStatusForEventPayload>
return [action.name, payload]
})
Expand Down Expand Up @@ -269,6 +274,8 @@ export async function getRunStatusPayloads({
force: false,
action,
sessionId,
// TODO: Once needed, send ActionRuntime information to Cloud; See getBuildStatusPayloads
runtime: undefined,
}) as ActionStatusPayload<RunStatusForEventPayload>

return [action.name, payload]
Expand Down
8 changes: 7 additions & 1 deletion core/src/events/action-status-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { actionStateTypes } from "../actions/types.js"
import type { BuildState } from "../plugin/handlers/Build/get-status.js"
import type { RunState } from "../plugin/plugin.js"
import type { ActionRuntime, RunState } from "../plugin/plugin.js"
import type { DeployState } from "../types/service.js"
import type { PickFromUnion } from "../util/util.js"

Expand Down Expand Up @@ -116,6 +116,12 @@ interface ActionStatusPayloadBase {
* The session ID for the command run the action belongs to.
*/
sessionId: string
/**
* Runtime information about the action. It can be undefined in some cases, e.g. if the getting-status handler failed.
*
* Currently runtime information is only provided for build actions, but feel free to change that if we need runtime information for other action kinds in Cloud.
*/
runtime: ActionRuntime | undefined
}

type ActionIncompleteState = PickFromUnion<ActionStateForEvent, "getting-status" | "unknown">
Expand Down
34 changes: 30 additions & 4 deletions core/src/events/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { Events, ActionStatusEventName } from "./events.js"
import { pick } from "lodash-es"
import type { BuildState } from "../plugin/handlers/Build/get-status.js"
import type { ActionStatusDetailedState, ActionCompleteState } from "./action-status-events.js"
import type { ActionRuntime } from "../plugin/base.js"

type ActionKind = "build" | "deploy" | "run" | "test"

Expand Down Expand Up @@ -61,12 +62,14 @@ export function makeActionStatusPayloadBase({
operation,
startedAt,
sessionId,
runtime,
}: {
action: Action
force: boolean
operation: "getStatus" | "process"
startedAt: string
sessionId: string
runtime: ActionRuntime | undefined
}) {
return {
actionName: action.name,
Expand All @@ -80,6 +83,7 @@ export function makeActionStatusPayloadBase({
startedAt,
operation,
force,
runtime,
}
}

Expand All @@ -94,13 +98,22 @@ export function makeActionGetStatusPayload({
force,
startedAt,
sessionId,
runtime,
}: {
action: Action
force: boolean
startedAt: string
sessionId: string
runtime: ActionRuntime | undefined
}) {
const payloadAttrs = makeActionStatusPayloadBase({ action, force, startedAt, sessionId, operation: "getStatus" })
const payloadAttrs = makeActionStatusPayloadBase({
action,
force,
startedAt,
sessionId,
runtime,
operation: "getStatus",
})

const payload = {
...payloadAttrs,
Expand All @@ -121,13 +134,22 @@ export function makeActionProcessingPayload({
force,
startedAt,
sessionId,
runtime,
}: {
action: Action
force: boolean
startedAt: string
sessionId: string
runtime: ActionRuntime | undefined
}) {
const payloadAttrs = makeActionStatusPayloadBase({ action, force, startedAt, sessionId, operation: "process" })
const payloadAttrs = makeActionStatusPayloadBase({
action,
force,
startedAt,
sessionId,
runtime,
operation: "process",
})
const actionKind = action.kind.toLowerCase() as Lowercase<Action["kind"]>

const payload = {
Expand Down Expand Up @@ -161,15 +183,17 @@ export function makeActionCompletePayload<
operation,
startedAt,
sessionId,
runtime,
}: {
result: R
action: Action
force: boolean
operation: "getStatus" | "process"
startedAt: string
sessionId: string
runtime: ActionRuntime | undefined
}) {
const payloadAttrs = makeActionStatusPayloadBase({ action, force, operation, startedAt, sessionId })
const payloadAttrs = makeActionStatusPayloadBase({ action, force, operation, startedAt, sessionId, runtime })
const actionKind = action.kind.toLowerCase() as Lowercase<Action["kind"]>

// Map the result state to one of the allowed "complete" states.
Expand Down Expand Up @@ -241,14 +265,16 @@ export function makeActionFailedPayload({
operation,
startedAt,
sessionId,
runtime,
}: {
action: Action
force: boolean
operation: "getStatus" | "process"
startedAt: string
sessionId: string
runtime: ActionRuntime | undefined
}) {
const payloadAttrs = makeActionStatusPayloadBase({ action, force, operation, startedAt, sessionId })
const payloadAttrs = makeActionStatusPayloadBase({ action, force, operation, startedAt, sessionId, runtime })

const payload = {
...payloadAttrs,
Expand Down
2 changes: 1 addition & 1 deletion core/src/graph/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export class ProcessTaskNode<T extends Task = Task> extends TaskNode<T> {
})
}

const status = statusResult?.result
const status = statusResult.result

if (!this.task.force && status?.state === "ready") {
return status
Expand Down
37 changes: 37 additions & 0 deletions core/src/plugin/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,43 @@ export const runBaseParams = () => ({
artifactsPath: artifactsPathSchema(),
})

// Action runtime type and schema. Used for the Cloud Builder UI, and maybe in the future Cloud Runner UI, etc.
export type ActionRuntime =
| {
actual: ActionRuntimeKind
// These are needed to make sure the type system understands that preferred and fallbackReason are required together.
preferred?: undefined
fallbackReason?: undefined
}
| {
actual: ActionRuntimeKind
preferred: ActionRuntimeKind
fallbackReason: string
}

export type ActionRuntimeKind = ActionRuntimeLocal | ActionRuntimeRemote

export type ActionRuntimeLocal = {
kind: "local"
}
// constant for convenience
export const ACTION_RUNTIME_LOCAL = {
actual: {
kind: "local",
},
} as const

export type ActionRuntimeRemote = ActionRuntimeRemoteGardenCloud | ActionRuntimeRemotePlugin
export type ActionRuntimeRemoteGardenCloud = {
kind: "remote"
type: "garden-cloud"
}
export type ActionRuntimeRemotePlugin = {
kind: "remote"
type: "plugin"
pluginName: string
}

// TODO-0.13.0: update this schema in 0.13.0
export interface RunResult {
success: boolean
Expand Down
21 changes: 7 additions & 14 deletions core/src/plugin/handlers/Build/get-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
*/

import { dedent } from "../../../util/string.js"
import type { PluginBuildActionParamsBase } from "../../base.js"
import type { ActionRuntime, PluginBuildActionParamsBase } from "../../base.js"
import { actionParamsSchema } from "../../base.js"
import type { BuildAction } from "../../../actions/build.js"
import { ActionTypeHandlerSpec } from "../base/base.js"
import { actionStatusSchema } from "../../../actions/base.js"
import type { ActionStatus, ActionStatusMap, Resolved } from "../../../actions/types.js"
import { createSchema, joi } from "../../../config/common.js"

/**
* - `fetched`: The build was fetched from a repository instead of building.
Expand All @@ -31,24 +30,18 @@ export interface BuildStatusForEventPayload {
type GetBuildStatusParams<T extends BuildAction = BuildAction> = PluginBuildActionParamsBase<T>

export interface BuildResult {
// Information about whether the action ran locally, or in a remote runner, and if the plugin decided to fall back to another mode of execution for some reason.
runtime: ActionRuntime
// The full log from the build.
buildLog?: string
// Set to true if the build was fetched from a remote registry.
fetched?: boolean
// Set to true if the build was performed, false if it was already built, or fetched from a registry
fresh?: boolean
// Additional information, specific to the provider.
details?: any
}

export const buildResultSchema = createSchema({
name: "build-result",
keys: () => ({
buildLog: joi.string().allow("").description("The full log from the build."),
fetched: joi.boolean().description("Set to true if the build was fetched from a remote registry."),
fresh: joi
.boolean()
.description("Set to true if the build was performed, false if it was already built, or fetched from a registry"),
details: joi.object().description("Additional information, specific to the provider."),
}),
})

export type BuildStatus<T extends BuildAction = BuildAction, D extends {} = BuildResult> = ActionStatus<T, D>

export interface BuildStatusMap extends ActionStatusMap<BuildAction> {
Expand Down
18 changes: 16 additions & 2 deletions core/src/plugins/container/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ export const getContainerBuildStatus: BuildActionHandler<"getStatus", ContainerB

const state = !!identifier ? "ready" : "not-ready"

return { state, detail: {}, outputs }
return {
state,
detail: {
runtime: await cloudBuilder.getActionRuntime(ctx, action),
},
outputs,
}
}

export const buildContainer: BuildActionHandler<"build", ContainerBuildAction> = async ({ ctx, action, log }) => {
Expand Down Expand Up @@ -106,7 +112,15 @@ export const buildContainer: BuildActionHandler<"build", ContainerBuildAction> =
return {
state: "ready",
outputs,
detail: { fresh: true, buildLog: res.all || "", outputs, details: { identifier } },
detail: {
fresh: true,
buildLog: res.all || "",
outputs,
runtime: await cloudBuilder.getActionRuntime(ctx, action),
details: {
identifier,
},
},
}
}

Expand Down
45 changes: 45 additions & 0 deletions core/src/plugins/container/cloudbuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { emitNonRepeatableWarning } from "../../warnings.js"
import { LRUCache } from "lru-cache"
import { getPlatform } from "../../util/arch-platform.js"
import { gardenEnv } from "../../constants.js"
import type { ActionRuntime, ActionRuntimeKind } from "../../plugin/base.js"

type CloudBuilderConfiguration = {
isInClusterBuildingConfigured: boolean
Expand Down Expand Up @@ -67,6 +68,50 @@ export const cloudBuilder = {
return availability.available
},

async getActionRuntime(ctx: PluginContext, action: Resolved<ContainerBuildAction>): Promise<ActionRuntime> {
const config = getConfiguration(ctx)

const fallback: ActionRuntimeKind = config.isInClusterBuildingConfigured
? // if in-cluster-building is configured, we are building remotely in the plugin.
{
kind: "remote",
type: "plugin",
pluginName: ctx.provider.name,
}
: // Otherwise we fall back to building locally.
{ kind: "local" }

const preferred: ActionRuntimeKind = cloudBuilder.isConfigured(ctx)
? // If cloud builder is configured, we prefer using cloud builder
{
kind: "remote",
type: "garden-cloud",
}
: // Otherwise we fall back to in-cluster building or building locally, whatever is configured.
fallback

// if cloud builder is configured AND available, that's our actual runtime. Otherwise we fall back to whatever is configured in the plugin.
const actual = (await cloudBuilder.isConfiguredAndAvailable(ctx, action)) ? preferred : fallback

if (actual === preferred) {
return {
actual,
}
} else {
const unavailable = await getAvailability(ctx, action)
if (unavailable.available) {
throw new InternalError({
message: `Inconsistent state: Should only fall back if Cloud Builder is not available`,
})
}
return {
actual,
preferred,
fallbackReason: unavailable.reason,
}
}
},

async withBuilder<T>(
ctx: PluginContext,
action: Resolved<ContainerBuildAction>,
Expand Down
13 changes: 11 additions & 2 deletions core/src/plugins/exec/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { styles } from "../../logger/styles.js"
import { execRunCommand } from "./common.js"
import { execCommonSchema, execEnvVarDoc, execRuntimeOutputsSchema, execStaticOutputsSchema } from "./config.js"
import { execProvider } from "./exec.js"
import { ACTION_RUNTIME_LOCAL } from "../../plugin/base.js"

const s = sdk.schema

Expand Down Expand Up @@ -52,7 +53,13 @@ export type ExecBuildConfig = GardenSdkActionDefinitionConfigType<typeof execBui
export type ExecBuild = GardenSdkActionDefinitionActionType<typeof execBuild>

export const execBuildHandler = execBuild.addHandler("build", async ({ action, log, ctx }) => {
const output: BuildStatus = { state: "ready", outputs: {}, detail: {} }
const output: BuildStatus = {
state: "ready",
outputs: {},
detail: {
runtime: ACTION_RUNTIME_LOCAL,
},
}
const command = action.getSpec("command")

let success = true
Expand All @@ -61,7 +68,9 @@ export const execBuildHandler = execBuild.addHandler("build", async ({ action, l
const result = await execRunCommand({ command, action, ctx, log })

if (!output.detail) {
output.detail = {}
output.detail = {
runtime: ACTION_RUNTIME_LOCAL,
}
}

output.detail.fresh = true
Expand Down
Loading

0 comments on commit 6bb5a7f

Please sign in to comment.