Skip to content

Bundle

Timoni bundles offer a declarative way of managing the lifecycle of applications and their infra dependencies.

A Timoni bundle is a CUE file for defining a group of instances together with their values and module references.

Example

The following is an example of a Bundle that defines a Redis master-replica cluster and a podinfo instance connected to the Redis instance.

bundle: {
    apiVersion: "v1alpha1"
    name:       "podinfo"
    instances: {
        redis: {
            module: {
                url:     "oci://ghcr.io/stefanprodan/modules/redis"
                version: "7.2.4"
            }
            namespace: "podinfo"
            values: maxmemory: 256
        }
        podinfo: {
            module: url:     "oci://ghcr.io/stefanprodan/modules/podinfo"
            module: version: "6.5.4"
            namespace: "podinfo"
            values: caching: {
                enabled:  true
                redisURL: "tcp://redis:6379"
            }
        }
    }
}

For the above example, Timoni performs the following actions at apply time:

  • Validates that the Bundle definition is in conformance with the API version specified by apiVersion.
  • For each instance, it fetches the module version from the registry using the module.url as the artifact repository address and the module.version as the artifact tag.
  • Creates the Kubernetes namespaces if they don't exist.
  • For each instance, it builds, validates and creates the Kubernetes resources using the specified values.
  • The list of managed resources along with the module reference and values are stored in the cluster in a Kubernetes Secret, in the same namespace with the instance.
  • If an instance already exists, Timoni performs a server-side apply dry-run to detect changes and applies only the resources with divergent state.
  • If previously applied resources are missing from the current revision, these resources are deleted from the cluster.
  • Waits for each instance's resources to become ready.

You can run this example by saving the Bundle into podinfo.bundle.cue.

Apply the Bundle on the cluster:

timoni bundle apply -f podinfo.bundle.cue
applying instance redis
pulling oci://ghcr.io/stefanprodan/modules/redis:7.2.4
using module timoni.sh/redis version 7.2.4
installing redis in namespace podinfo
Namespace/podinfo created
applying master
ServiceAccount/podinfo/redis created
ConfigMap/podinfo/redis created
Service/podinfo/redis created
Deployment/podinfo/redis-master created
PersistentVolumeClaim/podinfo/redis-master created
waiting for 5 resource(s) to become ready...
resources are ready
applying replica
Service/podinfo/redis-readonly created
Deployment/podinfo/redis-replica created
waiting for 2 resource(s) to become ready...
resources are ready
applying instance podinfo
pulling oci://ghcr.io/stefanprodan/modules/podinfo:6.5.4
using module timoni.sh/podinfo version 6.5.4
installing podinfo in namespace podinfo
ServiceAccount/podinfo/podinfo created
Service/podinfo/podinfo created
Deployment/podinfo/podinfo created
waiting for 3 resource(s) to become ready...
resources are ready

Build the Bundle and print the resulting Kubernetes resources for all the Bundle's instances:

timoni bundle build -f podinfo.bundle.cue
---
# Instance: redis
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
  app.kubernetes.io/part-of: redis
  app.kubernetes.io/version: 7.2.4
name: redis
namespace: podinfo
---
# Redis deployments omitted for brevity
---
# Instance: podinfo
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
  app.kubernetes.io/name: podinfo
  app.kubernetes.io/version: 6.5.4
name: podinfo
namespace: podinfo
---
# Podinfo deployment omitted for brevity

List the managed resources from a bundle and their rollout status:

timoni bundle status -f podinfo.bundle.cue
last applied 2024-03-03T20:21:19Z
module oci://ghcr.io/stefanprodan/modules/redis:7.2.4
digest: sha256:8cf531365742c7cab9628909dfe16958550853f7c994284eacad64f169f4c74a
ServiceAccount/podinfo/redis Current Resource is current
ConfigMap/podinfo/redis Current Resource is always ready
Service/podinfo/redis Current Service is ready
Service/podinfo/redis-readonly Current Service is ready
Deployment/podinfo/redis-master Current Deployment is available. Replicas: 1
Deployment/podinfo/redis-replica Current Deployment is available. Replicas: 1
PersistentVolumeClaim/podinfo/redis-master Current PVC is Bound

last applied 2024-03-03T20:21:19Z
module oci://ghcr.io/stefanprodan/modules/podinfo:6.5.4
digest: sha256:1dba385f9d56f9a79e5b87344bbec1502bd11f056df51834e18d3e054de39365
ServiceAccount/podinfo/podinfo Current Resource is always ready
Service/podinfo/podinfo Current Service is ready
Deployment/podinfo/podinfo Current Deployment is available. Replicas: 1

List the instances in Bundle podinfo across all namespaces:

