Skip to content

Commit

Permalink
feat: add 'delete service' command
Browse files Browse the repository at this point in the history
  • Loading branch information
eysi09 committed Aug 21, 2018
1 parent e68584d commit 2b067c6
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 38 deletions.
22 changes: 22 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,28 @@ resources.

garden delete environment

### garden delete service

Deletes a running service.

Deletes (i.e. un-deploys) the specified services. Note that this command does not take into account any
services depending on the deleted service, and might therefore leave the project in an unstable state.
Running `garden deploy` will re-deploy any missing services.

Examples:

garden delete service my-service # deletes my-service

##### Usage

garden delete service <service>

##### Arguments

| Argument | Required | Description |
| -------- | -------- | ----------- |
| `service` | Yes | The name of the service(s) to delete. Use comma as separator to specify multiple services.

### garden deploy

Deploy service(s) to your environment.
Expand Down
52 changes: 50 additions & 2 deletions garden-cli/src/commands/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import * as Bluebird from "bluebird"
import {
DeleteConfigResult,
EnvironmentStatusMap,
Expand All @@ -16,9 +17,11 @@ import {
CommandParams,
ParameterValues,
StringParameter,
StringsParameter,
} from "./base"
import { NotFoundError } from "../exceptions"
import dedent = require("dedent")
import { ServiceStatus } from "../types/service"

export class DeleteCommand extends Command {
name = "delete"
Expand All @@ -28,6 +31,7 @@ export class DeleteCommand extends Command {
subCommands = [
DeleteConfigCommand,
DeleteEnvironmentCommand,
DeleteServiceCommand,
]

async action() { return {} }
Expand All @@ -40,7 +44,7 @@ export const deleteConfigArgs = {
}),
}

export type DeleteArgs = ParameterValues<typeof deleteConfigArgs>
export type DeleteConfigArgs = ParameterValues<typeof deleteConfigArgs>

// TODO: add --all option to remove all configs

Expand All @@ -59,7 +63,7 @@ export class DeleteConfigCommand extends Command<typeof deleteConfigArgs> {

arguments = deleteConfigArgs

async action({ ctx, args }: CommandParams<DeleteArgs>): Promise<CommandResult<DeleteConfigResult>> {
async action({ ctx, args }: CommandParams<DeleteConfigArgs>): Promise<CommandResult<DeleteConfigResult>> {
const key = args.key.split(".")
const result = await ctx.deleteConfig({ key })

Expand Down Expand Up @@ -97,3 +101,47 @@ export class DeleteEnvironmentCommand extends Command {
return { result }
}
}

export const deleteServiceArgs = {
service: new StringsParameter({
help: "The name of the service(s) to delete. Use comma as separator to specify multiple services.",
required: true,
}),
}
export type DeleteServiceArgs = ParameterValues<typeof deleteServiceArgs>

export class DeleteServiceCommand extends Command {
name = "service"
help = "Deletes a running service."
arguments = deleteServiceArgs

description = dedent`
Deletes (i.e. un-deploys) the specified services. Note that this command does not take into account any
services depending on the deleted service, and might therefore leave the project in an unstable state.
Running \`garden deploy\` will re-deploy any missing services.
Examples:
garden delete service my-service # deletes my-service
`

async action({ ctx, args }: CommandParams<DeleteServiceArgs>): Promise<CommandResult> {
const services = await ctx.getServices(args.service)

if (services.length === 0) {
ctx.log.warn({ msg: "No services found. Aborting." })
return { result: {} }
}

ctx.log.header({ emoji: "skull_and_crossbones", command: `Delete service` })

const result: { [key: string]: ServiceStatus } = {}

await Bluebird.map(services, async service => {
result[service.name] = await ctx.deleteService({ serviceName: service.name })
})

ctx.log.finish()
return { result }
}
}
26 changes: 26 additions & 0 deletions garden-cli/src/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
BuildModuleParams,
DeleteConfigParams,
DeployServiceParams,
DeleteServiceParams,
ExecInServiceParams,
GetConfigParams,
GetModuleBuildStatusParams,
Expand Down Expand Up @@ -150,6 +151,8 @@ export interface PluginContext extends PluginContextGuard, WrappedFromGarden {
=> Promise<ServiceStatus>
deployService: <T extends Module>(params: PluginContextServiceParams<DeployServiceParams<T>>)
=> Promise<ServiceStatus>
deleteService: <T extends Module>(params: PluginContextServiceParams<DeleteServiceParams<T>>)
=> Promise<ServiceStatus>
getServiceOutputs: <T extends Module>(params: PluginContextServiceParams<GetServiceOutputsParams<T>>)
=> Promise<PrimitiveMap>
execInService: <T extends Module>(params: PluginContextServiceParams<ExecInServiceParams<T>>)
Expand Down Expand Up @@ -407,6 +410,19 @@ export function createPluginContext(garden: Garden): PluginContext {
return callServiceHandler({ params, actionType: "deployService" })
},

deleteService: async (params: PluginContextServiceParams<DeleteServiceParams>) => {
const logEntry = garden.log.info({
section: params.serviceName,
msg: "Deleting...",
entryStyle: EntryStyle.activity,
})
return callServiceHandler({
params: { ...params, logEntry },
actionType: "deleteService",
defaultHandler: dummyDeleteServiceHandler,
})
},

getServiceOutputs: async (params: PluginContextServiceParams<GetServiceOutputsParams>) => {
return callServiceHandler({
params,
Expand Down Expand Up @@ -497,3 +513,13 @@ const dummyLogStreamer = async ({ ctx, service }: GetServiceLogsParams) => {
const dummyPushHandler = async ({ module }: PushModuleParams) => {
return { pushed: false, message: chalk.yellow(`No push handler available for module type ${module.type}`) }
}

const dummyDeleteServiceHandler = async ({ ctx, module, logEntry }: DeleteServiceParams) => {
const msg = `No delete service handler available for module type ${module.type}`
if (logEntry) {
logEntry.setError(msg)
} else {
ctx.log.error(msg)
}
return {}
}
17 changes: 14 additions & 3 deletions garden-cli/src/plugins/kubernetes/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
RunModuleParams,
SetConfigParams,
TestModuleParams,
DeleteServiceParams,
} from "../../types/plugin/params"
import { ModuleVersion } from "../../vcs/base"
import { ContainerModule, helpers } from "../container"
Expand All @@ -44,7 +45,8 @@ import { KUBECTL_DEFAULT_TIMEOUT, kubectl } from "./kubectl"
import { DEFAULT_TEST_TIMEOUT } from "../../constants"
import { EntryStyle, LogSymbolType } from "../../logger/types"
import { name as providerName } from "./kubernetes"
import { getContainerServiceStatus } from "./deployment"
import { deleteContainerService, getContainerServiceStatus } from "./deployment"
import { ServiceStatus } from "../../types/service"

const MAX_STORED_USERNAMES = 5

Expand All @@ -56,13 +58,13 @@ export async function getEnvironmentStatus({ ctx, provider }: GetEnvironmentStat
await kubectl(context).call(["version"])
} catch (err) {
// TODO: catch error properly
if (err.output) {
if (err.detail.output) {
throw new DeploymentError(
`Unable to connect to Kubernetes cluster. ` +
`Please make sure it is running, reachable and that you have the right context configured.`,
{
context,
kubectlOutput: err.output,
kubectlOutput: err.detail.output,
},
)
}
Expand Down Expand Up @@ -124,6 +126,15 @@ export async function destroyEnvironment({ ctx, provider }: DestroyEnvironmentPa
return {}
}

export async function deleteService(params: DeleteServiceParams): Promise<ServiceStatus> {
const { ctx, logEntry, provider, service } = params
const namespace = await getAppNamespace(ctx, provider)

await deleteContainerService({ logEntry, namespace, provider, serviceName: service.name })

return getContainerServiceStatus(params)
}

export async function getServiceOutputs({ service }: GetServiceOutputsParams<ContainerModule>) {
return {
host: service.name,
Expand Down
27 changes: 27 additions & 0 deletions garden-cli/src/plugins/kubernetes/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import { KubernetesObject } from "./helm"
import { PluginContext } from "../../plugin-context"
import { KubernetesProvider } from "./kubernetes"
import { GARDEN_ANNOTATION_KEYS_VERSION } from "../../constants"
import { Provider } from "../../types/plugin/plugin"
import { extensionsApi } from "./api"
import { LogEntry } from "../../logger/logger"

export const DEFAULT_CPU_REQUEST = "10m"
export const DEFAULT_CPU_LIMIT = "500m"
Expand Down Expand Up @@ -337,3 +340,27 @@ export async function createDeployment(

return deployment
}

export async function deleteContainerService({ namespace, provider, serviceName, logEntry }: {
namespace: string,
provider: Provider,
serviceName: string,
logEntry?: LogEntry,
}) {
const { context } = provider.config
let found = true

try {
await extensionsApi(context).deleteNamespacedDeployment(serviceName, namespace, <any>{})
} catch (err) {
if (err.code === 404) {
found = false
} else {
throw err
}
}

if (logEntry) {
found ? logEntry.setSuccess("Service deleted") : logEntry.setWarn("Service not deployed")
}
}
69 changes: 40 additions & 29 deletions garden-cli/src/plugins/kubernetes/helm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
DeployServiceParams,
GetServiceStatusParams,
ParseModuleParams,
DeleteServiceParams,
} from "../../types/plugin/params"
import {
BuildResult,
Expand Down Expand Up @@ -133,35 +134,7 @@ export const helmHandlers: Partial<ModuleActions<HelmModule>> = {

getModuleBuildStatus: getGenericModuleBuildStatus,
buildModule,

async getServiceStatus(
{ ctx, env, provider, service, module, logEntry }: GetServiceStatusParams<HelmModule>,
): Promise<ServiceStatus> {
// need to build to be able to check the status
const buildStatus = await getGenericModuleBuildStatus({ ctx, env, provider, module, logEntry })
if (!buildStatus.ready) {
await buildModule({ ctx, env, provider, module, logEntry })
}

// first check if the installed objects on the cluster match the current code
const objects = await getChartObjects(ctx, provider, service)
const matched = await compareDeployedObjects(ctx, provider, objects)

if (!matched) {
return { state: "outdated" }
}

// then check if the rollout is complete
const version = module.version
const context = provider.config.context
const namespace = await getAppNamespace(ctx, provider)
const { ready } = await checkObjectStatus(context, namespace, objects)

// TODO: set state to "unhealthy" if any status is "unhealthy"
const state = ready ? "ready" : "deploying"

return { state, version: version.versionString }
},
getServiceStatus,

async deployService(
{ ctx, provider, module, service, logEntry }: DeployServiceParams<HelmModule>,
Expand Down Expand Up @@ -195,6 +168,15 @@ export const helmHandlers: Partial<ModuleActions<HelmModule>> = {

return {}
},

async deleteService(params: DeleteServiceParams): Promise<ServiceStatus> {
const { ctx, logEntry, provider, service } = params
const releaseName = getReleaseName(ctx, service)
await helm(provider, "delete", "--purge", releaseName)
logEntry && logEntry.setSuccess("Service deleted")

return await getServiceStatus(params)
},
}

async function buildModule({ ctx, provider, module, logEntry }: BuildModuleParams<HelmModule>): Promise<BuildResult> {
Expand Down Expand Up @@ -282,6 +264,35 @@ async function getChartObjects(ctx: PluginContext, provider: Provider, service:
})
}

async function getServiceStatus(
{ ctx, env, provider, service, module, logEntry }: GetServiceStatusParams<HelmModule>,
): Promise<ServiceStatus> {
// need to build to be able to check the status
const buildStatus = await getGenericModuleBuildStatus({ ctx, env, provider, module, logEntry })
if (!buildStatus.ready) {
await buildModule({ ctx, env, provider, module, logEntry })
}

// first check if the installed objects on the cluster match the current code
const objects = await getChartObjects(ctx, provider, service)
const matched = await compareDeployedObjects(ctx, provider, objects)

if (!matched) {
return { state: "outdated" }
}

// then check if the rollout is complete
const version = module.version
const context = provider.config.context
const namespace = await getAppNamespace(ctx, provider)
const { ready } = await checkObjectStatus(context, namespace, objects)

// TODO: set state to "unhealthy" if any status is "unhealthy"
const state = ready ? "ready" : "deploying"

return { state, version: version.versionString }
}

function getReleaseName(ctx: PluginContext, service: Service) {
return `garden--${ctx.projectName}--${service.name}`
}
Expand Down
2 changes: 2 additions & 0 deletions garden-cli/src/plugins/kubernetes/kubernetes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
configureEnvironment,
deleteConfig,
destroyEnvironment,
deleteService,
execInService,
getConfig,
getEnvironmentStatus,
Expand Down Expand Up @@ -105,6 +106,7 @@ export function gardenPlugin({ config }: { config: KubernetesConfig }): GardenPl
container: {
getServiceStatus: getContainerServiceStatus,
deployService: deployContainerService,
deleteService,
getServiceOutputs,
execInService,
runModule,
Expand Down
Loading

0 comments on commit 2b067c6

Please sign in to comment.