ADR0011 - CRD Controller Architecture
Problem Statement
We decided to use Helm charts as the primary interface between service maintainers and the service operators. Helm charts are the most used package format in the Kubernetes ecosystem, and both service maintainers and operators are familiar with them.
There are many projects that can apply charts in a reconcile loop such as ArgoCD and FluxCD.
All of those tools suffer from one deficiency: they take a unstructured values.yaml file as input, which makes parameter discovery and validation difficult.
This is especially true for complex services that have many parameters.
Kubernetes allows defining APIs using OpenAPI specifications through Custom Resource Definitions (CRDs). This allows defining a structured API which is well integrated into tooling and editors and can be used to validate the input parameters. OpenAPI specifications can be quite verbose and complex, which makes them hard to maintain.
The goal of this controller is to translate a Helm chart values.yaml file into a CRD with a well defined OpenAPI specification.
This keeps maintaining a service Helm chart simple, while allowing operators to use a structured API to configure the service.
High level goals
-
Provide a structured API for service configuration based on Helm chart values.
-
Allow service maintainers to continue using Helm charts without needing to define a CRD by hand.
-
Make CRDs less of a singleton to allow for multiple versions of the same service to be deployed in the same cluster for testing and development purposes.
Proposed Architecture
Relying on FluxCD GitOps controllers for OCI discovery and downloads
FluxCD brings multiple modular controllers to handle OCI repositories. It handles authentication, discovery, integrity checking, and downloading of Helm charts from OCI registries. By relying on FluxCD for this functionality, we don’t have to reimplement all those features and can focus on the core functionality of translating Helm chart values into CRDs.
values.yaml
The values.yaml file of a Helm chart defines the configuration options for the chart.
The controller will read the values.yaml file and generate a CRD with an OpenAPI schema that reflects the structure of the values.yaml file.
The generated CRD will have a .spec.values field that contains the configuration options defined in the values.yaml file.
# This will set the replicaset count.
replicaCount: 1
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
image:
repository: ghcr.io/nginx/nginx-unprivileged
# This sets the pull policy for images.
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
# Demonstrates a tagged null type
# @type: int
timeoutSeconds:
would be translated into the following CRD:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: juiceshops.v3.juiceshop.bundles.helmetica-bundles.io
spec:
group: v3.juiceshop.bundles.helmetica-bundles.io
names:
kind: JuiceShop
listKind: JuiceShopList
plural: juiceshops
singular: juiceshop
scope: Namespaced
versions:
- name: bundle
schema:
openAPIV3Schema:
properties:
spec:
description: Configures the desired state of the service.
properties:
desiredRevision:
description: The desired revision of the service.
type: string
values:
description: This field together with the `.spec.version` field defines
the configuration of the service. Every change to either of these
two fields creates a new revision of the service.
properties:
image:
description: 'This sets the container image more information can
be found here: https://kubernetes.io/docs/concepts/containers/images/'
properties:
pullPolicy:
description: This sets the pull policy for images.
type: string
repository:
type: string
tag:
description: Overrides the image tag whose default is the
chart appVersion.
type: string
type: object
replicaCount:
description: This will set the replicaset count.
type: integer
timeoutSeconds:
description: Demonstrates a tagged null type
type: integer
nullable: true
type: object
version:
description: The version of the service. Every change to this field
together with the `.spec.values` field creates a new revision of
the service.
type: string
type: object
type: object
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: null
storedVersions: null
Controller API
The API should be CRD based and should make it possible to define all possible configuration options for Flux. Errors should be reported in a structured and discoverable way.
apiVersion: helmetica.io/v1
kind: CustomResourceDefinitionSource
metadata:
name: v6.podinfo (1)
spec:
crdNames: (2)
kind: PodInfo
plural: podinfos
reference: (3)
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
name: podinfo-v6
versionDiscovery:
reference: (4)
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
name: podinfo
status:
appliedReferenceGeneration: 1 (5)
appliedReferenceRevision: 6.14.0@sha256:476bed61733536f99e7331b0fe4cc9fd70bc6497a855ad38ba49b72de50c1132
conditions: (6)
- message: CustomResourceDefinition is ready
reason: CustomResourceDefinitionReady
status: "True"
type: Ready
| 1 | Name is unique and if used in the CRD group it ensures that the generated CRD is unique in the cluster.
The generated name should be namespaced under a domain that does not conflict.
$NAME.helmetica-bundles.io |
| 2 | Allows overridding the generated CRD name and group. |
| 3 | Reference to the OCIRepository resource that contains the Helm chart. |
| 4 | Reference to the ImageRepository resource that discovers the Helm chart versions.
This is used to generate the enum for the .spec.version field in the CRD.
If left empty, the .spec.version field will be a string without any validation. |
| 5 | The generation and revision of the referenced OCIRepository resource that was used to generate the CRD. |
| 6 | The conditions of the generated CRD resource. Shows any errors that occurred during generation and the readiness of the generated CRD. |
FluxCD resources
The FluxCD resources used in the example above are defined as follows:
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: podinfo-v6
spec:
interval: 5m
provider: generic
ref:
semver: 6.x (1)
url: oci://ghcr.io/stefanprodan/charts/podinfo
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: podinfo
spec:
exclusionList:
- ^.*\.sig$
- ^sha256-.+$
image: ghcr.io/stefanprodan/charts/podinfo
interval: 5m
provider: generic
| 1 | For production use the semver should be set to a major version range to avoid breaking changes in the generated CRD.
The applied CRD will be checked for (accidental) breaking changes and will be rejected if the OpenAPI schema has changed in a way that is not backwards compatible.
Because of the breaking change detection, it is safe to use a semver range that allows for minor and patch version updates.
Users that want a specifig version for testing purposes can create a new OCIRepository plus CustomResourceDefinitionSource resource with a specific version to generate a CRD for that version. |
Implementation
The controller watches CustomResourceDefinitionSource resources and their referenced OCIRepository and ImageRepository resources.
On reconcile the controller will:
-
Download the Helm chart from the referenced
OCIRepositoryresource. -
Unpack the chart and read the
values.yamlfile. -
Generate a CRD with an OpenAPI schema based on the
values.yamlfile. -
If an
ImageRepositoryresource is referenced-
Download the chart versions from the
ImageRepository -
Filters versions based on the chart version
~MAJOR, ⇐ VERSION -
Generate an enum for the
.spec.versionfield in the CRD based on the filtered versions.
-
-
If a CRD with the same name already exists
-
Compare the OpenAPI schema of the existing CRD with the generated one.
-
If the schema has changed in a way that is not backwards compatible, reject the update and report an error in the
CustomResourceDefinitionSourcestatus.
-
-
Update the existing CRD with the new schema.