From 930b59a3f07facf6467fe04b655b0f6b0107cd39 Mon Sep 17 00:00:00 2001 From: Thorarinn Sigurdsson Date: Sun, 24 Oct 2021 20:49:33 -0700 Subject: [PATCH] fix(core): fix test dependencies for dev command This fixes a minor regression introduced when we fixed the task graph's batching algorithm (see commit `5625e79d`). Here, we also add an optional `devMode` field to service statuses (currently implemented/used by `container`, `kubernetes` and `helm` services), which will eventually enable us to start `garden dev` or `garden deploy --dev` without rebuilding dev-mode modules. This is done in preparation for planned changes/improvements in how the task graph works (which will introduce more dynamic dependency resolution, enabling us to skip unnecessary tasks in many situations). --- core/src/commands/dev.ts | 3 ++- core/src/commands/test.ts | 3 ++- .../plugins/kubernetes/container/status.ts | 9 ++++++- core/src/plugins/kubernetes/dev-mode.ts | 3 ++- core/src/plugins/kubernetes/helm/status.ts | 19 ++++++++++---- .../kubernetes/hot-reload/hot-reload.ts | 3 ++- .../kubernetes/kubernetes-module/handlers.ts | 13 +++++++--- core/src/plugins/kubernetes/status/status.ts | 25 +++++++++++++++++++ core/src/tasks/deploy.ts | 3 ++- core/src/tasks/test.ts | 8 ++++++ core/src/types/service.ts | 2 ++ .../kubernetes/container/deployment.ts | 4 +-- .../integ/src/plugins/kubernetes/dev-mode.ts | 1 + core/test/unit/src/commands/dev.ts | 8 ++++++ core/test/unit/src/tasks/test.ts | 21 ---------------- docs/reference/commands.md | 21 ++++++++++++++++ 16 files changed, 109 insertions(+), 37 deletions(-) diff --git a/core/src/commands/dev.ts b/core/src/commands/dev.ts index f7159bd825..6dd1ddd4f2 100644 --- a/core/src/commands/dev.ts +++ b/core/src/commands/dev.ts @@ -300,7 +300,7 @@ export async function getDevCommandWatchTasks({ }) if (!skipTests) { - const testModules: GardenModule[] = await updatedGraph.withDependantModules([module]) + const testModules: GardenModule[] = updatedGraph.withDependantModules([module]) tasks.push( ...flatten( await Bluebird.map(testModules, (m) => @@ -310,6 +310,7 @@ export async function getDevCommandWatchTasks({ module: m, graph: updatedGraph, filterNames: testNames, + fromWatch: true, devModeServiceNames, hotReloadServiceNames, }) diff --git a/core/src/commands/test.ts b/core/src/commands/test.ts index 889f4d3b01..35323ecf4d 100644 --- a/core/src/commands/test.ts +++ b/core/src/commands/test.ts @@ -171,7 +171,7 @@ export class TestCommand extends Command { initialTasks, watch: opts.watch, changeHandler: async (updatedGraph, module) => { - const modulesToProcess = await updatedGraph.withDependantModules([module]) + const modulesToProcess = updatedGraph.withDependantModules([module]) return flatten( await Bluebird.map(modulesToProcess, (m) => getTestTasks({ @@ -182,6 +182,7 @@ export class TestCommand extends Command { filterNames, force, forceBuild, + fromWatch: true, devModeServiceNames: [], hotReloadServiceNames: [], }) diff --git a/core/src/plugins/kubernetes/container/status.ts b/core/src/plugins/kubernetes/container/status.ts index 3b136adb5a..78c3a87d60 100644 --- a/core/src/plugins/kubernetes/container/status.ts +++ b/core/src/plugins/kubernetes/container/status.ts @@ -55,7 +55,13 @@ export async function getContainerServiceStatus({ enableHotReload: hotReload, blueGreen: provider.config.deploymentStrategy === "blue-green", }) - const { state, remoteResources } = await compareDeployedResources(k8sCtx, api, namespace, manifests, log) + const { state, remoteResources, deployedWithDevMode, deployedWithHotReloading } = await compareDeployedResources( + k8sCtx, + api, + namespace, + manifests, + log + ) const ingresses = await getIngresses(service, api, provider) const forwardablePorts: ForwardablePort[] = service.spec.ports @@ -78,6 +84,7 @@ export async function getContainerServiceStatus({ namespaceStatuses: [namespaceStatus], version: state === "ready" ? service.version : undefined, detail: { remoteResources, workload }, + devMode: deployedWithDevMode || deployedWithHotReloading, } if (state === "ready" && devMode) { diff --git a/core/src/plugins/kubernetes/dev-mode.ts b/core/src/plugins/kubernetes/dev-mode.ts index ab55578dfe..6bcdb9c5e8 100644 --- a/core/src/plugins/kubernetes/dev-mode.ts +++ b/core/src/plugins/kubernetes/dev-mode.ts @@ -34,6 +34,7 @@ import { } from "./mutagen" import { joi, joiIdentifier } from "../../config/common" import { KubernetesPluginContext, KubernetesProvider } from "./config" +import { isConfiguredForDevMode } from "./status/status" const syncUtilImageName = "gardendev/k8s-sync:0.1.1" @@ -188,7 +189,7 @@ export async function startDevModeSync({ return mutagenConfigLock.acquire("start-sync", async () => { // Validate the target - if (target.metadata.annotations?.[gardenAnnotationKey("dev-mode")] !== "true") { + if (!isConfiguredForDevMode(target)) { throw new ConfigurationError(`Resource ${resourceName} is not deployed in dev mode`, { target, }) diff --git a/core/src/plugins/kubernetes/helm/status.ts b/core/src/plugins/kubernetes/helm/status.ts index 1617507353..bec5992688 100644 --- a/core/src/plugins/kubernetes/helm/status.ts +++ b/core/src/plugins/kubernetes/helm/status.ts @@ -18,7 +18,7 @@ import { KubernetesServerResource } from "../types" import { getModuleNamespace, getModuleNamespaceStatus } from "../namespace" import { getServiceResource, getServiceResourceSpec } from "../util" import { startDevModeSync } from "../dev-mode" -import { gardenAnnotationKey } from "../../../util/string" +import { isConfiguredForDevMode } from "../status/status" const helmStatusMap: { [status: string]: ServiceState } = { unknown: "unknown", @@ -48,6 +48,7 @@ export async function getServiceStatus({ const detail: HelmStatusDetail = {} let state: ServiceState + let helmStatus: ServiceStatus const namespaceStatus = await getModuleNamespaceStatus({ ctx: k8sCtx, @@ -56,9 +57,12 @@ export async function getServiceStatus({ provider: k8sCtx.provider, }) + let deployedWithDevModeOrHotReloading: boolean | undefined + try { - const helmStatus = await getReleaseStatus({ ctx: k8sCtx, service, releaseName, log, devMode, hotReload }) + helmStatus = await getReleaseStatus({ ctx: k8sCtx, service, releaseName, log, devMode, hotReload }) state = helmStatus.state + deployedWithDevModeOrHotReloading = helmStatus.devMode } catch (err) { state = "missing" } @@ -84,7 +88,7 @@ export async function getServiceStatus({ // Make sure we don't fail if the service isn't actually properly configured (we don't want to throw in the // status handler, generally) - if (target.metadata.annotations?.[gardenAnnotationKey("dev-mode")] === "true") { + if (isConfiguredForDevMode(target)) { const namespace = target.metadata.namespace || (await getModuleNamespace({ @@ -115,6 +119,7 @@ export async function getServiceStatus({ state, version: state === "ready" ? service.version : undefined, detail, + devMode: deployedWithDevModeOrHotReloading, namespaceStatuses: [namespaceStatus], } } @@ -176,6 +181,9 @@ export async function getReleaseStatus({ let state = helmStatusMap[res.info.status] || "unknown" let values = {} + let devModeEnabled = false + let hotReloadEnabled = false + if (state === "ready") { // Make sure the right version is deployed values = JSON.parse( @@ -187,8 +195,8 @@ export async function getReleaseStatus({ }) ) const deployedVersion = values[".garden"] && values[".garden"].version - const devModeEnabled = values[".garden"] && values[".garden"].devMode === true - const hotReloadEnabled = values[".garden"] && values[".garden"].hotReload === true + devModeEnabled = values[".garden"] && values[".garden"].devMode === true + hotReloadEnabled = values[".garden"] && values[".garden"].hotReload === true if ( (devMode && !devModeEnabled) || @@ -203,6 +211,7 @@ export async function getReleaseStatus({ return { state, detail: { ...res, values }, + devMode: devModeEnabled || hotReloadEnabled, } } catch (err) { if (err.message.includes("release: not found")) { diff --git a/core/src/plugins/kubernetes/hot-reload/hot-reload.ts b/core/src/plugins/kubernetes/hot-reload/hot-reload.ts index 39652444b0..bdc30cdcb3 100644 --- a/core/src/plugins/kubernetes/hot-reload/hot-reload.ts +++ b/core/src/plugins/kubernetes/hot-reload/hot-reload.ts @@ -25,6 +25,7 @@ import { getManifests } from "../kubernetes-module/common" import { KubernetesModule, KubernetesService } from "../kubernetes-module/config" import { getHotReloadSpec, syncToService } from "./helpers" import { GardenModule } from "../../../types/module" +import { isConfiguredForHotReloading } from "../status/status" export type HotReloadableResource = KubernetesWorkload | KubernetesPod export type HotReloadableKind = "Deployment" | "DaemonSet" | "StatefulSet" @@ -142,7 +143,7 @@ export async function hotReloadContainer({ }, }) - const list = res.items.filter((r) => r.metadata.annotations![gardenAnnotationKey("hot-reload")] === "true") + const list = res.items.filter((r) => isConfiguredForHotReloading(r)) if (list.length === 0) { throw new RuntimeError(`Unable to find deployed instance of service ${service.name} with hot-reloading enabled`, { diff --git a/core/src/plugins/kubernetes/kubernetes-module/handlers.ts b/core/src/plugins/kubernetes/kubernetes-module/handlers.ts index 9edf03911d..a24a0cdd52 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/handlers.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/handlers.ts @@ -27,7 +27,7 @@ import { apply, deleteObjectsBySelector, KUBECTL_DEFAULT_TIMEOUT } from "../kube import { streamK8sLogs } from "../logs" import { getModuleNamespace, getModuleNamespaceStatus } from "../namespace" import { getForwardablePorts, getPortForwardHandler, killPortForwards } from "../port-forward" -import { compareDeployedResources, waitForResources } from "../status/status" +import { compareDeployedResources, isConfiguredForDevMode, waitForResources } from "../status/status" import { getTaskResult } from "../task-results" import { getTestResult } from "../test-results" import { BaseResource, KubernetesResource, KubernetesServerResource } from "../types" @@ -91,7 +91,13 @@ export async function getKubernetesServiceStatus({ manifests, }) - let { state, remoteResources } = await compareDeployedResources(k8sCtx, api, namespace, prepareResult.manifests, log) + let { state, remoteResources, deployedWithDevMode, deployedWithHotReloading } = await compareDeployedResources( + k8sCtx, + api, + namespace, + prepareResult.manifests, + log + ) const forwardablePorts = getForwardablePorts(remoteResources, service) @@ -107,7 +113,7 @@ export async function getKubernetesServiceStatus({ resourceSpec: serviceResourceSpec, }) - if (target.metadata.annotations?.[gardenAnnotationKey("dev-mode")] === "true") { + if (isConfiguredForDevMode(target)) { await startDevModeSync({ ctx, log, @@ -128,6 +134,7 @@ export async function getKubernetesServiceStatus({ state, version: state === "ready" ? service.version : undefined, detail: { remoteResources }, + devMode: deployedWithDevMode || deployedWithHotReloading, namespaceStatuses: [namespaceStatus], } } diff --git a/core/src/plugins/kubernetes/status/status.ts b/core/src/plugins/kubernetes/status/status.ts index c1eda3c729..530a3ba342 100644 --- a/core/src/plugins/kubernetes/status/status.ts +++ b/core/src/plugins/kubernetes/status/status.ts @@ -31,6 +31,7 @@ import { getPods, hashManifest } from "../util" import { checkWorkloadStatus } from "./workload" import { checkWorkloadPodStatus } from "./pod" import { deline, gardenAnnotationKey, stableStringify } from "../../../util/string" +import { HotReloadableResource } from "../hot-reload/hot-reload" export interface ResourceStatus { state: ServiceState @@ -266,6 +267,8 @@ export async function waitForResources({ interface ComparisonResult { state: ServiceState remoteResources: KubernetesResource[] + deployedWithDevMode: boolean + deployedWithHotReloading: boolean } /** @@ -290,6 +293,8 @@ export async function compareDeployedResources( const result: ComparisonResult = { state: "unknown", remoteResources: deployedResources.filter((o) => o !== null), + deployedWithDevMode: false, + deployedWithHotReloading: false, } const logDescription = (resource: KubernetesResource) => `${resource.kind}/${resource.metadata.name}` @@ -349,6 +354,15 @@ export async function compareDeployedResources( delete manifest.metadata.annotations[gardenAnnotationKey("manifest-hash")] } + if (manifest.kind === "DaemonSet" || manifest.kind === "Deployment" || manifest.kind === "StatefulSet") { + if (isConfiguredForDevMode(manifest)) { + result.deployedWithDevMode = true + } + if (isConfiguredForHotReloading(manifest)) { + result.deployedWithHotReloading = true + } + } + // Start by checking for "last applied configuration" annotations and comparing against those. // This can be more accurate than comparing against resolved resources. if (deployedResource.metadata && deployedResource.metadata.annotations) { @@ -390,6 +404,9 @@ export async function compareDeployedResources( // NOTE: this approach won't fly in the long run, but hopefully we can climb out of this mess when // `kubectl diff` is ready, or server-side apply/diff is ready if (manifest.kind === "DaemonSet" || manifest.kind === "Deployment" || manifest.kind === "StatefulSet") { + // NOTE: this approach won't fly in the long run, but hopefully we can climb out of this mess when + // `kubectl diff` is ready, or server-side apply/diff is ready + // handle properties that are omitted in the response because they have the default value // (another design issue in the K8s API) if (manifest.spec.minReadySeconds === 0) { @@ -423,6 +440,14 @@ export async function compareDeployedResources( return result } +export function isConfiguredForDevMode(resource: HotReloadableResource): boolean { + return resource.metadata.annotations?.[gardenAnnotationKey("dev-mode")] === "true" +} + +export function isConfiguredForHotReloading(resource: HotReloadableResource): boolean { + return resource.metadata.annotations?.[gardenAnnotationKey("hot-reload")] === "true" +} + export async function getDeployedResource( ctx: PluginContext, provider: KubernetesProvider, diff --git a/core/src/tasks/deploy.ts b/core/src/tasks/deploy.ts index 39c328912f..9c1bd7099c 100644 --- a/core/src/tasks/deploy.ts +++ b/core/src/tasks/deploy.ts @@ -192,6 +192,7 @@ export class DeployTask extends BaseTask { const actions = await this.garden.getActionRouter() let status = serviceStatuses[this.service.name] + const devModeSkipRedeploy = status.devMode && (devMode || hotReload) const log = this.log.info({ status: "active", @@ -199,7 +200,7 @@ export class DeployTask extends BaseTask { msg: `Deploying version ${version}...`, }) - if (!this.force && version === status.version && status.state === "ready") { + if (!this.force && status.state === "ready" && (version === status.version || devModeSkipRedeploy)) { // already deployed and ready log.setSuccess({ msg: chalk.green("Already deployed"), diff --git a/core/src/tasks/test.ts b/core/src/tasks/test.ts index fc44514cdc..16b69715f0 100644 --- a/core/src/tasks/test.ts +++ b/core/src/tasks/test.ts @@ -39,6 +39,7 @@ export interface TestTaskParams { test: GardenTest force: boolean forceBuild: boolean + fromWatch?: boolean devModeServiceNames: string[] hotReloadServiceNames: string[] silent?: boolean @@ -52,6 +53,7 @@ export class TestTask extends BaseTask { private test: GardenTest private graph: ConfigGraph private forceBuild: boolean + private fromWatch: boolean private devModeServiceNames: string[] private hotReloadServiceNames: string[] private silent: boolean @@ -63,6 +65,7 @@ export class TestTask extends BaseTask { test, force, forceBuild, + fromWatch = false, devModeServiceNames, hotReloadServiceNames, silent = true, @@ -73,6 +76,7 @@ export class TestTask extends BaseTask { this.graph = graph this.force = force this.forceBuild = forceBuild + this.fromWatch = fromWatch this.devModeServiceNames = devModeServiceNames this.hotReloadServiceNames = hotReloadServiceNames this.silent = silent @@ -92,6 +96,7 @@ export class TestTask extends BaseTask { recursive: false, filter: (depNode) => !( + this.fromWatch && depNode.type === "deploy" && includes([...this.devModeServiceNames, ...this.hotReloadServiceNames], depNode.name) ), @@ -242,6 +247,7 @@ export async function getTestTasks({ hotReloadServiceNames, force = false, forceBuild = false, + fromWatch = false, }: { garden: Garden log: LogEntry @@ -252,6 +258,7 @@ export async function getTestTasks({ hotReloadServiceNames: string[] force?: boolean forceBuild?: boolean + fromWatch?: boolean }) { // If there are no filters we return the test otherwise // we check if the test name matches against the filterNames array @@ -270,6 +277,7 @@ export async function getTestTasks({ log, force, forceBuild, + fromWatch, test: testFromConfig(module, testConfig, graph), devModeServiceNames, hotReloadServiceNames, diff --git a/core/src/types/service.ts b/core/src/types/service.ts index a1648a8486..380d04f096 100644 --- a/core/src/types/service.ts +++ b/core/src/types/service.ts @@ -182,6 +182,7 @@ const forwardablePortSchema = () => joi.object().keys(forwardablePortKeys()) export interface ServiceStatus { createdAt?: string detail: T + devMode?: boolean namespaceStatuses?: NamespaceStatus[] externalId?: string externalVersion?: string @@ -204,6 +205,7 @@ export const serviceStatusSchema = () => joi.object().keys({ createdAt: joi.string().description("When the service was first deployed by the provider."), detail: joi.object().meta({ extendable: true }).description("Additional detail, specific to the provider."), + devMode: joi.boolean().description("Whether the service was deployed with dev mode enabled."), namespaceStatuses: namespaceStatusesSchema().optional(), externalId: joi .string() diff --git a/core/test/integ/src/plugins/kubernetes/container/deployment.ts b/core/test/integ/src/plugins/kubernetes/container/deployment.ts index 9ef23e3a45..d3da81bc10 100644 --- a/core/test/integ/src/plugins/kubernetes/container/deployment.ts +++ b/core/test/integ/src/plugins/kubernetes/container/deployment.ts @@ -21,9 +21,9 @@ import { DeployTask } from "../../../../../../src/tasks/deploy" import { getServiceStatuses } from "../../../../../../src/tasks/base" import { expectError, grouped } from "../../../../../helpers" import stripAnsi = require("strip-ansi") -import { gardenAnnotationKey } from "../../../../../../src/util/string" import { kilobytesToString, millicpuToString } from "../../../../../../src/plugins/kubernetes/util" import { getResourceRequirements } from "../../../../../../src/plugins/kubernetes/container/util" +import { isConfiguredForDevMode } from "../../../../../../src/plugins/kubernetes/status/status" describe("kubernetes container deployment handlers", () => { let garden: Garden @@ -255,7 +255,7 @@ describe("kubernetes container deployment handlers", () => { blueGreen: false, }) - expect(resource.metadata.annotations![gardenAnnotationKey("dev-mode")]).to.eq("true") + expect(isConfiguredForDevMode(resource)).to.eq(true) const initContainer = resource.spec.template?.spec?.initContainers![0] expect(initContainer).to.exist diff --git a/core/test/integ/src/plugins/kubernetes/dev-mode.ts b/core/test/integ/src/plugins/kubernetes/dev-mode.ts index 0d5c84808e..50b7b62abe 100644 --- a/core/test/integ/src/plugins/kubernetes/dev-mode.ts +++ b/core/test/integ/src/plugins/kubernetes/dev-mode.ts @@ -88,6 +88,7 @@ describe("dev mode deployments and sync behavior", () => { devMode: true, hotReload: false, }) + expect(status.devMode).to.eql(true) const workload = status.detail.workload! diff --git a/core/test/unit/src/commands/dev.ts b/core/test/unit/src/commands/dev.ts index 899844a4d4..ef5ff165e9 100644 --- a/core/test/unit/src/commands/dev.ts +++ b/core/test/unit/src/commands/dev.ts @@ -79,6 +79,7 @@ describe("DevCommand", () => { const args = { services: undefined } const opts = withDefaultGlobalOpts({ "force-build": false, + "force": false, "hot-reload": undefined, "skip-tests": false, "test-names": undefined, @@ -134,6 +135,7 @@ describe("DevCommand", () => { // hot reloading don't request non-hot-reload-enabled deploys for those same services. hotReloadServiceNames: ["service-a"], skipTests: false, + forceDeploy: false, }) const withDeps = async (task: BaseTask) => { @@ -159,6 +161,7 @@ describe("DevCommand", () => { const args = { services: undefined } const opts = withDefaultGlobalOpts({ "force-build": false, + "force": false, "hot-reload": undefined, "skip-tests": false, "test-names": undefined, @@ -180,6 +183,7 @@ describe("DevCommand", () => { const args = { services: undefined } const opts = withDefaultGlobalOpts({ "force-build": false, + "force": false, "hot-reload": undefined, "skip-tests": false, "test-names": undefined, @@ -201,6 +205,7 @@ describe("DevCommand", () => { const args = { services: undefined } const opts = withDefaultGlobalOpts({ "force-build": false, + "force": false, "hot-reload": undefined, "skip-tests": false, "test-names": undefined, @@ -222,6 +227,7 @@ describe("DevCommand", () => { const args = { services: undefined } const opts = withDefaultGlobalOpts({ "force-build": false, + "force": false, "hot-reload": undefined, "skip-tests": false, "test-names": undefined, @@ -243,6 +249,7 @@ describe("DevCommand", () => { const args = { services: undefined } const opts = withDefaultGlobalOpts({ "force-build": false, + "force": false, "hot-reload": undefined, "skip-tests": false, "test-names": undefined, @@ -264,6 +271,7 @@ describe("DevCommand", () => { const args = { services: undefined } const opts = withDefaultGlobalOpts({ "force-build": false, + "force": false, "hot-reload": undefined, "skip-tests": false, "test-names": undefined, diff --git a/core/test/unit/src/tasks/test.ts b/core/test/unit/src/tasks/test.ts index d05ffaa231..9149effd9d 100644 --- a/core/test/unit/src/tasks/test.ts +++ b/core/test/unit/src/tasks/test.ts @@ -46,25 +46,4 @@ describe("TestTask", () => { expect(deps.map((d) => d.getKey())).to.eql(["build.module-a", "deploy.service-b", "task.task-a"]) }) }) - - describe("getTestTasks", () => { - it("should not return test tasks with deploy dependencies on services deployed with hot reloading", async () => { - const moduleA = graph.getModule("module-a") - - const tasks = await getTestTasks({ - garden, - log, - graph, - module: moduleA, - devModeServiceNames: [], - hotReloadServiceNames: ["service-b"], - }) - - const testTask = tasks[0] - const deps = await testTask.resolveDependencies() - - expect(tasks.length).to.eql(1) - expect(deps.map((d) => d.getKey())).to.eql(["build.module-a", "task.task-a"]) - }) - }) }) diff --git a/docs/reference/commands.md b/docs/reference/commands.md index b7fd48fa39..4ecdd3b4c2 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -112,6 +112,9 @@ deployments: # Additional detail, specific to the provider. detail: + # Whether the service was deployed with dev mode enabled. + devMode: + namespaceStatuses: - pluginName: @@ -468,6 +471,9 @@ serviceStatuses: # Additional detail, specific to the provider. detail: + # Whether the service was deployed with dev mode enabled. + devMode: + namespaceStatuses: - pluginName: @@ -579,6 +585,9 @@ Examples: # Additional detail, specific to the provider. detail: + # Whether the service was deployed with dev mode enabled. + devMode: + namespaceStatuses: - pluginName: @@ -742,6 +751,9 @@ deployments: # Additional detail, specific to the provider. detail: + # Whether the service was deployed with dev mode enabled. + devMode: + namespaceStatuses: - pluginName: @@ -2388,6 +2400,9 @@ services: # Additional detail, specific to the provider. detail: + # Whether the service was deployed with dev mode enabled. + devMode: + namespaceStatuses: - pluginName: @@ -3007,6 +3022,9 @@ deployments: # Additional detail, specific to the provider. detail: + # Whether the service was deployed with dev mode enabled. + devMode: + namespaceStatuses: - pluginName: @@ -3614,6 +3632,9 @@ deployments: # Additional detail, specific to the provider. detail: + # Whether the service was deployed with dev mode enabled. + devMode: + namespaceStatuses: - pluginName: