Skip to content

Commit

Permalink
refactor(plugin): make ServiceStatus detail type-safe
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald committed Sep 20, 2019
1 parent 5a5d539 commit 37ecd0a
Show file tree
Hide file tree
Showing 21 changed files with 150 additions and 87 deletions.
4 changes: 2 additions & 2 deletions garden-service/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import { HotReloadServiceParams, HotReloadServiceResult } from "./types/plugin/s
import { RunServiceParams } from "./types/plugin/service/runService"
import { GetTaskResultParams } from "./types/plugin/task/getTaskResult"
import { RunTaskParams, RunTaskResult } from "./types/plugin/task/runTask"
import { ServiceStatus, ServiceStatusMap } from "./types/service"
import { ServiceStatus, ServiceStatusMap, ServiceState } from "./types/service"
import { Omit } from "./util/util"
import { DebugInfoMap } from "./types/plugin/provider/getDebugInfo"
import { PrepareEnvironmentParams, PrepareEnvironmentResult } from "./types/plugin/provider/prepareEnvironment"
Expand Down Expand Up @@ -784,5 +784,5 @@ const dummyPublishHandler = async ({ module }) => {
const dummyDeleteServiceHandler = async ({ module, log }: DeleteServiceParams) => {
const msg = `No delete service handler available for module type ${module.type}`
log.setError(msg)
return {}
return { state: "missing" as ServiceState, detail: {} }
}
4 changes: 2 additions & 2 deletions garden-service/src/plugins/google/google-app-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const gardenPlugin = (): GardenPlugin => ({
// const services = await this.gcloud(project).json(["app", "services", "list"])
// const instances: any[] = await this.gcloud(project).json(["app", "instances", "list"])

return {}
return { state: "unknown", detail: {} }
},

async deployService({ ctx, service, runtimeContext, log }: DeployServiceParams<ContainerModule>) {
Expand Down Expand Up @@ -104,7 +104,7 @@ export const gardenPlugin = (): GardenPlugin => ({

log.info({ section: service.name, msg: `App deployed` })

return {}
return { state: "ready", detail: {} }
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export async function getServiceStatus(

if (!status) {
// not deployed yet
return {}
return { state: "missing", detail: {} }
}

// TODO: map states properly
Expand Down
33 changes: 19 additions & 14 deletions garden-service/src/plugins/kubernetes/container/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { KubernetesProvider, KubernetesPluginContext } from "../config"
import { configureHotReload } from "../hot-reload"
import { KubernetesResource, KubernetesServerResource } from "../types"
import { ConfigurationError } from "../../../exceptions"
import { getContainerServiceStatus } from "./status"
import { getContainerServiceStatus, ContainerServiceStatus } from "./status"
import { containerHelpers } from "../../container/helpers"
import { LogEntry } from "../../../logger/log-entry"
import { DeployServiceParams } from "../../../types/plugin/service/deployService"
Expand All @@ -35,7 +35,9 @@ import { RuntimeContext } from "../../../runtime-context"
export const DEFAULT_CPU_REQUEST = "10m"
export const DEFAULT_MEMORY_REQUEST = "64Mi"

export async function deployContainerService(params: DeployServiceParams<ContainerModule>): Promise<ServiceStatus> {
export async function deployContainerService(
params: DeployServiceParams<ContainerModule>,
): Promise<ContainerServiceStatus> {
const { deploymentStrategy } = params.ctx.provider.config

if (deploymentStrategy === "blue-green") {
Expand All @@ -46,7 +48,7 @@ export async function deployContainerService(params: DeployServiceParams<Contain
}

export async function deployContainerServiceRolling(
params: DeployServiceParams<ContainerModule>): Promise<ServiceStatus> {
params: DeployServiceParams<ContainerModule>): Promise<ContainerServiceStatus> {
const { ctx, service, runtimeContext, force, log, hotReload } = params
const k8sCtx = <KubernetesPluginContext>ctx

Expand Down Expand Up @@ -200,7 +202,7 @@ export async function deployContainerServiceBlueGreen(
return getContainerServiceStatus(params)
}

export async function createContainerObjects(
export async function createContainerManifests(
ctx: PluginContext,
log: LogEntry,
service: ContainerService,
Expand All @@ -213,18 +215,21 @@ export async function createContainerObjects(
const namespace = await getAppNamespace(k8sCtx, log, provider)
const api = await KubeApi.factory(log, provider)
const ingresses = await createIngressResources(api, provider, namespace, service)
const deployment = await createDeployment({ provider, service, runtimeContext, namespace, enableHotReload, log })
const workload = await createWorkloadResource({ provider, service, runtimeContext, namespace, enableHotReload, log })
const kubeservices = await createServiceResources(service, namespace)

const objects = [deployment, ...kubeservices, ...ingresses]
const manifests = [workload, ...kubeservices, ...ingresses]

return objects.map(obj => {
for (const obj of manifests) {
set(obj, ["metadata", "labels", gardenAnnotationKey("module")], service.module.name)
set(obj, ["metadata", "labels", gardenAnnotationKey("service")], service.name)
set(obj, ["metadata", "labels", gardenAnnotationKey("generated")], "true")
set(obj, ["metadata", "labels", gardenAnnotationKey("version")], version.versionString)
set(obj, ["metadata", "annotations", gardenAnnotationKey("generated")], "true")
set(obj, ["metadata", "annotations", gardenAnnotationKey("version")], version.versionString)
set(obj, ["metadata", "labels", "module"], service.module.name)
set(obj, ["metadata", "labels", "service"], service.name)
return obj
})
}

return { workload, manifests }
}

interface CreateDeploymentParams {
Expand All @@ -236,9 +241,9 @@ interface CreateDeploymentParams {
log: LogEntry,
}

export async function createDeployment(
export async function createWorkloadResource(
{ provider, service, runtimeContext, namespace, enableHotReload, log }: CreateDeploymentParams,
): Promise<KubernetesResource> {
): Promise<KubernetesWorkload> {

const spec = service.spec
let configuredReplicas = service.spec.replicas
Expand Down Expand Up @@ -569,7 +574,6 @@ export async function deleteService(params: DeleteServiceParams): Promise<Servic
includeUninitialized: false,
})

return { state: "missing" }
}

export async function deleteContainerDeployment(
Expand All @@ -593,4 +597,5 @@ export async function deleteContainerDeployment(
if (log) {
found ? log.setSuccess("Service deleted") : log.setWarn("Service not deployed")
}
return { state: "missing", detail: { remoteResources: [], workload: null } }
}
20 changes: 14 additions & 6 deletions garden-service/src/plugins/kubernetes/container/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,30 @@
import { PluginContext } from "../../../plugin-context"
import { LogEntry } from "../../../logger/log-entry"
import { Service, ServiceStatus, ForwardablePort } from "../../../types/service"
import { createContainerObjects } from "./deployment"
import { createContainerManifests } from "./deployment"
import { KUBECTL_DEFAULT_TIMEOUT } from "../kubectl"
import { DeploymentError } from "../../../exceptions"
import { sleep } from "../../../util/util"
import { GetServiceStatusParams } from "../../../types/plugin/service/getServiceStatus"
import { ContainerModule } from "../../container/config"
import { KubeApi } from "../api"
import { compareDeployedObjects } from "../status/status"
import { compareDeployedObjects as compareDeployedResources } from "../status/status"
import { getIngresses } from "./ingress"
import { getAppNamespace } from "../namespace"
import { KubernetesPluginContext } from "../config"
import { RuntimeContext } from "../../../runtime-context"
import { KubernetesServerResource, KubernetesWorkload } from "../types"

interface ContainerStatusDetail {
remoteResources: KubernetesServerResource[]
workload: KubernetesWorkload | null
}

export type ContainerServiceStatus = ServiceStatus<ContainerStatusDetail>

export async function getContainerServiceStatus(
{ ctx, module, service, runtimeContext, log, hotReload }: GetServiceStatusParams<ContainerModule>,
): Promise<ServiceStatus> {
): Promise<ContainerServiceStatus> {

const k8sCtx = <KubernetesPluginContext>ctx
// TODO: hash and compare all the configuration files (otherwise internal changes don't get deployed)
Expand All @@ -34,8 +42,8 @@ export async function getContainerServiceStatus(
const namespace = await getAppNamespace(k8sCtx, log, provider)

// FIXME: [objects, matched] and ingresses can be run in parallel
const objects = await createContainerObjects(k8sCtx, log, service, runtimeContext, hotReload)
const { state, remoteObjects } = await compareDeployedObjects(k8sCtx, api, namespace, objects, log, true)
const { workload, manifests } = await createContainerManifests(k8sCtx, log, service, runtimeContext, hotReload)
const { state, remoteResources } = await compareDeployedResources(k8sCtx, api, namespace, manifests, log, true)
const ingresses = await getIngresses(service, api, provider)

const forwardablePorts: ForwardablePort[] = service.spec.ports
Expand All @@ -55,7 +63,7 @@ export async function getContainerServiceStatus(
ingresses,
state,
version: state === "ready" ? version.versionString : undefined,
detail: { remoteObjects },
detail: { remoteResources, workload },
}
}

Expand Down
18 changes: 12 additions & 6 deletions garden-service/src/plugins/kubernetes/helm/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { ServiceStatus } from "../../../types/service"
import { getAppNamespace } from "../namespace"
import { waitForResources } from "../status/status"
import { helm } from "./helm-cli"
Expand All @@ -19,7 +18,7 @@ import {
getServiceResourceSpec,
getValueFileArgs,
} from "./common"
import { getReleaseStatus } from "./status"
import { getReleaseStatus, HelmServiceStatus } from "./status"
import { configureHotReload, HotReloadableResource } from "../hot-reload"
import { apply } from "../kubectl"
import { KubernetesPluginContext } from "../config"
Expand All @@ -31,7 +30,7 @@ import { getForwardablePorts } from "../port-forward"

export async function deployService(
{ ctx, module, service, log, force, hotReload }: DeployServiceParams<HelmModule>,
): Promise<ServiceStatus> {
): Promise<HelmServiceStatus> {
let hotReloadSpec: ContainerHotReloadSpec | null = null
let hotReloadTarget: HotReloadableResource | null = null

Expand Down Expand Up @@ -97,18 +96,25 @@ export async function deployService(

// FIXME: we should get these objects from the cluster, and not from the local `helm template` command, because
// they may be legitimately inconsistent.
await waitForResources({ ctx, provider, serviceName: service.name, resources: chartResources, log })
const remoteResources = await waitForResources({
ctx,
provider,
serviceName: service.name,
resources: chartResources,
log,
})

const forwardablePorts = getForwardablePorts(chartResources)

return {
forwardablePorts,
state: "ready",
version: module.version.versionString,
detail: { remoteResources },
}
}

export async function deleteService(params: DeleteServiceParams): Promise<ServiceStatus> {
export async function deleteService(params: DeleteServiceParams): Promise<HelmServiceStatus> {
const { ctx, log, module } = params

const k8sCtx = <KubernetesPluginContext>ctx
Expand All @@ -117,5 +123,5 @@ export async function deleteService(params: DeleteServiceParams): Promise<Servic
await helm({ ctx: k8sCtx, log, args: ["delete", "--purge", releaseName] })
log.setSuccess("Service deleted")

return { state: "missing" }
return { state: "missing", detail: { remoteResources: [] } }
}
17 changes: 12 additions & 5 deletions garden-service/src/plugins/kubernetes/helm/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { configureHotReload } from "../hot-reload"
import { getHotReloadSpec } from "./hot-reload"
import { KubernetesPluginContext } from "../config"
import { getForwardablePorts } from "../port-forward"
import { KubernetesServerResource } from "../types"

const helmStatusCodeMap: { [code: number]: ServiceState } = {
// see https://github.com/kubernetes/helm/blob/master/_proto/hapi/release/status.proto
Expand All @@ -35,9 +36,15 @@ const helmStatusCodeMap: { [code: number]: ServiceState } = {
8: "deploying", // PENDING_ROLLBACK
}

interface HelmStatusDetail {
remoteResources: KubernetesServerResource[]
}

export type HelmServiceStatus = ServiceStatus<HelmStatusDetail>

export async function getServiceStatus(
{ ctx, module, service, log, hotReload }: GetServiceStatusParams<HelmModule>,
): Promise<ServiceStatus> {
): Promise<HelmServiceStatus> {
const k8sCtx = <KubernetesPluginContext>ctx
// need to build to be able to check the status
const buildStatus = await getExecModuleBuildStatus({ ctx: k8sCtx, module, log })
Expand Down Expand Up @@ -65,11 +72,11 @@ export async function getServiceStatus(
const api = await KubeApi.factory(log, provider)
const namespace = await getAppNamespace(k8sCtx, log, provider)

let { state, remoteObjects } = await compareDeployedObjects(k8sCtx, api, namespace, chartResources, log, false)
let { state, remoteResources } = await compareDeployedObjects(k8sCtx, api, namespace, chartResources, log, false)

const forwardablePorts = getForwardablePorts(remoteObjects)
const forwardablePorts = getForwardablePorts(remoteResources)

const detail = { remoteObjects }
const detail = { remoteResources }

return {
forwardablePorts,
Expand All @@ -92,6 +99,6 @@ export async function getReleaseStatus(
}
} catch (_) {
// release doesn't exist
return { state: "missing" }
return { state: "missing", detail: {} }
}
}
29 changes: 17 additions & 12 deletions garden-service/src/plugins/kubernetes/kubernetes-module/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { safeLoadAll } from "js-yaml"
import { KubernetesModule, configureKubernetesModule, KubernetesService, describeType } from "./config"
import { getNamespace, getAppNamespace } from "../namespace"
import { KubernetesPluginContext } from "../config"
import { KubernetesResource } from "../types"
import { KubernetesResource, KubernetesServerResource } from "../types"
import { ServiceStatus } from "../../../types/service"
import { compareDeployedObjects, waitForResources } from "../status/status"
import { KubeApi } from "../api"
import { ModuleAndRuntimeActions } from "../../../types/plugin/plugin"
import { getAllLogs } from "../logs"
import { deleteObjectsByLabel, apply } from "../kubectl"
import { deleteObjectsBySelector, apply } from "../kubectl"
import { BuildModuleParams, BuildResult } from "../../../types/plugin/module/build"
import { GetServiceStatusParams } from "../../../types/plugin/service/getServiceStatus"
import { DeployServiceParams } from "../../../types/plugin/service/deployService"
Expand All @@ -42,6 +42,12 @@ export const kubernetesHandlers: Partial<ModuleAndRuntimeActions<KubernetesModul
getServiceStatus,
}

interface KubernetesStatusDetail {
remoteResources: KubernetesServerResource[]
}

export type KubernetesServiceStatus = ServiceStatus<KubernetesStatusDetail>

async function build({ module }: BuildModuleParams<KubernetesModule>): Promise<BuildResult> {
// Get the manifests here, just to validate that the files are there and are valid YAML
await readManifests(module)
Expand All @@ -50,7 +56,7 @@ async function build({ module }: BuildModuleParams<KubernetesModule>): Promise<B

async function getServiceStatus(
{ ctx, module, log }: GetServiceStatusParams<KubernetesModule>,
): Promise<ServiceStatus> {
): Promise<KubernetesServiceStatus> {
const k8sCtx = <KubernetesPluginContext>ctx
const namespace = await getNamespace({
log,
Expand All @@ -61,21 +67,21 @@ async function getServiceStatus(
const api = await KubeApi.factory(log, k8sCtx.provider)
const manifests = await getManifests(api, log, module, namespace)

const { state, remoteObjects } = await compareDeployedObjects(k8sCtx, api, namespace, manifests, log, false)
const { state, remoteResources } = await compareDeployedObjects(k8sCtx, api, namespace, manifests, log, false)

const forwardablePorts = getForwardablePorts(remoteObjects)
const forwardablePorts = getForwardablePorts(remoteResources)

return {
forwardablePorts,
state,
version: state === "ready" ? module.version.versionString : undefined,
detail: { remoteObjects },
detail: { remoteResources },
}
}

async function deployService(
params: DeployServiceParams<KubernetesModule>,
): Promise<ServiceStatus> {
): Promise<KubernetesServiceStatus> {
const { ctx, force, module, service, log } = params

const k8sCtx = <KubernetesPluginContext>ctx
Expand Down Expand Up @@ -104,25 +110,24 @@ async function deployService(
return getServiceStatus(params)
}

async function deleteService(params: DeleteServiceParams): Promise<ServiceStatus> {
async function deleteService(params: DeleteServiceParams): Promise<KubernetesServiceStatus> {
const { ctx, log, service, module } = params
const k8sCtx = <KubernetesPluginContext>ctx
const namespace = await getAppNamespace(k8sCtx, log, k8sCtx.provider)
const provider = k8sCtx.provider
const api = await KubeApi.factory(log, provider)
const manifests = await getManifests(api, log, module, namespace)

await deleteObjectsByLabel({
await deleteObjectsBySelector({
log,
provider,
namespace,
labelKey: gardenAnnotationKey("service"),
labelValue: service.name,
selector: `${gardenAnnotationKey("service")}=${service.name}`,
objectTypes: uniq(manifests.map(m => m.kind)),
includeUninitialized: false,
})

return { state: "missing" }
return { state: "missing", detail: { remoteResources: [] } }
}

async function getServiceLogs(params: GetServiceLogsParams<KubernetesModule>) {
Expand Down
Loading

0 comments on commit 37ecd0a

Please sign in to comment.