Skip to content
Products and Orders

Products and Orders

A Product defines an application completely: its source, how it renders, its base values and patches, what happens to its images, and what its Orders may change. An Order is one variant of a Product, such as one environment, and declares only what differs. A Product on its own is never rendered or deployed; its Orders are.

A common split: a platform team owns the Product, application teams own its Orders, and the Product’s overrides decide what the teams may change.

The Product

apiVersion: delivery.kokumi.dev/v1alpha1
kind: Product
metadata:
  name: podinfo
spec:
  # A Helm chart pushed to an OCI registry.
  source:
    url: oci://ghcr.io/stefanprodan/charts/podinfo
    version: "6.14.1"

  # Render the chart with these base values. The release name and namespace
  # default to each Order's name and namespace.
  render:
    helm:
      values:
        ui:
          color: "#EF6461"

  # Orders of this Product may set these values and nothing else. Without
  # overrides, Orders may not change anything.
  overrides:
    values:
      policy: Listed
      paths:
      - ui.message
      - replicaCount

The Product resolves its source tag to a digest, verifies its signature when required (see Source Verification), and publishes the result in status.source: URL, version, digest and verified signer. Orders render that digest, and only once the Product is Ready for its current generation, so every Order renders the same artifact even if the tag moves upstream. The Product resolves the tag again only when its source or vendor location changes.

kubectl get products -n shop
# NAME      READY   REASON     SOURCE                                      VERSION   VERIFIED   AGE
# podinfo   True    Resolved   oci://ghcr.io/stefanprodan/charts/podinfo   6.14.1               1m

A vendored Product reports Vendored instead of Resolved.

The Product’s registryRefs name the Registries that authenticate its source, its images and its vendored copy. See Registries and Repositories.

Sources

Kokumi renders four kinds of OCI artifacts:

SourceThe Product setsA Preparation holdsExample
A Helm chartspec.render.helmmanifest.yaml, the rendered chartgetting-started
YAML files at the artifact’s rootnothingone manifest.yaml, every top-level *.yaml and *.yml file mergedmanifest-bundle
YAML files at the artifact’s rootspec.render.manifest.layout: Multithe files, as they aremulti-file-manifests
A kustomization.yaml at the artifact’s rootnothingthe files, as they are; Argo CD builds the kustomization when it deployskustomize

Patches apply to every kind; Helm values only to charts.

apiVersion: delivery.kokumi.dev/v1alpha1
kind: Product
metadata:
  name: external-secrets
spec:
  # An OCI artifact with a single manifest.yaml at its root. Several files are
  # merged into one manifest.yaml unless render.manifest.layout is Multi.
  source:
    url: oci://ghcr.io/kokumi-dev/testdata/external-secrets
    version: "0.1.0"

  # Orders of this Product may only scale the webhook Deployment.
  overrides:
    patches:
      policy: Listed
      targets:
      - target:
          kind: Deployment
          name: external-secrets-webhook
        paths:
        - .spec.replicas
apiVersion: delivery.kokumi.dev/v1alpha1
kind: Order
metadata:
  name: external-secrets
spec:
  productRef:
    name: external-secrets
  environmentRef:
    name: local

  # Patches set fields of the rendered manifests, by path. Values are strings.
  patches:
  - target:
      kind: Deployment
      name: external-secrets-webhook
    set:
      .spec.replicas: "2"

A Helm chart renders with these options of spec.render.helm:

FieldDescriptionDefault
releaseNameThe Helm release nameThe Order’s name
includeCRDsInclude the chart’s CRDs in the outputfalse
valuesBase values; each Order’s spec.values are deep-merged over themnone

Helm renders into the Order’s target namespace.

A kustomization is built by Argo CD, not by Kokumi, so Kokumi never sees the resources it produces. Patches still apply to the files, but the Pin and Vendor image policies need rendered manifests and fail; see Container Images.

To publish a source of your own, with the annotations that link each Preparation back to its commit, see Producing Base Artifacts.

The Order

apiVersion: delivery.kokumi.dev/v1alpha1
kind: Order
metadata:
  name: podinfo
spec:
  productRef:
    name: podinfo

  # The Environment it deploys to, in the Order's namespace. Kokumi keeps the
  # Environment local in its own namespace, kokumi: the cluster it runs in.
  # The Environment cannot be changed later; another Environment is another
  # Order.
  environmentRef:
    name: local

  # Deep-merged over the Product's values; the Order's win.
  values:
    ui:
      message: "Hello from Kokumi"
    replicaCount: 2

  # Every change renders a new Preparation, an immutable OCI artifact in the
  # bundled registry. Nothing deploys until you promote a Preparation.
  promotion:
    mode: Manual
