From da24e77567d93415fc5e52d62ace56affcbf24c0 Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Wed, 18 Dec 2019 13:33:50 +0100 Subject: [PATCH] feat(providers): add conftest providers `conftest` is a handy provider for applying policies on your config files. You can use the `conftest` provider and module type directly to manually configure your tests, or you can use the "generator" providers `conftest-container` and `conftest-kubernetes` to automatically generate conftest modules. The former creates one test for each `container` module in your project (very similar to the `hadolint` provider), and the latter does the same for each `kubernetes` and `helm` module in your project. --- docs/README.md | 6 + docs/reference/module-types/README.md | 2 + docs/reference/module-types/conftest.md | 394 ++++++++++++++++++ docs/reference/module-types/hadolint.md | 358 ++++++++++++++++ .../reference/providers/conftest-container.md | 106 +++++ .../providers/conftest-kubernetes.md | 106 +++++ docs/reference/providers/conftest.md | 106 +++++ docs/reference/providers/hadolint.md | 97 +++++ examples/conftest/README.md | 21 + examples/conftest/garden.yml | 8 + examples/conftest/helm-chart/.helmignore | 22 + examples/conftest/helm-chart/Chart.yaml | 21 + examples/conftest/helm-chart/garden.yml | 3 + .../conftest/helm-chart/templates/NOTES.txt | 21 + .../helm-chart/templates/_helpers.tpl | 63 +++ .../helm-chart/templates/deployment.yaml | 55 +++ .../helm-chart/templates/ingress.yaml | 41 ++ .../helm-chart/templates/service.yaml | 15 + .../helm-chart/templates/serviceaccount.yaml | 8 + .../templates/tests/test-connection.yaml | 15 + examples/conftest/helm-chart/values.yaml | 66 +++ .../kubernetes-module/deployment.yaml | 25 ++ .../conftest/kubernetes-module/garden.yml | 6 + .../conftest/kubernetes-module/service.yaml | 11 + examples/conftest/policy/base_test.rego | 31 ++ examples/conftest/policy/deny.rego | 24 ++ examples/conftest/policy/kubernetes.rego | 9 + examples/conftest/policy/labels.rego | 20 + examples/conftest/policy/warn.rego | 11 + garden-service/package-lock.json | 21 +- garden-service/package.json | 2 + garden-service/src/docs/config.ts | 8 +- .../plugins/conftest/conftest-container.ts | 70 ++++ .../plugins/conftest/conftest-kubernetes.ts | 69 +++ .../src/plugins/conftest/conftest.ts | 256 ++++++++++++ .../src/plugins/kubernetes/helm/build.ts | 9 +- .../src/plugins/kubernetes/helm/common.ts | 35 +- garden-service/src/plugins/plugins.ts | 3 + garden-service/src/util/fs.ts | 26 +- .../conftest-container/Dockerfile | 3 + .../conftest-container/garden.yml | 10 + .../conftest-kubernetes/Dockerfile | 3 + .../custom-policy/statefulset.rego | 7 + .../conftest-kubernetes/garden.yml | 8 + .../conftest-kubernetes/helm/garden.yml | 6 + .../kubernetes/deployment.yaml | 1 + .../conftest-kubernetes/kubernetes/garden.yml | 4 + .../data/test-projects/conftest/garden.yml | 16 + .../data/test-projects/conftest/policy.rego | 11 + .../test-projects/conftest/warn-and-fail.yaml | 2 + .../data/test-projects/conftest/warn.yaml | 2 + .../plugins/conftest/conftest-container.ts | 92 ++++ .../plugins/conftest/conftest-kubernetes.ts | 75 ++++ .../integ/src/plugins/conftest/conftest.ts | 145 +++++++ 54 files changed, 2530 insertions(+), 25 deletions(-) create mode 100644 docs/reference/module-types/conftest.md create mode 100644 docs/reference/module-types/hadolint.md create mode 100644 docs/reference/providers/conftest-container.md create mode 100644 docs/reference/providers/conftest-kubernetes.md create mode 100644 docs/reference/providers/conftest.md create mode 100644 docs/reference/providers/hadolint.md create mode 100644 examples/conftest/README.md create mode 100644 examples/conftest/garden.yml create mode 100644 examples/conftest/helm-chart/.helmignore create mode 100644 examples/conftest/helm-chart/Chart.yaml create mode 100644 examples/conftest/helm-chart/garden.yml create mode 100644 examples/conftest/helm-chart/templates/NOTES.txt create mode 100644 examples/conftest/helm-chart/templates/_helpers.tpl create mode 100644 examples/conftest/helm-chart/templates/deployment.yaml create mode 100644 examples/conftest/helm-chart/templates/ingress.yaml create mode 100644 examples/conftest/helm-chart/templates/service.yaml create mode 100644 examples/conftest/helm-chart/templates/serviceaccount.yaml create mode 100644 examples/conftest/helm-chart/templates/tests/test-connection.yaml create mode 100644 examples/conftest/helm-chart/values.yaml create mode 100644 examples/conftest/kubernetes-module/deployment.yaml create mode 100644 examples/conftest/kubernetes-module/garden.yml create mode 100644 examples/conftest/kubernetes-module/service.yaml create mode 100644 examples/conftest/policy/base_test.rego create mode 100644 examples/conftest/policy/deny.rego create mode 100644 examples/conftest/policy/kubernetes.rego create mode 100644 examples/conftest/policy/labels.rego create mode 100644 examples/conftest/policy/warn.rego create mode 100644 garden-service/src/plugins/conftest/conftest-container.ts create mode 100644 garden-service/src/plugins/conftest/conftest-kubernetes.ts create mode 100644 garden-service/src/plugins/conftest/conftest.ts create mode 100644 garden-service/test/data/test-projects/conftest-container/Dockerfile create mode 100644 garden-service/test/data/test-projects/conftest-container/garden.yml create mode 100644 garden-service/test/data/test-projects/conftest-kubernetes/Dockerfile create mode 100644 garden-service/test/data/test-projects/conftest-kubernetes/custom-policy/statefulset.rego create mode 100644 garden-service/test/data/test-projects/conftest-kubernetes/garden.yml create mode 100644 garden-service/test/data/test-projects/conftest-kubernetes/helm/garden.yml create mode 100644 garden-service/test/data/test-projects/conftest-kubernetes/kubernetes/deployment.yaml create mode 100644 garden-service/test/data/test-projects/conftest-kubernetes/kubernetes/garden.yml create mode 100644 garden-service/test/data/test-projects/conftest/garden.yml create mode 100644 garden-service/test/data/test-projects/conftest/policy.rego create mode 100644 garden-service/test/data/test-projects/conftest/warn-and-fail.yaml create mode 100644 garden-service/test/data/test-projects/conftest/warn.yaml create mode 100644 garden-service/test/integ/src/plugins/conftest/conftest-container.ts create mode 100644 garden-service/test/integ/src/plugins/conftest/conftest-kubernetes.ts create mode 100644 garden-service/test/integ/src/plugins/conftest/conftest.ts diff --git a/docs/README.md b/docs/README.md index 98cda5bab9..76b2709ffb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,14 +32,20 @@ * [Commands Reference](./reference/commands.md) * [Config Files Reference](./reference/config.md) * [Module Types](./reference/module-types/README.md) + * [Conftest](./reference/module-types/conftest.md) * [Container](./reference/module-types/container.md) * [Exec](./reference/module-types/exec.md) + * [Hadolint](./reference/module-types/hadolint.md) * [Helm](./reference/module-types/helm.md) * [Kubernetes](./reference/module-types/kubernetes.md) * [Maven Container](./reference/module-types/maven-container.md) * [Openfaas](./reference/module-types/openfaas.md) * [Terraform](./reference/module-types/terraform.md) * [Providers](./reference/providers/README.md) + * [Conftest](./reference/providers/conftest.md) + * [Conftest Container](./reference/providers/conftest-container.md) + * [Conftest Kubernetes](./reference/providers/conftest-kubernetes.md) + * [Hadolint](./reference/providers/hadolint.md) * [Kubernetes](./reference/providers/kubernetes.md) * [Local Kubernetes](./reference/providers/local-kubernetes.md) * [Maven Container](./reference/providers/maven-container.md) diff --git a/docs/reference/module-types/README.md b/docs/reference/module-types/README.md index 09b940f024..27f93e1c7a 100644 --- a/docs/reference/module-types/README.md +++ b/docs/reference/module-types/README.md @@ -7,6 +7,8 @@ title: Module Types * [Exec](./exec.md) * [Container](./container.md) +* [Conftest](./conftest.md) +* [Hadolint](./hadolint.md) * [Helm](./helm.md) * [Kubernetes](./kubernetes.md) * [Maven Container](./maven-container.md) diff --git a/docs/reference/module-types/conftest.md b/docs/reference/module-types/conftest.md new file mode 100644 index 0000000000..5f13e411cb --- /dev/null +++ b/docs/reference/module-types/conftest.md @@ -0,0 +1,394 @@ +--- +title: Conftest +--- + +# `conftest` reference + +Runs `conftest` on the specified files, with the specified (or default) policy and namespace. + +> Note: In many cases, you'll let conftest providers (e.g. `conftest-container` and `conftest-kubernetes` +create this module type automatically, but you may in some cases want or need to manually specify files to test. + +See the [conftest docs](https://github.com/instramenta/conftest) for details on how to configure policies. + +Below is the schema reference. For an introduction to configuring Garden modules, please look at our [Configuration +guide](../../guides/configuration-files.md). + +The [first section](#complete-yaml-schema) contains the complete YAML schema, and the [second section](#configuration-keys) describes each schema key. + +`conftest` modules also export values that are available in template strings. See the [Outputs](#outputs) section below for details. + +## Complete YAML schema + +The values in the schema below are the default values. + +```yaml +# The schema version of this module's config (currently not used). +apiVersion: garden.io/v0 + +kind: Module + +# The type of this module. +type: + +# The name of this module. +name: + +description: + +# Specify a list of POSIX-style paths or globs that should be regarded as the source files for +# this +# module. Files that do *not* match these paths or globs are excluded when computing the version +# of the module, +# when responding to filesystem watch events, and when staging builds. +# +# Note that you can also _exclude_ files using the `exclude` field or by placing `.gardenignore` +# files in your +# source tree, which use the same format as `.gitignore` files. See the +# [Configuration Files +# guide](https://docs.garden.io/guides/configuration-files#including-excluding-files-and-directories) +# for details. +# +# Also note that specifying an empty list here means _no sources_ should be included. +include: + +# Specify a list of POSIX-style paths or glob patterns that should be excluded from the module. +# Files that +# match these paths or globs are excluded when computing the version of the module, when +# responding to filesystem +# watch events, and when staging builds. +# +# Note that you can also explicitly _include_ files using the `include` field. If you also specify +# the +# `include` field, the files/patterns specified here are filtered from the files matched by +# `include`. See the +# [Configuration Files +# guide](https://docs.garden.io/guides/configuration-files#including-excluding-files-and-directories)for +# details. +# +# Unlike the `modules.exclude` field in the project config, the filters here have _no effect_ on +# which files +# and directories are watched for changes. Use the project `modules.exclude` field to affect +# those, if you have +# large directories that should not be watched for changes. +exclude: + +# A remote repository URL. Currently only supports git servers. Must contain a hash suffix +# pointing to a specific branch or tag, with the format: # +# +# Garden will import the repository source code into this module, but read the module's +# config from the local garden.yml file. +repositoryUrl: + +# When false, disables pushing this module to remote registries. +allowPublish: true + +# Specify how to build the module. Note that plugins may define additional keys on this object. +build: + # A list of modules that must be built before this module is built. + dependencies: + # Module name to build ahead of this module. + - name: + # Specify one or more files or directories to copy from the built dependency to this module. + copy: + # POSIX-style path or filename of the directory or file(s) to copy to the target. + - source: + # POSIX-style path or filename to copy the directory or file(s), relative to the build + # directory. + # Defaults to to same as source path. + target: + +# Specify a module whose sources we want to test. +sourceModule: + +# POSIX-style path to a directory containing the policies to match the config against, or a +# specific .rego file, relative to the module root. +# Must be a relative path, and should in most cases be within the project root. +# Defaults to the `policyPath` set in the provider config. +policyPath: + +# The policy namespace in which to find _deny_ and _warn_ rules. +namespace: main + +# A list of files to test with the given policy. Must be POSIX-style paths, and may include +# wildcards. +files: +``` + +## Configuration keys + +### `apiVersion` + +The schema version of this module's config (currently not used). + +| Type | Required | Allowed Values | Default | +| -------- | -------- | -------------- | ---------------- | +| `string` | Yes | "garden.io/v0" | `"garden.io/v0"` | + +### `kind` + +| Type | Required | Allowed Values | Default | +| -------- | -------- | -------------- | ---------- | +| `string` | Yes | "Module" | `"Module"` | + +### `type` + +The type of this module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +type: "container" +``` + +### `name` + +The name of this module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +name: "my-sweet-module" +``` + +### `description` + +| Type | Required | +| -------- | -------- | +| `string` | No | + +### `include` + +Specify a list of POSIX-style paths or globs that should be regarded as the source files for this +module. Files that do *not* match these paths or globs are excluded when computing the version of the module, +when responding to filesystem watch events, and when staging builds. + +Note that you can also _exclude_ files using the `exclude` field or by placing `.gardenignore` files in your +source tree, which use the same format as `.gitignore` files. See the +[Configuration Files guide](https://docs.garden.io/guides/configuration-files#including-excluding-files-and-directories) for details. + +Also note that specifying an empty list here means _no sources_ should be included. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +include: + - Dockerfile + - my-app.js +``` + +### `exclude` + +Specify a list of POSIX-style paths or glob patterns that should be excluded from the module. Files that +match these paths or globs are excluded when computing the version of the module, when responding to filesystem +watch events, and when staging builds. + +Note that you can also explicitly _include_ files using the `include` field. If you also specify the +`include` field, the files/patterns specified here are filtered from the files matched by `include`. See the +[Configuration Files guide](https://docs.garden.io/guides/configuration-files#including-excluding-files-and-directories)for details. + +Unlike the `modules.exclude` field in the project config, the filters here have _no effect_ on which files +and directories are watched for changes. Use the project `modules.exclude` field to affect those, if you have +large directories that should not be watched for changes. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +exclude: + - tmp/**/* + - '*.log' +``` + +### `repositoryUrl` + +A remote repository URL. Currently only supports git servers. Must contain a hash suffix pointing to a specific branch or tag, with the format: # + +Garden will import the repository source code into this module, but read the module's +config from the local garden.yml file. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +Example: + +```yaml +repositoryUrl: "git+https://github.com/org/repo.git#v2.0" +``` + +### `allowPublish` + +When false, disables pushing this module to remote registries. + +| Type | Required | Default | +| --------- | -------- | ------- | +| `boolean` | No | `true` | + +### `build` + +Specify how to build the module. Note that plugins may define additional keys on this object. + +| Type | Required | Default | +| -------- | -------- | --------------------- | +| `object` | No | `{"dependencies":[]}` | + +### `build.dependencies[]` + +[build](#build) > dependencies + +A list of modules that must be built before this module is built. + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[object]` | No | `[]` | + +Example: + +```yaml +build: + ... + dependencies: + - name: some-other-module-name +``` + +### `build.dependencies[].name` + +[build](#build) > [dependencies](#builddependencies) > name + +Module name to build ahead of this module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +### `build.dependencies[].copy[]` + +[build](#build) > [dependencies](#builddependencies) > copy + +Specify one or more files or directories to copy from the built dependency to this module. + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[object]` | No | `[]` | + +### `build.dependencies[].copy[].source` + +[build](#build) > [dependencies](#builddependencies) > [copy](#builddependenciescopy) > source + +POSIX-style path or filename of the directory or file(s) to copy to the target. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +### `build.dependencies[].copy[].target` + +[build](#build) > [dependencies](#builddependencies) > [copy](#builddependenciescopy) > target + +POSIX-style path or filename to copy the directory or file(s), relative to the build directory. +Defaults to to same as source path. + +| Type | Required | Default | +| -------- | -------- | ------------------------- | +| `string` | No | `""` | + +### `sourceModule` + +Specify a module whose sources we want to test. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +### `policyPath` + +POSIX-style path to a directory containing the policies to match the config against, or a specific .rego file, relative to the module root. +Must be a relative path, and should in most cases be within the project root. +Defaults to the `policyPath` set in the provider config. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +### `namespace` + +The policy namespace in which to find _deny_ and _warn_ rules. + +| Type | Required | Default | +| -------- | -------- | -------- | +| `string` | No | `"main"` | + +### `files` + +A list of files to test with the given policy. Must be POSIX-style paths, and may include wildcards. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | Yes | + + +## Outputs + +### Module outputs + +The following keys are available via the `${modules.}` template string key for `conftest` +modules. + +### `${modules..buildPath}` + +The build path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +my-variable: ${modules.my-module.buildPath} +``` + +### `${modules..path}` + +The local path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +my-variable: ${modules.my-module.path} +``` + +### `${modules..version}` + +The current version of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +my-variable: ${modules.my-module.version} +``` + diff --git a/docs/reference/module-types/hadolint.md b/docs/reference/module-types/hadolint.md new file mode 100644 index 0000000000..19a2ea0941 --- /dev/null +++ b/docs/reference/module-types/hadolint.md @@ -0,0 +1,358 @@ +--- +title: Hadolint +--- + +# `hadolint` reference + +Runs `hadolint` on the specified Dockerfile. + +> Note: In most cases, you'll let the provider create this module type automatically, but you may in some cases want or need to manually specify a Dockerfile to lint. + +To configure `hadolint`, you can use `.hadolint.yaml` config files. For each test, we first look for one in +the module root. If none is found there, we check the project root, and if none is there we fall back to default +configuration. Note that for reasons of portability, we do not fall back to global/user configuration files. + +See the [hadolint docs](https://github.com/hadolint/hadolint#configure) for details on how to configure it. + +Below is the schema reference. For an introduction to configuring Garden modules, please look at our [Configuration +guide](../../guides/configuration-files.md). + +The [first section](#complete-yaml-schema) contains the complete YAML schema, and the [second section](#configuration-keys) describes each schema key. + +`hadolint` modules also export values that are available in template strings. See the [Outputs](#outputs) section below for details. + +## Complete YAML schema + +The values in the schema below are the default values. + +```yaml +# The schema version of this module's config (currently not used). +apiVersion: garden.io/v0 + +kind: Module + +# The type of this module. +type: + +# The name of this module. +name: + +description: + +# Specify a list of POSIX-style paths or globs that should be regarded as the source files for +# this +# module. Files that do *not* match these paths or globs are excluded when computing the version +# of the module, +# when responding to filesystem watch events, and when staging builds. +# +# Note that you can also _exclude_ files using the `exclude` field or by placing `.gardenignore` +# files in your +# source tree, which use the same format as `.gitignore` files. See the +# [Configuration Files +# guide](https://docs.garden.io/guides/configuration-files#including-excluding-files-and-directories) +# for details. +# +# Also note that specifying an empty list here means _no sources_ should be included. +include: + +# Specify a list of POSIX-style paths or glob patterns that should be excluded from the module. +# Files that +# match these paths or globs are excluded when computing the version of the module, when +# responding to filesystem +# watch events, and when staging builds. +# +# Note that you can also explicitly _include_ files using the `include` field. If you also specify +# the +# `include` field, the files/patterns specified here are filtered from the files matched by +# `include`. See the +# [Configuration Files +# guide](https://docs.garden.io/guides/configuration-files#including-excluding-files-and-directories)for +# details. +# +# Unlike the `modules.exclude` field in the project config, the filters here have _no effect_ on +# which files +# and directories are watched for changes. Use the project `modules.exclude` field to affect +# those, if you have +# large directories that should not be watched for changes. +exclude: + +# A remote repository URL. Currently only supports git servers. Must contain a hash suffix +# pointing to a specific branch or tag, with the format: # +# +# Garden will import the repository source code into this module, but read the module's +# config from the local garden.yml file. +repositoryUrl: + +# When false, disables pushing this module to remote registries. +allowPublish: true + +# Specify how to build the module. Note that plugins may define additional keys on this object. +build: + # A list of modules that must be built before this module is built. + dependencies: + # Module name to build ahead of this module. + - name: + # Specify one or more files or directories to copy from the built dependency to this module. + copy: + # POSIX-style path or filename of the directory or file(s) to copy to the target. + - source: + # POSIX-style path or filename to copy the directory or file(s), relative to the build + # directory. + # Defaults to to same as source path. + target: + +# POSIX-style path to a Dockerfile that you want to lint with `hadolint`. +dockerfilePath: +``` + +## Configuration keys + +### `apiVersion` + +The schema version of this module's config (currently not used). + +| Type | Required | Allowed Values | Default | +| -------- | -------- | -------------- | ---------------- | +| `string` | Yes | "garden.io/v0" | `"garden.io/v0"` | + +### `kind` + +| Type | Required | Allowed Values | Default | +| -------- | -------- | -------------- | ---------- | +| `string` | Yes | "Module" | `"Module"` | + +### `type` + +The type of this module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +type: "container" +``` + +### `name` + +The name of this module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +name: "my-sweet-module" +``` + +### `description` + +| Type | Required | +| -------- | -------- | +| `string` | No | + +### `include` + +Specify a list of POSIX-style paths or globs that should be regarded as the source files for this +module. Files that do *not* match these paths or globs are excluded when computing the version of the module, +when responding to filesystem watch events, and when staging builds. + +Note that you can also _exclude_ files using the `exclude` field or by placing `.gardenignore` files in your +source tree, which use the same format as `.gitignore` files. See the +[Configuration Files guide](https://docs.garden.io/guides/configuration-files#including-excluding-files-and-directories) for details. + +Also note that specifying an empty list here means _no sources_ should be included. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +include: + - Dockerfile + - my-app.js +``` + +### `exclude` + +Specify a list of POSIX-style paths or glob patterns that should be excluded from the module. Files that +match these paths or globs are excluded when computing the version of the module, when responding to filesystem +watch events, and when staging builds. + +Note that you can also explicitly _include_ files using the `include` field. If you also specify the +`include` field, the files/patterns specified here are filtered from the files matched by `include`. See the +[Configuration Files guide](https://docs.garden.io/guides/configuration-files#including-excluding-files-and-directories)for details. + +Unlike the `modules.exclude` field in the project config, the filters here have _no effect_ on which files +and directories are watched for changes. Use the project `modules.exclude` field to affect those, if you have +large directories that should not be watched for changes. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +exclude: + - tmp/**/* + - '*.log' +``` + +### `repositoryUrl` + +A remote repository URL. Currently only supports git servers. Must contain a hash suffix pointing to a specific branch or tag, with the format: # + +Garden will import the repository source code into this module, but read the module's +config from the local garden.yml file. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +Example: + +```yaml +repositoryUrl: "git+https://github.com/org/repo.git#v2.0" +``` + +### `allowPublish` + +When false, disables pushing this module to remote registries. + +| Type | Required | Default | +| --------- | -------- | ------- | +| `boolean` | No | `true` | + +### `build` + +Specify how to build the module. Note that plugins may define additional keys on this object. + +| Type | Required | Default | +| -------- | -------- | --------------------- | +| `object` | No | `{"dependencies":[]}` | + +### `build.dependencies[]` + +[build](#build) > dependencies + +A list of modules that must be built before this module is built. + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[object]` | No | `[]` | + +Example: + +```yaml +build: + ... + dependencies: + - name: some-other-module-name +``` + +### `build.dependencies[].name` + +[build](#build) > [dependencies](#builddependencies) > name + +Module name to build ahead of this module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +### `build.dependencies[].copy[]` + +[build](#build) > [dependencies](#builddependencies) > copy + +Specify one or more files or directories to copy from the built dependency to this module. + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[object]` | No | `[]` | + +### `build.dependencies[].copy[].source` + +[build](#build) > [dependencies](#builddependencies) > [copy](#builddependenciescopy) > source + +POSIX-style path or filename of the directory or file(s) to copy to the target. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +### `build.dependencies[].copy[].target` + +[build](#build) > [dependencies](#builddependencies) > [copy](#builddependenciescopy) > target + +POSIX-style path or filename to copy the directory or file(s), relative to the build directory. +Defaults to to same as source path. + +| Type | Required | Default | +| -------- | -------- | ------------------------- | +| `string` | No | `""` | + +### `dockerfilePath` + +POSIX-style path to a Dockerfile that you want to lint with `hadolint`. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + + +## Outputs + +### Module outputs + +The following keys are available via the `${modules.}` template string key for `hadolint` +modules. + +### `${modules..buildPath}` + +The build path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +my-variable: ${modules.my-module.buildPath} +``` + +### `${modules..path}` + +The local path of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +my-variable: ${modules.my-module.path} +``` + +### `${modules..version}` + +The current version of the module. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +my-variable: ${modules.my-module.version} +``` + diff --git a/docs/reference/providers/conftest-container.md b/docs/reference/providers/conftest-container.md new file mode 100644 index 0000000000..e1acb273d2 --- /dev/null +++ b/docs/reference/providers/conftest-container.md @@ -0,0 +1,106 @@ +--- +title: Conftest Container +--- + +# `conftest-container` reference + +Below is the schema reference for the `conftest-container` provider. For an introduction to configuring a Garden project with providers, please look at our [configuration guide](../../guides/configuration-files.md). + +The reference is divided into two sections. The [first section](#complete-yaml-schema) contains the complete YAML schema, and the [second section](#configuration-keys) describes each schema key. + +## Complete YAML schema + +The values in the schema below are the default values. + +```yaml +providers: + # The name of the provider plugin to use. + - name: + # If specified, this provider will only be used in the listed environments. Note that an empty + # array effectively disables the provider. To use a provider in all environments, omit this + # field. + environments: + # Path to the default policy directory or rego file to use for `conftest` modules. + policyPath: ./policy + # Default policy namespace to use for `conftest` modules. + namespace: + # Set this to `"warn"` if you'd like tests to be marked as failed if one or more _warn_ rules + # are matched. + # Set to `"none"` to always mark the tests as successful. + testFailureThreshold: error +``` +## Configuration keys + +### `providers` + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[object]` | No | `[]` | + +### `providers[].name` + +[providers](#providers) > name + +The name of the provider plugin to use. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +providers: + - name: "local-kubernetes" +``` + +### `providers[].environments[]` + +[providers](#providers) > environments + +If specified, this provider will only be used in the listed environments. Note that an empty array effectively disables the provider. To use a provider in all environments, omit this field. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +providers: + - environments: + - dev + - stage +``` + +### `providers[].policyPath` + +[providers](#providers) > policyPath + +Path to the default policy directory or rego file to use for `conftest` modules. + +| Type | Required | Default | +| -------- | -------- | ------------ | +| `string` | No | `"./policy"` | + +### `providers[].namespace` + +[providers](#providers) > namespace + +Default policy namespace to use for `conftest` modules. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +### `providers[].testFailureThreshold` + +[providers](#providers) > testFailureThreshold + +Set this to `"warn"` if you'd like tests to be marked as failed if one or more _warn_ rules are matched. +Set to `"none"` to always mark the tests as successful. + +| Type | Required | Default | +| -------- | -------- | --------- | +| `string` | No | `"error"` | + diff --git a/docs/reference/providers/conftest-kubernetes.md b/docs/reference/providers/conftest-kubernetes.md new file mode 100644 index 0000000000..33194fd833 --- /dev/null +++ b/docs/reference/providers/conftest-kubernetes.md @@ -0,0 +1,106 @@ +--- +title: Conftest Kubernetes +--- + +# `conftest-kubernetes` reference + +Below is the schema reference for the `conftest-kubernetes` provider. For an introduction to configuring a Garden project with providers, please look at our [configuration guide](../../guides/configuration-files.md). + +The reference is divided into two sections. The [first section](#complete-yaml-schema) contains the complete YAML schema, and the [second section](#configuration-keys) describes each schema key. + +## Complete YAML schema + +The values in the schema below are the default values. + +```yaml +providers: + # The name of the provider plugin to use. + - name: + # If specified, this provider will only be used in the listed environments. Note that an empty + # array effectively disables the provider. To use a provider in all environments, omit this + # field. + environments: + # Path to the default policy directory or rego file to use for `conftest` modules. + policyPath: ./policy + # Default policy namespace to use for `conftest` modules. + namespace: + # Set this to `"warn"` if you'd like tests to be marked as failed if one or more _warn_ rules + # are matched. + # Set to `"none"` to always mark the tests as successful. + testFailureThreshold: error +``` +## Configuration keys + +### `providers` + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[object]` | No | `[]` | + +### `providers[].name` + +[providers](#providers) > name + +The name of the provider plugin to use. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +providers: + - name: "local-kubernetes" +``` + +### `providers[].environments[]` + +[providers](#providers) > environments + +If specified, this provider will only be used in the listed environments. Note that an empty array effectively disables the provider. To use a provider in all environments, omit this field. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +providers: + - environments: + - dev + - stage +``` + +### `providers[].policyPath` + +[providers](#providers) > policyPath + +Path to the default policy directory or rego file to use for `conftest` modules. + +| Type | Required | Default | +| -------- | -------- | ------------ | +| `string` | No | `"./policy"` | + +### `providers[].namespace` + +[providers](#providers) > namespace + +Default policy namespace to use for `conftest` modules. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +### `providers[].testFailureThreshold` + +[providers](#providers) > testFailureThreshold + +Set this to `"warn"` if you'd like tests to be marked as failed if one or more _warn_ rules are matched. +Set to `"none"` to always mark the tests as successful. + +| Type | Required | Default | +| -------- | -------- | --------- | +| `string` | No | `"error"` | + diff --git a/docs/reference/providers/conftest.md b/docs/reference/providers/conftest.md new file mode 100644 index 0000000000..c970dc3bb4 --- /dev/null +++ b/docs/reference/providers/conftest.md @@ -0,0 +1,106 @@ +--- +title: Conftest +--- + +# `conftest` reference + +Below is the schema reference for the `conftest` provider. For an introduction to configuring a Garden project with providers, please look at our [configuration guide](../../guides/configuration-files.md). + +The reference is divided into two sections. The [first section](#complete-yaml-schema) contains the complete YAML schema, and the [second section](#configuration-keys) describes each schema key. + +## Complete YAML schema + +The values in the schema below are the default values. + +```yaml +providers: + # The name of the provider plugin to use. + - name: + # If specified, this provider will only be used in the listed environments. Note that an empty + # array effectively disables the provider. To use a provider in all environments, omit this + # field. + environments: + # Path to the default policy directory or rego file to use for `conftest` modules. + policyPath: ./policy + # Default policy namespace to use for `conftest` modules. + namespace: + # Set this to `"warn"` if you'd like tests to be marked as failed if one or more _warn_ rules + # are matched. + # Set to `"none"` to always mark the tests as successful. + testFailureThreshold: error +``` +## Configuration keys + +### `providers` + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[object]` | No | `[]` | + +### `providers[].name` + +[providers](#providers) > name + +The name of the provider plugin to use. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +providers: + - name: "local-kubernetes" +``` + +### `providers[].environments[]` + +[providers](#providers) > environments + +If specified, this provider will only be used in the listed environments. Note that an empty array effectively disables the provider. To use a provider in all environments, omit this field. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +providers: + - environments: + - dev + - stage +``` + +### `providers[].policyPath` + +[providers](#providers) > policyPath + +Path to the default policy directory or rego file to use for `conftest` modules. + +| Type | Required | Default | +| -------- | -------- | ------------ | +| `string` | No | `"./policy"` | + +### `providers[].namespace` + +[providers](#providers) > namespace + +Default policy namespace to use for `conftest` modules. + +| Type | Required | +| -------- | -------- | +| `string` | No | + +### `providers[].testFailureThreshold` + +[providers](#providers) > testFailureThreshold + +Set this to `"warn"` if you'd like tests to be marked as failed if one or more _warn_ rules are matched. +Set to `"none"` to always mark the tests as successful. + +| Type | Required | Default | +| -------- | -------- | --------- | +| `string` | No | `"error"` | + diff --git a/docs/reference/providers/hadolint.md b/docs/reference/providers/hadolint.md new file mode 100644 index 0000000000..3e2ab53a45 --- /dev/null +++ b/docs/reference/providers/hadolint.md @@ -0,0 +1,97 @@ +--- +title: Hadolint +--- + +# `hadolint` reference + +Below is the schema reference for the `hadolint` provider. For an introduction to configuring a Garden project with providers, please look at our [configuration guide](../../guides/configuration-files.md). + +The reference is divided into two sections. The [first section](#complete-yaml-schema) contains the complete YAML schema, and the [second section](#configuration-keys) describes each schema key. + +## Complete YAML schema + +The values in the schema below are the default values. + +```yaml +providers: + # The name of the provider plugin to use. + - name: + # If specified, this provider will only be used in the listed environments. Note that an empty + # array effectively disables the provider. To use a provider in all environments, omit this + # field. + environments: + # By default, the provider automatically creates a `hadolint` module for every `container` + # module in your + # project. Set this to `false` to disable this behavior. + autoInject: true + # Set this to `"warning"` if you'd like tests to be marked as failed if one or more warnings + # are returned. + # Set to `"none"` to always mark the tests as successful. + testFailureThreshold: error +``` +## Configuration keys + +### `providers` + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[object]` | No | `[]` | + +### `providers[].name` + +[providers](#providers) > name + +The name of the provider plugin to use. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +providers: + - name: "local-kubernetes" +``` + +### `providers[].environments[]` + +[providers](#providers) > environments + +If specified, this provider will only be used in the listed environments. Note that an empty array effectively disables the provider. To use a provider in all environments, omit this field. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +providers: + - environments: + - dev + - stage +``` + +### `providers[].autoInject` + +[providers](#providers) > autoInject + +By default, the provider automatically creates a `hadolint` module for every `container` module in your +project. Set this to `false` to disable this behavior. + +| Type | Required | Default | +| --------- | -------- | ------- | +| `boolean` | No | `true` | + +### `providers[].testFailureThreshold` + +[providers](#providers) > testFailureThreshold + +Set this to `"warning"` if you'd like tests to be marked as failed if one or more warnings are returned. +Set to `"none"` to always mark the tests as successful. + +| Type | Required | Default | +| -------- | -------- | --------- | +| `string` | No | `"error"` | + diff --git a/examples/conftest/README.md b/examples/conftest/README.md new file mode 100644 index 0000000000..78c7ecb163 --- /dev/null +++ b/examples/conftest/README.md @@ -0,0 +1,21 @@ +# conftest example + +This simple example shows you how you can easily drop [conftest](https://github.com/instrumenta/conftest) into your project to validate your Kubernetes manifests. + +The [project config](./garden.yml) contains a single line that automatically creates a `conftest` test for each `kubernetes` and `helm` module in your project: + +```yaml +kind: Project +name: conftest +environments: + - name: local +providers: + - name: local-kubernetes + - name: conftest-kubernetes # <------ +``` + +For the example, we've copied the [kubernetes example](https://github.com/instrumenta/conftest/tree/master/examples/kubernetes) from the conftest repository, and added a `helm` module type for good measure. + +To test this, simply run `garden test` in this directory. You should quickly see a few tests failing because resources don't match the policies defined under the `policy` directory. + +Note that you could also manually specify tests using the [conftest module type](https://docs.garden.io/reference/module-types/conftest). diff --git a/examples/conftest/garden.yml b/examples/conftest/garden.yml new file mode 100644 index 0000000000..d3c190c007 --- /dev/null +++ b/examples/conftest/garden.yml @@ -0,0 +1,8 @@ +kind: Project +name: conftest +environments: + - name: local +providers: + - name: local-kubernetes + # Auto-inject tests for kubernetes and helm modules + - name: conftest-kubernetes diff --git a/examples/conftest/helm-chart/.helmignore b/examples/conftest/helm-chart/.helmignore new file mode 100644 index 0000000000..50af031725 --- /dev/null +++ b/examples/conftest/helm-chart/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/examples/conftest/helm-chart/Chart.yaml b/examples/conftest/helm-chart/Chart.yaml new file mode 100644 index 0000000000..4e87b26cd4 --- /dev/null +++ b/examples/conftest/helm-chart/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: helm-chart +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. +appVersion: 1.16.0 diff --git a/examples/conftest/helm-chart/garden.yml b/examples/conftest/helm-chart/garden.yml new file mode 100644 index 0000000000..f7baf377fa --- /dev/null +++ b/examples/conftest/helm-chart/garden.yml @@ -0,0 +1,3 @@ +kind: Module +type: helm +name: helm-chart diff --git a/examples/conftest/helm-chart/templates/NOTES.txt b/examples/conftest/helm-chart/templates/NOTES.txt new file mode 100644 index 0000000000..1f4127e384 --- /dev/null +++ b/examples/conftest/helm-chart/templates/NOTES.txt @@ -0,0 +1,21 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "helm-chart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "helm-chart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "helm-chart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "helm-chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/examples/conftest/helm-chart/templates/_helpers.tpl b/examples/conftest/helm-chart/templates/_helpers.tpl new file mode 100644 index 0000000000..7fe5d33739 --- /dev/null +++ b/examples/conftest/helm-chart/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "helm-chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "helm-chart.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "helm-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "helm-chart.labels" -}} +helm.sh/chart: {{ include "helm-chart.chart" . }} +{{ include "helm-chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "helm-chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "helm-chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "helm-chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "helm-chart.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/examples/conftest/helm-chart/templates/deployment.yaml b/examples/conftest/helm-chart/templates/deployment.yaml new file mode 100644 index 0000000000..e6b0e932bf --- /dev/null +++ b/examples/conftest/helm-chart/templates/deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "helm-chart.fullname" . }} + labels: + {{- include "helm-chart.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "helm-chart.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "helm-chart.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "helm-chart.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/examples/conftest/helm-chart/templates/ingress.yaml b/examples/conftest/helm-chart/templates/ingress.yaml new file mode 100644 index 0000000000..f91328e978 --- /dev/null +++ b/examples/conftest/helm-chart/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "helm-chart.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "helm-chart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/examples/conftest/helm-chart/templates/service.yaml b/examples/conftest/helm-chart/templates/service.yaml new file mode 100644 index 0000000000..50310712a0 --- /dev/null +++ b/examples/conftest/helm-chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "helm-chart.fullname" . }} + labels: + {{- include "helm-chart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "helm-chart.selectorLabels" . | nindent 4 }} diff --git a/examples/conftest/helm-chart/templates/serviceaccount.yaml b/examples/conftest/helm-chart/templates/serviceaccount.yaml new file mode 100644 index 0000000000..6b2002999e --- /dev/null +++ b/examples/conftest/helm-chart/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "helm-chart.serviceAccountName" . }} + labels: +{{ include "helm-chart.labels" . | nindent 4 }} +{{- end -}} diff --git a/examples/conftest/helm-chart/templates/tests/test-connection.yaml b/examples/conftest/helm-chart/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..1a0b0b0940 --- /dev/null +++ b/examples/conftest/helm-chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "helm-chart.fullname" . }}-test-connection" + labels: +{{ include "helm-chart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "helm-chart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/examples/conftest/helm-chart/values.yaml b/examples/conftest/helm-chart/values.yaml new file mode 100644 index 0000000000..10ae85916c --- /dev/null +++ b/examples/conftest/helm-chart/values.yaml @@ -0,0 +1,66 @@ +# Default values for helm-chart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/examples/conftest/kubernetes-module/deployment.yaml b/examples/conftest/kubernetes-module/deployment.yaml new file mode 100644 index 0000000000..04a11a6b9d --- /dev/null +++ b/examples/conftest/kubernetes-module/deployment.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-kubernetes + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/version: "5.7.21" + app.kubernetes.io/component: database + app.kubernetes.io/part-of: wordpress + app.kubernetes.io/managed-by: helm +spec: + replicas: 3 + selector: + matchLabels: + app: hello-kubernetes + template: + metadata: + labels: + app: hello-kubernetes + spec: + containers: + - name: hello-kubernetes + image: paulbouwer/hello-kubernetes:1.5 + ports: + - containerPort: 8080 diff --git a/examples/conftest/kubernetes-module/garden.yml b/examples/conftest/kubernetes-module/garden.yml new file mode 100644 index 0000000000..54bc4ff8ec --- /dev/null +++ b/examples/conftest/kubernetes-module/garden.yml @@ -0,0 +1,6 @@ +kind: Module +type: kubernetes +name: kubernetes-module +files: + - deployment.yaml + - service.yaml diff --git a/examples/conftest/kubernetes-module/service.yaml b/examples/conftest/kubernetes-module/service.yaml new file mode 100644 index 0000000000..e0634fe623 --- /dev/null +++ b/examples/conftest/kubernetes-module/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: hello-kubernetes +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 8080 + selector: + app: hello-kubernetes diff --git a/examples/conftest/policy/base_test.rego b/examples/conftest/policy/base_test.rego new file mode 100644 index 0000000000..efb4698953 --- /dev/null +++ b/examples/conftest/policy/base_test.rego @@ -0,0 +1,31 @@ +package main + +empty(value) { + count(value) == 0 +} + +no_violations { + empty(deny) +} + +no_warnings { + empty(warn) +} + +test_deployment_without_security_context { + deny["Containers must not run as root in Deployment sample"] with input as {"kind": "Deployment", "metadata": { "name": "sample" }} +} + +test_deployment_with_security_context { + no_violations with input as {"kind": "Deployment", "metadata": {"name": "sample"}, "spec": { + "selector": { "matchLabels": { "app": "something", "release": "something" }}, + "template": { "spec": { "securityContext": { "runAsNonRoot": true }}}}} +} + +test_services_not_denied { + no_violations with input as {"kind": "Service", "metadata": { "name": "sample" }} +} + +test_services_issue_warning { + warn["Found service sample but services are not allowed"] with input as {"kind": "Service", "metadata": { "name": "sample" }} +} diff --git a/examples/conftest/policy/deny.rego b/examples/conftest/policy/deny.rego new file mode 100644 index 0000000000..bd589dc8b5 --- /dev/null +++ b/examples/conftest/policy/deny.rego @@ -0,0 +1,24 @@ +package main + +import data.kubernetes + +name = input.metadata.name + +deny[msg] { + kubernetes.is_deployment + not input.spec.template.spec.securityContext.runAsNonRoot + + msg = sprintf("Containers must not run as root in Deployment %s", [name]) +} + +labels { + input.spec.selector.matchLabels.app + input.spec.selector.matchLabels.release +} + +deny[msg] { + kubernetes.is_deployment + not labels + + msg = sprintf("Deployment %s must provide app/release labels for pod selectors", [name]) +} diff --git a/examples/conftest/policy/kubernetes.rego b/examples/conftest/policy/kubernetes.rego new file mode 100644 index 0000000000..bb1671e712 --- /dev/null +++ b/examples/conftest/policy/kubernetes.rego @@ -0,0 +1,9 @@ +package kubernetes + +is_service { + input.kind = "Service" +} + +is_deployment { + input.kind = "Deployment" +} diff --git a/examples/conftest/policy/labels.rego b/examples/conftest/policy/labels.rego new file mode 100644 index 0000000000..9ac39922c2 --- /dev/null +++ b/examples/conftest/policy/labels.rego @@ -0,0 +1,20 @@ +package main + +import data.kubernetes + +name = input.metadata.name + +labels { + input.metadata.labels["app.kubernetes.io/name"] + input.metadata.labels["app.kubernetes.io/instance"] + input.metadata.labels["app.kubernetes.io/version"] + input.metadata.labels["app.kubernetes.io/component"] + input.metadata.labels["app.kubernetes.io/part-of"] + input.metadata.labels["app.kubernetes.io/managed-by"] +} + +deny[msg] { + kubernetes.is_deployment + not labels + msg = sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels ", [name]) +} diff --git a/examples/conftest/policy/warn.rego b/examples/conftest/policy/warn.rego new file mode 100644 index 0000000000..5105f487cc --- /dev/null +++ b/examples/conftest/policy/warn.rego @@ -0,0 +1,11 @@ +package main + +import data.kubernetes + + +name = input.metadata.name + +warn[msg] { + kubernetes.is_service + msg = sprintf("Found service %s but services are not allowed", [name]) +} diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index d31510d010..aea8b37adb 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -5005,9 +5005,9 @@ } }, "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8411,6 +8411,21 @@ "minimatch": "3.0.4", "mkdirp": "0.5.1", "supports-color": "5.4.0" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "supports-color": { diff --git a/garden-service/package.json b/garden-service/package.json index 56768484db..fe6c1b7322 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -62,6 +62,7 @@ "execa": "^3.4.0", "fs-extra": "^8.1.0", "get-port": "^5.0.0", + "glob": "^7.1.6", "gray-matter": "^4.0.2", "has-ansi": "^4.0.0", "hasha": "^5.1.0", @@ -138,6 +139,7 @@ "@types/deep-diff": "1.0.0", "@types/dockerode": "^2.5.20", "@types/fs-extra": "^8.0.1", + "@types/glob": "^7.1.1", "@types/google-cloud__kms": "^1.5.0", "@types/hapi__joi": "^15.0.4", "@types/has-ansi": "^3.0.0", diff --git a/garden-service/src/docs/config.ts b/garden-service/src/docs/config.ts index cb13d58a8d..9d8dc3b187 100644 --- a/garden-service/src/docs/config.ts +++ b/garden-service/src/docs/config.ts @@ -41,6 +41,8 @@ const maxWidth = 100 const moduleTypes = [ { name: "exec" }, { name: "container" }, + { name: "conftest", pluginName: "conftest" }, + { name: "hadolint" }, { name: "helm", pluginName: "local-kubernetes" }, { name: "kubernetes", pluginName: "local-kubernetes" }, { name: "maven-container" }, @@ -627,8 +629,12 @@ export async function writeConfigReferenceDocs(docsRoot: string) { }, ], providers: [ - { name: "local-kubernetes" }, + { name: "conftest" }, + { name: "conftest-container" }, + { name: "conftest-kubernetes" }, + { name: "hadolint" }, { name: "kubernetes" }, + { name: "local-kubernetes" }, { name: "maven-container" }, { name: "openfaas" }, { name: "terraform" }, diff --git a/garden-service/src/plugins/conftest/conftest-container.ts b/garden-service/src/plugins/conftest/conftest-container.ts new file mode 100644 index 0000000000..1ff18c6ce2 --- /dev/null +++ b/garden-service/src/plugins/conftest/conftest-container.ts @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import Bluebird from "bluebird" +import { relative, resolve } from "path" +import { createGardenPlugin } from "../../types/plugin/plugin" +import { containerHelpers } from "../container/helpers" +import { ConftestProvider } from "./conftest" + +/** + * Auto-generates a conftest module for each container module in your project + */ +export const gardenPlugin = createGardenPlugin({ + name: "conftest-container", + base: "conftest", + dependencies: ["container"], + handlers: { + augmentGraph: async ({ ctx, modules }) => { + const provider = ctx.provider as ConftestProvider + + const allModuleNames = new Set(modules.map((m) => m.name)) + + const existingConftestModuleDockerfiles = modules + .filter((m) => m.compatibleTypes.includes("conftest")) + .map((m) => resolve(m.path, m.spec.dockerfilePath)) + + return { + addModules: await Bluebird.filter(modules, async (module) => { + const dockerfilePath = containerHelpers.getDockerfileSourcePath(module) + + return ( + // Pick all container or container-based modules + module.compatibleTypes.includes("container") && + // Make sure we don't step on an existing custom conftest module + !existingConftestModuleDockerfiles.includes(dockerfilePath) && + // Only create for modules with Dockerfiles + (await containerHelpers.hasDockerfile(module)) + ) + }).map((module) => { + const baseName = "conftest-" + module.name + + let name = baseName + let i = 2 + + while (allModuleNames.has(name)) { + name = `${baseName}-${i++}` + } + + allModuleNames.add(name) + + return { + kind: "Module", + type: "conftest", + name, + description: `conftest test for module '${module.name}' (auto-generated by conftest-container)`, + path: module.path, + policyPath: provider.config.policyPath, + namespace: provider.config.namespace, + files: [relative(module.path, containerHelpers.getDockerfileSourcePath(module))], + } + }), + } + }, + }, +}) diff --git a/garden-service/src/plugins/conftest/conftest-kubernetes.ts b/garden-service/src/plugins/conftest/conftest-kubernetes.ts new file mode 100644 index 0000000000..d811532a86 --- /dev/null +++ b/garden-service/src/plugins/conftest/conftest-kubernetes.ts @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import Bluebird from "bluebird" +import { createGardenPlugin } from "../../types/plugin/plugin" +import { ConftestProvider } from "./conftest" +import { relative, resolve } from "path" + +/** + * Auto-generates a conftest module for each helm and kubernetes module in your project + */ +export const gardenPlugin = createGardenPlugin({ + name: "conftest-kubernetes", + base: "conftest", + dependencies: ["kubernetes"], + handlers: { + augmentGraph: async ({ ctx, modules }) => { + const provider = ctx.provider as ConftestProvider + + const allModuleNames = new Set(modules.map((m) => m.name)) + + return { + addModules: await Bluebird.filter(modules, async (module) => { + return ( + // Pick all kubernetes or helm modules + module.compatibleTypes.includes("helm") || module.compatibleTypes.includes("kubernetes") + ) + }).map((module) => { + const baseName = "conftest-" + module.name + + let name = baseName + let i = 2 + + while (allModuleNames.has(name)) { + name = `${baseName}-${i++}` + } + + allModuleNames.add(name) + + // For helm modules, we only want to look at the rendered YAML output + // (which is output when building Helm modules) + const isHelmModule = module.compatibleTypes.includes("helm") + const files = isHelmModule + ? [".rendered.yaml"] + : module.include || ["*.yaml", "**/*.yaml", "*.yml", "**/*.yml"] + + const policyPath = relative(module.path, resolve(ctx.projectRoot, provider.config.policyPath)) + + return { + kind: "Module", + type: "conftest", + name, + description: `conftest test for module '${module.name}' (auto-generated by conftest-kubernetes)`, + path: module.path, + sourceModule: module.name, + policyPath, + namespace: provider.config.namespace, + files, + } + }), + } + }, + }, +}) diff --git a/garden-service/src/plugins/conftest/conftest.ts b/garden-service/src/plugins/conftest/conftest.ts new file mode 100644 index 0000000000..202115e035 --- /dev/null +++ b/garden-service/src/plugins/conftest/conftest.ts @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { resolve, relative } from "path" +import { createGardenPlugin } from "../../types/plugin/plugin" +import { providerConfigBaseSchema, ProviderConfig, Provider } from "../../config/provider" +import { joi, joiIdentifier } from "../../config/common" +import { dedent, naturalList } from "../../util/string" +import { TestModuleParams } from "../../types/plugin/module/testModule" +import { Module } from "../../types/module" +import { BinaryCmd } from "../../util/ext-tools" +import chalk from "chalk" +import { baseBuildSpecSchema } from "../../config/module" +import { matchGlobs, listDirectory } from "../../util/fs" +import { PluginError } from "../../exceptions" + +interface ConftestProviderConfig extends ProviderConfig { + policyPath: string + namespace?: string + testFailureThreshold: "deny" | "warn" | "none" +} + +export interface ConftestProvider extends Provider {} + +export const configSchema = providerConfigBaseSchema + .keys({ + policyPath: joi + .string() + .posixPath({ relativeOnly: true, subPathOnly: true }) + .default("./policy") + .description("Path to the default policy directory or rego file to use for `conftest` modules."), + namespace: joi.string().description("Default policy namespace to use for `conftest` modules."), + testFailureThreshold: joi + .string() + .allow("deny", "warn", "none") + .default("error") + .description( + dedent` + Set this to \`"warn"\` if you'd like tests to be marked as failed if one or more _warn_ rules are matched. + Set to \`"none"\` to always mark the tests as successful. + ` + ), + }) + .unknown(false) + +interface ConftestModuleSpec { + policyPath: string + namespace: string + files: string[] + sourceModule: string +} + +type ConftestModule = Module + +export const gardenPlugin = createGardenPlugin({ + name: "conftest", + dependencies: [], + configSchema, + createModuleTypes: [ + { + name: "conftest", + docs: dedent` + Runs \`conftest\` on the specified files, with the specified (or default) policy and namespace. + + > Note: In many cases, you'll let conftest providers (e.g. \`conftest-container\` and \`conftest-kubernetes\` + create this module type automatically, but you may in some cases want or need to manually specify files to test. + + See the [conftest docs](https://github.com/instramenta/conftest) for details on how to configure policies. + `, + schema: joi.object().keys({ + build: baseBuildSpecSchema, + sourceModule: joiIdentifier().description("Specify a module whose sources we want to test."), + policyPath: joi + .string() + .posixPath({ relativeOnly: true }) + .description( + dedent` + POSIX-style path to a directory containing the policies to match the config against, or a specific .rego file, relative to the module root. + Must be a relative path, and should in most cases be within the project root. + Defaults to the \`policyPath\` set in the provider config. + ` + ), + namespace: joi + .string() + .default("main") + .description("The policy namespace in which to find _deny_ and _warn_ rules."), + files: joi + .array() + .items(joi.string().posixPath({ subPathOnly: true, relativeOnly: true, allowGlobs: true })) + .required() + .description( + dedent` + A list of files to test with the given policy. Must be POSIX-style paths, and may include wildcards. + ` + ), + }), + handlers: { + configure: async ({ moduleConfig }) => { + if (moduleConfig.spec.sourceModule) { + moduleConfig.build.dependencies.push({ name: moduleConfig.spec.sourceModule, copy: [] }) + } + + moduleConfig.include = moduleConfig.spec.files + moduleConfig.testConfigs = [{ name: "test", dependencies: [], spec: {}, timeout: 10 }] + return { moduleConfig } + }, + testModule: async ({ ctx, log, module, testConfig }: TestModuleParams) => { + const startedAt = new Date() + const provider = ctx.provider as ConftestProvider + + const defaultPolicyPath = relative(module.path, resolve(ctx.projectRoot, provider.config.policyPath)) + const policyPath = resolve(module.path, module.spec.policyPath || defaultPolicyPath) + const namespace = module.spec.namespace || provider.config.namespace + + const buildPath = module.spec.sourceModule + ? module.buildDependencies[module.spec.sourceModule].buildPath + : module.buildPath + const buildPathFiles = await listDirectory(buildPath) + + // TODO: throw if a specific file is listed under `module.spec.files` but isn't found? + const files = matchGlobs(buildPathFiles, module.spec.files) + + if (files.length === 0) { + return { + testName: testConfig.name, + moduleName: module.name, + command: [], + version: module.version.versionString, + success: true, + startedAt, + completedAt: new Date(), + log: "No files to test", + } + } + + const args = ["test", "--policy", policyPath, "--output", "json"] + if (namespace) { + args.push("--namespace", namespace) + } + args.push(...files) + + const result = await conftest.exec({ log, args, ignoreError: true, cwd: buildPath }) + + let success = true + let parsed: any = [] + + try { + parsed = JSON.parse(result.stdout) + } catch (err) { + throw new PluginError(`Error running conftest: ${result.all}`, { result }) + } + + const allFailures = parsed.filter((p: any) => p.Failures?.length > 0) + const allWarnings = parsed.filter((p: any) => p.Warnings?.length > 0) + + const resultCategories: string[] = [] + let formattedResult = "OK" + + if (allFailures.length > 0) { + resultCategories.push(`${allFailures.length} failure(s)`) + } + + if (allWarnings.length > 0) { + resultCategories.push(`${allWarnings.length} warning(s)`) + } + + let formattedHeader = `conftest reported ${naturalList(resultCategories)}` + + if (allFailures.length > 0 || allWarnings.length > 0) { + const lines = [`${formattedHeader}:\n`] + + // We let the format match the conftest output + for (const { filename, Warnings, Failures } of parsed) { + for (const failure of Failures) { + lines.push( + chalk.redBright.bold("FAIL") + + chalk.gray(" - ") + + chalk.redBright(filename) + + chalk.gray(" - ") + + failure + ) + } + for (const warning of Warnings) { + lines.push( + chalk.yellowBright.bold("WARN") + + chalk.gray(" - ") + + chalk.yellowBright(filename) + + chalk.gray(" - ") + + warning + ) + } + } + + formattedResult = lines.join("\n") + } + + const threshold = provider.config.testFailureThreshold + + if (allWarnings.length > 0 && threshold === "warn") { + success = false + } else if (allFailures.length > 0 && threshold !== "none") { + success = false + } else if (allWarnings.length > 0) { + log.warn(chalk.yellow(formattedHeader)) + } + + return { + testName: testConfig.name, + moduleName: module.name, + command: ["conftest", ...args], + version: module.version.versionString, + success, + startedAt, + completedAt: new Date(), + log: formattedResult, + } + }, + }, + }, + ], +}) + +const conftest = new BinaryCmd({ + name: "conftest", + specs: { + darwin: { + url: "https://github.com/instrumenta/conftest/releases/download/v0.15.0/conftest_0.15.0_Darwin_x86_64.tar.gz", + sha256: "73cea42e467edf7bec58648514096f5975353b0523a5f2b309833ff4a972765e", + extract: { + format: "tar", + targetPath: ["conftest"], + }, + }, + linux: { + url: "https://github.com/instrumenta/conftest/releases/download/v0.15.0/conftest_0.15.0_Linux_x86_64.tar.gz", + sha256: "23c6af69dcd2c9fe935ee3cd5652cc14ffc9d7cf0fd55d4abc6a5c3bd470b692", + extract: { + format: "tar", + targetPath: ["conftest"], + }, + }, + win32: { + url: "https://github.com/instrumenta/conftest/releases/download/v0.15.0/conftest_0.15.0_Windows_x86_64.zip", + sha256: "c452bb4b71d6fbf5d918e1b3ed28092f7bc3a157f44e0ecd6fa1968e1cad4bec", + extract: { + format: "zip", + targetPath: ["conftest.exe"], + }, + }, + }, +}) diff --git a/garden-service/src/plugins/kubernetes/helm/build.ts b/garden-service/src/plugins/kubernetes/helm/build.ts index 83d9d32c73..35299cf0c8 100644 --- a/garden-service/src/plugins/kubernetes/helm/build.ts +++ b/garden-service/src/plugins/kubernetes/helm/build.ts @@ -6,8 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { join } from "path" +import { writeFile } from "fs-extra" import { HelmModule } from "./config" -import { containsBuildSource, getChartPath, getGardenValuesPath, getBaseModule } from "./common" +import { containsBuildSource, getChartPath, getGardenValuesPath, getBaseModule, renderTemplates } from "./common" import { helm } from "./helm-cli" import { ConfigurationError } from "../../../exceptions" import { deline } from "../../../util/string" @@ -73,6 +75,11 @@ export async function buildHelmModule({ ctx, module, log }: BuildModuleParamsctx - const namespace = await getNamespace({ - log, - projectName: k8sCtx.projectName, - provider: k8sCtx.provider, - skipCreate: true, - }) - const releaseName = getReleaseName(module) - const objects = loadTemplate( - await helm({ - ctx: k8sCtx, - log, - namespace, - args: ["template", releaseName, "--namespace", namespace, ...(await getValueArgs(module, hotReload)), chartPath], - }) - ) + const objects = loadTemplate(await renderTemplates(k8sCtx, module, hotReload, log)) const resources = objects.filter((obj) => { // Don't try to check status of hooks @@ -92,6 +77,24 @@ export async function getChartResources(ctx: PluginContext, module: Module, hotR return flattenResources(resources) } +export async function renderTemplates(ctx: KubernetesPluginContext, module: Module, hotReload: boolean, log: LogEntry) { + const chartPath = await getChartPath(module) + const releaseName = getReleaseName(module) + const namespace = await getNamespace({ + log, + projectName: ctx.projectName, + provider: ctx.provider, + skipCreate: true, + }) + + return helm({ + ctx, + log, + namespace, + args: ["template", releaseName, "--namespace", namespace, ...(await getValueArgs(module, hotReload)), chartPath], + }) +} + /** * Returns the base module of the specified Helm module, or undefined if none is specified. * Throws an error if the referenced module is missing, or is not a Helm module. diff --git a/garden-service/src/plugins/plugins.ts b/garden-service/src/plugins/plugins.ts index e86e69d7d5..d799ef03b0 100644 --- a/garden-service/src/plugins/plugins.ts +++ b/garden-service/src/plugins/plugins.ts @@ -8,6 +8,9 @@ // These plugins are always registered export const builtinPlugins = [ + require("./conftest/conftest"), + require("./conftest/conftest-container"), + require("./conftest/conftest-kubernetes"), require("./container/container"), require("./exec"), require("./google/google-app-engine"), diff --git a/garden-service/src/util/fs.ts b/garden-service/src/util/fs.ts index 4fe55e4990..45d7df8d02 100644 --- a/garden-service/src/util/fs.ts +++ b/garden-service/src/util/fs.ts @@ -7,6 +7,7 @@ */ import klaw = require("klaw") +import glob from "glob" import _spawn from "cross-spawn" import Bluebird from "bluebird" import { pathExists, readFile, writeFile } from "fs-extra" @@ -177,10 +178,25 @@ export function normalizeLocalRsyncPath(path: string) { } /** - * Checks if the given `path` matches any of the given glob `patterns`. + * Return a list of all files in directory at `path` */ -export function matchGlobs(path: string, patterns: string[]): boolean { - return some(patterns, (pattern) => minimatch(path, pattern)) +export async function listDirectory(path: string): Promise { + return new Promise((resolve, reject) => { + glob("**/*", { cwd: path, dot: true }, (err, files) => { + if (err) { + reject(err) + } else { + resolve(files) + } + }) + }) +} + +/** + * Given a list of `paths`, return a list of paths that match any of the given `patterns` + */ +export function matchGlobs(paths: string[], patterns: string[]): string[] { + return paths.filter((path) => some(patterns, (pattern) => minimatch(path, pattern))) } /** @@ -191,7 +207,9 @@ export function matchGlobs(path: string, patterns: string[]): boolean { * @param exclude List of globs to match for exclusion, or undefined */ export function matchPath(path: string, include?: string[], exclude?: string[]) { - return (!include || matchGlobs(path, include)) && (!exclude || !matchGlobs(path, exclude)) + return ( + (!include || matchGlobs([path], include).length === 1) && (!exclude || matchGlobs([path], exclude).length === 0) + ) } /** diff --git a/garden-service/test/data/test-projects/conftest-container/Dockerfile b/garden-service/test/data/test-projects/conftest-container/Dockerfile new file mode 100644 index 0000000000..09712b9774 --- /dev/null +++ b/garden-service/test/data/test-projects/conftest-container/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox:1.31.1 + +RUN echo foo diff --git a/garden-service/test/data/test-projects/conftest-container/garden.yml b/garden-service/test/data/test-projects/conftest-container/garden.yml new file mode 100644 index 0000000000..fad7f0f15c --- /dev/null +++ b/garden-service/test/data/test-projects/conftest-container/garden.yml @@ -0,0 +1,10 @@ +kind: Project +name: conftest-container +environments: + - local +providers: + - name: conftest-container +--- +kind: Module +type: container +name: container diff --git a/garden-service/test/data/test-projects/conftest-kubernetes/Dockerfile b/garden-service/test/data/test-projects/conftest-kubernetes/Dockerfile new file mode 100644 index 0000000000..09712b9774 --- /dev/null +++ b/garden-service/test/data/test-projects/conftest-kubernetes/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox:1.31.1 + +RUN echo foo diff --git a/garden-service/test/data/test-projects/conftest-kubernetes/custom-policy/statefulset.rego b/garden-service/test/data/test-projects/conftest-kubernetes/custom-policy/statefulset.rego new file mode 100644 index 0000000000..38af046cd1 --- /dev/null +++ b/garden-service/test/data/test-projects/conftest-kubernetes/custom-policy/statefulset.rego @@ -0,0 +1,7 @@ +package main + +deny[msg] { + input.kind = StatefulSet + input.spec.replicas = 1 + msg = "StatefulSet replicas should not be 1" +} diff --git a/garden-service/test/data/test-projects/conftest-kubernetes/garden.yml b/garden-service/test/data/test-projects/conftest-kubernetes/garden.yml new file mode 100644 index 0000000000..38f8eca5eb --- /dev/null +++ b/garden-service/test/data/test-projects/conftest-kubernetes/garden.yml @@ -0,0 +1,8 @@ +kind: Project +name: conftest-kubernetes +environments: + - name: local +providers: + - name: local-kubernetes + - name: conftest-kubernetes + policyPath: custom-policy diff --git a/garden-service/test/data/test-projects/conftest-kubernetes/helm/garden.yml b/garden-service/test/data/test-projects/conftest-kubernetes/helm/garden.yml new file mode 100644 index 0000000000..d350ad69f6 --- /dev/null +++ b/garden-service/test/data/test-projects/conftest-kubernetes/helm/garden.yml @@ -0,0 +1,6 @@ +kind: Module +description: Test Helm chart +type: helm +name: helm +chart: stable/postgresql +version: 3.9.2 diff --git a/garden-service/test/data/test-projects/conftest-kubernetes/kubernetes/deployment.yaml b/garden-service/test/data/test-projects/conftest-kubernetes/kubernetes/deployment.yaml new file mode 100644 index 0000000000..201f0a4e1a --- /dev/null +++ b/garden-service/test/data/test-projects/conftest-kubernetes/kubernetes/deployment.yaml @@ -0,0 +1 @@ +kind: Deployment \ No newline at end of file diff --git a/garden-service/test/data/test-projects/conftest-kubernetes/kubernetes/garden.yml b/garden-service/test/data/test-projects/conftest-kubernetes/kubernetes/garden.yml new file mode 100644 index 0000000000..69377bcf8f --- /dev/null +++ b/garden-service/test/data/test-projects/conftest-kubernetes/kubernetes/garden.yml @@ -0,0 +1,4 @@ +kind: Module +type: kubernetes +name: kubernetes +files: [deployment.yaml] diff --git a/garden-service/test/data/test-projects/conftest/garden.yml b/garden-service/test/data/test-projects/conftest/garden.yml new file mode 100644 index 0000000000..3622fbfa7a --- /dev/null +++ b/garden-service/test/data/test-projects/conftest/garden.yml @@ -0,0 +1,16 @@ +kind: Project +name: conftest +environments: + - local +providers: + - name: conftest +--- +kind: Module +type: conftest +name: warn +files: [warn.yaml] +--- +kind: Module +type: conftest +name: warn-and-fail +files: [warn-and-fail.yaml] diff --git a/garden-service/test/data/test-projects/conftest/policy.rego b/garden-service/test/data/test-projects/conftest/policy.rego new file mode 100644 index 0000000000..bcff123a70 --- /dev/null +++ b/garden-service/test/data/test-projects/conftest/policy.rego @@ -0,0 +1,11 @@ +package main + +warn[msg] { + input.shouldBeTrue = false + msg = "shouldBeTrue should be true" +} + +deny[msg] { + input.shouldDefinitelyNotBeTrue = true + msg = "shouldDefinitelyNotBeTrue must be false" +} \ No newline at end of file diff --git a/garden-service/test/data/test-projects/conftest/warn-and-fail.yaml b/garden-service/test/data/test-projects/conftest/warn-and-fail.yaml new file mode 100644 index 0000000000..1f12cf2d5c --- /dev/null +++ b/garden-service/test/data/test-projects/conftest/warn-and-fail.yaml @@ -0,0 +1,2 @@ +shouldBeTrue: false +shouldDefinitelyNotBeTrue: true \ No newline at end of file diff --git a/garden-service/test/data/test-projects/conftest/warn.yaml b/garden-service/test/data/test-projects/conftest/warn.yaml new file mode 100644 index 0000000000..f27ae09ecc --- /dev/null +++ b/garden-service/test/data/test-projects/conftest/warn.yaml @@ -0,0 +1,2 @@ +shouldBeTrue: false +shouldDefinitelyNotBeTrue: false \ No newline at end of file diff --git a/garden-service/test/integ/src/plugins/conftest/conftest-container.ts b/garden-service/test/integ/src/plugins/conftest/conftest-container.ts new file mode 100644 index 0000000000..debec3cdfc --- /dev/null +++ b/garden-service/test/integ/src/plugins/conftest/conftest-container.ts @@ -0,0 +1,92 @@ +import { ProjectConfig } from "../../../../../src/config/project" +import { DEFAULT_API_VERSION } from "../../../../../src/constants" +import { Garden } from "../../../../../src/garden" +import { getDataDir } from "../../../../helpers" +import { expect } from "chai" + +describe("conftest-container provider", () => { + const projectRoot = getDataDir("test-projects", "conftest-container") + const projectConfig: ProjectConfig = { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "test", + path: projectRoot, + defaultEnvironment: "default", + dotIgnoreFiles: [], + environments: [{ name: "default", variables: {} }], + providers: [{ name: "conftest-container", policyPath: "dockerfile.rego" }], + variables: {}, + } + + it("should add a conftest module for each container module with a Dockerfile", async () => { + const garden = await Garden.factory(projectRoot, { + plugins: [], + config: projectConfig, + }) + + const graph = await garden.getConfigGraph(garden.log) + const containerModule = await graph.getModule("container") + const module = await graph.getModule("conftest-container") + + expect(module.path).to.equal(containerModule.path) + expect(module.spec).to.eql({ + build: { dependencies: [] }, + files: ["Dockerfile"], + namespace: "main", + policyPath: "dockerfile.rego", + }) + }) + + it("should add a conftest module for module types inheriting from container", async () => { + const foo = { + name: "foo", + dependencies: ["container"], + createModuleTypes: [ + { + name: "foo", + base: "container", + docs: "foo", + handlers: {}, + }, + ], + } + + const garden = await Garden.factory(projectRoot, { + plugins: [foo], + config: { + ...projectConfig, + providers: [...projectConfig.providers, { name: "foo" }], + }, + }) + + let graph = await garden.getConfigGraph(garden.log) + const containerModule = await graph.getModule("container") + + garden["moduleConfigs"] = { + foo: { + apiVersion: DEFAULT_API_VERSION, + name: "foo", + type: "foo", + allowPublish: false, + build: { dependencies: [] }, + outputs: {}, + path: containerModule.path, + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: { dockerfile: "Dockerfile" }, + }, + } + + graph = await garden.getConfigGraph(garden.log) + const module = await graph.getModule("conftest-foo") + + expect(module.path).to.equal(projectRoot) + expect(module.spec).to.eql({ + build: { dependencies: [] }, + files: ["Dockerfile"], + namespace: "main", + policyPath: "dockerfile.rego", + }) + }) +}) diff --git a/garden-service/test/integ/src/plugins/conftest/conftest-kubernetes.ts b/garden-service/test/integ/src/plugins/conftest/conftest-kubernetes.ts new file mode 100644 index 0000000000..8a75e56ff6 --- /dev/null +++ b/garden-service/test/integ/src/plugins/conftest/conftest-kubernetes.ts @@ -0,0 +1,75 @@ +import { Garden } from "../../../../../src/garden" +import { getDataDir } from "../../../../helpers" +import { expect } from "chai" +import stripAnsi = require("strip-ansi") +import { dedent } from "../../../../../src/util/string" +import { TestTask } from "../../../../../src/tasks/test" + +describe("conftest-kubernetes provider", () => { + const projectRoot = getDataDir("test-projects", "conftest-kubernetes") + + it("should add a conftest module for each helm module", async () => { + const garden = await Garden.factory(projectRoot) + + const graph = await garden.getConfigGraph(garden.log) + const helmModule = await graph.getModule("helm") + const module = await graph.getModule("conftest-helm") + + expect(module.path).to.equal(helmModule.path) + expect(module.spec).to.eql({ + build: { dependencies: [] }, + files: [".rendered.yaml"], + namespace: "main", + policyPath: "../custom-policy", + sourceModule: "helm", + }) + }) + + it("should add a conftest module for each kubernetes module", async () => { + const garden = await Garden.factory(projectRoot) + + const graph = await garden.getConfigGraph(garden.log) + const kubernetesModule = await graph.getModule("kubernetes") + const module = await graph.getModule("conftest-kubernetes") + + expect(module.path).to.equal(kubernetesModule.path) + expect(module.spec).to.eql({ + build: { dependencies: [] }, + files: kubernetesModule.spec.files, + namespace: "main", + policyPath: "../custom-policy", + sourceModule: "kubernetes", + }) + }) + + describe("testModule", () => { + it("should be able to test files in a remote Helm chart", async () => { + const garden = await Garden.factory(projectRoot) + + const graph = await garden.getConfigGraph(garden.log) + const module = await graph.getModule("conftest-helm") + + const testTask = new TestTask({ + garden, + module, + log: garden.log, + graph, + testConfig: module.testConfigs[0], + force: true, + forceBuild: true, + version: module.version, + }) + + const key = testTask.getKey() + const { [key]: result } = await garden.processTasks([testTask]) + + expect(result).to.exist + expect(result!.error).to.exist + expect(stripAnsi(result!.error!.message)).to.equal(dedent` + conftest reported 1 failure(s): + + FAIL - .rendered.yaml - StatefulSet replicas should not be 1 + `) + }) + }) +}) diff --git a/garden-service/test/integ/src/plugins/conftest/conftest.ts b/garden-service/test/integ/src/plugins/conftest/conftest.ts new file mode 100644 index 0000000000..5717da40d5 --- /dev/null +++ b/garden-service/test/integ/src/plugins/conftest/conftest.ts @@ -0,0 +1,145 @@ +import { ProjectConfig } from "../../../../../src/config/project" +import { DEFAULT_API_VERSION } from "../../../../../src/constants" +import { Garden } from "../../../../../src/garden" +import { getDataDir } from "../../../../helpers" +import { expect } from "chai" +import stripAnsi from "strip-ansi" +import { dedent } from "../../../../../src/util/string" +import { TestTask } from "../../../../../src/tasks/test" + +describe("conftest provider", () => { + const projectRoot = getDataDir("test-projects", "conftest") + const projectConfig: ProjectConfig = { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "test", + path: projectRoot, + defaultEnvironment: "default", + dotIgnoreFiles: [], + environments: [{ name: "default", variables: {} }], + providers: [{ name: "conftest", policyPath: "policy.rego" }], + variables: {}, + } + + describe("testModule", () => { + it("should format warnings and errors nicely", async () => { + const garden = await Garden.factory(projectRoot, { + plugins: [], + config: projectConfig, + }) + + const graph = await garden.getConfigGraph(garden.log) + const module = await graph.getModule("warn-and-fail") + + const testTask = new TestTask({ + garden, + module, + log: garden.log, + graph, + testConfig: module.testConfigs[0], + force: true, + forceBuild: false, + version: module.version, + }) + + const key = testTask.getKey() + const { [key]: result } = await garden.processTasks([testTask]) + + expect(result).to.exist + expect(result!.error).to.exist + expect(stripAnsi(result!.error!.message)).to.equal(dedent` + conftest reported 1 failure(s) and 1 warning(s): + + FAIL - warn-and-fail.yaml - shouldDefinitelyNotBeTrue must be false + WARN - warn-and-fail.yaml - shouldBeTrue should be true + `) + }) + + it("should set success=false with a linting warning if testFailureThreshold=warn", async () => { + const garden = await Garden.factory(projectRoot, { + plugins: [], + config: { + ...projectConfig, + providers: [{ name: "conftest", policyPath: "policy.rego", testFailureThreshold: "warn" }], + }, + }) + + const graph = await garden.getConfigGraph(garden.log) + const module = await graph.getModule("warn") + + const testTask = new TestTask({ + garden, + module, + log: garden.log, + graph, + testConfig: module.testConfigs[0], + force: true, + forceBuild: false, + version: module.version, + }) + + const key = testTask.getKey() + const { [key]: result } = await garden.processTasks([testTask]) + + expect(result).to.exist + expect(result!.error).to.exist + }) + + it("should set success=true with a linting warning if testFailureThreshold=error", async () => { + const garden = await Garden.factory(projectRoot, { + plugins: [], + config: projectConfig, + }) + + const graph = await garden.getConfigGraph(garden.log) + const module = await graph.getModule("warn") + + const testTask = new TestTask({ + garden, + module, + log: garden.log, + graph, + testConfig: module.testConfigs[0], + force: true, + forceBuild: false, + version: module.version, + }) + + const key = testTask.getKey() + const { [key]: result } = await garden.processTasks([testTask]) + + expect(result).to.exist + expect(result!.error).to.not.exist + }) + + it("should set success=true with warnings and errors if testFailureThreshold=none", async () => { + const garden = await Garden.factory(projectRoot, { + plugins: [], + config: { + ...projectConfig, + providers: [{ name: "conftest", policyPath: "policy.rego", testFailureThreshold: "none" }], + }, + }) + + const graph = await garden.getConfigGraph(garden.log) + const module = await graph.getModule("warn-and-fail") + + const testTask = new TestTask({ + garden, + module, + log: garden.log, + graph, + testConfig: module.testConfigs[0], + force: true, + forceBuild: false, + version: module.version, + }) + + const key = testTask.getKey() + const { [key]: result } = await garden.processTasks([testTask]) + + expect(result).to.exist + expect(result!.error).to.not.exist + }) + }) +})