mirror of
https://github.com/holos-run/holos.git
synced 2026-03-20 17:25:01 +00:00
Without this patch the validator fails if a component manages two of the same kind of resource, which is common. This patch updates the example to use the metadata namespace and name as lookup keys. This works for most components, but may not for ClusterResources. Use the kind top level field in that case and pass the field name of the validator as a tag value to vary by component.
429 lines
12 KiB
Plaintext
429 lines
12 KiB
Plaintext
---
|
|
slug: validators
|
|
title: Validators
|
|
description: Validate rendered manifests against policy definitions.
|
|
sidebar_position: 60
|
|
---
|
|
|
|
import Tabs from '@theme/Tabs';
|
|
import TabItem from '@theme/TabItem';
|
|
import RenderingOverview from '@site/src/diagrams/rendering-overview.mdx';
|
|
|
|
# Validators
|
|
|
|
## Overview
|
|
|
|
Sometimes Helm charts render Secrets we do not wanted committed to version
|
|
control for security. Helm charts often render incorrect manifests, even if
|
|
they're accepted by the api server. For example, passing `null` to collection
|
|
fields. We'll solve both of these issues using a [Validator] to block artifacts
|
|
with a Secret resource, and verifying the artifact against Kubernetes type
|
|
definitions.
|
|
|
|
1. If a Helm chart renders a Secret, Holos errors before writing the artifact
|
|
and suggests an ExternalSecret instead.
|
|
2. Each resource is validated against a field named by the value of the kind
|
|
field. For example, a `kind: Secret` resource validates against `secret: {}` in
|
|
CUE. `kind: Deployment` validates against `deployment: {}` in CUE.
|
|
3. The final artifact is validated, covering the output of all generators and
|
|
transformers.
|
|
|
|
<RenderingOverview />
|
|
|
|
## The Code
|
|
|
|
### Generating the Structure
|
|
|
|
Use `holos` to generate a minimal platform directory structure. First, create
|
|
and navigate into a blank directory. Then, use the `holos generate platform`
|
|
command to generate a minimal platform.
|
|
|
|
```shell
|
|
mkdir holos-validators-tutorial && cd holos-validators-tutorial
|
|
holos init platform v1alpha5
|
|
```
|
|
|
|
### Creating the Component
|
|
|
|
Create the directory for the `podinfo` component. Create an empty file, then add
|
|
the following CUE configuration to it.
|
|
|
|
```bash
|
|
mkdir -p components/podinfo
|
|
```
|
|
```bash
|
|
cat <<EOF > components/podinfo/podinfo.cue
|
|
```
|
|
```cue showLineNumbers
|
|
package holos
|
|
|
|
// export the component build plan to holos
|
|
holos: Component.BuildPlan
|
|
|
|
// Component is a Helm chart
|
|
Component: #Helm & {
|
|
Name: "podinfo"
|
|
Namespace: "default"
|
|
// Add metadata.namespace to all resources with kustomize.
|
|
KustomizeConfig: Kustomization: namespace: Namespace
|
|
Chart: {
|
|
version: "6.6.2"
|
|
repository: {
|
|
name: "podinfo"
|
|
url: "https://stefanprodan.github.io/podinfo"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
```bash
|
|
EOF
|
|
```
|
|
|
|
Register the component with the platform.
|
|
|
|
```bash
|
|
cat <<EOF > platform/podinfo.cue
|
|
```
|
|
```cue showLineNumbers
|
|
package holos
|
|
|
|
Platform: Components: podinfo: {
|
|
name: "podinfo"
|
|
path: "components/podinfo"
|
|
}
|
|
```
|
|
```bash
|
|
EOF
|
|
```
|
|
|
|
Render the platform.
|
|
|
|
<Tabs groupId="tutorial-hello-render-manifests">
|
|
<TabItem value="command" label="Command">
|
|
```bash
|
|
holos render platform
|
|
```
|
|
</TabItem>
|
|
<TabItem value="output" label="Output">
|
|
```
|
|
cached podinfo 6.6.2
|
|
rendered podinfo in 1.938665041s
|
|
rendered platform in 1.938759417s
|
|
```
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
Add and commit the initial configuration.
|
|
|
|
```bash
|
|
git init . && git add . && git commit -m initial
|
|
```
|
|
|
|
### Define the Valid Schema
|
|
|
|
We'll use a CUE package named `policy` so the entire platform configuration in
|
|
package `holos` isn't loaded every time we validate an artifact.
|
|
|
|
Create `policy/validation-schema.cue` with the following content.
|
|
|
|
```shell
|
|
mkdir -p policy
|
|
cat <<EOF > policy/validation-schema.cue
|
|
```
|
|
```cue showLineNumbers
|
|
package policy
|
|
|
|
import apps "k8s.io/api/apps/v1"
|
|
|
|
// Organize by kind then name to avoid conflicts.
|
|
kind: [KIND=string]: [NAME=string]: {...}
|
|
|
|
// Useful when one component manages the same resource kind and name across
|
|
// multiple namespaces.
|
|
let KIND = kind
|
|
namespace: [NS=string]: KIND
|
|
|
|
// Block Secret resources. kind will not unify with "Secret"
|
|
kind: secret: [NAME=string]: kind: "Use an ExternalSecret instead. Forbidden by security policy. secret/\(NAME)"
|
|
|
|
// Validate Deployment against Kubernetes type definitions.
|
|
kind: deployment: [_]: apps.#Deployment
|
|
```
|
|
```shell
|
|
EOF
|
|
```
|
|
|
|
### Configuring Validators
|
|
|
|
Configure the Validators [ComponentConfig] field to configure each [BuildPlan]
|
|
to validate the rendered [Artifact] files.
|
|
|
|
```shell
|
|
cat <<EOF > validators.cue
|
|
```
|
|
```cue showLineNumbers
|
|
package holos
|
|
|
|
// Configure all component kinds to validate against the policy directory.
|
|
#ComponentConfig: Validators: cue: {
|
|
kind: "Command"
|
|
// Note --path maps each resource to a top level field named by the kind.
|
|
command: args: [
|
|
"holos",
|
|
"cue",
|
|
"vet",
|
|
"./policy",
|
|
"--path=\"namespace\"",
|
|
"--path=metadata.namespace",
|
|
"--path=strings.ToLower(kind)",
|
|
"--path=metadata.name",
|
|
]
|
|
}
|
|
```
|
|
```shell
|
|
EOF
|
|
```
|
|
|
|
### Patching Errors
|
|
|
|
Render the platform to see validation fail. The podinfo chart has no Secret,
|
|
but it produces an invalid Deployment because it sets the container resource
|
|
limits field to `null`.
|
|
|
|
```shell
|
|
holos render platform
|
|
```
|
|
|
|
```txt
|
|
deployment.spec.template.spec.containers.0.resources.limits: conflicting values null and {[string]:"k8s.io/apimachinery/pkg/api/resource".#Quantity} (mismatched types null and struct):
|
|
./cue.mod/gen/k8s.io/api/apps/v1/types_go_gen.cue:355:9
|
|
./cue.mod/gen/k8s.io/api/apps/v1/types_go_gen.cue:376:12
|
|
./cue.mod/gen/k8s.io/api/core/v1/types_go_gen.cue:2840:11
|
|
./cue.mod/gen/k8s.io/api/core/v1/types_go_gen.cue:2968:14
|
|
./cue.mod/gen/k8s.io/api/core/v1/types_go_gen.cue:3882:15
|
|
./cue.mod/gen/k8s.io/api/core/v1/types_go_gen.cue:3882:18
|
|
./cue.mod/gen/k8s.io/api/core/v1/types_go_gen.cue:5027:9
|
|
./cue.mod/gen/k8s.io/api/core/v1/types_go_gen.cue:6407:16
|
|
./policy/validation-schema.cue:9:13
|
|
../../../../../var/folders/22/T/holos.validate1636392304/components/podinfo/podinfo.gen.yaml:104:19
|
|
could not run: terminating because of errors
|
|
could not run: could not validate podinfo path ./components/podinfo: could not run command: holos cue vet ./policy --path strings.ToLower(kind) /var/folders/22/T/holos.validate1636392304/components/podinfo/podinfo.gen.yaml: exit status 1 at builder/v1alpha5/builder.go:411
|
|
could not run: could not render component: could not run command: holos --log-level info --log-format console render component --inject holos_component_name=podinfo --inject holos_component_path=components/podinfo ./components/podinfo: exit status 1 at cli/render/render.go:155
|
|
```
|
|
|
|
We'll use a [Kustomize] patch [Transformer] to replace the `null` limits field
|
|
with a valid equivalent value.
|
|
|
|
:::important
|
|
This configuration is defined in CUE, not YAML, even though we're configuring a
|
|
Kustomize patch transformer. CUE gives us access to the unified platform
|
|
configuration.
|
|
:::
|
|
|
|
```shell
|
|
cat <<EOF > components/podinfo/patch.cue
|
|
```
|
|
```cue showLineNumbers
|
|
package holos
|
|
|
|
import "encoding/yaml"
|
|
|
|
Component: KustomizeConfig: Kustomization: {
|
|
_patches: limits: {
|
|
target: kind: "Deployment"
|
|
patch: yaml.Marshal([{
|
|
op: "test"
|
|
path: "/spec/template/spec/containers/0/resources/limits"
|
|
value: null
|
|
}, {
|
|
op: "replace"
|
|
path: "/spec/template/spec/containers/0/resources/limits"
|
|
value: {}
|
|
}])
|
|
}
|
|
patches: [for x in _patches {x}]
|
|
}
|
|
```
|
|
```shell
|
|
EOF
|
|
```
|
|
|
|
Now the platform renders.
|
|
|
|
<Tabs groupId="3A050092-8E56-49D4-84A9-71E544A21276">
|
|
<TabItem value="command" label="Command">
|
|
```bash
|
|
holos render platform
|
|
```
|
|
</TabItem>
|
|
<TabItem value="output" label="Output">
|
|
```txt
|
|
rendered podinfo in 181.875083ms
|
|
rendered platform in 181.975833ms
|
|
```
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
## Inspecting the BuildPlan
|
|
|
|
The BuildPlan patches the output of the upstream helm chart without modifying
|
|
it, then validates the artifact against the Kubernetes type definitions.
|
|
|
|
<Tabs groupId="1DAB4C46-0793-4CCA-8930-7B2E60BDA1BE">
|
|
<TabItem value="command" label="Command">
|
|
```bash
|
|
holos show buildplans
|
|
```
|
|
</TabItem>
|
|
<TabItem value="output" label="Output">
|
|
```yaml showLineNumbers
|
|
kind: BuildPlan
|
|
apiVersion: v1alpha5
|
|
metadata:
|
|
name: podinfo
|
|
spec:
|
|
artifacts:
|
|
- artifact: components/podinfo/podinfo.gen.yaml
|
|
generators:
|
|
- kind: Helm
|
|
output: helm.gen.yaml
|
|
helm:
|
|
chart:
|
|
name: podinfo
|
|
version: 6.6.2
|
|
release: podinfo
|
|
repository:
|
|
name: podinfo
|
|
url: https://stefanprodan.github.io/podinfo
|
|
values: {}
|
|
namespace: default
|
|
- kind: Resources
|
|
output: resources.gen.yaml
|
|
transformers:
|
|
- kind: Kustomize
|
|
inputs:
|
|
- helm.gen.yaml
|
|
- resources.gen.yaml
|
|
output: components/podinfo/podinfo.gen.yaml
|
|
kustomize:
|
|
kustomization:
|
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
kind: Kustomization
|
|
namespace: default
|
|
patches:
|
|
- patch: |
|
|
- op: test
|
|
path: /spec/template/spec/containers/0/resources/limits
|
|
value: null
|
|
- op: replace
|
|
path: /spec/template/spec/containers/0/resources/limits
|
|
value: {}
|
|
target:
|
|
kind: Deployment
|
|
name: ""
|
|
resources:
|
|
- helm.gen.yaml
|
|
- resources.gen.yaml
|
|
validators:
|
|
- kind: Command
|
|
inputs:
|
|
- components/podinfo/podinfo.gen.yaml
|
|
command:
|
|
args:
|
|
- holos
|
|
- cue
|
|
- vet
|
|
- ./policy
|
|
- --path
|
|
- strings.ToLower(kind)
|
|
```
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
## Catching Mistakes
|
|
|
|
Suppose a teammate downloads a helm chart that includes a Secret unbeknown to
|
|
them. Holos catches the problem and suggests an ExternalSecret instead.
|
|
|
|
Mix in a Secret to see what happens
|
|
|
|
```shell
|
|
cat <<EOF > components/podinfo/secret.cue
|
|
```
|
|
```cue showLineNumbers
|
|
package holos
|
|
|
|
Component: Resources: Secret: example: metadata: name: "example"
|
|
```
|
|
```shell
|
|
EOF
|
|
```
|
|
|
|
Render the platform to see the error.
|
|
|
|
```shell
|
|
holos render platform
|
|
```
|
|
```txt
|
|
secret.kind: conflicting values "Use an ExternalSecret instead. Forbidden by security policy." and "Secret":
|
|
./policy/validation-schema.cue:6:15
|
|
../../../../../var/folders/22/T/holos.validate2549739170/components/podinfo/podinfo.gen.yaml:1:7
|
|
could not run: terminating because of errors
|
|
could not run: could not validate podinfo path ./components/podinfo: could not run command: holos cue vet ./policy --path strings.ToLower(kind) /var/folders/22/T/holos.validate2549739170/components/podinfo/podinfo.gen.yaml: exit status 1 at builder/v1alpha5/builder.go:411
|
|
could not run: could not render component: could not run command: holos --log-level info --log-format console render component --inject holos_component_name=podinfo --inject holos_component_path=components/podinfo ./components/podinfo: exit status 1 at cli/render/render.go:155
|
|
```
|
|
|
|
:::important
|
|
Holos quickly returns an error if validated artifacts have a Secret.
|
|
:::
|
|
|
|
Remove the secret to resolve the issue.
|
|
|
|
```shell
|
|
rm components/podinfo/secret.cue
|
|
```
|
|
|
|
## Inspecting the diff
|
|
|
|
The validation and patch results in a correct Deployment, verified against the
|
|
Kubernetes type definitions.
|
|
|
|
```shell
|
|
git diff
|
|
```
|
|
```diff
|
|
diff --git a/deploy/components/podinfo/podinfo.gen.yaml b/deploy/components/podinfo/podinfo.gen.yaml
|
|
index 6e4aec0..a145e3f 100644
|
|
--- a/deploy/components/podinfo/podinfo.gen.yaml
|
|
+++ b/deploy/components/podinfo/podinfo.gen.yaml
|
|
@@ -101,7 +101,7 @@ spec:
|
|
successThreshold: 1
|
|
timeoutSeconds: 5
|
|
resources:
|
|
- limits: null
|
|
+ limits: {}
|
|
requests:
|
|
cpu: 1m
|
|
memory: 16Mi
|
|
```
|
|
|
|
## Trying Locally
|
|
|
|
Optionally, apply the manifests rendered by Holos to a [Local Cluster] for
|
|
testing.
|
|
|
|
[Local Cluster]: ../topics/local-cluster.mdx
|
|
[ExternalSecret]: https://external-secrets.io/latest/api/externalsecret/
|
|
[Artifact]: ../api/core.md#Artifact
|
|
[BuildPlan]: ../api/core.md#BuildPlan
|
|
[Resources]: ../api/core.md#Resources
|
|
[Validator]: ../api/core.md#Validator
|
|
[Transformer]: ../api/core.md#Transformer
|
|
[Kustomize]: ../api/core.md#Kustomize
|
|
[Generator]: ../api/core.md#Generator
|
|
[Hello Holos]: ./hello-holos.mdx
|
|
[cue.mod/gen/external-secrets.io/externalsecret/v1beta1/types_gen.cue]: https://github.com/holos-run/holos/blob/main/internal/generate/platforms/cue.mod/gen/external-secrets.io/externalsecret/v1beta1/types_gen.cue#L13
|
|
[ComponentConfig]: ../api/author.md#ComponentConfig
|
|
[timoni]: https://timoni.sh/install/
|
|
[resources.cue]: https://github.com/holos-run/holos/blob/main/internal/generate/platforms/v1alpha5/resources.cue#L33
|