FieldWhat it does
productRefThe Product, in the Order’s namespace. It cannot be changed after creation.
environmentRefThe Environment the Order deploys to, in its namespace. Required, and it cannot be changed: another Environment is another Order.
valuesHelm values deep-merged over the Product’s render.helm.values; the Order wins. Only valid for a Helm source.
patchesPatches merged over the Product’s, per target; on the same path the Order wins.
editsPatches the UI writes when you edit a rendered file, applied after patches. They change rendered resources; the editor refuses added or renamed ones.
targetNamespaceWhere the workload lands on the clusters. See The target namespace.
destinationWhere Preparations are pushed. Defaults to <bundled registry>/<namespace>/<order>.
registryRefsThe Registries that authenticate the destination, and only the destination.
promotionHow Preparations are promoted: the mode, and the gates. See Promotion and Rollback.

An Order’s name is at most 63 characters. Each Order has its own Preparation history, its own Serving, and its own gates, so a new Product version can deploy to podinfo-dev automatically while podinfo-prod waits for approvals.

kubectl get orders -n shop
# NAME      READY   REASON   PRODUCT   ENVIRONMENT   LATEST PREPARATION     AGE
# podinfo   True    Ready    podinfo   local         podinfo-d7ce0c46a686   1m

While its Product is missing, not Ready, or being deleted, an Order reports Ready=False with reason ProductNotFound, ProductNotReady or ProductDeleting and waits; the UI shows it as Waiting. An Order that sets what its Product does not allow, or whose render fails, reports ProcessingFailed with the reason in the message. A failed render is retried at growing intervals, up to about 17 minutes apart, and keeps reporting its failure meanwhile; a change of the Order, its Product or its Registries renders at once. An Order whose patches or edits do not apply reports PatchFailed instead, and is not retried: it renders again once the Order or its Product changes.

Describing a change

The annotation delivery.kokumi.dev/commit-message on the Order becomes the description of the next Preparation, like a commit message. Kokumi removes it after the render. Without it, the first Preparation is described as Initial commit and later ones as Automatically generated.

kubectl annotate order podinfo -n shop delivery.kokumi.dev/commit-message="Scale to 3 replicas"
kubectl patch order podinfo -n shop --type=merge -p '{"spec":{"values":{"replicaCount":3}}}'

Patches

A patch sets fields of the resources of one target, by path:

spec:
  patches:
  - target:
      kind: Deployment
      name: podinfo
      # namespace: shop      # optional; without it, any namespace matches
    set:
      .spec.replicas: "3"
      .spec.template.spec.containers[0].imagePullPolicy: Always
  • A path is dot-separated, with [n] for a list index. Missing fields are created; a missing list, an index out of bounds, or a path through a scalar or a list without an index fails.
  • A patch that does not apply fails the render: a path that cannot be set, or a target that matches no resource in any file of the source. The Order reports PatchFailed, naming the target and the path, and no Preparation is made; its earlier Preparations and its Serving stay. The same holds for edits, so an edit whose resource a new Product version renamed shows up, and you drop it from the Order. A Product patch on a resource an Order turns off through its values fails that Order too.
  • Values are strings in the Order, and are written as booleans or numbers when they read as one: "3" becomes 3, "true" becomes true.
  • Every document is written back with 2-space indentation, whether a patch matches it or not, so the same input always renders the same bytes. A document that is not valid YAML fails the render with its file and document number.

Restrict what Orders may change

spec.overrides on the Product decides what its Orders may change, separately for Helm values and for patches. The patch policy covers edits made in the UI too.

policyOrders may set
None (default)nothing
Listedonly the listed Helm value paths (leaf paths, dot-separated), or the listed patch targets and paths
Allanything
spec:
  overrides:
    values:
      policy: Listed
      paths:
      - replicaCount
      - ui.message
    patches:
      policy: Listed
      targets:
      - target:
          kind: Deployment
          name: podinfo
        paths:
        - .spec.replicas

paths and targets are required with Listed and rejected otherwise. A target without a namespace matches the resource in any namespace; when several targets match, the paths of all of them are allowed. An Order that sets anything else fails with ProcessingFailed, naming the offending value or patch.

The target namespace

The target namespace is where the workload lands on every cluster of the Order’s Environment: the Order’s targetNamespace, else the Product’s, else the Order’s own namespace. Helm renders into it, and Argo CD deploys resources without a namespace there, creating the namespace when it is missing.