timoni list --bundle podinfo -A
NAME    NAMESPACE         MODULE                                          VERSION LAST APPLIED          BUNDLE
podinfo podinfo           oci://ghcr.io/stefanprodan/modules/podinfo      6.5.4   2024-03-03T16:20:07Z  podinfo
redis   podinfo           oci://ghcr.io/stefanprodan/modules/redis        7.2.4   2024-03-03T16:20:00Z  podinfo

Writing a Bundle spec

A Bundle file must contain a definition that matches the following schema:

#Bundle: {
    apiVersion: string
    name:       string
    instances: [string]: {
        module: {
            url:     string
            digest?: string
            version: *"latest" | string
        }
        namespace: string
        values: {...}
    }
}

Bundle files can contain arithmetic operations, string interpolation and everything else that CUE std lib supports.

API version

The apiVersion is a required field that specifies the version of the Bundle schema.

Currently, the only supported value is v1alpha1.

Name

The name is a required field used to track the ownership of instances deployed to a Kubernetes cluster.

Note that Bundles should have unique names per cluster, using the same name for different bundles will result in ownership conflict.

Instances

The instances array is a required field that specifies the list of Instances part of this Bundle.

A Bundle must contain at least one instance with the following required fields:

bundle: {
    apiVersion: "v1alpha1"
    name:       "podinfo"
    instances: {
        podinfo: {
            module: url: "oci://ghcr.io/stefanprodan/modules/podinfo"
            namespace: "podinfo"
        }
    }
}

Instance Module

The instance.module is a required field that specifies the OCI URL, version and/or digest of the instance's module.

URL

The instance.module.url is a required field that specifies the source of the module. It can be either an OCI repository address (preferred) or a local path to a module (useful during development).

When using an OCI repository, the url field must be in the format oci://<registry-host>/<repo-name>.

When using a Local path, the url field must be in the format file://path/to/module.

Tip

Relative paths are always computed relatively to the path of the bundle file containing the value.

Version

The instance.module.version is an optional field that specifies the version number of the module. The version number must follow Timoni's semantic versioning. When not specified, the version defaults to latest, which pulls the module OCI artifact tagged as latest.

module: {
    url:     "oci://ghcr.io/stefanprodan/modules/podinfo"
    version: "6.5.4"
}

Default version

When not specified, the version defaults to latest, which pulls the module OCI artifact tagged as latest. Note that using version: "latest" is not recommended for production system, unless you also specify a digest.

Digest

The instance.module.digest is an optional field that specifies the OCI digest of the module.

module: {
    url:    "oci://ghcr.io/stefanprodan/modules/podinfo"
    digest: "sha256:1dba385f9d56f9a79e5b87344bbec1502bd11f056df51834e18d3e054de39365"
}

When both the version number and the digest are specified, Timoni will verify that the upstream digest of the version matches the specified instance.module.digest.

module: {
    url:     "oci://ghcr.io/stefanprodan/modules/podinfo"
    version: "6.5.4"
    digest:  "sha256:1dba385f9d56f9a79e5b87344bbec1502bd11f056df51834e18d3e054de39365"
}

If the version is set to latest and a digest is specified, Timoni will ignore the version and will pull the module by its OCI digest.

Instance Namespace

The instance.namespace is a required field that specifies the Kubernetes namespace where the instance is created.

If the specified namespace does not exist, Timoni will first create the namespace, then it will apply the instance's resources in that namespace.

Instance Values

The instance.values is an optional field that specifies custom values used to configure the instance.

At apply time, Timoni merges the custom values with the defaults, validates the final values against the config schema and creates the instance.

Values from runtime

The @timoni(runtime:[string|number|bool]:[VAR_NAME]) CUE attribute can be placed next to a field to set its value from the Runtime.

values: {
    host:    "example.com" @timoni(runtime:string:MY_HOST)
    enabled: true          @timoni(runtime:bool:MY_ENABLED)
    score:   1             @timoni(runtime:number:MY_SCORE)
}

To make a Runtime attribute required, the field value can be set to its type:

values: {
    host:    string @timoni(runtime:string:MY_HOST)
    enabled: bool   @timoni(runtime:bool:MY_ENABLED)
    score:   int    @timoni(runtime:number:MY_SCORE)
}

The Runtime values can come from Kubernetes API and/or from the environment variables, for more details please see the Bundle Runtime documentation.

Working with Bundles

Install and Upgrade

To install or upgrade the instances defined in a Bundle file, you can use the timoni bundle apply command.

Example:

timoni bundle apply -f bundle.cue

The apply command performs the following actions for each instance:

  • Pulls the module version from the specified container registry.
  • If the registry is private, uses the credentials found in ~/.docker/config.json.
  • If the registry credentials are specified with --creds, these take priority over the docker ones.
  • Merges the custom values supplied in the Bundle with the default values found in the module.
  • Builds the module by passing the instance name, namespace and values.
  • Labels the resulting Kubernetes resources with the instance name and namespace.
  • Creates the instance namespace if it doesn't exist.
  • Applies the Kubernetes resources on the cluster.
  • Creates or updates the instance inventory with the last applied resources IDs.

