Skip to content

Keystone Operator Packaging

Reference documentation for the Keystone operator packaging artifacts. This covers the multi-stage Dockerfile, Helm chart configuration, FluxCD HelmRelease integration, dependency chain, and CRD installation behavior. These artifacts package the Keystone operator for deployment into Kubernetes clusters via the GitOps pipeline.

Directory Layout

text
operators/keystone/
├── Dockerfile                          Multi-stage operator image build
├── helm/
│   └── keystone-operator/
│       ├── Chart.yaml                  Helm chart metadata (v0.1.0)
│       ├── values.yaml                 Default configuration values
│       ├── values.schema.json          JSON Schema for values validation
│       ├── crds/
│       │   └── keystone.openstack.c5c3.io_keystones.yaml   CRD (auto-installed by Helm)
│       └── templates/
│           ├── _helpers.tpl            Template helper functions
│           ├── serviceaccount.yaml     ServiceAccount (conditional)
│           ├── clusterrole.yaml        ClusterRole with RBAC rules
│           ├── clusterrolebinding.yaml ClusterRoleBinding
│           ├── deployment.yaml         Operator Deployment
│           ├── service.yaml            ClusterIP Service (webhook + metrics)
│           └── webhook-configuration.yaml  Mutating + Validating webhooks (conditional)
deploy/flux-system/
├── kustomization.yaml                  Base kustomization (includes keystone-operator release)
└── releases/
    └── keystone-operator.yaml          FluxCD HelmRelease

Dockerfile

Location: operators/keystone/Dockerfile

The Dockerfile uses a multi-stage build to produce a minimal, statically-linked operator binary in a distroless runtime image. The build context must be the workspace root (/workspace) because the Go workspace (go.work) uses replace directives that reference sibling modules.

Build Stages

StageBase ImagePurpose
buildergolang:1.25Compiles the operator binary with CGO disabled
runtimegcr.io/distroless/static:nonrootMinimal runtime with no shell or package manager

Image Layers

The builder stage is structured for optimal Docker layer caching:

  1. Layer 1 — Dependency manifests: Copies go.work, go.work.sum, and all go.mod/go.sum files for workspace modules. This layer is cached as long as dependency versions do not change.

    dockerfile
    COPY go.work go.work.sum ./
    COPY internal/common/go.mod internal/common/go.sum ./internal/common/
    COPY operators/keystone/go.mod operators/keystone/go.sum ./operators/keystone/
    COPY operators/c5c3/go.mod operators/c5c3/go.sum ./operators/c5c3/
  2. Layer 2 — Module download: Runs go mod download to fetch all dependencies. Cached when dependency manifests are unchanged.

  3. Layer 3 — Source code: Copies the full source trees for internal/common/, operators/keystone/, and operators/c5c3/. Invalidated on any source change.

  4. Layer 4 — Compilation: Builds the static binary from operators/keystone/main.go.

    dockerfile
    CGO_ENABLED=0 GOOS=linux go build -o manager main.go

The runtime stage copies only the compiled /manager binary from the builder stage.

Build Context

The build context must be the workspace root, not the operator directory. The Go workspace file (go.work) contains replace directives pointing to relative paths (internal/common, operators/c5c3) that must be resolvable at build time.

bash
# Correct: build from workspace root
docker build -f operators/keystone/Dockerfile .

# Incorrect: will fail because go.work references are unresolvable
docker build operators/keystone/

Build Arguments

The Dockerfile does not declare any ARG instructions. All build configuration is determined by the Go workspace and module files.

Runtime Image Properties

PropertyValue
Base imagegcr.io/distroless/static:nonroot
Binary/manager
User65532:65532 (nonroot)
Entrypoint["/manager"]
ShellNone (distroless)
Package managerNone (distroless)

OCI Annotations

Static OCI Image Spec annotations are embedded in the runtime stage via LABEL instructions:

AnnotationValue
org.opencontainers.image.titlekeystone-operator
org.opencontainers.image.descriptionCobaltCore Keystone Operator for managing OpenStack Identity Service
org.opencontainers.image.licensesApache-2.0
org.opencontainers.image.vendorSAP SE

In CI, docker/metadata-action supplements these with dynamic labels (created, revision, source, url, version) at push time.

Local Build

bash
# From workspace root
docker build -f operators/keystone/Dockerfile -t keystone-operator:dev .

# Verify
docker run --rm keystone-operator:dev --help

Helm Chart

Location: operators/keystone/helm/keystone-operator/

