diff --git a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/immutable.cue b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/immutable.cue new file mode 100644 index 00000000..7b31c23e --- /dev/null +++ b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/immutable.cue @@ -0,0 +1,49 @@ +// Copyright 2024 Stefan Prodan +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "encoding/json" + "strings" + "uuid" +) + +#ConfigMapKind: "ConfigMap" +#SecretKind: "Secret" + +// ImmutableConfig is a generator for immutable Kubernetes ConfigMaps and Secrets. +// The metadata.name of the generated object is suffixed with the hash of the input data. +#ImmutableConfig: { + // Kind of the generated object. + #Kind: *#ConfigMapKind | #SecretKind + + // Metadata of the generated object. + #Meta: #Metadata + + // Optional suffix appended to the generate name. + #Suffix: *"" | string + + // Data of the generated object. + #Data: {[string]: string} + + let hash = strings.Split(uuid.SHA1(uuid.ns.DNS, json.Marshal(#Data)), "-")[0] + + apiVersion: "v1" + kind: #Kind + metadata: { + name: #Meta.name + #Suffix + "-" + hash + namespace: #Meta.namespace + labels: #Meta.labels + if #Meta.annotations != _|_ { + annotations: #Meta.annotations + } + } + immutable: true + if kind == #ConfigMapKind { + data: #Data + } + if kind == #SecretKind { + stringData: #Data + } +} diff --git a/docs/cue/module/immutable-config.md b/docs/cue/module/immutable-config.md new file mode 100644 index 00000000..f59ae746 --- /dev/null +++ b/docs/cue/module/immutable-config.md @@ -0,0 +1,109 @@ +# Immutable ConfigMaps and Secrets + +Timoni offers a CUE definition `#ImmutableConfig` for generating immutable Kubernetes ConfigMaps and Secrets. + +When the ConfigMap or Secret data changes, Timoni will create a new object with a new name suffix, +and it will update the references to the new object, triggering a rolling update for the +application's Deployments, StatefulSets, DaemonSets, etc. +The old ConfigMaps and Secrets will be deleted from the cluster after the rolling update is completed. + +## Example + +Assuming you want to populate the app Deployment environment variables from a Kubernetes Secret, +with data that end-users can set at installation and upgrade time. + +### Create the `Secret` template + +In the `templates` directory, create a `secret.cue` file with the following content: + +```cue +package templates + +import ( + timoniv1 "timoni.sh/core/v1alpha1" +) + +#Secret: timoniv1.#ImmutableConfig & { + #config: #Config + #Kind: timoniv1.#SecretKind + #Meta: #config.metadata + #Data: { + "LOGGING_LEVEL_ROOT": #config.logLevel + } +} + +``` + +The `#ImmutableConfig` definition will generate an immutable `Secret` resource with the +`metadata.name` set to`-`, where `` is a hash +of the `#Data` object. This ensures that the `Secret` name will change when the +`#Data` content changes. + +!!! tip "ConfigMap generator" + + If you want to generate a Kubernetes ConfigMap instead of a Secret, + set the `#Kind` to `timoniv1.#ConfigMapKind`. + + If you want to generate multiple ConfigMaps and Secrets, to avoid name collisions, + set the `#Suffix` to a unique string, e.g. `#Suffix: "-cm1"`. + +### Reference the `Secret` in the `Deployment` template + +In the `templates/deployment.cue` file, define the `secretName` as an input parameter, +and reference it in `envFrom`: + +```cue +#Deployment: appsv1.#Deployment & { + #config: #Config + #secretName: string + + spec: { + template: { + spec: { + containers: [{ + envFrom: [{ + secretRef: { + name: #secretName + } + }] + }] + } + } + } +} + +``` + +We need to pass the `secretName` to the `Deployment` template so that every time the +`Secret` name changes, the `Deployment` spec will be updated with the new name. + +### Add the `logLevel` to the `Config` definition + +In the `templates/config.cue` file, add the `logLevel` configuration: + +```cue +#Config: { + logLevel: *"INFO" | "DEBUG" | "WARN" | "ERROR" +} +``` + +### Add the `Secret` to the `Instance` definition + +In the `templates/config.cue` file, add the `Secret` resource to the instance objects, +and pass the generated `secret.metadata.name` to the `Deployment` template: + +```cue +#Instance: { + config: #Config + + objects: { + secret: #Secret & {#config: config} + + deploy: #Deployment & { + #config: config + #secretName: secret.metadata.name + } + } +} + +``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 8042ba53..c0af424a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -35,40 +35,90 @@ you have to specify the container registry address and the version of a module. For example, to install the latest stable version of [podinfo](https://github.com/stefanprodan/podinfo) in a new namespace: -```console -$ timoni -n test apply podinfo oci://ghcr.io/stefanprodan/modules/podinfo --version latest -pulling oci://ghcr.io/stefanprodan/modules/podinfo:latest -using module timoni.sh/podinfo version 6.5.4 -installing podinfo in namespace test -Namespace/test created -ServiceAccount/test/podinfo created -Service/test/podinfo created -Deployment/test/podinfo created -waiting for 3 resource(s) to become ready... -all resources are ready -``` +=== "command" + + ```shell + timoni -n test apply podinfo oci://ghcr.io/stefanprodan/modules/podinfo + ``` + +=== "output" + + ```text + pulling oci://ghcr.io/stefanprodan/modules/podinfo:latest + using module timoni.sh/podinfo version 6.5.4 + installing podinfo in namespace test + Namespace/test created + ServiceAccount/test/podinfo created + Service/test/podinfo created + Deployment/test/podinfo created + waiting for 3 resource(s) to become ready... + all resources are ready + ``` + +The apply command pulls the module from the container registry, +creates the Kubernetes resources in the specified namespace, +and waits for all resources to become ready. + +To learn more about all the available apply options, use `timoni apply --help`. ## List and inspect instances -You can list all instances in a cluster with `timoni ls -A`. +You can list all instances in a cluster with: -To get more information on an instance, you can use the `timoni inspect` sub-commands: +=== "command" -```console -$ timoni -n test inspect module podinfo -name: timoni.sh/podinfo -version: 6.5.4 -repository: oci://ghcr.io/stefanprodan/modules/podinfo -digest: sha256:1dba385f9d56f9a79e5b87344bbec1502bd11f056df51834e18d3e054de39365 -``` + ```shell + timoni list -A + ``` -To learn more about the available commands, use `timoni inspect --help`. +=== "output" + + ```text + NAME NAMESPACE MODULE VERSION LAST APPLIED BUNDLE + podinfo test oci://ghcr.io/stefanprodan/modules/podinfo 6.5.4 2024-01-20T19:51:17Z - + ``` To see the status of the Kubernetes resources managed by an instance: -```shell -timoni -n test status podinfo -``` +=== "command" + + ```shell + timoni -n test status podinfo + ``` + +=== "output" + + ```text + last applied 2024-01-20T19:51:17Z + module oci://ghcr.io/stefanprodan/modules/podinfo:6.5.4 + digest sha256:1dba385f9d56f9a79e5b87344bbec1502bd11f056df51834e18d3e054de39365 + container image ghcr.io/curl/curl-container/curl-multi:master + container image ghcr.io/stefanprodan/podinfo:6.5.4 + ServiceAccount/test/podinfo Current - Resource is current + Service/test/podinfo Current - Service is ready + Deployment/test/podinfo Current - Deployment is available. Replicas: 1 + ``` + +To get more information on an instance, you can use the `timoni inspect` sub-commands. + +For example, to list the module URL, version and OCI digest of the podinfo instance: + +=== "command" + + ```shell + timoni -n test inspect module podinfo + ``` + +=== "output" + + ```text + digest: sha256:1dba385f9d56f9a79e5b87344bbec1502bd11f056df51834e18d3e054de39365 + name: timoni.sh/podinfo + repository: oci://ghcr.io/stefanprodan/modules/podinfo + version: 6.5.4 + ``` + +To learn more about the available commands, use `timoni inspect --help`. ## Configure a module instance @@ -89,24 +139,50 @@ values: { Apply the config to the podinfo module to perform an upgrade: -```shell -timoni -n test apply podinfo \ - oci://ghcr.io/stefanprodan/modules/podinfo \ - --values qos-values.cue -``` +=== "command" + + ```shell + timoni -n test apply podinfo oci://ghcr.io/stefanprodan/modules/podinfo \ + --values qos-values.cue + ``` + +=== "output" + + ```text + pulling oci://ghcr.io/stefanprodan/modules/podinfo:latest + using module timoni.sh/podinfo version 6.5.4 + upgrading podinfo in namespace test + ServiceAccount/test/podinfo unchanged + Service/test/podinfo unchanged + Deployment/test/podinfo configured + resources are ready + ``` Before running an upgrade, you can review the changes that will be made on the cluster with `timoni apply --dry-run --diff`. -To learn more about all the available apply options, use `timoni apply --help`. - ## Uninstall a module instance To uninstall an instance and delete all the managed Kubernetes resources: -```shell -timoni -n test delete podinfo --wait -``` +=== "command" + + ```shell + timoni -n test delete podinfo + ``` + +=== "output" + + ```text + deleting 3 resource(s)... + Deployment/test/podinfo deleted + Service/test/podinfo deleted + ServiceAccount/test/podinfo deleted + all resources have been deleted + ``` + +By default, the delete command will wait for all the resources to be removed. +To skip waiting, use the `--wait=false` flag. ## Bundling instances diff --git a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/immutable.cue b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/immutable.cue new file mode 100644 index 00000000..7b31c23e --- /dev/null +++ b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/immutable.cue @@ -0,0 +1,49 @@ +// Copyright 2024 Stefan Prodan +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "encoding/json" + "strings" + "uuid" +) + +#ConfigMapKind: "ConfigMap" +#SecretKind: "Secret" + +// ImmutableConfig is a generator for immutable Kubernetes ConfigMaps and Secrets. +// The metadata.name of the generated object is suffixed with the hash of the input data. +#ImmutableConfig: { + // Kind of the generated object. + #Kind: *#ConfigMapKind | #SecretKind + + // Metadata of the generated object. + #Meta: #Metadata + + // Optional suffix appended to the generate name. + #Suffix: *"" | string + + // Data of the generated object. + #Data: {[string]: string} + + let hash = strings.Split(uuid.SHA1(uuid.ns.DNS, json.Marshal(#Data)), "-")[0] + + apiVersion: "v1" + kind: #Kind + metadata: { + name: #Meta.name + #Suffix + "-" + hash + namespace: #Meta.namespace + labels: #Meta.labels + if #Meta.annotations != _|_ { + annotations: #Meta.annotations + } + } + immutable: true + if kind == #ConfigMapKind { + data: #Data + } + if kind == #SecretKind { + stringData: #Data + } +} diff --git a/examples/minimal/templates/config.cue b/examples/minimal/templates/config.cue index 35073334..a171cd0a 100644 --- a/examples/minimal/templates/config.cue +++ b/examples/minimal/templates/config.cue @@ -103,7 +103,7 @@ import ( deploy: #Deployment & { #config: config - _cmName: objects.cm.metadata.name + #cmName: objects.cm.metadata.name } } diff --git a/examples/minimal/templates/configmap.cue b/examples/minimal/templates/configmap.cue index 3c5d63bb..7f591f15 100644 --- a/examples/minimal/templates/configmap.cue +++ b/examples/minimal/templates/configmap.cue @@ -1,28 +1,14 @@ package templates import ( - "encoding/yaml" - "strings" - "uuid" - - corev1 "k8s.io/api/core/v1" + timoniv1 "timoni.sh/core/v1alpha1" ) -#ConfigMap: corev1.#ConfigMap & { - #config: #Config - apiVersion: "v1" - kind: "ConfigMap" - metadata: { - name: "\(#config.metadata.name)-\(_checksum)" - namespace: #config.metadata.namespace - labels: #config.metadata.labels - if #config.metadata.annotations != _|_ { - annotations: #config.metadata.annotations - } - } - immutable: true - let _checksum = strings.Split(uuid.SHA1(uuid.ns.DNS, yaml.Marshal(data)), "-")[0] - data: { +#ConfigMap: timoniv1.#ImmutableConfig & { + #config: #Config + #Kind: timoniv1.#ConfigMapKind + #Meta: #config.metadata + #Data: { "nginx.default.conf": """ server { listen 8080; diff --git a/examples/minimal/templates/deployment.cue b/examples/minimal/templates/deployment.cue index 164ea03f..e73b35eb 100644 --- a/examples/minimal/templates/deployment.cue +++ b/examples/minimal/templates/deployment.cue @@ -7,7 +7,7 @@ import ( #Deployment: appsv1.#Deployment & { #config: #Config - _cmName: string + #cmName: string apiVersion: "apps/v1" kind: "Deployment" metadata: #config.metadata @@ -65,7 +65,7 @@ import ( { name: "config" configMap: { - name: _cmName + name: #cmName items: [{ key: "nginx.default.conf" path: key @@ -75,7 +75,7 @@ import ( { name: "html" configMap: { - name: _cmName + name: #cmName items: [{ key: "index.html" path: key diff --git a/mkdocs.yml b/mkdocs.yml index 7ea0ab9d..839cd2b6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -107,6 +107,7 @@ nav: - CUE Walkthrough: cue/walkthrough.md - Module Development: - Get started with modules: cue/module/initialization.md + - Immutable ConfigMaps and Secrets: cue/module/immutable-config.md - Add custom resources: cue/module/custom-resources.md - Cluster version constraints: cue/module/semver-constraints.md - Control the apply behavior: cue/module/apply-behavior.md diff --git a/schemas/README.md b/schemas/README.md index c3fd3102..c2239e16 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -24,6 +24,11 @@ The Timoni's CUE schemas are included in the modules generated with `timoni mod - `#ObjectReference` - Schema for generating Kubernetes object references based on `apiVersion`, `kind`, `name` and `namespace`. +### Immutable ConfigMaps and Secrets + +- `#ImmutableConfig` - Schema for generating immutable Kubernetes ConfigMaps and Secrets. + The `metadata.name` of the generated object is suffixed with the hash of the input data. + ### Container Image - `#Image` - Schema for generating a container image and pull policy diff --git a/schemas/timoni.sh/core/v1alpha1/immutable.cue b/schemas/timoni.sh/core/v1alpha1/immutable.cue new file mode 100644 index 00000000..7b31c23e --- /dev/null +++ b/schemas/timoni.sh/core/v1alpha1/immutable.cue @@ -0,0 +1,49 @@ +// Copyright 2024 Stefan Prodan +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "encoding/json" + "strings" + "uuid" +) + +#ConfigMapKind: "ConfigMap" +#SecretKind: "Secret" + +// ImmutableConfig is a generator for immutable Kubernetes ConfigMaps and Secrets. +// The metadata.name of the generated object is suffixed with the hash of the input data. +#ImmutableConfig: { + // Kind of the generated object. + #Kind: *#ConfigMapKind | #SecretKind + + // Metadata of the generated object. + #Meta: #Metadata + + // Optional suffix appended to the generate name. + #Suffix: *"" | string + + // Data of the generated object. + #Data: {[string]: string} + + let hash = strings.Split(uuid.SHA1(uuid.ns.DNS, json.Marshal(#Data)), "-")[0] + + apiVersion: "v1" + kind: #Kind + metadata: { + name: #Meta.name + #Suffix + "-" + hash + namespace: #Meta.namespace + labels: #Meta.labels + if #Meta.annotations != _|_ { + annotations: #Meta.annotations + } + } + immutable: true + if kind == #ConfigMapKind { + data: #Data + } + if kind == #SecretKind { + stringData: #Data + } +}