Skip to content

Commit

Permalink
feat(k8s): add configmap module type, mountable on container modules
Browse files Browse the repository at this point in the history
This facilitates mounting ConfigMap resources as volumes in container
modules.

The added docs:

---

## Mounting Kubernetes ConfigMaps

Very similar to the above example, you can also mount ConfigMaps on
Kubernetes using the
[`configmap` module type](../reference/module-types/configmap.md),
supported by the `kubernetes` provider. Here's a simple example:

Example:

```yaml
kind: Module
name: my-configmap
type: configmap
data:
  config.properties: |
    some: data
    or: something
---
kind: Module
name: my-module
type: container
services:
  - name: my-service
    volumes:
      - name: my-configmap
        module: my-configmap
        containerPath: /config
    ...
```

This mounts all the keys in the `data` field on the `my-configmap`
module under the `/config` directory in the container. In this case,
you'll find the file `/config/config.properties` there, with the value
above (`some: data ...`) as the file contents.

You can do the same for tests and tasks using the
[`tests.volumes`](../reference/module-types/container.md#testsvolumes)
and
[`tasks.volumes`](../reference/module-types/container.md#tasksvolumes) 
fields. `configmap` volumes can of course also be referenced in
`kubernetes` and `helm` modules, since they are deployed as standard
ConfigMap resources.
  • Loading branch information
edvald committed Aug 16, 2021
1 parent d485bfa commit 809dcb8
Show file tree
Hide file tree
Showing 11 changed files with 897 additions and 11 deletions.
1 change: 1 addition & 0 deletions core/src/docs/module-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const populateModuleSchema = (schema: Joi.ObjectSchema) => baseModuleSpecSchema(
export const moduleTypes = [
{ name: "exec" },
{ name: "container" },
{ name: "configmap", pluginName: "local-kubernetes" },
{ name: "conftest", pluginName: "conftest" },
{ name: "hadolint" },
{ name: "helm", pluginName: "local-kubernetes" },
Expand Down
27 changes: 17 additions & 10 deletions core/src/plugins/kubernetes/container/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,22 +702,29 @@ export function configureVolumes(
// Make sure the module is a supported type
const volumeModule = module.buildDependencies[volume.module]

if (!volumeModule.compatibleTypes.includes("persistentvolumeclaim")) {
if (volumeModule.compatibleTypes.includes("persistentvolumeclaim")) {
volumes.push({
name: volumeName,
persistentVolumeClaim: {
claimName: volume.module,
},
})
} else if (volumeModule.compatibleTypes.includes("configmap")) {
volumes.push({
name: volumeName,
configMap: {
name: volume.module,
},
})
} else {
throw new ConfigurationError(
chalk.red(deline`Container module ${chalk.white(module.name)} specifies a unsupported module
${chalk.white(volumeModule.name)} for volume mount ${chalk.white(volumeName)}. Only persistentvolumeclaim
modules are supported at this time.
${chalk.white(volumeModule.name)} for volume mount ${chalk.white(volumeName)}. Only \`persistentvolumeclaim\`
and \`configmap\` modules are supported at this time.
`),
{ volumeSpec: volume }
)
}

volumes.push({
name: volumeName,
persistentVolumeClaim: {
claimName: volume.module,
},
})
} else {
volumes.push({
name: volumeName,
Expand Down
2 changes: 2 additions & 0 deletions core/src/plugins/kubernetes/kubernetes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { sternSpec } from "./logs"
import { isString } from "lodash"
import { mutagenCliSpec } from "./mutagen"
import { Warning } from "../../db/entities/warning"
import { configMapModuleDefinition } from "./volumes/configmap"

export async function configureProvider({
log,
Expand Down Expand Up @@ -254,6 +255,7 @@ export const gardenPlugin = () =>
handlers: kubernetesHandlers,
},
pvcModuleDefinition(),
configMapModuleDefinition(),
],
extendModuleTypes: [
{
Expand Down
154 changes: 154 additions & 0 deletions core/src/plugins/kubernetes/volumes/configmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2018-2021 Garden Technologies, Inc. <[email protected]>
*
* 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 { joiIdentifier, joi, joiSparseArray, joiStringMap } from "../../../config/common"
import { dedent } from "../../../util/string"
import { BaseVolumeSpec } from "../../base-volume"
import { V1ConfigMap } from "@kubernetes/client-node"
import { ModuleTypeDefinition } from "../../../types/plugin/plugin"
import { DOCS_BASE_URL } from "../../../constants"
import { baseBuildSpecSchema } from "../../../config/module"
import { ConfigureModuleParams } from "../../../types/plugin/module/configure"
import { GetServiceStatusParams } from "../../../types/plugin/service/getServiceStatus"
import { GardenModule } from "../../../types/module"
import { KubernetesModule, KubernetesModuleConfig } from "../kubernetes-module/config"
import { KubernetesResource } from "../types"
import { getKubernetesServiceStatus, deployKubernetesService } from "../kubernetes-module/handlers"
import { DeployServiceParams } from "../../../types/plugin/service/deployService"
import { getModuleTypeUrl } from "../../../docs/common"
import { GardenService } from "../../../types/service"

// TODO: If we make a third one in addition to this and `persistentvolumeclaim`, we should dedupe some code.

export interface ConfigMapSpec extends BaseVolumeSpec {
dependencies: string[]
namespace: string
data: Required<V1ConfigMap["data"]>
}

type ConfigMapModule = GardenModule<ConfigMapSpec, ConfigMapSpec>
type ConfigMapService = GardenService<ConfigMapModule>

const containerTypeUrl = getModuleTypeUrl("container")

export const configMapModuleDefinition = (): ModuleTypeDefinition => ({
name: "configmap",
docs: dedent`
Creates a [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) in your namespace, that can be referenced and mounted by other resources and [container modules](${containerTypeUrl}).
See the [Mounting Kubernetes ConfigMaps](${DOCS_BASE_URL}/guides/container-modules#mounting-kubernetes-configmaps) guide for more info and usage examples.
`,
schema: joi.object().keys({
build: baseBuildSpecSchema(),
dependencies: joiSparseArray(joiIdentifier()).description(
"List of services and tasks to deploy/run before deploying this ConfigMap."
),
namespace: joiIdentifier().description(
"The namespace to deploy the ConfigMap in. Note that any module referencing the ConfigMap must be in the same namespace, so in most cases you should leave this unset."
),
data: joiStringMap(joi.string()).required().description("The ConfigMap data, as a key/value map of string values."),
}),
handlers: {
async configure({ moduleConfig }: ConfigureModuleParams) {
// No need to scan for files
moduleConfig.include = []

moduleConfig.spec.accessModes = ["ReadOnlyMany"]

moduleConfig.serviceConfigs = [
{
dependencies: moduleConfig.spec.dependencies,
disabled: moduleConfig.spec.disabled,
hotReloadable: false,
name: moduleConfig.name,
spec: moduleConfig.spec,
},
]

return { moduleConfig }
},

async getServiceStatus(params: GetServiceStatusParams) {
params.service = getKubernetesService(params.service)
params.module = params.service.module

return getKubernetesServiceStatus({
...params,
devMode: false,
})
},

async deployService(params: DeployServiceParams) {
params.service = getKubernetesService(params.service)
params.module = params.service.module

return deployKubernetesService({
...params,
devMode: false,
})
},
},
})

/**
* Maps a `configmap` module to a `kubernetes` module (so we can re-use those handlers).
*/
function getKubernetesService(configMapService: ConfigMapService): GardenService<KubernetesModule, KubernetesModule> {
const configMapManifest: KubernetesResource<V1ConfigMap> = {
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: configMapService.name,
},
data: configMapService.spec.data,
}

const spec = {
dependencies: configMapService.spec.dependencies,
files: [],
manifests: [configMapManifest],
tasks: [],
tests: [],
}

const serviceConfig = {
...configMapService.config,
spec,
}

const config: KubernetesModuleConfig = {
...configMapService.module,
serviceConfigs: [serviceConfig],
spec,
taskConfigs: [],
testConfigs: [],
}

const module: KubernetesModule = {
...configMapService.module,
_config: config,
...config,
spec: {
...configMapService.spec,
files: [],
manifests: [configMapManifest],
tasks: [],
tests: [],
},
}

return {
name: configMapService.name,
config: serviceConfig,
disabled: configMapService.disabled,
module,
sourceModule: module,
spec,
version: configMapService.version,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
kind: Module
name: configmap-reference
description: Test module for configmap module references
type: container
image: busybox:1.31.1
include: []
build:
dependencies: [simple-service]
services:
- name: configmap-reference
command: [sh, -c, "nc -l -p 8080"]
ports:
- name: http
containerPort: 8080
volumes:
- name: test
module: configmap-module
containerPath: /config
---
kind: Module
name: configmap-module
type: configmap
data:
a.config: foo
b.config: bar
30 changes: 29 additions & 1 deletion core/test/integ/src/plugins/kubernetes/container/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,34 @@ describe("kubernetes container deployment handlers", () => {
expect(resource.spec.template?.spec?.containers[0].volumeMounts).to.eql([{ name: "test", mountPath: "/volume" }])
})

it("should correctly mount a referenced ConfigMap module", async () => {
const service = graph.getService("configmap-reference")
const namespace = provider.config.namespace!.name!

const resource = await createWorkloadManifest({
api,
provider,
service,
runtimeContext: emptyRuntimeContext,
namespace,
enableDevMode: false,
enableHotReload: false,
log: garden.log,
production: false,
blueGreen: false,
})

expect(resource.spec.template?.spec?.volumes).to.eql([
{
name: "test",
configMap: {
name: "configmap-module",
},
},
])
expect(resource.spec.template?.spec?.containers[0].volumeMounts).to.eql([{ name: "test", mountPath: "/config" }])
})

it("should throw if incompatible module is specified as a volume module", async () => {
const service = graph.getService("volume-reference")
const namespace = provider.config.namespace!.name!
Expand All @@ -440,7 +468,7 @@ describe("kubernetes container deployment handlers", () => {
}),
(err) =>
expect(stripAnsi(err.message)).to.equal(
"Container module volume-reference specifies a unsupported module simple-service for volume mount test. Only persistentvolumeclaim modules are supported at this time."
"Container module volume-reference specifies a unsupported module simple-service for volume mount test. Only `persistentvolumeclaim` and `configmap` modules are supported at this time."
)
)
})
Expand Down
Loading

0 comments on commit 809dcb8

Please sign in to comment.