Chart Metadata

File: Chart.yaml

FieldValue
apiVersionv2
namekeystone-operator
descriptionA Helm chart for deploying the Keystone OpenStack operator
typeapplication
version0.1.0
appVersion0.1.0

Configuration Reference

File: values.yaml

All configurable parameters with their types, defaults, and descriptions:

Image

ParameterTypeDefaultDescription
image.repositorystringghcr.io/c5c3/keystone-operatorContainer image registry and repository
image.tagstring"" (appVersion)Image tag. When empty, defaults to appVersion from Chart.yaml
image.pullPolicystringIfNotPresentKubernetes image pull policy (Always, IfNotPresent, Never)

Replicas

ParameterTypeDefaultDescription
replicasinteger2Number of operator pod replicas. Use 2+ for high availability with leader election

Resources

ParameterTypeDefaultDescription
resources.limits.cpustring500mCPU limit per operator pod
resources.limits.memorystring128MiMemory limit per operator pod
resources.requests.cpustring10mCPU request per operator pod
resources.requests.memorystring64MiMemory request per operator pod

Leader Election

ParameterTypeDefaultDescription
leaderElection.enabledbooleantrueEnable leader election for controller manager. Required when running multiple replicas to ensure only one active controller

When enabled, the --leader-elect flag is passed to the manager binary. When disabled, the flag is omitted (not set to false), and all replicas process reconciliation events concurrently. Disable only for single-replica development deployments.

Webhook

ParameterTypeDefaultDescription
webhook.enabledbooleantrueEnable admission webhooks (MutatingWebhookConfiguration and ValidatingWebhookConfiguration). Requires cert-manager for TLS certificate injection

When disabled, the webhook container port (9443) is omitted from the Deployment and no webhook configuration resources are created. The operator continues to function without admission validation — CRs are not validated or defaulted at admission time.

Metrics

ParameterTypeDefaultDescription
metrics.portinteger8080Port for the Prometheus metrics endpoint. Exposed via both the container port and the Service

Service Account

ParameterTypeDefaultDescription
serviceAccount.createbooleantrueCreate a ServiceAccount for the operator. Set to false to use an existing ServiceAccount
serviceAccount.namestring"" (fullname)Name of the ServiceAccount. When empty, defaults to the Helm release fullname

Rendered Resources

The chart renders the following Kubernetes resources with default values:

ResourceKindName PatternConditional
ServiceAccountv1/ServiceAccount{fullname}serviceAccount.create
ClusterRolerbac.authorization.k8s.io/v1/ClusterRole{fullname}Always
ClusterRoleBindingrbac.authorization.k8s.io/v1/ClusterRoleBinding{fullname}Always
Deploymentapps/v1/Deployment{fullname}Always
Servicev1/Service{fullname}Always
MutatingWebhookConfigurationadmissionregistration.k8s.io/v1{fullname}-mutatingwebhook.enabled
ValidatingWebhookConfigurationadmissionregistration.k8s.io/v1{fullname}-validatingwebhook.enabled

The {fullname} pattern resolves to {release-name}-keystone-operator unless fullnameOverride is set.

Standard Labels

All resources include standard Helm labels via the keystone-operator.labels helper:

LabelValue
helm.sh/chartkeystone-operator-0.1.0
app.kubernetes.io/namekeystone-operator
app.kubernetes.io/instance{release-name}
app.kubernetes.io/version0.1.0
app.kubernetes.io/managed-byHelm

Selector labels (used by Deployment and Service) are a subset: app.kubernetes.io/name and app.kubernetes.io/instance.

Deployment Configuration

The operator Deployment is configured with the following fixed settings:

Container arguments:

ArgumentValueConfigurable
--leader-electPresent when leaderElection.enabled=trueYes
--metrics-bind-address:{{ .Values.metrics.port }} (default :8080)Yes (port)
--health-probe-bind-address:8081No (hardcoded in bootstrap.Run)

Health probes:

ProbePathPortProtocol
Liveness/healthz8081HTTP
Readiness/readyz8081HTTP

The health probe port (8081) is hardcoded in the bootstrap.Run() defaults and is not configurable via Helm values.

Container ports:

NamePortConditional
metrics{{ .Values.metrics.port }} (default 8080)Always
health8081Always
webhook9443webhook.enabled

Pod security context:

FieldValue
runAsNonRoottrue
runAsUser65532
runAsGroup65532
fsGroup65532
seccompProfile.typeRuntimeDefault

Container security context:

