An action names its targets and defines onStart, onEnd, or both. Each step applies a partial YAML or JSON object using server-side apply (SSA). The controller supplies identity and concurrency preconditions; templates cannot redirect a patch to another object.

Downscale and restore

The complete example captures each Deployment’s replica count from /scale, scales it to zero overnight, and restores the captured value at the end. It also blocks user writes and provides advance warnings.

apiVersion: addons.projectcapsule.dev/v1alpha1
kind: ChangeWindow
metadata:
  name: development-downtime
spec:
  namespaceSelectors:
    - matchLabels:
        environment: development
    - matchLabels:
        environment: staging
  schedules:
    - name: weekday-nights
      cron: "0 19 * * 1-5"
      timeZone: Europe/Zurich
      duration: 12h
    - name: weekend
      cron: "0 7 * * 6"
      timeZone: Europe/Zurich
      duration: 48h
  serviceAccount:
    namespace: schedule-action-controller-system
    name: change-window-executor
  targets:
    - name: deployments
      apiVersion: apps/v1
      kind: Deployment
      selector:
        matchLabels:
          downscaling: enabled
  actions:
    - name: downscale
      targets: [deployments]
      capture:
        replicas:
          subresource: scale
          path: /spec/replicas
      onStart:
        apply:
          subresource: scale
          force: true
          template: |
            spec:
              replicas: 0
      onEnd:
        apply:
          subresource: scale
          template: |
            spec:
              replicas: {{ .captured.replicas }}
  admission:
    - name: freeze-deployments
      targets: [deployments]
      operations: [CREATE, UPDATE, DELETE]
      subresources: ["", scale]
      when: Active
      action: Deny
      message: >-
        Deployment changes are blocked during maintenance. Retry after {{ .window.end }}.
  config:
    admission:
      warnings:
        - name: upcoming-maintenance
          targets: [deployments]
          operations: [CREATE, UPDATE, DELETE]
          subresources: ["", scale]
          when: BeforeStart
          within: 1h
          message: >-
            Maintenance starts at {{ .window.start }} (in {{ .window.startsIn }}).
        - name: active-maintenance
          targets: [deployments]
          operations: [CREATE, UPDATE, DELETE]
          subresources: ["", scale]
          when: Active
          message: Maintenance is active until {{ .window.end }}.

Download addons_v1alpha1_changewindow.yaml

Set up executor permissions before applying this example. The included RBAC grants access only to an existing namespace named development; additional selected namespaces need their own grants.

The start patch explicitly uses force: true. This transfers conflicting SSA field ownership. It is useful when taking control of replicas managed by another actor, but requires deliberate coordination with HPAs and GitOps controllers. The default is false, so ownership conflicts normally fail. Restoring replicas restores their value, not the previous field manager’s ownership.

Capture and retry

Capture paths are RFC 6901 JSON pointers, for example /spec/replicas. Escape / inside a key as ~1, and ~ as ~0. Capture only values needed by the action; they may contain sensitive configuration.

Before any workload mutation, captures and rendered patches are persisted in a private Secret in the controller namespace. A window is limited to 256 action/resource participants and a 512 KiB journal. Exceeding those limits fails before workload mutation.

Failed start and end steps retry; successful steps stop. A crash after the API server accepts a patch can cause that same patch to be repeated. Use idempotent templates. Different ChangeWindows execute independently, so avoid competing forced writes to the same fields.

Templates and subresources

ValueMeaning
.targetThe selected parent Kubernetes object.
.captured.NAMEA named captured value for that resource.
.window.startWindow start timestamp.
.window.endKnown end timestamp, or an empty string beyond the scheduling horizon.
.window.startsInTime until the upcoming window starts.
.window.phaseCurrent template window phase.

Templates use Go text/template, standard template functions, toJson, and quote. Missing keys cause errors. Action templates render once when the journal is prepared, keeping retry content stable. No shell, network, or Capsule resource-loading functions are available.

Capture and apply accept a real Kubernetes subresource, such as scale. Discovery determines the payload kind: Deployment /scale accepts autoscaling/v1 Scale. Unsupported endpoints or endpoints that reject SSA return an error; the controller does not silently patch the parent instead.

Rollout restart

actions:
  - name: restart
    targets: [deployments]
    onStart:
      apply:
        template: |
          spec:
            template:
              metadata:
                annotations:
                  kubectl.kubernetes.io/restartedAt: {{ .window.start | quote }}

A Deployment has no /rollout subresource. Restart it by patching its pod template on the parent resource. The stable window timestamp prevents retries from continually triggering new rollouts. The executor needs get, list, and patch on Deployments for this example.

Editing and deleting an active action

Spec edits are rejected while the restoration finalizer is present. Wait for pending actions to finish before changing the spec. Deleting a ChangeWindow triggers its required end actions through the finalizer. A failed restoration keeps the journal and blocks deletion; resolve the underlying permissions or ownership error instead of stripping the finalizer.