Diff Upgrade

After editing a bundle file, you can review the changes that will be made on the cluster with timoni bundle apply --diff.

Example:

timoni bundle apply --dry-run --diff -f bundle.cue

Force Upgrade

If an upgrade contains changes to immutable fields, such as changing the image tag of a Kubernetes Job, you need to set the --force flag.

Example:

timoni bundle apply --force -f bundle.cue

With --force, Timoni will recreate only the resources that contain changes to immutable fields.

Transfer ownership

If an install or upgrade involves Instances already created, either separately or as a part of another Bundle, the operation will fail. To transfer ownership to the current Bundle, you need to set the --overwrite-ownership flag.

Example:

timoni bundle apply --overwrite-ownership -f bundle.cue

Status

To list the current status of the managed resources for each instance including the last applied date, the module url and digest, you can use the timoni bundle status.

Example using the bundle name:

timoni bundle status my-bundle

Example using a bundle CUE file:

timoni bundle status -f bundle.cue

Build

To build the instances defined in a Bundle file and print the resulting Kubernetes resources, you can use the timoni bundle build command.

Example:

timoni bundle build -f bundle.cue

Use values from JSON and YAML files

A bundle can be defined in multiple files of different formats:

timoni bundle build -f bundle.cue -f extras1.json -f extras2.yaml

Timoni extracts the CUE values from the JSON and YAML files, and unifies them with the bundle value. Note that setting the same field in multiple files is not supported.

Timoni supports the following extensions: .cue, .json, .yml, .yaml.

Uninstall

To uninstall all the instances belonging to a Bundle, you can use the timoni bundle delete command.

Example using the bundle name:

timoni bundle delete my-bundle

Example using a bundle CUE file:

timoni bundle delete -f bundle.cue

Timoni will search the cluster and delete all the instances having the bundle.timoni.sh/name: <name> label matching the given bundle name. The instances are uninstalled in reverse order, first created instance is last to be deleted.

Garbage collection

Timoni's garbage collector keeps track of the applied resources and prunes the Kubernetes objects that were previously applied but are missing from the current revision.

Example:

timoni bundle apply --prune -f bundle.cue

To prevent the garbage collector from deleting certain resources such as Kubernetes Persistent Volumes, these resources can be annotated with action.timoni.sh/prune: "disabled".

The garbage collection is enabled by default, to opt-out set --prune=false.

Readiness checks

By default, Timoni applies the instances in order, and will wait for each instance's resources to become ready, before moving to the next instance.

The readiness check is performed for the Kubernetes resources with the following types:

  • Kubernetes built-in kinds: Deployment, DaemonSet, StatefulSet, PersistentVolumeClaim, Pod, PodDisruptionBudget, Job, CronJob, Service, Secret, ConfigMap, CustomResourceDefinition
  • Custom resources that are compatible with kstatus

Example:

timoni bundle apply --wait --timeout=5m -f bundle.cue

With --timeout, Timoni will retry the readiness checks until the specified timeout period expires. If an instance's resource fails to become ready, the apply command will exit with an error.

The readiness check is enabled by default, to opt-out set --wait=false.

Vetting

To verify that one or more CUE files contain a valid Bundle definition, you can use the timoni bundle vet command.

Example:

timoni bundle vet -f bundle.cue -f extras.cue

If the validation passes, Timoni will list all the instances found in the computed bundle.

When --print-value is specified, Timoni will write the Bundle computed value to stdout.

Example:

timoni bundle vet -f bundle.cue --runtime-from-env --print-value

Printing the computed value is particular useful when debugging runtime attributes.

Format

To format Bundle files, you can use the cue fmt command.

Example:

cue fmt bundle.cue

Referencing local modules

When developing and testing Timoni Modules, you can reference them from a Bundle file using relative local paths.

Example repo structure:

├── modules
│   ├── app1
│   └── app2
└── bundles
    └── apps-test.cue

Example Bundle file:

bundle: {
    apiVersion: "v1alpha1"
    name:       "apps-test"
    instances: {
        "app1": {
            module: url: "file://../modules/app1"
            namespace: "app1"
            values: {...}
        }
        "app2": {
            module: url: "file://../modules/app2"
            namespace: "app2"
            values: {...}
        }
    }
}

When using local paths, the url field must be in the format file://path/to/module and the module path is computed relatively to the path of the bundle file location.

Note that when using local modules, the module's version and digest are ignored, as these are only relevant when pulling modules from a container registry. All instances created from modules referenced with local paths have the module version set to 0.0.0-devel.