FieldValue
allowPrivilegeEscalationfalse
capabilities.drop[ALL]
readOnlyRootFilesystemtrue
seccompProfile.typeRuntimeDefault

Service Configuration

The Service is type ClusterIP with two ports:

NamePortTarget PortPurposeConditional
webhook4439443Admission webhook callbacks from the API serverwebhook.enabled
metrics{{ .Values.metrics.port }}{{ .Values.metrics.port }}Prometheus metrics scrapingAlways

RBAC Configuration

The ClusterRole includes permissions derived from kubebuilder RBAC markers in operators/keystone/internal/controller/keystone_controller.go. These are the minimum permissions required for the operator to manage Keystone resources and their dependencies:

API GroupResourcesVerbs
keystone.openstack.c5c3.iokeystonesget, list, watch, create, update, patch, delete
keystone.openstack.c5c3.iokeystones/statusget, update, patch
keystone.openstack.c5c3.iokeystones/finalizersupdate
appsdeploymentsget, list, watch, create, update, patch, delete
"" (core)services, configmaps, secrets, serviceaccountsget, list, watch, create, update, patch, delete
"" (core)eventscreate, patch
batchjobs, cronjobsget, list, watch, create, update, patch, delete
k8s.mariadb.comdatabases, users, grantsget, list, watch, create, update, patch, delete
k8s.mariadb.commariadbsget, list, watch
external-secrets.ioexternalsecrets, pushsecretsget, list, watch, create, update, patch
rbac.authorization.k8s.ioroles, rolebindingsget, list, watch, create, update, patch, delete

Notable verb restrictions:

  • events has only create and patch — the operator emits events but never reads or deletes them.
  • external-secrets.io resources have no delete verb — the operator creates and updates ExternalSecret/PushSecret CRs but does not delete them (secret lifecycle is managed by the External Secrets Operator).

The ClusterRoleBinding binds the ClusterRole to the operator's ServiceAccount in the release namespace only.

Webhook Configuration

Two webhook configurations are rendered when webhook.enabled=true:

MutatingWebhookConfiguration ({fullname}-mutating):

FieldValue
Webhook namemkeystone.kb.io
Path/mutate-keystone-openstack-c5c3-io-v1alpha1-keystone
OperationsCREATE, UPDATE
API groupkeystone.openstack.c5c3.io
API versionv1alpha1
Resourcekeystones
Failure policyFail
Side effectsNone
Admission review versionsv1

ValidatingWebhookConfiguration ({fullname}-validating):

FieldValue
Webhook namevkeystone.kb.io
Path/validate-keystone-openstack-c5c3-io-v1alpha1-keystone
OperationsCREATE, UPDATE, DELETE
API groupkeystone.openstack.c5c3.io
API versionv1alpha1
Resourcekeystones
Failure policyFail
Side effectsNone
Admission review versionsv1

Both configurations include the annotation:

yaml
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "keystone-operator.fullname" . }}-webhook

This instructs cert-manager to inject the CA bundle from the named Certificate resource into the webhook caBundle field automatically. The Certificate resource must exist in the release namespace with the name {fullname}-webhook.

FluxCD HelmRelease

File: deploy/flux-system/releases/keystone-operator.yaml

The HelmRelease deploys the Keystone operator chart via FluxCD's helm-controller, following the established pattern used by other operators in the project (memcached-operator, mariadb-operator).

PropertyValue
API versionhelm.toolkit.fluxcd.io/v2
Namekeystone-operator
Target namespaceopenstack
Reconciliation interval30m
Chartkeystone-operator
Version constraint>=0.1.0 <1.0.0
Sourcec5c3-charts HelmRepository in flux-system namespace

Helm values applied by the HelmRelease:

KeyValuePurpose
replicas2High availability with leader election
leaderElection.enabledtrueSingle active controller with 2 replicas

All other values use chart defaults.

Install settings:

SettingValue
install.crdsCreateReplace
install.createNamespacetrue
install.remediation.retries3

Upgrade settings:

SettingValue
upgrade.crdsCreateReplace
upgrade.remediation.retries3

Kustomization inclusion: The HelmRelease is listed in deploy/flux-system/kustomization.yaml under the resources list as releases/keystone-operator.yaml.

Dependency Chain

The Keystone operator depends on four infrastructure operators that must be running before it starts. FluxCD enforces this ordering via spec.dependsOn:

