Skip to content

Keystone Operator Packaging ​

Reference documentation for the Keystone operator packaging artifacts. This covers the operator image, 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/
├── vendor.json                         Vendor hash of the operator image (make nix-lock)
├── config/rbac/role.yaml               controller-gen ClusterRole from the RBAC markers (make manifests)
├── helm/
│   └── keystone-operator/
│       ├── Chart.yaml                  Helm chart metadata
│       ├── Chart.lock                  Pinned operator-library dependency
│       ├── values.yaml                 Default configuration values
│       ├── values.schema.json          JSON Schema for values validation (generated)
│       ├── values.schema.extras.json   Chart-specific schema additions (federation)
│       ├── crds/
│       │   └── keystone.openstack.c5c3.io_keystones.yaml   CRD (auto-installed by Helm)
│       └── templates/
│           ├── _helpers.tpl            Chart-specific helpers: the operator-library.chart.* hook overrides
│           ├── _rbac-rules.tpl         RBAC rules, generated from config/rbac/role.yaml (make sync-helm-rbac)
│           ├── _webhook-rbac-rules.tpl RBAC rules of the standalone webhook, generated from the same file
│           ├── serviceaccount.yaml     ServiceAccount (conditional)
│           ├── clusterrole.yaml        ClusterRole (cluster-scoped RBAC, default)
│           ├── clusterrolebinding.yaml ClusterRoleBinding (default)
│           ├── role.yaml               Namespace-scoped Role (rbac.namespaceScoped)
│           ├── rolebinding.yaml        Namespace-scoped RoleBinding (rbac.namespaceScoped)
│           ├── deployment.yaml         Operator Deployment
│           ├── service.yaml            ClusterIP Service (webhook + metrics)
│           ├── pdb.yaml                PodDisruptionBudget (replicas > 1)
│           ├── certificate.yaml        Self-signed Issuer + webhook Certificate (conditional)
│           ├── networkpolicy.yaml      Operator pod NetworkPolicy (opt-in)
│           ├── servicemonitor.yaml     Prometheus ServiceMonitor (opt-in)
│           └── webhook-configuration.yaml  Mutating + Validating webhooks (conditional)
operators/shared/helm/
├── operator-library/                   Library chart: every manifest template above, defined once
└── operator-library-testbed/           Test consumer of the library; runs the shared helm-unittest suites
deploy/flux-system/
├── kustomization.yaml                  Base kustomization (includes keystone-operator release)
└── releases/
    └── keystone-operator.yaml          FluxCD HelmRelease

Image ​

Location: nix/lib/operator-image.nix (shared by all operators) and operators/keystone/vendor.json, the vendor hash of the keystone module

The root flake builds the image as the attribute keystone-operator-image, a dockerTools.streamLayeredImage around a statically linked operator binary (Operator images).

Source ​

The build runs the keystone module on its own with GOWORK=off, so operators/keystone/go.mod and its replace directive for internal/common decide the module graph. The source holds internal/common and operators/keystone, without the _test.go files and the helm/, dashboards/ and config/ directories, which no go:embed reads. The embedded option catalogs under api/v1alpha1/catalogs/ and the scripts under internal/controller/scripts/ are part of it. A commit to another operator or to the chart leaves the image unchanged.

Toolchain ​

buildGo127Module of the locked nixpkgs-images compiles the binary with CGO_ENABLED=0 and the ldflags -s -w. proxyVendor keeps the fixed-output derivation to the module download, whose hash is the vendorHash of operators/keystone/vendor.json; make nix-lock writes it when the go.mod or go.sum files it derives from move. The unit tests run under make test, not in the build.

Runtime Image Properties ​

The image is nix/lib/static-image.nix, the stand-in for gcr.io/distroless/static:nonroot:

PropertyValue
Binary/usr/bin/manager
User65532:65532 (nonroot)
Working directory/home/nonroot
Entrypoint["/usr/bin/manager"]
ShellNone
Package managerNone

