diff --git a/docs/README.md b/docs/README.md index b0f4bec9a8..5e61097542 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,6 +7,7 @@ * [Using Garden](./using-garden/README.md) * [Development workflows](./using-garden/development-workflows.md) * [Configuration files](./using-garden/configuration-files.md) + * [Container modules](./using-garden/container-modules.md) * [Local Kubernetes](./using-garden/local-kubernetes.md) * [Remote Kubernetes](./using-garden/remote-kubernetes.md) * [In-cluster building](./using-garden/in-cluster-building.md) diff --git a/docs/reference/config.md b/docs/reference/config.md index e6afcd55c0..41032bd013 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -64,8 +64,8 @@ Example: ```yaml environmentDefaults: - providers: [] - variables: {} + providers: [] + variables: {} ``` ### `environmentDefaults.providers[]` @@ -92,8 +92,8 @@ Example: ```yaml environmentDefaults: - providers: [] - variables: {} + providers: [] + variables: {} ... providers: - name: "local-kubernetes" @@ -113,8 +113,8 @@ Example: ```yaml environmentDefaults: - providers: [] - variables: {} + providers: [] + variables: {} ... providers: - environments: diff --git a/docs/reference/module-types/container.md b/docs/reference/module-types/container.md index 77a0806f7d..babeb9dc41 100644 --- a/docs/reference/module-types/container.md +++ b/docs/reference/module-types/container.md @@ -423,12 +423,24 @@ The name of the container port where the specified paths should be routed. [services](#services) > env -Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives. +Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives or references to secrets. | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +Example: + +```yaml +services: + - env: + MY_VAR: some-value + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key +``` + ### `services[].healthCheck` [services](#services) > healthCheck @@ -781,12 +793,24 @@ tests: [tests](#tests) > env -Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives. +Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives or references to secrets. | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +Example: + +```yaml +tests: + - env: + MY_VAR: some-value + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key +``` + ### `tasks` A list of tasks that can be run from this container module. These can be used as dependencies for services (executed before the service is deployed) or for other tasks. @@ -877,12 +901,24 @@ tasks: [tasks](#tasks) > env -Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives. +Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives or references to secrets. | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +Example: + +```yaml +tasks: + - env: + MY_VAR: some-value + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key +``` + ## Complete YAML schema ```yaml @@ -964,22 +1000,91 @@ tasks: ## Outputs -The following keys are available via the `${modules..outputs}` template string key for `container` +The following keys are available via the `${modules.}` template string key for `container` modules. +### `modules..buildPath` + +The build path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +buildPath: "/home/me/code/my-project/.garden/build/my-module" +``` + +### `modules..path` + +The local path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +path: "/home/me/code/my-project/my-module" +``` + +### `modules..version` + +The current version of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +version: "v-17ad4cb3fd" +``` + +### `modules..outputs` + +The outputs defined by the module. + +| Type | Required | +| -------- | -------- | +| `object` | Yes | + ### `modules..outputs.local-image-name` +[outputs](#outputs) > local-image-name + The name of the image (without tag/version) that the module uses for local builds and deployments. | Type | Required | | -------- | -------- | | `string` | Yes | +Example: + +```yaml +outputs: + ... + local-image-name: "my-module" +``` + ### `modules..outputs.deployment-image-name` +[outputs](#outputs) > deployment-image-name + The name of the image (without tag/version) that the module will use during deployment. | Type | Required | | -------- | -------- | | `string` | Yes | +Example: + +```yaml +outputs: + ... + deployment-image-name: "my-deployment-registry.io/my-org/my-module" +``` diff --git a/docs/reference/module-types/exec.md b/docs/reference/module-types/exec.md index 43e6f26019..5fb427a29c 100644 --- a/docs/reference/module-types/exec.md +++ b/docs/reference/module-types/exec.md @@ -8,6 +8,9 @@ guide](../../using-garden/configuration-files.md). The [first section](#configuration-keys) lists and describes the available schema keys. The [second section](#complete-yaml-schema) contains the complete YAML schema. +`exec` modules also export values that are available in template strings under `${modules..outputs}`. +See the [Outputs](#outputs) section below for details. + ## Configuration keys ### `apiVersion` @@ -335,8 +338,7 @@ build: copy: - source: target: - command: - [] + command: [] env: {} tasks: - name: @@ -351,3 +353,58 @@ tests: command: env: {} ``` + +## Outputs + +The following keys are available via the `${modules.}` template string key for `exec` +modules. + +### `modules..buildPath` + +The build path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +buildPath: "/home/me/code/my-project/.garden/build/my-module" +``` + +### `modules..path` + +The local path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +path: "/home/me/code/my-project/my-module" +``` + +### `modules..version` + +The current version of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +version: "v-17ad4cb3fd" +``` + +### `modules..outputs` + +The outputs defined by the module. + +| Type | Required | +| -------- | -------- | +| `object` | Yes | diff --git a/docs/reference/module-types/helm.md b/docs/reference/module-types/helm.md index d8997a3e4d..84133c3ab6 100644 --- a/docs/reference/module-types/helm.md +++ b/docs/reference/module-types/helm.md @@ -461,12 +461,24 @@ The arguments to pass to the pod used for execution. [tasks](#tasks) > env -Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives. +Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives or references to secrets. | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +Example: + +```yaml +tasks: + - env: + MY_VAR: some-value + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key +``` + ### `tests` The test suite definitions for this module. @@ -602,12 +614,24 @@ The arguments to pass to the pod used for testing. [tests](#tests) > env -Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives. +Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives or references to secrets. | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +Example: + +```yaml +tests: + - env: + MY_VAR: some-value + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key +``` + ### `version` The chart version to deploy. @@ -684,14 +708,65 @@ values: {} ## Outputs -The following keys are available via the `${modules..outputs}` template string key for `helm` +The following keys are available via the `${modules.}` template string key for `helm` modules. +### `modules..buildPath` + +The build path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +buildPath: "/home/me/code/my-project/.garden/build/my-module" +``` + +### `modules..path` + +The local path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +path: "/home/me/code/my-project/my-module" +``` + +### `modules..version` + +The current version of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +version: "v-17ad4cb3fd" +``` + +### `modules..outputs` + +The outputs defined by the module. + +| Type | Required | +| -------- | -------- | +| `object` | Yes | + ### `modules..outputs.release-name` +[outputs](#outputs) > release-name + The Helm release name of the service. | Type | Required | | -------- | -------- | | `string` | Yes | - diff --git a/docs/reference/module-types/kubernetes.md b/docs/reference/module-types/kubernetes.md index 3ae9c54f46..73817a6128 100644 --- a/docs/reference/module-types/kubernetes.md +++ b/docs/reference/module-types/kubernetes.md @@ -16,6 +16,9 @@ guide](../../using-garden/configuration-files.md). The [first section](#configuration-keys) lists and describes the available schema keys. The [second section](#complete-yaml-schema) contains the complete YAML schema. +`kubernetes` modules also export values that are available in template strings under `${modules..outputs}`. +See the [Outputs](#outputs) section below for details. + ## Configuration keys ### `apiVersion` @@ -268,3 +271,58 @@ manifests: name: files: [] ``` + +## Outputs + +The following keys are available via the `${modules.}` template string key for `kubernetes` +modules. + +### `modules..buildPath` + +The build path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +buildPath: "/home/me/code/my-project/.garden/build/my-module" +``` + +### `modules..path` + +The local path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +path: "/home/me/code/my-project/my-module" +``` + +### `modules..version` + +The current version of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +version: "v-17ad4cb3fd" +``` + +### `modules..outputs` + +The outputs defined by the module. + +| Type | Required | +| -------- | -------- | +| `object` | Yes | diff --git a/docs/reference/module-types/maven-container.md b/docs/reference/module-types/maven-container.md index 51ba3d64d7..1c481dc27c 100644 --- a/docs/reference/module-types/maven-container.md +++ b/docs/reference/module-types/maven-container.md @@ -428,12 +428,24 @@ The name of the container port where the specified paths should be routed. [services](#services) > env -Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives. +Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives or references to secrets. | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +Example: + +```yaml +services: + - env: + MY_VAR: some-value + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key +``` + ### `services[].healthCheck` [services](#services) > healthCheck @@ -786,12 +798,24 @@ tests: [tests](#tests) > env -Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives. +Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives or references to secrets. | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +Example: + +```yaml +tests: + - env: + MY_VAR: some-value + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key +``` + ### `tasks` A list of tasks that can be run from this container module. These can be used as dependencies for services (executed before the service is deployed) or for other tasks. @@ -882,12 +906,24 @@ tasks: [tasks](#tasks) > env -Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives. +Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives or references to secrets. | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +Example: + +```yaml +tasks: + - env: + MY_VAR: some-value + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key +``` + ### `jarPath` POSIX-style path to the packaged JAR artifact, relative to the module directory. @@ -1002,22 +1038,91 @@ mvnOpts: [] ## Outputs -The following keys are available via the `${modules..outputs}` template string key for `maven-container` +The following keys are available via the `${modules.}` template string key for `maven-container` modules. +### `modules..buildPath` + +The build path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +buildPath: "/home/me/code/my-project/.garden/build/my-module" +``` + +### `modules..path` + +The local path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +path: "/home/me/code/my-project/my-module" +``` + +### `modules..version` + +The current version of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +version: "v-17ad4cb3fd" +``` + +### `modules..outputs` + +The outputs defined by the module. + +| Type | Required | +| -------- | -------- | +| `object` | Yes | + ### `modules..outputs.local-image-name` +[outputs](#outputs) > local-image-name + The name of the image (without tag/version) that the module uses for local builds and deployments. | Type | Required | | -------- | -------- | | `string` | Yes | +Example: + +```yaml +outputs: + ... + local-image-name: "my-module" +``` + ### `modules..outputs.deployment-image-name` +[outputs](#outputs) > deployment-image-name + The name of the image (without tag/version) that the module will use during deployment. | Type | Required | | -------- | -------- | | `string` | Yes | +Example: + +```yaml +outputs: + ... + deployment-image-name: "my-deployment-registry.io/my-org/my-module" +``` diff --git a/docs/reference/module-types/openfaas.md b/docs/reference/module-types/openfaas.md index f8ff9197e1..001b2ca4b6 100644 --- a/docs/reference/module-types/openfaas.md +++ b/docs/reference/module-types/openfaas.md @@ -7,6 +7,9 @@ guide](../../using-garden/configuration-files.md). The [first section](#configuration-keys) lists and describes the available schema keys. The [second section](#complete-yaml-schema) contains the complete YAML schema. +`openfaas` modules also export values that are available in template strings under `${modules..outputs}`. +See the [Outputs](#outputs) section below for details. + ## Configuration keys ### `apiVersion` @@ -190,3 +193,58 @@ build: - source: target: ``` + +## Outputs + +The following keys are available via the `${modules.}` template string key for `openfaas` +modules. + +### `modules..buildPath` + +The build path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +buildPath: "/home/me/code/my-project/.garden/build/my-module" +``` + +### `modules..path` + +The local path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +path: "/home/me/code/my-project/my-module" +``` + +### `modules..version` + +The current version of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +version: "v-17ad4cb3fd" +``` + +### `modules..outputs` + +The outputs defined by the module. + +| Type | Required | +| -------- | -------- | +| `object` | Yes | diff --git a/docs/reference/providers/kubernetes.md b/docs/reference/providers/kubernetes.md index a76df87da8..d1b75ae601 100644 --- a/docs/reference/providers/kubernetes.md +++ b/docs/reference/providers/kubernetes.md @@ -684,8 +684,8 @@ Example: providers: - tlsCertificates: - secretRef: - name: my-tls-secret - namespace: default + name: my-tls-secret + namespace: default ``` ### `providers[].tlsCertificates[].secretRef.name` @@ -704,8 +704,8 @@ Example: providers: - tlsCertificates: - secretRef: - name: my-tls-secret - namespace: default + name: my-tls-secret + namespace: default ... name: "my-secret" ``` diff --git a/docs/reference/providers/local-kubernetes.md b/docs/reference/providers/local-kubernetes.md index bdf7d0f3e5..4ced663f3f 100644 --- a/docs/reference/providers/local-kubernetes.md +++ b/docs/reference/providers/local-kubernetes.md @@ -684,8 +684,8 @@ Example: providers: - tlsCertificates: - secretRef: - name: my-tls-secret - namespace: default + name: my-tls-secret + namespace: default ``` ### `providers[].tlsCertificates[].secretRef.name` @@ -704,8 +704,8 @@ Example: providers: - tlsCertificates: - secretRef: - name: my-tls-secret - namespace: default + name: my-tls-secret + namespace: default ... name: "my-secret" ``` diff --git a/docs/reference/template-strings.md b/docs/reference/template-strings.md index 81e62ad31f..3020b4bb5a 100644 --- a/docs/reference/template-strings.md +++ b/docs/reference/template-strings.md @@ -193,7 +193,9 @@ providers: {} # # Example: # my-module: +# buildPath: /home/me/code/my-project/.garden/build/my-module # path: /home/me/code/my-project/my-module +# outputs: {} # version: v-17ad4cb3fd # modules: {} diff --git a/docs/using-garden/README.md b/docs/using-garden/README.md index 3355f05980..aa7d2355d9 100644 --- a/docs/using-garden/README.md +++ b/docs/using-garden/README.md @@ -8,6 +8,10 @@ In this article we discuss how to set up a new Garden project, the basic develop This one is all about Garden's configuration files—an overview of project and module configs, setting up services, and a primer on tests. +## [Container modules](./container-modules.md) + +One of the most commonly used module types for Garden is the `container` module type. This guide walks through its usage and configuration. + ## [Local Kubernetes](./local-kubernetes.md) Garden works great with local Kubernetes setups. Here you'll find installation and usage instructions for some diff --git a/docs/using-garden/container-modules.md b/docs/using-garden/container-modules.md new file mode 100644 index 0000000000..f41733003d --- /dev/null +++ b/docs/using-garden/container-modules.md @@ -0,0 +1,218 @@ +# Container modules + +Garden includes a `container` module type, which provides a high-level abstraction around container-based services, that's easy to understand and use. + +`container` modules can be used to just _build_ container images, or they can specify deployable services through the optional `services` key, as well as `tasks` and `tests`. So you might in one scenario use a `container` module to both build and deploy services, and in another you might only build the image using a `container` module, and then refer to that image in a `helm` or `kubernetes` module. + +Below we'll walk through some usage examples. For a full reference of the `container` module type, please take a look at the [reference](../reference/module-types/container.md). + +_Note: Even though we've spent the most time on supporting Kubernetes, we've tried to design this module type in a way that makes it generically applicable to other container orchestrators as well, such as Docker Swarm, Docker Compose, AWS ECS etc. This will come in handy as we add more providers, that can then use the same module type._ + +## Building images + +A bare minimum `container` module just specifies common required fields: + +```yaml +# garden.yml +kind: Module +type: container +name: my-container +``` + +If you have a `Dockerfile` next to this file, this is enough to tell Garden to build it. You can also specify `dockerfile: ` if you need to override the Dockerfile name. You might also want to explictly [include or exclude](./configuration-files.md#including-excluding-files) files in the build context. + +## Using remote images + +If you're not building the container image yourself and just need to deploy an external image, you can skip the Dockerfile and specify the `image` field: + +```yaml +# garden.yml +kind: Module +type: container +name: redis +image: redis:5.0.5-alpine # <- replace with any docker image ID +services: + ... +``` + +## Publishing images + +When you do have your own Dockerfile to build, and want to publish it, you also need to use the `image` field: + +```yaml +# garden.yml +kind: Module +type: container +name: my-container +image: my-org/my-container # <- your image repo ID +``` + +This tells Garden which namespace, and optionally registry hostname (e.g. `gcr.io` or `quay.io`), to publish the image to when you run `garden publish`. + +If you specify a tag as well, for example `image: my-org/my-container:v1.2.3`, that tag will also be used when publishing. If you omit it, Garden will automatically set a tag based on the source hash of the module, e.g. `v-0c61a773cb`. + +## Deploying services + +`container` modules also have an optional `services` field, which you can use to deploy the container image using your configured providers (such as `kubernetes`/`local-kubernetes`). + +In the case of Kubernetes, Garden will take the simplified `container` service specification and convert it to the corresponding Kubernetes manifests, i.e. Deployment, Service and (if applicable) Ingress resources. + +Here, for example, is the spec for the `frontend` service in our example [demo project](https://github.com/garden-io/garden/tree/master/examples/demo-project): + +```yaml +kind: Module +name: frontend +description: Frontend service container +type: container +services: + - name: frontend + ports: + - name: http + containerPort: 8080 + healthCheck: + httpGet: + path: /hello-frontend + port: http + ingresses: + - path: /hello-frontend + port: http + - path: /call-backend + port: http + dependencies: + - backend +... +``` + +This, first of all, tells Garden that it should deploy the built `frontend` container as a service with the same name. We also configure a health check, a couple of ingress endpoints, and specify that this service depends on the `backend` service. There is a number of other options, which you can find in the `container` module [reference](../reference/module-types/container.md#services). + +If you need to use advanced (or otherwise very specific) features of the underlying platform, you may need to use more platform-specific module types (e.g. `kubernetes` or `helm`). The `container` module type is not intended to capture all those features. + +### Environment variables + +Container services can specify environment variables, using the `services[].env` field: + +```yaml +kind: Module +type: container +name: my-container +services: + - name: my-container-service + ... + env: + MY_ENV_VAR: foo + MY_TEMPLATED_ENV_VAR: ${var.some-project-variable} + ... +... +``` + +`env` is a simple mapping of "name: value". Above we see a simple example with a string value, but you'll also commonly use [template strings](./configuration-files.md#template-strings) to interpolate variables to be consumed by the container service. + +#### Secrets + +As of Garden v0.10.1 you can reference secrets in environment variables. For Kubernetes, this translates to `valueFrom.secretKeyRef` fields in the Pod specs, which direct Kubernetes to mount values from `Secret` resources that you have created in the application namespace, as environment variables in the Pod. + +For example: + +```yaml +kind: Module +type: container +name: my-container +services: + - name: my-container-service + ... + env: + MY_SECRET_VAR: + secretRef: + name: my-secret + key: some-key-in-secret + ... +... +``` + +This will pull the `some-key-in-secret` key from the `my-secret` Secret resource in the application namespace, and make available as an environment variable. + +_Note that you must create the Secret manually for the Pod to be able to reference it._ + +For Kubernetes, this is commonly done using `kubectl`. For example, to create a basic generic secret you could use: + +```sh +kubectl --namespace create secret generic --from-literal=some-key-in-secret=foo +``` + +Where `` is your project namespace (which is either set with `namespace` in your provider config, or defaults to your project name). There are notably other, more secure ways to create secrets via `kubectl`. Please refer to the offical [Kubernetes Secrets docs](https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret-using-kubectl-create-secret) for details. + +Also check out the [Kubernetes Secrets example project](https://github.com/garden-io/garden/tree/master/examples/kubernetes-secrets) for a working example. + +## Running tests + +You can define both tests and tasks as part of any container module. The two are configured in very similar ways, using the `tests` and `tasks` keys, respectively. Here, for example, is a configuration for two different test suites: + +```yaml +kind: Module +type: container +name: my-container +... +tests: + - name: unit + command: [npm, test] + - name: integ + command: [npm, run, integ] + dependencies: + - some-service +... +``` + +Here we first define a `unit` test suite, which has no dependencies, and simply runs `npm test` in the container. The `integ` suite is similar but adds a _runtime dependency_. This means that before the `integ` test is run, Garden makes sure that `some-service` is running and up-to-date. + +When you run `garden test` or `garden dev` we will run those tests. In both cases, the tests will be executed by running the container with the specified command _in your configured environment_ (as opposed to locally on the machine you're running the `garden` CLI from). + +The names and commands to run are of course completely up to you, but we suggest naming the test suites consistently across your different modules. + +See the [reference](../reference/module-types/container.md#tests) for all the configurable parameters for container tests. + +## Running tasks + +Tasks are defined very similarly to tests: + +```yaml +kind: Module +type: container +name: my-container +... +tasks: + - name: db-migrate + command: [rake, db:migrate] + dependencies: + - my-database +... +``` + +In this example, we define a `db-migrate` task that runs `rake db:migrate` (which is commonly used for database migrations, but you can run anything you like of course). The task has a dependency on `my-database`, so that Garden will make sure the database is up and running before running the migration task. + +Unlike tests, tasks can also be dependencies for services and other tasks. For example, you might define another task or a service with `db-migrate` as a dependency, so that it only runs after the migrations have been executed. + +One thing to note, is that tasks should in most cases be _idempotent_, meaning that running the same task multiple times should be safe. + +See the [reference](../reference/module-types/container.md#tasks) for all the configurable parameters for container tasks. + +## Referencing from other modules + +Modules can reference outputs from each other using [template strings](./configuration-files.md#template-strings). `container` modules are, for instance, often referenced by other module types such as `helm` module types. For example: + +```yaml +kind: Module +description: Helm chart for the worker container +type: helm +name: my-service +... +build: + dependencies: [my-image] +values: + image: + name: ${modules.my-image.outputs.deployment-image-name} + tag: ${modules.my-image.version} +``` + +Here, we declare `my-image` as a dependency for the `my-service` Helm chart. In order for the Helm chart to be able to reference the built container image, we must provide the correct image name and version. + +For a full list of keys that are available for the `container` module type, take a look at the [outputs reference](../reference/module-types/container.md#outputs). diff --git a/examples/demo-project/frontend/app.js b/examples/demo-project/frontend/app.js index f04bbf5a3b..a721f38b9c 100644 --- a/examples/demo-project/frontend/app.js +++ b/examples/demo-project/frontend/app.js @@ -3,7 +3,6 @@ const request = require('request-promise') const app = express(); const backendServiceEndpoint = `http://backend/hello-backend` -// const backendServiceEndpoint = `http://demo-project.local.app.garden/hello-backend` app.get('/hello-frontend', (req, res) => res.send('Hello from the frontend!')); diff --git a/examples/demo-project/garden.yml b/examples/demo-project/garden.yml index 67dafcec6b..303a1ae097 100644 --- a/examples/demo-project/garden.yml +++ b/examples/demo-project/garden.yml @@ -18,10 +18,3 @@ environments: namespace: demo-project-testing-${local.env.CIRCLE_BUILD_NUM || "default"} defaultHostname: demo-project-testing.dev-1.sys.garden buildMode: cluster-docker - - name: dev-2 - providers: - - name: kubernetes - context: gke_garden-dev-200012_europe-west3-a_dev-2 - namespace: ${local.env.USER || "default"}-demo-project - defaultHostname: ${local.env.USER || "default"}-demo-project.dev-1.sys.garden - buildMode: cluster-docker diff --git a/examples/kubernetes-secrets/README.md b/examples/kubernetes-secrets/README.md new file mode 100644 index 0000000000..94766d798e --- /dev/null +++ b/examples/kubernetes-secrets/README.md @@ -0,0 +1,36 @@ +# Kubernetes Secrets + +This is a simple variation on the [demo project](../demo-project/README.md) example, adding a Secret reference to +one of the modules. + +## Setup + +The commands below assume you're running a local Kubernetes cluster. Please adjust the commands accordingly if you're +running against a remote environment (setting the `--env` parameter and the correct `--namespace` for kubectl). + +### 1. Initialize the project + +```sh +garden init +``` + +### 2. Create the Secret + +```sh +kubectl --namespace=kubernetes-secrets create secret generic my-secret --from-literal=my-key=superdupersecret +``` + +## Usage + +First deploy the services: + +```sh +garden deploy +``` + +Then try calling the frontend service and observe the value you set in the Secret above: + +```sh +garden call backend +# Outputs: superdupersecret +``` diff --git a/examples/kubernetes-secrets/backend/.dockerignore b/examples/kubernetes-secrets/backend/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/examples/kubernetes-secrets/backend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/examples/kubernetes-secrets/backend/.gitignore b/examples/kubernetes-secrets/backend/.gitignore new file mode 100644 index 0000000000..eb086d61c3 --- /dev/null +++ b/examples/kubernetes-secrets/backend/.gitignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* diff --git a/examples/kubernetes-secrets/backend/Dockerfile b/examples/kubernetes-secrets/backend/Dockerfile new file mode 100644 index 0000000000..eca3125f6e --- /dev/null +++ b/examples/kubernetes-secrets/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.8.3-alpine +MAINTAINER Aurelien PERRIER + +ENV webserver_path /go/src/github.com/perriea/webserver/ +ENV PATH $PATH:$webserver_path + +WORKDIR $webserver_path +COPY webserver/ . + +RUN go build . + +ENTRYPOINT ./webserver + +EXPOSE 8080 diff --git a/examples/kubernetes-secrets/backend/garden.yml b/examples/kubernetes-secrets/backend/garden.yml new file mode 100644 index 0000000000..601549e496 --- /dev/null +++ b/examples/kubernetes-secrets/backend/garden.yml @@ -0,0 +1,19 @@ +kind: Module +name: backend +description: Backend service container +type: container +services: + - name: backend + ports: + - name: http + containerPort: 8080 + # Maps service:80 -> container:8080 + servicePort: 80 + ingresses: + - path: /hello-backend + port: http + env: + SECRET_VAR: + secretRef: + name: my-secret + key: my-key diff --git a/examples/kubernetes-secrets/backend/webserver/main.go b/examples/kubernetes-secrets/backend/webserver/main.go new file mode 100644 index 0000000000..c5ff20ad01 --- /dev/null +++ b/examples/kubernetes-secrets/backend/webserver/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "net/http" + "os" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, os.Getenv("SECRET_VAR")) +} + +func main() { + http.HandleFunc("/hello-backend", handler) + fmt.Println("Server running...") + + http.ListenAndServe(":8080", nil) +} diff --git a/examples/kubernetes-secrets/frontend/.dockerignore b/examples/kubernetes-secrets/frontend/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/examples/kubernetes-secrets/frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/examples/kubernetes-secrets/frontend/Dockerfile b/examples/kubernetes-secrets/frontend/Dockerfile new file mode 100644 index 0000000000..4a418d7d1e --- /dev/null +++ b/examples/kubernetes-secrets/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:9-alpine + +ENV PORT=8080 +EXPOSE ${PORT} +WORKDIR /app + +ADD package.json /app +RUN npm install + +ADD . /app + +CMD ["npm", "start"] diff --git a/examples/kubernetes-secrets/frontend/app.js b/examples/kubernetes-secrets/frontend/app.js new file mode 100644 index 0000000000..a721f38b9c --- /dev/null +++ b/examples/kubernetes-secrets/frontend/app.js @@ -0,0 +1,27 @@ +const express = require('express'); +const request = require('request-promise') +const app = express(); + +const backendServiceEndpoint = `http://backend/hello-backend` + +app.get('/hello-frontend', (req, res) => res.send('Hello from the frontend!')); + +app.get('/call-backend', (req, res) => { + // Query the backend and return the response + request.get(backendServiceEndpoint) + .then(message => { + message = `Backend says: '${message}'` + res.json({ + message, + }) + }) + .catch(err => { + res.statusCode = 500 + res.json({ + error: err, + message: "Unable to reach service at " + backendServiceEndpoint, + }) + }); +}); + +module.exports = { app } diff --git a/examples/kubernetes-secrets/frontend/garden.yml b/examples/kubernetes-secrets/frontend/garden.yml new file mode 100644 index 0000000000..df6d0d42fa --- /dev/null +++ b/examples/kubernetes-secrets/frontend/garden.yml @@ -0,0 +1,27 @@ +kind: Module +name: frontend +description: Frontend service container +type: container +services: + - name: frontend + ports: + - name: http + containerPort: 8080 + healthCheck: + httpGet: + path: /hello-frontend + port: http + ingresses: + - path: /hello-frontend + port: http + - path: /call-backend + port: http + dependencies: + - backend +tests: + - name: unit + args: [npm, test] + - name: integ + args: [npm, run, integ] + dependencies: + - backend diff --git a/examples/kubernetes-secrets/frontend/main.js b/examples/kubernetes-secrets/frontend/main.js new file mode 100644 index 0000000000..ab66491126 --- /dev/null +++ b/examples/kubernetes-secrets/frontend/main.js @@ -0,0 +1,3 @@ +const { app } = require('./app'); + +app.listen(process.env.PORT, '0.0.0.0', () => console.log('Frontend service started')); diff --git a/examples/kubernetes-secrets/frontend/package.json b/examples/kubernetes-secrets/frontend/package.json new file mode 100644 index 0000000000..e3da030191 --- /dev/null +++ b/examples/kubernetes-secrets/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "frontend", + "version": "1.0.0", + "description": "Simple Node.js docker service", + "main": "main.js", + "scripts": { + "start": "node main.js", + "test": "echo OK", + "integ": "node_modules/mocha/bin/mocha test/integ.js" + }, + "author": "garden.io ", + "license": "ISC", + "dependencies": { + "express": "^4.16.2", + "request": "^2.83.0", + "request-promise": "^4.2.2" + }, + "devDependencies": { + "mocha": "^5.1.1", + "supertest": "^3.0.0" + } +} diff --git a/examples/kubernetes-secrets/frontend/test/integ.js b/examples/kubernetes-secrets/frontend/test/integ.js new file mode 100644 index 0000000000..e6958c13da --- /dev/null +++ b/examples/kubernetes-secrets/frontend/test/integ.js @@ -0,0 +1,17 @@ +const supertest = require("supertest") +const { app } = require("../app") + +describe('GET /call-backend', () => { + const agent = supertest.agent(app) + + it('should respond with a message from the backend service', (done) => { + agent + .get("/call-backend") + .expect(200, { message: "Backend says: 'superdupersecret'" }) + .end((err) => { + if (err) return done(err) + done() + }) + }) +}) + diff --git a/examples/kubernetes-secrets/garden.yml b/examples/kubernetes-secrets/garden.yml new file mode 100644 index 0000000000..96f8ecf941 --- /dev/null +++ b/examples/kubernetes-secrets/garden.yml @@ -0,0 +1,13 @@ +kind: Project +name: kubernetes-secrets +environments: + - name: local + providers: + - name: local-kubernetes + - name: testing + providers: + - name: kubernetes + context: gke_garden-dev-200012_europe-west1-b_garden-dev-1 + namespace: kubernetes-secrets-testing-${local.env.CIRCLE_BUILD_NUM || local.username} + defaultHostname: kubernetes-secrets-testing-${local.env.CIRCLE_BUILD_NUM || local.username}.dev-1.sys.garden + buildMode: cluster-docker diff --git a/examples/project-variables/frontend/app.js b/examples/project-variables/frontend/app.js index f04bbf5a3b..a721f38b9c 100644 --- a/examples/project-variables/frontend/app.js +++ b/examples/project-variables/frontend/app.js @@ -3,7 +3,6 @@ const request = require('request-promise') const app = express(); const backendServiceEndpoint = `http://backend/hello-backend` -// const backendServiceEndpoint = `http://demo-project.local.app.garden/hello-backend` app.get('/hello-frontend', (req, res) => res.send('Hello from the frontend!')); diff --git a/garden-service/src/config/config-context.ts b/garden-service/src/config/config-context.ts index 3c628eaff0..b52efb63f2 100644 --- a/garden-service/src/config/config-context.ts +++ b/garden-service/src/config/config-context.ts @@ -299,9 +299,10 @@ export class ProviderConfigContext extends ProjectConfigContext { const exampleOutputs = { endpoint: "http://my-service/path/to/endpoint" } const exampleVersion = "v-17ad4cb3fd" -class ModuleContext extends ConfigContext { +export class ModuleContext extends ConfigContext { @schema( joi.string() + .required() .description("The build path of the module.") .example("/home/me/code/my-project/.garden/build/my-module"), ) @@ -309,6 +310,7 @@ class ModuleContext extends ConfigContext { @schema( joiIdentifierMap(joiPrimitive()) + .required() .description( "The outputs defined by the module (see individual module type " + "[references](https://docs.garden.io/reference/module-types) for details).", @@ -317,10 +319,20 @@ class ModuleContext extends ConfigContext { ) public outputs: PrimitiveMap - @schema(joi.string().description("The local path of the module.").example("/home/me/code/my-project/my-module")) + @schema( + joi.string() + .required() + .description("The local path of the module.") + .example("/home/me/code/my-project/my-module"), + ) public path: string - @schema(joi.string().description("The current version of the module.").example(exampleVersion)) + @schema( + joi.string() + .required() + .description("The current version of the module.") + .example(exampleVersion), + ) public version: string constructor(root: ConfigContext, moduleConfig: ModuleConfig, buildPath: string, version: ModuleVersion) { @@ -332,6 +344,13 @@ class ModuleContext extends ConfigContext { } } +const exampleModule = { + buildPath: "/home/me/code/my-project/.garden/build/my-module", + path: "/home/me/code/my-project/my-module", + outputs: {}, + version: exampleVersion, +} + /** * This context is available for template strings under the `module` key in configuration files. * It is a superset of the context available under the `project` key. @@ -340,7 +359,7 @@ export class ModuleConfigContext extends ProviderConfigContext { @schema( joiIdentifierMap(ModuleContext.getSchema()) .description("Retrieve information about modules that are defined in the project.") - .example({ "my-module": { path: "/home/me/code/my-project/my-module", version: exampleVersion } }), + .example({ "my-module": exampleModule }), ) public modules: Map Promise> diff --git a/garden-service/src/docs/config.ts b/garden-service/src/docs/config.ts index efb2b2dce3..3a92aee61a 100644 --- a/garden-service/src/docs/config.ts +++ b/garden-service/src/docs/config.ts @@ -31,6 +31,7 @@ import { mavenContainerConfigSchema } from "../plugins/maven-container/maven-con import { Garden } from "../garden" import { GARDEN_SERVICE_ROOT } from "../constants" import { indent, renderMarkdownTable } from "./util" +import { ModuleContext } from "../config/config-context" export const TEMPLATES_DIR = resolve(GARDEN_SERVICE_ROOT, "src", "docs", "templates") @@ -288,9 +289,10 @@ export function renderSchemaDescriptionYaml( const isArrayItem = parent && parent.type === "array" const isFirstArrayItem = isArrayItem && isFirstChild const isPrimitive = type !== "array" && type !== "object" - const stringifiedDefaultVal = JSON.stringify(defaultValue) - const exceptionallyTreatAsPrimitive = !hasChildren && !example - && stringifiedDefaultVal === "[]" || stringifiedDefaultVal === "{}" + + const stringifiedDefaultVal = useExampleForValue ? example : JSON.stringify(defaultValue) + const exceptionallyTreatAsPrimitive = !hasChildren + && (stringifiedDefaultVal === "[]" || stringifiedDefaultVal === "{}") // Prepend new line if applicable (easier then appending). We skip the new line if comments not shown. if (prevDesc && showComment) { @@ -331,7 +333,8 @@ export function renderSchemaDescriptionYaml( let value: string | string[] | undefined if (example && useExampleForValue) { - value = isPrimitive ? example : indent(example.split("\n"), 1) + const levels = type === "object" ? 2 : 1 + value = isPrimitive || exceptionallyTreatAsPrimitive ? example : indent(example.split("\n"), levels) } else { // Non-primitive values get rendered in the line below, indented by one value = isPrimitive || exceptionallyTreatAsPrimitive @@ -398,7 +401,7 @@ function renderModuleTypeReference( ) { const moduleTemplatePath = resolve(TEMPLATES_DIR, "module-type.hbs") const { markdownReference, yaml } = renderConfigReference(schema) - const outputsReference = renderConfigReference(outputsSchema, "modules..outputs.").markdownReference + const outputsReference = renderConfigReference(outputsSchema, "modules..").markdownReference const template = handlebars.compile(readFileSync(moduleTemplatePath).toString()) return template({ name, docs, markdownReference, yaml, outputsReference }) } @@ -457,10 +460,16 @@ export async function writeConfigReferenceDocs(docsRoot: string) { const actions = await garden.getActionHelper() const { docs, outputsSchema, schema, title } = await actions.describeType(name) + const moduleOutputsSchema = ModuleContext.getSchema().keys({ + outputs: outputsSchema + .required() + .description("The outputs defined by the module."), + }) + console.log("->", path) writeFileSync(path, renderModuleTypeReference( populateModuleSchema(schema), - outputsSchema, + moduleOutputsSchema, name, docs, )) diff --git a/garden-service/src/docs/templates/module-type.hbs b/garden-service/src/docs/templates/module-type.hbs index 6d469b45ea..b6d814f672 100644 --- a/garden-service/src/docs/templates/module-type.hbs +++ b/garden-service/src/docs/templates/module-type.hbs @@ -23,7 +23,6 @@ See the [Outputs](#outputs) section below for details. ## Outputs -The following keys are available via the `${modules..outputs}` template string key for `{{{name}}}` +The following keys are available via the `${modules.}` template string key for `{{{name}}}` modules. -{{{outputsReference}}} -{{/if}} \ No newline at end of file +{{{outputsReference}}}{{/if}} \ No newline at end of file diff --git a/garden-service/src/plugins/container/config.ts b/garden-service/src/plugins/container/config.ts index de186e8459..5d250ab53f 100644 --- a/garden-service/src/plugins/container/config.ts +++ b/garden-service/src/plugins/container/config.ts @@ -10,12 +10,13 @@ import deline = require("deline") import { Module, FileCopySpec } from "../../types/module" import { - joiEnvVars, joiUserIdentifier, joiArray, PrimitiveMap, joiPrimitive, joi, + envVarRegex, + Primitive, } from "../../config/common" import { Service, ingressHostnameSchema } from "../../types/service" import { DEFAULT_PORT_PROTOCOL } from "../../constants" @@ -133,6 +134,46 @@ export type ContainerServiceConfig = ServiceConfig const annotationsSchema = joiStringMap(joi.string()) .default(() => ({}), "{}") +export interface EnvSecretRef { + secretRef: { + name: string, + key?: string, + } +} + +const secretRefSchema = joi.object() + .keys({ + secretRef: joi.object() + .keys({ + name: joi.string() + .required() + .description("The name of the secret to refer to."), + key: joi.string() + .description("The key to read from in the referenced secret. May be required for some providers."), + }), + }) + .description( + "A reference to a secret, that should be applied to the environment variable. " + + "Note that this secret must already be defined in the provider.", + ) + +export interface ContainerEnvVars { + [key: string]: Primitive | EnvSecretRef +} + +export const containerEnvVarsSchema = joi.object() + .pattern(envVarRegex, joi.alternatives(joiPrimitive(), secretRefSchema)) + .default(() => ({}), "{}") + .unknown(false) + .description( + "Key/value map of environment variables. Keys must be valid POSIX environment variable names " + + "(must not start with `GARDEN`) and values must be primitives or references to secrets.", + ) + .example([{ + MY_VAR: "some-value", + MY_SECRET_VAR: { secretRef: { name: "my-secret", key: "some-key" } }, + }, {}]) + const ingressSchema = joi.object() .keys({ annotations: annotationsSchema @@ -245,7 +286,7 @@ const serviceSchema = baseServiceSpecSchema [{ path: "/api", port: "http" }], {}, ]), - env: joiEnvVars(), + env: containerEnvVarsSchema, healthCheck: healthCheckSchema .description("Specify how the service's health should be checked after deploying."), hotReloadCommand: joi.array().items(joi.string()) @@ -309,7 +350,7 @@ export interface ContainerService extends Service { } export interface ContainerTestSpec extends BaseTestSpec { command?: string[], args: string[], - env: { [key: string]: string }, + env: ContainerEnvVars, } export const containerTestSchema = baseTestSpecSchema @@ -320,13 +361,13 @@ export const containerTestSchema = baseTestSpecSchema args: joi.array().items(joi.string()) .description("The arguments used to run the test inside the container.") .example([["npm", "test"], {}]), - env: joiEnvVars(), + env: containerEnvVarsSchema, }) export interface ContainerTaskSpec extends BaseTaskSpec { command?: string[], args: string[] - env: PrimitiveMap + env: ContainerEnvVars } export const containerTaskSchema = baseTaskSpecSchema @@ -337,7 +378,7 @@ export const containerTaskSchema = baseTaskSpecSchema args: joi.array().items(joi.string()) .description("The arguments used to run the task inside the container.") .example([["rake", "db:migrate"], {}]), - env: joiEnvVars(), + env: containerEnvVarsSchema, }) .description("A task that can be run in the container.") diff --git a/garden-service/src/plugins/container/container.ts b/garden-service/src/plugins/container/container.ts index c5b890622e..389dbbebdf 100644 --- a/garden-service/src/plugins/container/container.ts +++ b/garden-service/src/plugins/container/container.ts @@ -26,10 +26,12 @@ export const containerModuleOutputsSchema = joi.object() .required() .description( "The name of the image (without tag/version) that the module uses for local builds and deployments.", - ), + ) + .example("my-module"), "deployment-image-name": joi.string() .required() - .description("The name of the image (without tag/version) that the module will use during deployment."), + .description("The name of the image (without tag/version) that the module will use during deployment.") + .example("my-deployment-registry.io/my-org/my-module"), }) export async function configureContainerModule({ ctx, moduleConfig }: ConfigureModuleParams) { diff --git a/garden-service/src/plugins/kubernetes/config.ts b/garden-service/src/plugins/kubernetes/config.ts index 9424b27cd1..c5171dfe1a 100644 --- a/garden-service/src/plugins/kubernetes/config.ts +++ b/garden-service/src/plugins/kubernetes/config.ts @@ -16,7 +16,7 @@ import { deline } from "../../util/string" export const name = "kubernetes" -export interface SecretRef { +export interface ProviderSecretRef { name: string namespace: string } @@ -24,7 +24,7 @@ export interface SecretRef { export interface IngressTlsCertificate { name: string hostnames?: string[] - secretRef: SecretRef + secretRef: ProviderSecretRef } interface KubernetesResourceSpec { @@ -63,7 +63,7 @@ export interface KubernetesBaseConfig extends ProviderConfig { defaultHostname?: string defaultUsername?: string forceSsl: boolean - imagePullSecrets: SecretRef[] + imagePullSecrets: ProviderSecretRef[] ingressHttpPort: number ingressHttpsPort: number ingressClass?: string diff --git a/garden-service/src/plugins/kubernetes/container/build.ts b/garden-service/src/plugins/kubernetes/container/build.ts index 2cea14997f..00b8b09c52 100644 --- a/garden-service/src/plugins/kubernetes/container/build.ts +++ b/garden-service/src/plugins/kubernetes/container/build.ts @@ -264,71 +264,62 @@ async function runKaniko(provider: KubernetesProvider, log: LogEntry, module: Co const registryHostname = getRegistryHostname() return runPod({ - args, context: provider.config.context, - envVars: {}, ignoreError: false, image: kanikoImage, interactive: false, + log, module, namespace: systemNamespace, - log, - overrides: { - metadata: { - // Workaround to make sure sidecars are not injected, - // due to https://github.com/kubernetes/kubernetes/issues/25908 - annotations: { "sidecar.istio.io/inject": "false" }, - }, - spec: { - shareProcessNamespace: true, - containers: [ - { - name: "kaniko", - image: kanikoImage, - args, - volumeMounts: [{ - name: syncDataVolumeName, - mountPath: "/garden-build", - }], - resources: { - limits: { - cpu: millicpuToString(provider.config.resources.builder.limits.cpu), - memory: megabytesToString(provider.config.resources.builder.limits.memory), - }, - requests: { - cpu: millicpuToString(provider.config.resources.builder.requests.cpu), - memory: megabytesToString(provider.config.resources.builder.requests.memory), - }, + spec: { + shareProcessNamespace: true, + containers: [ + { + name: "kaniko", + image: kanikoImage, + args, + volumeMounts: [{ + name: syncDataVolumeName, + mountPath: "/garden-build", + }], + resources: { + limits: { + cpu: millicpuToString(provider.config.resources.builder.limits.cpu), + memory: megabytesToString(provider.config.resources.builder.limits.memory), }, - }, - { - name: "proxy", - image: "basi/socat:v0.1.0", - command: ["/bin/sh", "-c", `socat TCP-LISTEN:5000,fork TCP:${registryHostname}:5000 || exit 0`], - ports: [{ - name: "proxy", - containerPort: registryPort, - protocol: "TCP", - }], - readinessProbe: { - tcpSocket: { port: registryPort }, + requests: { + cpu: millicpuToString(provider.config.resources.builder.requests.cpu), + memory: megabytesToString(provider.config.resources.builder.requests.memory), }, }, - // This is a little workaround so that the socat proxy doesn't just keep running after the build finishes. - { - name: "killer", - image: "busybox", - command: [ - "sh", "-c", - "while true; do if pidof executor > /dev/null; then sleep 0.5; else killall socat; exit 0; fi done", - ], + }, + { + name: "proxy", + image: "basi/socat:v0.1.0", + command: ["/bin/sh", "-c", `socat TCP-LISTEN:5000,fork TCP:${registryHostname}:5000 || exit 0`], + ports: [{ + name: "proxy", + containerPort: registryPort, + protocol: "TCP", + }], + readinessProbe: { + tcpSocket: { port: registryPort }, }, - ], - volumes: [{ - name: syncDataVolumeName, - persistentVolumeClaim: { claimName: syncDataVolumeName }, - }], - }, + }, + // This is a little workaround so that the socat proxy doesn't just keep running after the build finishes. + { + name: "killer", + image: "busybox", + command: [ + "sh", "-c", + "while true; do if pidof executor > /dev/null; then sleep 0.5; else killall socat; exit 0; fi done", + ], + }, + ], + volumes: [{ + name: syncDataVolumeName, + persistentVolumeClaim: { claimName: syncDataVolumeName }, + }], }, podName, timeout: buildTimeout, diff --git a/garden-service/src/plugins/kubernetes/container/deployment.ts b/garden-service/src/plugins/kubernetes/container/deployment.ts index ae050ed810..f9a4b46a18 100644 --- a/garden-service/src/plugins/kubernetes/container/deployment.ts +++ b/garden-service/src/plugins/kubernetes/container/deployment.ts @@ -7,7 +7,7 @@ */ import { V1Container } from "@kubernetes/client-node" -import { extend, keyBy, set, toPairs } from "lodash" +import { extend, keyBy, set } from "lodash" import { RuntimeContext, Service, ServiceStatus } from "../../../types/service" import { ContainerModule, ContainerService } from "../../container/config" import { createIngressResources } from "./ingress" @@ -19,14 +19,14 @@ import { PluginContext } from "../../../plugin-context" import { KubeApi } from "../api" import { KubernetesProvider, KubernetesPluginContext } from "../config" import { configureHotReload } from "../hot-reload" -import { KubernetesResource, KubeEnvVar } from "../types" +import { KubernetesResource } from "../types" import { ConfigurationError } from "../../../exceptions" import { getContainerServiceStatus } from "./status" import { containerHelpers } from "../../container/helpers" import { LogEntry } from "../../../logger/log-entry" import { DeployServiceParams } from "../../../types/plugin/service/deployService" import { DeleteServiceParams } from "../../../types/plugin/service/deleteService" -import { millicpuToString, kilobytesToString } from "../util" +import { millicpuToString, kilobytesToString, prepareEnvVars } from "../util" import { gardenAnnotationKey } from "../../../util/string" import chalk from "chalk" @@ -100,7 +100,6 @@ export async function createDeployment( const spec = service.spec let configuredReplicas = service.spec.replicas const deployment: any = deploymentConfig(service, configuredReplicas, namespace) - const envVars = { ...runtimeContext.envVars, ...service.spec.env } if (enableHotReload && service.spec.replicas > 1) { log.warn({ @@ -110,7 +109,7 @@ export async function createDeployment( configuredReplicas = 1 } - const env: KubeEnvVar[] = toPairs(envVars).map(([name, value]) => ({ name, value: value + "" })) + const env = prepareEnvVars({ ...runtimeContext.envVars, ...service.spec.env }) // expose some metadata to the container env.push({ @@ -128,6 +127,11 @@ export async function createDeployment( valueFrom: { fieldRef: { fieldPath: "status.podIP" } }, }) + env.push({ + name: "POD_SERVICE_ACCOUNT", + valueFrom: { fieldRef: { fieldPath: "spec.serviceAccountName" } }, + }) + const registryConfig = provider.config.deploymentRegistry const imageId = await containerHelpers.getDeploymentImageId(service.module, registryConfig) diff --git a/garden-service/src/plugins/kubernetes/container/run.ts b/garden-service/src/plugins/kubernetes/container/run.ts index 5f3fe95564..b9da606b64 100644 --- a/garden-service/src/plugins/kubernetes/container/run.ts +++ b/garden-service/src/plugins/kubernetes/container/run.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { includes, extend } from "lodash" +import { includes } from "lodash" import { DeploymentError } from "../../../exceptions" import { ContainerModule } from "../../container/config" import { KubeApi } from "../api" @@ -23,7 +23,9 @@ import { RunResult } from "../../../types/plugin/base" import { RunServiceParams } from "../../../types/plugin/service/runService" import { RunTaskParams, RunTaskResult } from "../../../types/plugin/task/runTask" import { LogEntry } from "../../../logger/log-entry" -import { getWorkloadPods } from "../util" +import { getWorkloadPods, prepareEnvVars } from "../util" +import { uniqByName } from "../../../util/util" +import { V1PodSpec } from "@kubernetes/client-node" export async function execInService(params: ExecInServiceParams) { const { ctx, log, service, command, interactive } = params @@ -96,20 +98,32 @@ export async function runContainerModule( const provider = ctx.provider const context = provider.config.context const namespace = await getAppNamespace(ctx, log, provider) + + // Apply overrides const image = await containerHelpers.getDeploymentImageId(module, provider.config.deploymentRegistry) + const envVars = runtimeContext.envVars + const env = uniqByName(prepareEnvVars(envVars)) + + const spec: V1PodSpec = { + containers: [{ + name: "main", + image, + ...command && { command }, + ...args && { args }, + env, + }], + } return runPod({ context, - namespace, - module, - envVars: runtimeContext.envVars, - command, - args, image, interactive, ignoreError, - timeout, log, + module, + namespace, + spec, + timeout, }) } @@ -132,28 +146,36 @@ export async function runContainerService( export async function runContainerTask( { ctx, log, module, task, taskVersion, interactive, runtimeContext }: RunTaskParams, ): Promise { - extend(runtimeContext.envVars, task.spec.env || {}) - const provider = ctx.provider const context = provider.config.context const namespace = await getAppNamespace(ctx, log, provider) + + // Apply overrides + const { args, command } = task.spec const image = await containerHelpers.getDeploymentImageId(module, provider.config.deploymentRegistry) - const { command, args } = task.spec + const envVars = { ...runtimeContext.envVars, ...task.spec.env } + const env = uniqByName(prepareEnvVars(envVars)) + + const spec: V1PodSpec = { + containers: [{ + name: "main", + image, + ...command && { command }, + ...args && { args }, + env, + }], + } const res = await runPod({ context, - namespace, - module, - envVars: runtimeContext.envVars, - command, - args, image, interactive, ignoreError: false, - timeout: task.spec.timeout || 9999, - // Workaround to make sure sidecars are not injected, due to https://github.com/kubernetes/kubernetes/issues/25908 - overrides: { metadata: { annotations: { "sidecar.istio.io/inject": "false" } } }, log, + module, + namespace, + spec, + timeout: task.spec.timeout || 9999, }) const result = { ...res, taskName: task.name } diff --git a/garden-service/src/plugins/kubernetes/container/test.ts b/garden-service/src/plugins/kubernetes/container/test.ts index 03983f9f10..5abbd27c08 100644 --- a/garden-service/src/plugins/kubernetes/container/test.ts +++ b/garden-service/src/plugins/kubernetes/container/test.ts @@ -8,30 +8,52 @@ import { ContainerModule } from "../../container/config" import { DEFAULT_TEST_TIMEOUT } from "../../../constants" -import { runContainerModule } from "./run" import { storeTestResult } from "../test-results" import { TestModuleParams } from "../../../types/plugin/module/testModule" import { TestResult } from "../../../types/plugin/module/getTestResult" +import { uniqByName } from "../../../util/util" +import { prepareEnvVars } from "../util" +import { V1PodSpec } from "@kubernetes/client-node" +import { containerHelpers } from "../../container/helpers" +import { KubernetesProvider } from "../config" +import { runPod } from "../run" +import { getAppNamespace } from "../namespace" export async function testContainerModule( { ctx, interactive, module, runtimeContext, testConfig, testVersion, log }: TestModuleParams, ): Promise { - const testName = testConfig.name + const provider = ctx.provider as KubernetesProvider const { command, args } = testConfig.spec - runtimeContext.envVars = { ...runtimeContext.envVars, ...testConfig.spec.env } + const testName = testConfig.name const timeout = testConfig.timeout || DEFAULT_TEST_TIMEOUT + const namespace = await getAppNamespace(ctx, log, provider) - const result = await runContainerModule({ - ctx, - module, - command, - args, + // Apply overrides + const image = await containerHelpers.getDeploymentImageId(module, provider.config.deploymentRegistry) + const envVars = { ...runtimeContext.envVars, ...testConfig.spec.env } + const env = uniqByName(prepareEnvVars(envVars)) + + const spec: V1PodSpec = { + containers: [{ + name: "main", + image, + ...command && { command }, + ...args && { args }, + env, + }], + } + + const result = await runPod({ + context: provider.config.context, + image, interactive, ignoreError: true, // to ensure results get stored when an error occurs - runtimeContext, - timeout, log, + module, + namespace, + spec, + timeout, }) return storeTestResult({ ctx, log, module, testName, testVersion, result }) diff --git a/garden-service/src/plugins/kubernetes/helm/common.ts b/garden-service/src/plugins/kubernetes/helm/common.ts index 152262c330..73cadd381e 100644 --- a/garden-service/src/plugins/kubernetes/helm/common.ts +++ b/garden-service/src/plugins/kubernetes/helm/common.ts @@ -23,9 +23,11 @@ import { HotReloadableResource } from "../hot-reload" import { ConfigurationError, PluginError } from "../../../exceptions" import { Module } from "../../../types/module" import { findByName } from "../../../util/util" -import { deline } from "../../../util/string" +import { deline, tailString } from "../../../util/string" import { getAnnotation, flattenResources } from "../util" import { KubernetesPluginContext } from "../config" +import { RunResult } from "../../../types/plugin/base" +import { MAX_RUN_RESULT_OUTPUT_LENGTH } from "../constants" /** * Returns true if the specified Helm module contains a template (as opposed to just referencing a remote template). @@ -330,3 +332,10 @@ function loadTemplate(template: string) { return obj }) } + +export function trimRunOutput(result: RunResult): RunResult { + return { + ...result, + output: tailString(result.output, MAX_RUN_RESULT_OUTPUT_LENGTH, true), + } +} diff --git a/garden-service/src/plugins/kubernetes/helm/config.ts b/garden-service/src/plugins/kubernetes/helm/config.ts index 958cf31a1c..feeb7e3857 100644 --- a/garden-service/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/src/plugins/kubernetes/helm/config.ts @@ -13,7 +13,6 @@ import { joiPrimitive, joiArray, joiIdentifier, - joiEnvVars, joiUserIdentifier, DeepPrimitiveMap, joi, @@ -26,7 +25,7 @@ import { HotReloadableKind, hotReloadableKinds } from "../hot-reload" import { BaseTestSpec, baseTestSpecSchema } from "../../../config/test" import { BaseTaskSpec } from "../../../config/task" import { Service } from "../../../types/service" -import { ContainerModule } from "../../container/config" +import { ContainerModule, ContainerEnvVars, containerEnvVarsSchema } from "../../container/config" import { baseBuildSpecSchema } from "../../../config/module" import { ConfigureModuleParams, ConfigureModuleResult } from "../../../types/plugin/module/configure" @@ -46,14 +45,14 @@ export interface HelmTaskSpec extends BaseTaskSpec { resource: HelmResourceSpec command?: string[] args: string[] - env: { [key: string]: string } + env: ContainerEnvVars } export interface HelmTestSpec extends BaseTestSpec { resource: HelmResourceSpec command?: string[] args: string[] - env: { [key: string]: string } + env: ContainerEnvVars } export interface HelmModule extends Module { } @@ -110,7 +109,7 @@ export const execTaskSchema = baseTestSpecSchema ), args: joi.array().items(joi.string()) .description("The arguments to pass to the pod used for execution."), - env: joiEnvVars(), + env: containerEnvVarsSchema, }) export const execTestSchema = baseTestSpecSchema @@ -123,7 +122,7 @@ export const execTestSchema = baseTestSpecSchema ), args: joi.array().items(joi.string()) .description("The arguments to pass to the pod used for testing."), - env: joiEnvVars(), + env: containerEnvVarsSchema, }) export interface HelmServiceSpec extends ServiceSpec { diff --git a/garden-service/src/plugins/kubernetes/helm/run.ts b/garden-service/src/plugins/kubernetes/helm/run.ts index 88f68a97dd..b8e50e856f 100644 --- a/garden-service/src/plugins/kubernetes/helm/run.ts +++ b/garden-service/src/plugins/kubernetes/helm/run.ts @@ -6,20 +6,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { HelmModule, HelmResourceSpec } from "./config" +import { HelmModule } from "./config" import { getAppNamespace } from "../namespace" import { runPod } from "../run" import { findServiceResource, getChartResources, getResourceContainer, getServiceResourceSpec } from "./common" -import { PluginContext } from "../../../plugin-context" -import { LogEntry } from "../../../logger/log-entry" import { ConfigurationError } from "../../../exceptions" import { KubernetesPluginContext } from "../config" import { storeTaskResult } from "../task-results" import { RunModuleParams } from "../../../types/plugin/module/runModule" import { RunResult } from "../../../types/plugin/base" import { RunTaskParams, RunTaskResult } from "../../../types/plugin/task/runTask" -import { MAX_RUN_RESULT_OUTPUT_LENGTH } from "../constants" -import { tailString } from "../../../util/string" +import { uniqByName } from "../../../util/util" +import { prepareEnvVars } from "../util" +import { V1PodSpec } from "@kubernetes/client-node" export async function runHelmModule( { @@ -29,9 +28,9 @@ export async function runHelmModule( const k8sCtx = ctx const context = k8sCtx.provider.config.context const namespace = await getAppNamespace(k8sCtx, log, k8sCtx.provider) - const serviceResourceSpec = getServiceResourceSpec(module) + const resourceSpec = getServiceResourceSpec(module) - if (!serviceResourceSpec) { + if (!resourceSpec) { throw new ConfigurationError( `Helm module ${module.name} does not specify a \`serviceResource\`. ` + `Please configure that in order to run the module ad-hoc.`, @@ -39,51 +38,77 @@ export async function runHelmModule( ) } - const image = await getImage(k8sCtx, module, log, serviceResourceSpec) + const chartResources = await getChartResources(k8sCtx, module, log) + const target = await findServiceResource({ ctx: k8sCtx, log, chartResources, module, resourceSpec }) + const container = getResourceContainer(target, resourceSpec.containerName) + + // Apply overrides + const env = uniqByName([...prepareEnvVars(runtimeContext.envVars), ...container.env || []]) + + const spec: V1PodSpec = { + containers: [{ + ...container, + ...command && { command }, + ...args && { args }, + env, + }], + } return runPod({ context, - namespace, - module, - envVars: runtimeContext.envVars, - command, - args, - image, + image: container.image, interactive, ignoreError, - timeout, log, + module, + namespace, + spec, + timeout, }) } export async function runHelmTask( - { ctx, log, module, task, taskVersion, interactive, runtimeContext, timeout }: RunTaskParams, + { ctx, log, module, task, taskVersion, interactive, timeout }: RunTaskParams, ): Promise { + // TODO: deduplicate this from testHelmModule const k8sCtx = ctx const context = k8sCtx.provider.config.context const namespace = await getAppNamespace(k8sCtx, log, k8sCtx.provider) const { command, args } = task.spec - const image = await getImage(k8sCtx, module, log, task.spec.resource || getServiceResourceSpec(module)) + const chartResources = await getChartResources(k8sCtx, module, log) + const resourceSpec = task.spec.resource || getServiceResourceSpec(module) + const target = await findServiceResource({ ctx: k8sCtx, log, chartResources, module, resourceSpec }) + const container = getResourceContainer(target, resourceSpec.containerName) + + // Apply overrides + const env = uniqByName([...prepareEnvVars(task.spec.env), ...container.env || []]) + + const spec: V1PodSpec = { + containers: [{ + ...container, + ...command && { command }, + ...args && { args }, + env, + // TODO: consider supporting volume mounts in ad-hoc runs (would need specific logic and testing) + volumeMounts: [], + }], + } const res = await runPod({ context, - namespace, - module, - envVars: { ...runtimeContext.envVars, ...task.spec.env }, - command, - args, - image, + image: container.image, interactive, ignoreError: false, - timeout, log, + module, + namespace, + spec, + timeout, }) const result = { ...res, - // Make sure we don't exceed max length of ConfigMap - output: tailString(res.output, MAX_RUN_RESULT_OUTPUT_LENGTH, true), taskName: task.name, } @@ -98,12 +123,3 @@ export async function runHelmTask( return result } - -async function getImage(ctx: PluginContext, module: HelmModule, log: LogEntry, resourceSpec: HelmResourceSpec) { - // find the relevant resource, and from that the container image to run - const chartResources = await getChartResources(ctx, module, log) - const resource = await findServiceResource({ ctx, log, module, chartResources, resourceSpec }) - const container = getResourceContainer(resource) - - return container.image -} diff --git a/garden-service/src/plugins/kubernetes/helm/test.ts b/garden-service/src/plugins/kubernetes/helm/test.ts index ca8248b71b..7be47369c1 100644 --- a/garden-service/src/plugins/kubernetes/helm/test.ts +++ b/garden-service/src/plugins/kubernetes/helm/test.ts @@ -15,39 +15,62 @@ import { findServiceResource, getChartResources, getResourceContainer, getServic import { KubernetesPluginContext } from "../config" import { TestModuleParams } from "../../../types/plugin/module/testModule" import { TestResult } from "../../../types/plugin/module/getTestResult" +import { V1PodSpec } from "@kubernetes/client-node" +import { uniqByName } from "../../../util/util" +import { prepareEnvVars } from "../util" export async function testHelmModule( { ctx, log, interactive, module, runtimeContext, testConfig, testVersion }: TestModuleParams, ): Promise { - const testName = testConfig.name - const { command, args } = testConfig.spec - runtimeContext.envVars = { ...runtimeContext.envVars, ...testConfig.spec.env } - const timeout = testConfig.timeout || DEFAULT_TEST_TIMEOUT - const k8sCtx = ctx const context = k8sCtx.provider.config.context const namespace = await getAppNamespace(k8sCtx, log, k8sCtx.provider) + // Get the container spec to use for running const chartResources = await getChartResources(k8sCtx, module, log) const resourceSpec = testConfig.spec.resource || getServiceResourceSpec(module) const target = await findServiceResource({ ctx: k8sCtx, log, chartResources, module, resourceSpec }) const container = getResourceContainer(target, resourceSpec.containerName) + + const testName = testConfig.name + const { command, args } = testConfig.spec const image = container.image + const timeout = testConfig.timeout || DEFAULT_TEST_TIMEOUT + + // Apply overrides + const envVars = { ...runtimeContext.envVars, ...testConfig.spec.env } + const env = uniqByName([...prepareEnvVars(envVars), ...container.env || []]) + + const spec: V1PodSpec = { + containers: [{ + ...container, + ...command && { command }, + ...args && { args }, + env, + // TODO: consider supporting volume mounts in ad-hoc runs (would need specific logic and testing) + volumeMounts: [], + }], + } const result = await runPod({ context, - namespace, - module, - envVars: runtimeContext.envVars, - command, - args, image, interactive, ignoreError: true, // to ensure results get stored when an error occurs - timeout, log, + module, + namespace, + spec, + timeout, }) - return storeTestResult({ ctx: k8sCtx, log, module, testName, testVersion, result }) + return storeTestResult({ + ctx: k8sCtx, + log, + module, + testName, + testVersion, + result, + }) } diff --git a/garden-service/src/plugins/kubernetes/run.ts b/garden-service/src/plugins/kubernetes/run.ts index 447f2944b4..f9f5f2ccf2 100644 --- a/garden-service/src/plugins/kubernetes/run.ts +++ b/garden-service/src/plugins/kubernetes/run.ts @@ -8,79 +8,77 @@ import { RunResult } from "../../types/plugin/base" import { kubectl } from "./kubectl" -import { PrimitiveMap } from "../../config/common" import { Module } from "../../types/module" import { LogEntry } from "../../logger/log-entry" +import { V1PodSpec } from "@kubernetes/client-node" +import { PluginError } from "../../exceptions" interface RunPodParams { context: string, image: string, - envVars: PrimitiveMap, - command?: string[], - args: string[], interactive: boolean, ignoreError: boolean, log: LogEntry, module: Module, namespace: string, - overrides?: any, + annotations?: { [key: string]: string } + spec: V1PodSpec, podName?: string, timeout?: number, } export async function runPod( { - args, - command, context, - envVars, ignoreError, image, interactive, log, module, namespace, - overrides, + annotations, + spec, podName, timeout, }: RunPodParams, ): Promise { - const envArgs = Object.entries(envVars).map(([k, v]) => `--env=${k}=${v}`) + const overrides: any = { + metadata: { + annotations: { + // Workaround to make sure sidecars are not injected, + // due to https://github.com/kubernetes/kubernetes/issues/25908 + "sidecar.istio.io/inject": "false", + ...annotations || {}, + }, + }, + spec, + } - const cmd = (command && command.length) ? command : [] + if (!spec.containers || spec.containers.length === 0) { + throw new PluginError(`Pod spec for runPod must contain at least one container`, { + spec, + }) + } - const opts = [ + const kubecmd = [ + "run", + podName || `run-${module.name}-${Math.round(new Date().getTime())}`, `--image=${image}`, "--restart=Never", "--quiet", "--rm", // Need to attach to get the log output and exit code. "-i", + // This is a little messy, but it works... + "--overrides", `${JSON.stringify(overrides)}`, ] - if (overrides) { - opts.push("--overrides", `${JSON.stringify(overrides)}`) - } - if (interactive) { - opts.push("--tty") + kubecmd.push("--tty") } - if (cmd.length) { - opts.push("--command") - } - - const kubecmd = [ - "run", - podName || `run-${module.name}-${Math.round(new Date().getTime())}`, - ...opts, - ...envArgs, - "--", - ...cmd, - ...args, - ] - - log.verbose(`Running ${cmd.join(" ")} '${args.join(" ")}'`) + const command = [...spec.containers[0].command || [], ...spec.containers[0].args || []] + log.verbose(`Running '${command.join(" ")}'`) const startedAt = new Date() @@ -96,7 +94,7 @@ export async function runPod( return { moduleName: module.name, - command: [...cmd, ...args], + command, version: module.version.versionString, startedAt, completedAt: new Date(), diff --git a/garden-service/src/plugins/kubernetes/secrets.ts b/garden-service/src/plugins/kubernetes/secrets.ts index e730ff308c..137646cb12 100644 --- a/garden-service/src/plugins/kubernetes/secrets.ts +++ b/garden-service/src/plugins/kubernetes/secrets.ts @@ -9,7 +9,7 @@ import { V1Secret } from "@kubernetes/client-node" import { KubeApi } from "./api" -import { SecretRef, KubernetesPluginContext } from "./config" +import { ProviderSecretRef, KubernetesPluginContext } from "./config" import { ConfigurationError } from "../../exceptions" import { getMetadataNamespace } from "./namespace" import { GetSecretParams } from "../../types/plugin/provider/getSecret" @@ -88,7 +88,7 @@ export async function deleteSecret({ ctx, log, key }: DeleteSecretParams) { /** * Make sure the specified secret exists in the target namespace, copying it if necessary. */ -export async function ensureSecret(api: KubeApi, secretRef: SecretRef, targetNamespace: string) { +export async function ensureSecret(api: KubeApi, secretRef: ProviderSecretRef, targetNamespace: string) { let secret: KubernetesResource try { diff --git a/garden-service/src/plugins/kubernetes/task-results.ts b/garden-service/src/plugins/kubernetes/task-results.ts index 916ac47d9c..f930b47ca6 100644 --- a/garden-service/src/plugins/kubernetes/task-results.ts +++ b/garden-service/src/plugins/kubernetes/task-results.ts @@ -17,11 +17,11 @@ import { RunTaskResult } from "../../types/plugin/task/runTask" import { deserializeValues } from "../../util/util" import { PluginContext } from "../../plugin-context" import { LogEntry } from "../../logger/log-entry" -import { gardenAnnotationKey, tailString } from "../../util/string" +import { gardenAnnotationKey } from "../../util/string" import { Module } from "../../types/module" import * as hasha from "hasha" import { upsertConfigMap } from "./util" -import { MAX_RUN_RESULT_OUTPUT_LENGTH } from "./constants" +import { trimRunOutput } from "./helm/common" export async function getTaskResult( { ctx, log, module, task, taskVersion }: GetTaskResultParams, @@ -87,10 +87,6 @@ export async function storeTaskResult( [gardenAnnotationKey("moduleVersion")]: module.version.versionString, [gardenAnnotationKey("version")]: taskVersion.versionString, }, - data: { - ...result, - // Make sure the output isn't too large for a ConfigMap - output: tailString(result.output, MAX_RUN_RESULT_OUTPUT_LENGTH, true), - }, + data: trimRunOutput(result), }) } diff --git a/garden-service/src/plugins/kubernetes/test-results.ts b/garden-service/src/plugins/kubernetes/test-results.ts index 3403c8714d..6aa671eaab 100644 --- a/garden-service/src/plugins/kubernetes/test-results.ts +++ b/garden-service/src/plugins/kubernetes/test-results.ts @@ -21,6 +21,7 @@ import { RunResult } from "../../types/plugin/base" import * as hasha from "hasha" import { gardenAnnotationKey } from "../../util/string" import { upsertConfigMap } from "./util" +import { trimRunOutput } from "./helm/common" const testResultNamespace = systemMetadataNamespace @@ -77,7 +78,7 @@ export async function storeTestResult( const api = await KubeApi.factory(log, k8sCtx.provider.config.context) const testResult: TestResult = { - ...result, + ...trimRunOutput(result), testName, } diff --git a/garden-service/src/plugins/kubernetes/types.ts b/garden-service/src/plugins/kubernetes/types.ts index d579263478..44abc80420 100644 --- a/garden-service/src/plugins/kubernetes/types.ts +++ b/garden-service/src/plugins/kubernetes/types.ts @@ -86,9 +86,3 @@ export type KubernetesWorkload = KubernetesResource | KubernetesResource | KubernetesResource - -export interface KubeEnvVar { - name: string - value?: string - valueFrom?: { fieldRef: { fieldPath: string } } -} diff --git a/garden-service/src/plugins/kubernetes/util.ts b/garden-service/src/plugins/kubernetes/util.ts index f51e8f8bd4..3acd1ca28b 100644 --- a/garden-service/src/plugins/kubernetes/util.ts +++ b/garden-service/src/plugins/kubernetes/util.ts @@ -11,7 +11,7 @@ import { get, flatten, uniqBy } from "lodash" import { ChildProcess } from "child_process" import getPort = require("get-port") const AsyncLock = require("async-lock") -import { V1Pod } from "@kubernetes/client-node" +import { V1Pod, V1EnvVar } from "@kubernetes/client-node" import { KubernetesResource, KubernetesWorkload, KubernetesPod, KubernetesServerResource } from "./types" import { splitLast, serializeValues } from "../../util/util" @@ -23,6 +23,8 @@ import { kubectl } from "./kubectl" import { registerCleanupFunction } from "../../util/util" import { gardenAnnotationKey, base64 } from "../../util/string" import { MAX_CONFIGMAP_DATA_SIZE } from "./constants" +import { ContainerEnvVars } from "../container/config" +import { ConfigurationError } from "../../exceptions" export const workloadTypes = ["Deployment", "DaemonSet", "ReplicaSet", "StatefulSet"] @@ -274,3 +276,33 @@ export async function upsertConfigMap( export function flattenResources(resources: KubernetesResource[]) { return flatten(resources.map((r: any) => r.apiVersion === "v1" && r.kind === "List" ? r.items : [r])) } + +/** + * Maps an array of env vars, as specified on a container module, to a list of Kubernetes `V1EnvVar`s. + */ +export function prepareEnvVars(env: ContainerEnvVars): V1EnvVar[] { + return Object.entries(env) + .map(([name, value]) => { + if (value === null) { + return { name, value: "null" } + } else if (typeof value === "object") { + if (!value.secretRef.key) { + throw new ConfigurationError(`kubernetes: Must specify \`key\` on secretRef for env variable ${name}`, { + name, + value, + }) + } + return { + name, + valueFrom: { + secretKeyRef: { + name: value.secretRef.name, + key: value.secretRef.key!, + }, + }, + } + } else { + return { name, value: value.toString() } + } + }) +} diff --git a/garden-service/test/unit/src/docs/config.ts b/garden-service/test/unit/src/docs/config.ts index fafcca3e2d..b471d6f4d0 100644 --- a/garden-service/test/unit/src/docs/config.ts +++ b/garden-service/test/unit/src/docs/config.ts @@ -6,10 +6,10 @@ import { } from "../../../../src/docs/config" import { expect } from "chai" import dedent = require("dedent") -import { joiArray, joi } from "../../../../src/config/common" +import { joiArray, joi, joiEnvVars } from "../../../../src/config/common" describe("config", () => { - const serivcePortSchema = joi.number().default((context) => context.containerPort, "") + const servicePortSchema = joi.number().default((context) => context.containerPort, "") .example("8080") .description("description") @@ -27,7 +27,7 @@ describe("config", () => { }) .description("test object") - const testArray = joiArray(serivcePortSchema) + const testArray = joiArray(servicePortSchema) .description("test array") const portSchema = joi.object() @@ -35,7 +35,7 @@ describe("config", () => { containerPort: joi.number() .required() .description("description"), - servicePort: serivcePortSchema, + servicePort: servicePortSchema, testObject, testArray, }) @@ -121,6 +121,27 @@ describe("config", () => { testArray: [] `) }) + + it("should correctly render object example values", () => { + const schema = joi.object() + .keys({ + env: joiEnvVars() + .example({ + foo: "bar", + boo: "far", + }), + }) + const schemaDescriptions = normalizeDescriptions(schema.describe()) + const yaml = renderSchemaDescriptionYaml( + schemaDescriptions, + { showComment: false, showEllipsisBetweenKeys: true, useExampleForValue: true }, + ) + expect(yaml).to.equal(dedent` + env: + foo: bar + boo: far + `) + }) }) describe("getDefaultValue", () => { @@ -130,7 +151,7 @@ describe("config", () => { }) it("should get the default value of a function with context", () => { - const value = getDefaultValue(serivcePortSchema.describe()) + const value = getDefaultValue(servicePortSchema.describe()) expect(value).to.eq("") }) })