Skip to content

Commit

Permalink
Merge pull request #321 from stefanprodan/immutable-cfg-gen
Browse files Browse the repository at this point in the history
Add `#ImmutableConfig` generator to Timoni's CUE schemas
  • Loading branch information
stefanprodan authored Jan 21, 2024
2 parents cc8e65b + 106f6a3 commit 3844e03
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
}
109 changes: 109 additions & 0 deletions docs/cue/module/immutable-config.md
Original file line number Diff line number Diff line change
@@ -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`<instance-name>-<data-hash>`, where `<data-hash>` 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
}
}
}
```
146 changes: 111 additions & 35 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
49 changes: 49 additions & 0 deletions examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/immutable.cue
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion examples/minimal/templates/config.cue
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ import (

deploy: #Deployment & {
#config: config
_cmName: objects.cm.metadata.name
#cmName: objects.cm.metadata.name
}
}

Expand Down
Loading

0 comments on commit 3844e03

Please sign in to comment.