OCI Annotations ​

The image carries four static labels and no per-build label:

AnnotationValue
org.opencontainers.image.titlekeystone-operator
org.opencontainers.image.descriptionCobaltCore keystone operator
org.opencontainers.image.licensesApache-2.0
org.opencontainers.image.vendorSAP SE

The merge-operator-images job of the CI workflow records the per-build metadata on the published manifest list as index annotations: the commit (org.opencontainers.image.revision), the version, the creation time, and the source and URL of the repository.

Local Build ​

bash
# From the repository root, with Nix and flakes
make image OPERATOR=keystone IMG=keystone-operator:dev

# Verify: prints the flags of the manager
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.5.0
appVersion0.1.0

Shared Library Subchart ​

The chart depends on the operator-library library chart (operators/shared/helm/operator-library), declared in Chart.yaml:

yaml
dependencies:
  - name: operator-library
    version: 0.1.0
    repository: "file://../../../shared/helm/operator-library"

The library holds, in one place, the naming and label helpers (operator-library.fullname, operator-library.labels, operator-library.selectorLabels, operator-library.serviceAccountName) and every manifest template the operator charts render: the Deployment skeleton, the ClusterRole and Role, the NetworkPolicy, certificate, Service, ServiceAccount, bindings, PDB, ServiceMonitor and webhook configurations. Each templates/*.yaml in this chart is a one-line include. What only this chart knows goes through the operator-library.chart.* hooks: the library defines each hook empty, and a chart overrides it by defining a template of the same name in its _helpers.tpl (Helm loads the parent chart's templates last, so its definition wins). The keystone-operator overrides operator-library.chart.args to render --federation-metadata-allow-cidrs.

The RBAC rules are not written by hand: make manifests has controller-gen assemble config/rbac/role.yaml from the +kubebuilder:rbac markers in the Go sources, and make sync-helm-rbac copies its rules into templates/_rbac-rules.tpl as the keystone-operator.rbacRules template the library's ClusterRole and Role include (plus the leader-election leases rule). make verify-helm-rbac fails on drift; the rationale for each grant lives as a comment next to its marker. The operator-library's own helm-unittest suites run once against operators/shared/helm/operator-library-testbed, a test consumer that renders every shared template with the values contract the operator charts share; this chart's suites cover only what it adds. A library change is released to every consumer with make helm-bump-library VERSION=x.

Chart.lock pins the dependency and is committed; the vendored copy under charts/ is a build artifact (git-ignored). helm dependency build vendors it from the local path. Run make helm-deps before helm lint/template/ unittest, and make helm-package does it automatically so the published tarball is self-contained.

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

Scheduling ​

ParameterTypeDefaultDescription
nodeSelectorobject{}Node labels the operator pods must match. Empty renders no nodeSelector
tolerationsarray[]Taints the operator pods tolerate. Empty renders no tolerations
priorityClassNamestring""PriorityClass of the operator pods; the class must exist. Empty renders none

RBAC ​

ParameterTypeDefaultDescription
rbac.namespaceScopedbooleanfalseWhen true, renders a namespace-scoped Role/RoleBinding instead of the ClusterRole/ClusterRoleBinding, restricting the operator to its release namespace. See the Multi-Tenant Deployment guide

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
webhook.standalonebooleanfalseRun the release as a standalone admission webhook for the namespace-scoped installs of the chart. Requires webhook.enabled=true and rbac.namespaceScoped=false

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. A namespace-scoped install sets webhook.enabled=false, which the values schema requires beside rbac.namespaceScoped=true.

With webhook.standalone=true the release reconciles nothing. The Deployment passes --enable-controllers=false and no --leader-elect, so the manager registers no controller, elects no leader and serves only the admission webhooks. The ClusterRole renders the standalone webhook rule set. The Service, Issuer, Certificate and both webhook configurations render as with webhook.enabled=true, and the configurations admit the Keystone and KeystoneIdentityBackend CRs of every namespace. One such release per cluster admits the CRs of every namespace-scoped install of the chart; see Multi-Tenant Deployment → Admission webhooks.

Metrics ​

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

Logging ​

ParameterTypeDefaultDescription
logging.developmentbooleanfalseWhen true, passes --zap-devel (console encoder, debug verbosity). Leave false for the production profile (JSON encoder, info level)
logging.levelstring""--zap-log-level: debug, info, error, panic, or a positive integer. Empty uses the mode default
logging.encoderstring""--zap-encoder: json or console. Empty uses the mode default

Each value maps to a controller-runtime --zap-* flag and is omitted from the operator args when left at its default.

Monitoring ​

ParameterTypeDefaultDescription
monitoring.serviceMonitor.enabledbooleanfalseRender a monitoring.coreos.com/v1 ServiceMonitor targeting the metrics port. Requires prometheus-operator CRDs in the cluster
monitoring.serviceMonitor.intervalstring30sScrape interval on the ServiceMonitor endpoint

See Enable Keystone Operator Metrics for the end-to-end scraping setup.

NetworkPolicy ​

ParameterTypeDefaultDescription
networkPolicy.enabledbooleanfalseOpt-in NetworkPolicy that default-denies operator pod traffic and opens explicit rules (kube-apiserver, DNS, webhook, metrics)
networkPolicy.kubeApiServer.cidrs / .portslist[]API-server egress targets. Both must be non-empty when the policy is enabled (fail-closed template guard)
networkPolicy.dns.*—kube-dnsDNS egress selectors (UDP/TCP 53)
networkPolicy.allowMetricsFromlist[]Ingress peers allowed to scrape metrics
networkPolicy.webhookClients.cidrslist[]Webhook ingress override; falls back to kubeApiServer.cidrs

Full semantics live in the Operator NetworkPolicy reference.

Federation ​

ParameterTypeDefaultDescription
federation.metadataAllowCidrslist of CIDR strings[]CIDRs the SSRF-guarded federation-metadata client may additionally dial to fetch identity-provider metadata (OIDC discovery documents and SAML IdP metadata) — e.g. the cluster's Service CIDR 10.96.0.0/12 for a trusted in-cluster IdP. Rendered as --federation-metadata-allow-cidrs. Empty (the default) keeps the operator refusing every non-public address; loopback, link-local (cloud IMDS), multicast, and unspecified addresses stay blocked even when an entry covers them. Operator-level only — never derived from a CR

See Attach an OIDC Federation Backend to Keystone for the discovery workflow this value enables.

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}rbac.namespaceScoped=false (default); carries the standalone webhook rule set instead of the manager rules under webhook.standalone=true
ClusterRoleBindingrbac.authorization.k8s.io/v1/ClusterRoleBinding{fullname}rbac.namespaceScoped=false (default)
Rolerbac.authorization.k8s.io/v1/Role{fullname}rbac.namespaceScoped=true
RoleBindingrbac.authorization.k8s.io/v1/RoleBinding{fullname}rbac.namespaceScoped=true
Deploymentapps/v1/Deployment{fullname}Always
Servicev1/Service{fullname}Always
PodDisruptionBudgetpolicy/v1/PodDisruptionBudget{fullname}replicas > 1
Issuercert-manager.io/v1/Issuer{fullname}-selfsignedwebhook.enabled
Certificatecert-manager.io/v1/Certificate{fullname}-webhookwebhook.enabled
NetworkPolicynetworking.k8s.io/v1/NetworkPolicy{fullname}networkPolicy.enabled
ServiceMonitormonitoring.coreos.com/v1/ServiceMonitor{fullname}monitoring.serviceMonitor.enabled
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 operator-library.labels helper:

LabelValue
helm.sh/chartkeystone-operator-0.5.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)
--federation-metadata-allow-cidrsComma-separated .Values.federation.metadataAllowCidrs; omitted when the list is emptyYes

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

Scheduling and disruption:

The pod template carries a best-effort topology spread constraint (maxSkew: 1, topologyKey: kubernetes.io/hostname, whenUnsatisfiable: ScheduleAnyway) so replicas are scheduled on different nodes where possible while single-node clusters (kind) stay schedulable. Together with the PodDisruptionBudget (minAvailable: 1, rendered only when replicas > 1), a voluntary disruption such as a node drain can never evict every replica, and with it the in-process admission webhook, at once. nodeSelector, tolerations and priorityClassName render on the pod spec only when set (see Scheduling).

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 generated from the kubebuilder RBAC markers spread across the reconciler files in operators/keystone/internal/controller/ (see the RBAC paragraph under Helm Chart). controller-gen merges resources that share a verb set into one rule and sorts the verbs, so the rendered rules group differently from the table below; the grants are the same. 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
autoscalinghorizontalpodautoscalersget, list, watch, create, update, patch, delete
autoscaling.k8s.ioverticalpodautoscalersget, list, watch, create, update, patch, delete
"" (core)services, configmaps, secrets, serviceaccountsget, list, watch, create, update, patch, delete
"" (core)eventscreate, patch
"" (core)podsget, list
batchjobs, cronjobsget, list, watch, create, update, patch, delete
cert-manager.iocertificatesget, list, watch, create, update, patch, delete
k8s.mariadb.comdatabases, users, grantsget, list, watch, create, update, patch, delete
k8s.mariadb.commariadbsget, list, watch
external-secrets.ioexternalsecretsget, list, watch, create, update, patch
external-secrets.iopushsecretsget, list, watch, create, update, patch, delete
external-secrets.ioclustersecretstoresget, list, watch
gateway.networking.k8s.iohttproutesget, list, watch, create, update, patch, delete
gateway.networking.k8s.iohttproutes/statusget
networking.k8s.ionetworkpoliciesget, list, watch, create, update, patch, delete
policypoddisruptionbudgetsget, list, watch, create, update, patch, delete
rbac.authorization.k8s.ioroles, rolebindingsget, list, watch, create, update, patch, delete
scheduling.k8s.iopriorityclassesget, list, watch

Notable verb restrictions:

  • events has only create and patch: the operator emits events but never reads or deletes them.
  • externalsecrets has no delete verb: ExternalSecret cleanup is left to owner-reference garbage collection. pushsecrets does include delete: the OpenBao finalizer deletes the key-backup PushSecrets when a Keystone CR is deleted so ESO stops pushing to OpenBao.
  • mariadbs, clustersecretstores, priorityclasses, pods, and httproutes/status are read-only: the operator observes them but never mutates them.

The ClusterRoleBinding binds the ClusterRole to the operator's ServiceAccount in the release namespace only. With rbac.namespaceScoped=true the identical rule set is rendered as a namespaced Role/RoleBinding instead (both library templates include the generated keystone-operator.rbacRules partial). The rule set's cluster-scoped entries, clustersecretstores and priorityclasses, grant nothing in a Role. The operator started with --namespace neither watches nor reads them: it registers no ClusterSecretStore watch and refuses a Keystone whose effective store is cluster-scoped, and only the webhook, which this mode disables, reads PriorityClasses.

Standalone webhook rule set ​

Under webhook.standalone=true the ClusterRole carries only what the admission webhooks read through their uncached API reader:

API GroupResourcesVerbsRead by
scheduling.k8s.iopriorityclassesgetThe validating Keystone webhook, for spec.deployment.priorityClassName and the Job priority classes
keystone.openstack.c5c3.iokeystoneidentitybackendslistThe validating KeystoneIdentityBackend webhook, for the backends in the namespace

It grants nothing on secrets, rolebindings or leases. Each read is declared with a +kubebuilder:rbac marker ending in roleName=keystone-webhook beside the +kubebuilder:webhook markers of the file that performs it (operators/keystone/api/v1alpha1/keystone_webhook.go, keystoneidentitybackend_webhook.go). controller-gen writes those markers into a second ClusterRole of config/rbac/role.yaml, keystone-webhook, and make sync-helm-rbac copies its rules into templates/_webhook-rbac-rules.tpl as the keystone-operator.webhookRbacRules template, without a leases rule.

The generator also guards coverage: every (apiGroup, resource, verb) the keystone-webhook ClusterRole grants must be granted by the manager rules too, and make sync-helm-rbac and make verify-helm-rbac exit 1 otherwise. The in-process webhook of a cluster-wide release runs under the manager identity, so a read declared for the webhook role alone would fail there. The manager rules keep priorityclasses at get, list, watch; controller-gen merges the verbs of markers per resource.

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
API groupkeystone.openstack.c5c3.io
API versionv1alpha1
Resourcekeystones
Failure policyFail
Side effectsNone
Admission review versionsv1

Neither configuration intercepts DELETE: the webhook is served in-process by the operator, or by the standalone webhook release, so with failurePolicy: Fail a DELETE rule would let a down webhook pod block CR (and thereby namespace) deletion.

Both configurations include the annotation:

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

This instructs cert-manager to inject the CA bundle from the named Certificate resource into the webhook caBundle field automatically. The chart renders that Certificate itself: templates/certificate.yaml creates a self-signed Issuer ({fullname}-selfsigned) and the {fullname}-webhook Certificate in the release namespace whenever webhook.enabled=true.

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 namespacekeystone-system
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:

The install order of the Flux resources of deploy/flux-system, in four layers. Layer 1 declares no dependency: cert-manager, mariadb-operator-crds and prometheus-operator-crds. Layer 2 waits for cert-manager: mariadb-operator, which also waits for mariadb-operator-crds, memcached-operator, which also waits for prometheus-operator-crds, external-secrets, garage-operator, openbao, openbao-operator and ovn-operator. Layer 3 is keystone-operator, which waits for mariadb-operator, memcached-operator and external-secrets. Layer 4 waits for keystone-operator: horizon-operator, glance-operator, placement-operator, cinder-operator, nova-operator, barbican-operator, which also waits for openbao-operator, neutron-operator, which also waits for ovn-operator, and c5c3-operator. A solid arrow is a dependsOn entry of a HelmRelease, and the one solid arrow between Flux Kustomizations says that the Kustomization messaging-topology-operator waits for the Kustomization rabbitmq-cluster-operator. Two dotted arrows are dependencies that no manifest declares, because each crosses between a HelmRelease and a Flux Kustomization: the Kustomization rabbitmq-cluster-operator needs the CRDs of cert-manager and retries until they exist, and c5c3-operator starts only once the Kustomization k-orc has installed the K-ORC CRDs.

The keystone-operator HelmRelease sits in layer 3 of the figure: it waits for cert-manager, mariadb-operator, memcached-operator and 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:

How the Flux helm-controller installs the keystone-operator HelmRelease, in seven steps. It reconciles the HelmRelease, checking that cert-manager, mariadb-operator, memcached-operator and external-secrets are ready; fetches the chart from the c5c3-charts OCI HelmRepository; installs the CRDs from crds/, keystone.openstack.c5c3.io_keystones.yaml; renders the templates with the chart defaults merged with the HelmRelease values, which yields the ServiceAccount, ClusterRole, ClusterRoleBinding, Deployment, Service, PodDisruptionBudget, Issuer, Certificate, the two webhook configurations, and the NetworkPolicy and ServiceMonitor when opted in; and applies them into the keystone-system namespace. Then cert-manager fills caBundle of the webhook configurations from the Certificate through the inject-ca-from annotation, and the operator pods start, where leader election picks the active replica that begins reconciling Keystone resources.