The target namespace is part of the intent, so a Preparation deploys to the same namespace wherever it is promoted. Changing it renders a new Preparation, and deploying that one moves the workload: Argo CD deletes it in the old namespace.

In the example target-namespace, the Product deploys into storefront, one Order keeps that namespace, and the other picks its own:

apiVersion: delivery.kokumi.dev/v1alpha1
kind: Product
metadata:
  name: storefront
spec:
  source:
    url: oci://ghcr.io/stefanprodan/charts/podinfo
    version: "6.14.1"

  # Every Order of the Product deploys into the namespace storefront, not
  # into its own namespace, unless the Order names another one. Helm renders
  # the release into it, and Argo CD creates it when it is missing.
  targetNamespace: storefront

  render:
    helm:
      values:
        ui:
          message: "Hello from the storefront"
apiVersion: delivery.kokumi.dev/v1alpha1
kind: Order
metadata:
  name: storefront
spec:
  productRef:
    name: storefront
  environmentRef:
    name: local

  # No targetNamespace: the Order deploys into the Product's, storefront.

  # Every Ready Preparation deploys at once.
  promotion:
    mode: Automatic
apiVersion: delivery.kokumi.dev/v1alpha1
kind: Order
metadata:
  name: storefront-review
spec:
  productRef:
    name: storefront
  environmentRef:
    name: local

  # An Order may always pick its own target namespace, here one next to the
  # Product's. The target namespace is part of the intent: changing it renders
  # a new Preparation, and deploying that one moves the workload.
  targetNamespace: storefront-review

  promotion:
    mode: Automatic

kubectl apply -n shop -k "https://github.com/kokumi-dev/kokumi//examples/target-namespace?ref=0.17.0"

What renders a new Preparation

Kokumi merges an Order over its Product into the intent, the effective configuration that determines the artifact. Whenever the intent changes, the Order renders a new Preparation; while it stays the same, nothing is rendered.

Part of the intentNot part of the intent
The source: URL, version and resolved digestThe promotion mode
The merged render config and Helm valuesThe overrides
The merged patches, and the editsThe Environment
The target namespace, alwaysThe commit message
The image policy and field specs, unless they are the defaults, and the vendor location under VendorThe digests images resolve to: a tag that moves upstream renders nothing
The gates: approvals, upstream stages and checksThe Registries, their Secrets, and the Kitchen’s image mirrors
The destination
The Product revision

Changing the gates renders a new Preparation that needs fresh approvals. Because the intent holds the Product revision, a change to the Product’s source, render config, patches or image handling renders every Order again, also an Order whose own values or patches hide the change.

The intent is stored inside the artifact, so anyone who can pull the artifact can read it: Argo CD, cluster nodes, and anyone the artifact is copied to. Do not put secrets into Helm values or patches.

Artifacts in the Registry explains how the intent is stored and hashed.

Vendoring the source

A Product can copy its source, unchanged, to another OCI registry, for example to prepare an air-gapped installation. Kokumi copies the source by the digest that verified, together with its referrers, so the copy keeps the publisher’s signatures and attestations and still verifies against the publisher’s key. Orders then render from the copy:

spec:
  source:
    url: oci://ghcr.io/kokumi-dev/testdata/external-secrets
    version: "0.1.0"
  vendor:
    url: oci://harbor.corp/mirror/external-secrets   # vendor: {} copies to <namespace>/vendor/<name> in the bundled registry
  registryRefs:
  - name: harbor-mirror        # its url covers the vendor location

The Product’s Registries authenticate both the upstream source and the copy, and status.source names the copy, so Orders need no knowledge of the vendoring. Signatures that cosign stored under legacy sha256-<digest>.sig tags are not referrers and are not copied. The Product’s Artifact tab draws the source and the copy side by side.

The images of the rendered manifests can be vendored below the copy too; see Container Images.

Deleting

  • A Product cannot be deleted while Orders reference it. The finalizer delivery.kokumi.dev/product-protection keeps it, and its Ready condition reports InUse with the remaining Orders. The UI shows it as Deleting and offers no new Orders for it.
  • An Order takes its Preparations and its Serving with it, and the Serving deletes its Argo CD Applications. The workloads keep running: the Applications carry no resources-finalizer.argocd.argoproj.io, so Argo CD leaves their resources in place. The artifacts stay in the registry. kubectl delete order <name> --cascade=orphan keeps the Serving, and a later Order of the same name adopts it.
  • Preparations are never pruned while their Order exists, so every version can be promoted again.
Last updated on