From 1488cd8295cf05cd06af767a8d2fbe939d6f49cd Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Thu, 28 Mar 2019 03:48:19 +0100 Subject: [PATCH] feat(k8s): add kubernetes module type For those times when you want to deploy your own manifests, but don't need the features and complexity of Helm modules. See the kubernetes-module example for usage. --- docs/reference/module-types/kubernetes.md | 148 +++++++ examples/kubernetes-module/README.md | 17 + examples/kubernetes-module/garden.yml | 8 + .../kubernetes-module/postgres/garden.yml | 168 ++++++++ examples/kubernetes-module/redis/garden.yml | 5 + examples/kubernetes-module/redis/redis.yml | 372 ++++++++++++++++++ garden-service/src/docs/config.ts | 2 + .../kubernetes/kubernetes-module/config.ts | 87 ++++ .../kubernetes/kubernetes-module/handlers.ts | 140 +++++++ .../src/plugins/kubernetes/kubernetes.ts | 2 + 10 files changed, 949 insertions(+) create mode 100644 docs/reference/module-types/kubernetes.md create mode 100644 examples/kubernetes-module/README.md create mode 100644 examples/kubernetes-module/garden.yml create mode 100644 examples/kubernetes-module/postgres/garden.yml create mode 100644 examples/kubernetes-module/redis/garden.yml create mode 100644 examples/kubernetes-module/redis/redis.yml create mode 100644 garden-service/src/plugins/kubernetes/kubernetes-module/config.ts create mode 100644 garden-service/src/plugins/kubernetes/kubernetes-module/handlers.ts diff --git a/docs/reference/module-types/kubernetes.md b/docs/reference/module-types/kubernetes.md new file mode 100644 index 0000000000..60be33be01 --- /dev/null +++ b/docs/reference/module-types/kubernetes.md @@ -0,0 +1,148 @@ +# `kubernetes` reference + +Below is the schema reference for the `kubernetes` module type. For an introduction to configuring Garden modules, please look at our [Configuration guide](../../using-garden/configuration-files.md). + +The reference is divided into two sections. The [first section](#configuration-keys) lists and describes the available schema keys. The [second section](#complete-yaml-schema) contains the complete YAML schema. + +## Configuration keys + +### `module` + + + +| Type | Required | +| ---- | -------- | +| `object` | No +### `module.build` +[module](#module) > build + +Specify how to build the module. Note that plugins may define additional keys on this object. + +| Type | Required | +| ---- | -------- | +| `object` | No +### `module.build.dependencies[]` +[module](#module) > [build](#module.build) > dependencies + +A list of modules that must be built before this module is built. + +| Type | Required | +| ---- | -------- | +| `array[object]` | No + +Example: +```yaml +module: + ... + build: + ... + dependencies: + - name: some-other-module-name +``` +### `module.build.dependencies[].name` +[module](#module) > [build](#module.build) > [dependencies](#module.build.dependencies[]) > name + +Module name to build ahead of this module. + +| Type | Required | +| ---- | -------- | +| `string` | Yes +### `module.build.dependencies[].copy[]` +[module](#module) > [build](#module.build) > [dependencies](#module.build.dependencies[]) > copy + +Specify one or more files or directories to copy from the built dependency to this module. + +| Type | Required | +| ---- | -------- | +| `array[object]` | No +### `module.build.dependencies[].copy[].source` +[module](#module) > [build](#module.build) > [dependencies](#module.build.dependencies[]) > [copy](#module.build.dependencies[].copy[]) > source + +POSIX-style path or filename of the directory or file(s) to copy to the target. + +| Type | Required | +| ---- | -------- | +| `string` | Yes +### `module.build.dependencies[].copy[].target` +[module](#module) > [build](#module.build) > [dependencies](#module.build.dependencies[]) > [copy](#module.build.dependencies[].copy[]) > target + +POSIX-style path or filename to copy the directory or file(s) to (defaults to same as source path). + +| Type | Required | +| ---- | -------- | +| `string` | No +### `module.dependencies[]` +[module](#module) > dependencies + +List of names of services that should be deployed before this chart. + +| Type | Required | +| ---- | -------- | +| `array[string]` | No +### `module.manifests[]` +[module](#module) > manifests + +List of Kubernetes resource manifests to deploy. Use this instead of the `files` field if you need to resolve template strings in any of the manifests. + +| Type | Required | +| ---- | -------- | +| `array[object]` | No +### `module.manifests[].apiVersion` +[module](#module) > [manifests](#module.manifests[]) > apiVersion + +The API version of the resource. + +| Type | Required | +| ---- | -------- | +| `string` | Yes +### `module.manifests[].kind` +[module](#module) > [manifests](#module.manifests[]) > kind + +The kind of the resource. + +| Type | Required | +| ---- | -------- | +| `string` | Yes +### `module.manifests[].metadata` +[module](#module) > [manifests](#module.manifests[]) > metadata + + + +| Type | Required | +| ---- | -------- | +| `object` | Yes +### `module.manifests[].metadata.name` +[module](#module) > [manifests](#module.manifests[]) > [metadata](#module.manifests[].metadata) > name + +The name of the resource. + +| Type | Required | +| ---- | -------- | +| `string` | Yes +### `module.files[]` +[module](#module) > files + +POSIX-style paths to YAML files to load manifests from. Each can contain multiple manifests. + +| Type | Required | +| ---- | -------- | +| `array[string]` | No + + +## Complete YAML schema +```yaml +module: + build: + dependencies: + - name: + copy: + - source: + target: '' + dependencies: [] + manifests: + - apiVersion: + kind: + metadata: + name: + files: [] +``` diff --git a/examples/kubernetes-module/README.md b/examples/kubernetes-module/README.md new file mode 100644 index 0000000000..200d912761 --- /dev/null +++ b/examples/kubernetes-module/README.md @@ -0,0 +1,17 @@ +# `kubernetes` module type example + +This is a simple example demonstrating the `kubernetes` module type. +The `kubernetes` module type is useful when you want to deploy your own manifests to Kubernetes, but don't need the +features (and complexity) of `helm` modules. + +This example contains a `redis` module and a `postgres` module. Both contain manifests that are, for the purposes of +this example, rendered from their respective official Helm charts (by running the `helm template` command). + +The `redis` module has its manifests in a separate YAML file, whereas the `postgres` module has the manifests inlined +in the `garden.yml` file, which allows us to use template strings to set variable values. +We set the Postgres instance password through a variable in the project `garden.yml` to demonstrate that capability. + +To give it a spin, just run `garden deploy` in the module directory. + +To see that the Postgres password was correctly set, run `kubectl -n kubernetes-module get secret postgres -o yaml` +and check that the password matches the one in the project `garden.yml` file. diff --git a/examples/kubernetes-module/garden.yml b/examples/kubernetes-module/garden.yml new file mode 100644 index 0000000000..8b771dcb07 --- /dev/null +++ b/examples/kubernetes-module/garden.yml @@ -0,0 +1,8 @@ +kind: Project +name: kubernetes-module +environments: + - name: local + providers: + - name: local-kubernetes + variables: + postgres-password: "Y0RGQjBHUmpWZQ==" diff --git a/examples/kubernetes-module/postgres/garden.yml b/examples/kubernetes-module/postgres/garden.yml new file mode 100644 index 0000000000..6a6a2a658c --- /dev/null +++ b/examples/kubernetes-module/postgres/garden.yml @@ -0,0 +1,168 @@ +kind: Module +type: kubernetes +name: postgres +description: Postgres deployment with kubernetes manifests inlined (extracted from the stable/postgresql Helm chart) +manifests: + # Source: postgresql/templates/secrets.yaml + - apiVersion: v1 + kind: Secret + metadata: + name: postgres + labels: + app: postgresql + chart: postgresql-3.9.2 + release: "postgres" + heritage: "Tiller" + type: Opaque + data: + postgresql-password: ${variables.postgres-password} + # Source: postgresql/templates/svc-headless.yaml + - apiVersion: v1 + kind: Service + metadata: + name: postgres-headless + labels: + app: postgresql + chart: postgresql-3.9.2 + release: "postgres" + heritage: "Tiller" + spec: + type: ClusterIP + clusterIP: None + ports: + - name: postgresql + port: 5432 + targetPort: postgresql + selector: + app: postgresql + release: "postgres" + # Source: postgresql/templates/svc.yaml + - apiVersion: v1 + kind: Service + metadata: + name: postgres + labels: + app: postgresql + chart: postgresql-3.9.2 + release: "postgres" + heritage: "Tiller" + spec: + type: ClusterIP + ports: + - name: postgresql + port: 5432 + targetPort: postgresql + selector: + app: postgresql + release: "postgres" + role: master + - # Source: postgresql/templates/statefulset.yaml + apiVersion: apps/v1beta2 + kind: StatefulSet + metadata: + name: postgres + labels: + app: postgresql + chart: postgresql-3.9.2 + release: "postgres" + heritage: "Tiller" + spec: + serviceName: postgres-headless + replicas: 1 + updateStrategy: + type: RollingUpdate + selector: + matchLabels: + app: postgresql + release: "postgres" + role: master + template: + metadata: + name: postgres + labels: + app: postgresql + chart: postgresql-3.9.2 + release: "postgres" + heritage: "Tiller" + role: master + spec: + securityContext: + fsGroup: 1001 + runAsUser: 1001 + initContainers: + - name: init-chmod-data + image: docker.io/bitnami/minideb:latest + imagePullPolicy: "Always" + resources: + requests: + cpu: 250m + memory: 256Mi + + command: + - sh + - -c + - | + chown -R 1001:1001 /bitnami + if [ -d /bitnami/postgresql/data ]; then + chmod 0700 /bitnami/postgresql/data; + fi + securityContext: + runAsUser: 0 + volumeMounts: + - name: data + mountPath: /bitnami/postgresql + containers: + - name: postgres + image: docker.io/bitnami/postgresql:10.6.0 + imagePullPolicy: "Always" + resources: + requests: + cpu: 250m + memory: 256Mi + + env: + - name: POSTGRESQL_USERNAME + value: "postgres" + - name: POSTGRESQL_PASSWORD + valueFrom: + secretKeyRef: + name: postgres + key: postgresql-password + ports: + - name: postgresql + containerPort: 5432 + livenessProbe: + exec: + command: + - sh + - -c + - exec pg_isready -U "postgres" -h localhost + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 6 + readinessProbe: + exec: + command: + - sh + - -c + - exec pg_isready -U "postgres" -h localhost + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 6 + volumeMounts: + - name: data + mountPath: /bitnami/postgresql + volumes: + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: "8Gi" diff --git a/examples/kubernetes-module/redis/garden.yml b/examples/kubernetes-module/redis/garden.yml new file mode 100644 index 0000000000..67515f6a0f --- /dev/null +++ b/examples/kubernetes-module/redis/garden.yml @@ -0,0 +1,5 @@ +kind: Module +type: kubernetes +name: redis +description: Redis deployment with kubernetes manifests in a file ((extracted from the stable/redis Helm chart)) +files: [redis.yml] diff --git a/examples/kubernetes-module/redis/redis.yml b/examples/kubernetes-module/redis/redis.yml new file mode 100644 index 0000000000..fce0e38a82 --- /dev/null +++ b/examples/kubernetes-module/redis/redis.yml @@ -0,0 +1,372 @@ +--- +# Source: redis/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: redis + labels: + app: redis + chart: redis-6.1.4 + release: "redis" + heritage: "Tiller" +type: Opaque +data: + redis-password: "OHlWVEVYeVdrMg==" +--- +# Source: redis/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: redis + chart: redis-6.1.4 + heritage: Tiller + release: redis + name: redis +data: + redis.conf: |- + # User-supplied configuration: + # maxmemory-policy volatile-lru + master.conf: |- + dir /data + rename-command FLUSHDB "" + rename-command FLUSHALL "" + replica.conf: |- + dir /data + rename-command FLUSHDB "" + rename-command FLUSHALL "" + +--- +# Source: redis/templates/health-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: redis + chart: redis-6.1.4 + heritage: Tiller + release: redis + name: redis-health +data: + ping_local.sh: |- + response=$( + timeout -s 9 $1 \ + redis-cli \ + -a $REDIS_PASSWORD \ + -h localhost \ + -p $REDIS_PORT \ + ping + ) + if [ "$response" != "PONG" ]; then + echo "$response" + exit 1 + fi + ping_master.sh: |- + response=$( + timeout -s 9 $1 \ + redis-cli \ + -a $REDIS_MASTER_PASSWORD \ + -h $REDIS_MASTER_HOST \ + -p $REDIS_MASTER_PORT_NUMBER \ + ping + ) + if [ "$response" != "PONG" ]; then + echo "$response" + exit 1 + fi + ping_local_and_master.sh: |- + script_dir="$(dirname "$0")" + exit_status=0 + "$script_dir/ping_local.sh" $1 || exit_status=$? + "$script_dir/ping_master.sh" $1 || exit_status=$? + exit $exit_status + +--- +# Source: redis/templates/redis-master-svc.yaml +apiVersion: v1 +kind: Service +metadata: + name: redis-master + labels: + app: redis + chart: redis-6.1.4 + release: "redis" + heritage: "Tiller" +spec: + type: ClusterIP + ports: + - name: redis + port: 6379 + targetPort: redis + selector: + app: redis + release: "redis" + role: master + +--- +# Source: redis/templates/redis-slave-svc.yaml + +apiVersion: v1 +kind: Service +metadata: + name: redis-slave + labels: + app: redis + chart: redis-6.1.4 + release: "redis" + heritage: "Tiller" +spec: + type: ClusterIP + ports: + - name: redis + port: 6379 + targetPort: redis + selector: + app: redis + release: "redis" + role: slave + +--- +# Source: redis/templates/redis-slave-deployment.yaml + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: redis-slave + labels: + app: redis + chart: redis-6.1.4 + release: "redis" + heritage: "Tiller" +spec: + replicas: 1 + selector: + matchLabels: + release: "redis" + role: slave + app: redis + template: + metadata: + labels: + release: "redis" + chart: redis-6.1.4 + role: slave + app: redis + annotations: + checksum/health: 0d265b1764dff8b0866f417fa0435130cfcbb943027072f9d3277ff2dc7cec80 + checksum/configmap: f863a1b96078520044ba90b6f93228047259b97d410934284a22029eaf6672ac + checksum/secret: bed46c594bad5453acf41624e4cebddad3188d1fd50adf5f3360cb5b80a8ecb6 + spec: + securityContext: + fsGroup: 1001 + serviceAccountName: "default" + containers: + - name: redis + image: docker.io/bitnami/redis:4.0.13 + imagePullPolicy: "Always" + securityContext: + runAsUser: 1001 + command: + - /bin/bash + - -c + - | + if [[ -n $REDIS_PASSWORD_FILE ]]; then + password_aux=`cat ${REDIS_PASSWORD_FILE}` + export REDIS_PASSWORD=$password_aux + fi + if [[ -n $REDIS_MASTER_PASSWORD_FILE ]]; then + password_aux=`cat ${REDIS_MASTER_PASSWORD_FILE}` + export REDIS_MASTER_PASSWORD=$password_aux + fi + ARGS=("--port" "${REDIS_PORT}") + ARGS+=("--requirepass" "${REDIS_PASSWORD}") + ARGS+=("--slaveof" "${REDIS_MASTER_HOST}" "${REDIS_MASTER_PORT_NUMBER}") + ARGS+=("--masterauth" "${REDIS_MASTER_PASSWORD}") + ARGS+=("--include" "/opt/bitnami/redis/etc/redis.conf") + ARGS+=("--include" "/opt/bitnami/redis/etc/replica.conf") + /run.sh "${ARGS[@]}" + env: + - name: REDIS_REPLICATION_MODE + value: slave + - name: REDIS_MASTER_HOST + value: redis-master + - name: REDIS_PORT + value: "6379" + - name: REDIS_MASTER_PORT_NUMBER + value: "6379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: redis + key: redis-password + - name: REDIS_MASTER_PASSWORD + valueFrom: + secretKeyRef: + name: redis + key: redis-password + ports: + - name: redis + containerPort: 6379 + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 5 + exec: + command: + - sh + - -c + - /health/ping_local_and_master.sh 5 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 5 + exec: + command: + - sh + - -c + - /health/ping_local_and_master.sh 1 + resources: null + + volumeMounts: + - name: health + mountPath: /health + - name: redis-data + mountPath: /data + - name: config + mountPath: /opt/bitnami/redis/etc + volumes: + - name: health + configMap: + name: redis-health + defaultMode: 0755 + - name: config + configMap: + name: redis + - name: redis-data + emptyDir: {} + +--- +# Source: redis/templates/redis-master-statefulset.yaml +apiVersion: apps/v1beta2 +kind: StatefulSet +metadata: + name: redis-master + labels: + app: redis + chart: redis-6.1.4 + release: "redis" + heritage: "Tiller" +spec: + selector: + matchLabels: + release: "redis" + role: master + app: redis + serviceName: redis-master + template: + metadata: + labels: + release: "redis" + chart: redis-6.1.4 + role: master + app: redis + annotations: + checksum/health: 0d265b1764dff8b0866f417fa0435130cfcbb943027072f9d3277ff2dc7cec80 + checksum/configmap: f863a1b96078520044ba90b6f93228047259b97d410934284a22029eaf6672ac + checksum/secret: c1a9aff755c75a492d5c1a55d5e0ad4ffb86b94ab710ec4a1f286b54255bc005 + spec: + securityContext: + fsGroup: 1001 + serviceAccountName: "default" + containers: + - name: redis + image: "docker.io/bitnami/redis:4.0.13" + imagePullPolicy: "Always" + securityContext: + runAsUser: 1001 + command: + - /bin/bash + - -c + - | + if [[ -n $REDIS_PASSWORD_FILE ]]; then + password_aux=`cat ${REDIS_PASSWORD_FILE}` + export REDIS_PASSWORD=$password_aux + fi + ARGS=("--port" "${REDIS_PORT}") + ARGS+=("--requirepass" "${REDIS_PASSWORD}") + ARGS+=("--include" "/opt/bitnami/redis/etc/redis.conf") + ARGS+=("--include" "/opt/bitnami/redis/etc/master.conf") + /run.sh ${ARGS[@]} + env: + - name: REDIS_REPLICATION_MODE + value: master + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: redis + key: redis-password + - name: REDIS_PORT + value: "6379" + ports: + - name: redis + containerPort: 6379 + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 5 + exec: + command: + - sh + - -c + - /health/ping_local.sh 5 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 5 + exec: + command: + - sh + - -c + - /health/ping_local.sh 5 + resources: null + + volumeMounts: + - name: health + mountPath: /health + - name: redis-data + mountPath: /data + subPath: + - name: config + mountPath: /opt/bitnami/redis/etc + volumes: + - name: health + configMap: + name: redis-health + defaultMode: 0755 + - name: config + configMap: + name: redis + volumeClaimTemplates: + - metadata: + name: redis-data + labels: + app: "redis" + component: "master" + release: "redis" + heritage: "Tiller" + spec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: "8Gi" + updateStrategy: + type: RollingUpdate diff --git a/garden-service/src/docs/config.ts b/garden-service/src/docs/config.ts index 756a70788d..c4e8f2fc09 100644 --- a/garden-service/src/docs/config.ts +++ b/garden-service/src/docs/config.ts @@ -31,6 +31,7 @@ import { openfaasModuleSpecSchema } from "../plugins/openfaas/openfaas" import { helmModuleSpecSchema } from "../plugins/kubernetes/helm/config" import { joiArray } from "../config/common" import { mavenContainerModuleSpecSchema, mavenContainerConfigSchema } from "../plugins/maven-container/maven-container" +import { kubernetesModuleSpecSchema } from "../plugins/kubernetes/kubernetes-module/config" const baseProjectSchema = Joi.object().keys({ project: projectSchema, @@ -57,6 +58,7 @@ const moduleTypes = [ { name: "openfaas", schema: populateModuleSchema(openfaasModuleSpecSchema) }, { name: "helm", schema: populateModuleSchema(helmModuleSpecSchema) }, { name: "maven-container", schema: populateModuleSchema(mavenContainerModuleSpecSchema) }, + { name: "kubernetes", schema: populateModuleSchema(kubernetesModuleSpecSchema) }, ] const providers = [ diff --git a/garden-service/src/plugins/kubernetes/kubernetes-module/config.ts b/garden-service/src/plugins/kubernetes/kubernetes-module/config.ts new file mode 100644 index 0000000000..f909ff6ab1 --- /dev/null +++ b/garden-service/src/plugins/kubernetes/kubernetes-module/config.ts @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * 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 Joi = require("joi") + +import { ServiceSpec } from "../../../config/service" +import { joiArray, joiIdentifier, validateWithPath } from "../../../config/common" +import { Module } from "../../../types/module" +import { ConfigureModuleParams } from "../../../types/plugin/params" +import { ConfigureModuleResult } from "../../../types/plugin/outputs" +import { Service } from "../../../types/service" +import { ContainerModule } from "../../container/config" +import { baseBuildSpecSchema } from "../../../config/module" +import { KubernetesResource } from "../types" +import { deline } from "../../../util/string" + +// A Kubernetes Module always maps to a single Service +export type KubernetesModuleSpec = KubernetesServiceSpec + +export interface KubernetesModule extends Module { } +export type KubernetesModuleConfig = KubernetesModule["_ConfigType"] + +export interface KubernetesServiceSpec extends ServiceSpec { + dependencies: string[] + files: string[] + manifests: KubernetesResource[] +} + +export type KubernetesService = Service + +const kubernetesResourceSchema = Joi.object() + .keys({ + apiVersion: Joi.string() + .required() + .description("The API version of the resource."), + kind: Joi.string() + .required() + .description("The kind of the resource."), + metadata: Joi.object() + .required() + .keys({ + name: Joi.string() + .required() + .description("The name of the resource."), + }) + .unknown(true), + }) + .unknown(true) + +export const kubernetesModuleSpecSchema = Joi.object() + .keys({ + build: baseBuildSpecSchema, + dependencies: joiArray(joiIdentifier()) + .description("List of names of services that should be deployed before this chart."), + manifests: joiArray(kubernetesResourceSchema) + .description( + deline` + List of Kubernetes resource manifests to deploy. Use this instead of the \`files\` field if you need to + resolve template strings in any of the manifests.`), + files: joiArray(Joi.string().uri({ relativeOnly: true })) + .description("POSIX-style paths to YAML files to load manifests from. Each can contain multiple manifests."), + }) + +export async function configureKubernetesModule({ ctx, moduleConfig }: ConfigureModuleParams) + : Promise> { + moduleConfig.spec = validateWithPath({ + config: moduleConfig.spec, + schema: kubernetesModuleSpecSchema, + name: moduleConfig.name, + path: moduleConfig.path, + projectRoot: ctx.projectRoot, + }) + + moduleConfig.serviceConfigs = [{ + name: moduleConfig.name, + dependencies: moduleConfig.spec.dependencies, + outputs: {}, + spec: moduleConfig.spec, + }] + + return moduleConfig +} diff --git a/garden-service/src/plugins/kubernetes/kubernetes-module/handlers.ts b/garden-service/src/plugins/kubernetes/kubernetes-module/handlers.ts new file mode 100644 index 0000000000..20933ee761 --- /dev/null +++ b/garden-service/src/plugins/kubernetes/kubernetes-module/handlers.ts @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * 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 { resolve } from "path" +import { readFile } from "fs-extra" +import * as Bluebird from "bluebird" +import { flatten, set, uniq } from "lodash" +import { safeLoadAll } from "js-yaml" + +import { + BuildModuleParams, + GetServiceStatusParams, + DeployServiceParams, + DeleteServiceParams, + GetServiceLogsParams, +} from "../../../types/plugin/params" +import { KubernetesModule, configureKubernetesModule, KubernetesService } from "./config" +import { BuildResult } from "../../../types/plugin/outputs" +import { getNamespace, getAppNamespace } from "../namespace" +import { KubernetesPluginContext } from "../kubernetes" +import { KubernetesResource } from "../types" +import { ServiceStatus } from "../../../types/service" +import { applyMany, deleteObjectsByLabel } from "../kubectl" +import { GARDEN_ANNOTATION_KEYS_SERVICE } from "../../../constants" +import { compareDeployedObjects, waitForResources } from "../status" +import { KubeApi } from "../api" +import { ModuleAndRuntimeActions } from "../../../types/plugin/plugin" +import { getAllLogs } from "../logs" + +export const kubernetesHandlers: Partial> = { + build, + configure: configureKubernetesModule, + deleteService, + deployService, + getServiceLogs, + getServiceStatus, +} + +async function build({ module }: BuildModuleParams): Promise { + // Get the manifests here, just to validate that the files are there and are valid YAML + await getManifests(module) + return { fresh: true } +} + +async function getServiceStatus( + { ctx, module, log }: GetServiceStatusParams, +): Promise { + const k8sCtx = ctx + const namespace = await getNamespace({ ctx: k8sCtx, provider: k8sCtx.provider, skipCreate: true }) + const context = ctx.provider.config.context + const api = new KubeApi(context) + const manifests = await getManifests(module) + + const { state, remoteObjects } = await compareDeployedObjects(k8sCtx, api, namespace, manifests, log) + + return { + state, + version: state === "ready" ? module.version.versionString : undefined, + detail: { remoteObjects }, + } +} + +async function deployService( + params: DeployServiceParams, +): Promise { + const { ctx, force, module, service, log } = params + + const k8sCtx = ctx + const namespace = await getNamespace({ ctx: k8sCtx, provider: k8sCtx.provider, skipCreate: true }) + const context = ctx.provider.config.context + const manifests = await getManifests(module) + + const pruneSelector = getSelector(service) + await applyMany(context, manifests, { force, namespace, pruneSelector }) + + await waitForResources({ + ctx: k8sCtx, + provider: k8sCtx.provider, + serviceName: service.name, + resources: manifests, + log, + }) + + return getServiceStatus(params) +} + +async function deleteService(params: DeleteServiceParams): Promise { + const { ctx, service, module } = params + const k8sCtx = ctx + const namespace = await getAppNamespace(k8sCtx, k8sCtx.provider) + const provider = k8sCtx.provider + const manifests = await getManifests(module) + + const context = provider.config.context + await deleteObjectsByLabel({ + context, + namespace, + labelKey: GARDEN_ANNOTATION_KEYS_SERVICE, + labelValue: service.name, + objectTypes: uniq(manifests.map(m => m.kind)), + includeUninitialized: false, + }) + + return getServiceStatus({ ...params, hotReload: false }) +} + +async function getServiceLogs(params: GetServiceLogsParams) { + const { ctx, module } = params + const k8sCtx = ctx + const context = k8sCtx.provider.config.context + const namespace = await getAppNamespace(k8sCtx, k8sCtx.provider) + const manifests = await getManifests(module) + + return getAllLogs({ ...params, context, namespace, resources: manifests }) +} + +function getSelector(service: KubernetesService) { + return `${GARDEN_ANNOTATION_KEYS_SERVICE}=${service.name}` +} + +async function getManifests(module: KubernetesModule): Promise { + const fileManifests = flatten(await Bluebird.map(module.spec.files, async (path) => { + const absPath = resolve(module.buildPath, path) + return safeLoadAll((await readFile(absPath)).toString()) + })) + + const manifests = [...module.spec.manifests, ...fileManifests] + + // Add a label, so that we can identify the manifests as part of this module, and prune if needed + return manifests.map(manifest => { + set(manifest, ["metadata", "annotations", GARDEN_ANNOTATION_KEYS_SERVICE], module.name) + set(manifest, ["metadata", "labels", GARDEN_ANNOTATION_KEYS_SERVICE], module.name) + return manifest + }) +} diff --git a/garden-service/src/plugins/kubernetes/kubernetes.ts b/garden-service/src/plugins/kubernetes/kubernetes.ts index b2d2cc5386..2b9a0f553e 100644 --- a/garden-service/src/plugins/kubernetes/kubernetes.ts +++ b/garden-service/src/plugins/kubernetes/kubernetes.ts @@ -18,6 +18,7 @@ import { containerRegistryConfigSchema, ContainerRegistryConfig } from "../conta import { getRemoteEnvironmentStatus, prepareRemoteEnvironment, cleanupEnvironment } from "./init" import { containerHandlers, mavenContainerHandlers } from "./container/handlers" import { PluginContext } from "../../plugin-context" +import { kubernetesHandlers } from "./kubernetes-module/handlers" export const name = "kubernetes" @@ -158,6 +159,7 @@ export function gardenPlugin(): GardenPlugin { // TODO: we should find a way to avoid having to explicitly specify the key here "maven-container": mavenContainerHandlers, "helm": helmHandlers, + "kubernetes": kubernetesHandlers, }, } }