text
cert-manager (cert-manager namespace)
├── mariadb-operator (mariadb-system namespace)
├── memcached-operator (memcached-system namespace)
├── external-secrets (external-secrets namespace)
└── keystone-operator (openstack namespace)
    ├── dependsOn: cert-manager/cert-manager
    ├── dependsOn: mariadb-operator/mariadb-system
    ├── dependsOn: memcached-operator/memcached-system
    └── dependsOn: external-secrets/external-secrets

Why Each Dependency Is Required

DependencyNamespaceReason
cert-managercert-managerProvides TLS certificate injection for admission webhooks via cert-manager.io/inject-ca-from annotation. Without cert-manager, webhook TLS is not provisioned and the API server cannot call admission webhooks
mariadb-operatormariadb-systemInstalls the k8s.mariadb.com CRDs (Database, User, Grant) that the Keystone operator creates to provision database resources for each Keystone CR
memcached-operatormemcached-systemInstalls the memcached.c5c3.io CRDs (Memcached) that the Keystone operator references for cache discovery
external-secretsexternal-secretsInstalls the external-secrets.io CRDs (ExternalSecret, PushSecret) that the Keystone operator creates to manage secret synchronization from the secret store

Deployment Sequence

FluxCD resolves the dependency graph and deploys in this order:

  1. cert-manager — base layer, no dependencies
  2. mariadb-operator, memcached-operator, and external-secrets — depend only on cert-manager, can install in parallel
  3. keystone-operator — depends on all four, installs last

If any dependency is not ready (HelmRelease not in Ready condition), the keystone-operator HelmRelease remains in a pending state until all dependencies are satisfied.

CRD Installation Behavior

CRD file: operators/keystone/helm/keystone-operator/crds/keystone.openstack.c5c3.io_keystones.yaml

Helm CRD Lifecycle

The CRD is placed in the chart's crds/ directory (not templates/). Helm handles CRDs in crds/ with special behavior:

  1. On install: Helm installs CRDs from crds/ before rendering and applying templates. This ensures the CRD exists before any templates that reference it are created, avoiding chicken-and-egg ordering issues.

  2. On upgrade (with FluxCD crds: CreateReplace): FluxCD's helm-controller replaces the existing CRD with the version from the chart. This enables CRD schema updates when the chart version is upgraded.

  3. On uninstall: Helm does not delete CRDs from the crds/ directory on helm uninstall. This is intentional — CRDs are cluster-scoped resources and deleting them would destroy all custom resources of that type across all namespaces.

CRD Source

The CRD file in crds/ is an exact copy of the generated CRD at operators/keystone/config/crd/bases/keystone.openstack.c5c3.io_keystones.yaml. It defines the Keystone kind in the keystone.openstack.c5c3.io API group.

Important: The CRD in crds/ must remain an exact copy of the source CRD. Manual modifications would cause divergence between the Helm-installed CRD and the kubebuilder-generated source. When the source CRD changes (e.g., new spec fields are added), the copy in crds/ must be updated to match.

FluxCD CRD Policy

The HelmRelease configures both install and upgrade to use crds: CreateReplace:

yaml
install:
  crds: CreateReplace
upgrade:
  crds: CreateReplace
PolicyBehavior
CreateReplaceCreate CRDs if they do not exist; replace (overwrite) if they do. This ensures CRD schema updates are applied on chart upgrades
AlternativesSkip (never touch CRDs), Create (create only, never update). CreateReplace is recommended for operator charts where CRD evolution is expected

Data Flow

End-to-end deployment flow from FluxCD reconciliation to operator startup:

text
FluxCD helm-controller

  ├─ 1. Reconciles HelmRelease (keystone-operator)
  │     Checks dependsOn: cert-manager ✓, mariadb-operator ✓, memcached-operator ✓

  ├─ 2. Fetches chart from c5c3-charts OCI HelmRepository

  ├─ 3. Installs CRDs from crds/ directory
  │     → keystone.openstack.c5c3.io_keystones.yaml applied to cluster

  ├─ 4. Renders templates with merged values (chart defaults + HelmRelease values)
  │     → ServiceAccount, ClusterRole, ClusterRoleBinding, Deployment, Service,
  │       MutatingWebhookConfiguration, ValidatingWebhookConfiguration

  ├─ 5. Applies rendered resources to openstack namespace

  ├─ 6. cert-manager detects inject-ca-from annotation on webhook configurations
  │     → Injects CA bundle from Certificate resource into caBundle field

  └─ 7. Operator pods start, leader election determines active replica
        → Active replica begins reconciling Keystone CRs