Website Operator
A Kubernetes controller that watches Website resources and reconciles them
into infrastructure.
Responsibilities
For each Website, the operator creates or maintains:
Public frontend — public Ingress (example-public), ClusterIP Service,
frontend Deployment (example-frontend), RWX published PVC
(example-published) and ServiceAccount.
CMS backend — plain routing Ingress (example-cms, TLS offload only, no
auth annotations), ClusterIP Service, CMS Deployment (example-cms), private
PVC (example-private), the OAuth2/OIDC authentication Secret mount,
ServiceAccount and the CMS status Role/RoleBinding (<site>-cms-status).
Generated NetworkPolicies are temporarily disabled. Reconciliation removes policies generated by previous operator versions.
Workload configuration is passed via environment variables (VIKING_*).
Optional Ipseity integration additionally provisions
retained client and cross-realm administrator ConfigMaps and a CMS auth Secret,
and labels the Website namespace as an Ipseity realm.
Planned, not generated — HorizontalPodAutoscaler, PodDisruptionBudget and monitoring resources. Current generated site containers do not set CPU/memory requests/limits; platform admission policies and capacity planning must account for that. The core controller manifest does set its own requests/limits.
Reconciliation
Reconciliation is idempotent, safe to retry and resistant to partial failure. It must detect and handle missing PVCs, failed volume provisioning, invalid hostnames, hostname collisions, missing Secrets, Deployment failures, failed ingress configuration, insufficient replica availability, resource deletion and configuration changes. Unsupported ingress features must produce clear status conditions rather than silently insecure configurations.
CMS authentication (flattened CRD)
spec.cms.authentication is a single flattened reference
(secretRef.name) to the Secret the CMS reads its OAuth2/OIDC client
configuration from — there is no oauth2/basic mechanism node. The operator:
- validates the referenced Secret (required keys:
clientID,jwtSecretat least 32 bytes raw or base64-encoded, eitherdiscoveryURLor all ofauthURL+tokenURL+jwksURL— not both — andclientSecretunlesspublicClientis"true"); problems are surfaced through theReadycondition and the Secret contents are never logged. A missing or invalid Secret blocks CMS Service routing and scales its Deployment to zero; valid configuration restores both. Public serving and storage are retained; - mounts it read-only as volume
authat/etc/viking/auth(defaultMode0444) and setsVIKING_AUTH_SECRET_DIR=/etc/viking/authplusVIKING_COOKIE_SECURE=trueon the CMS Deployment; - generates the CMS Ingress as a plain routing Ingress (TLS offload only) — the CMS backend is the authentication boundary, so no ingress auth annotations are used;
- defaults an omitted CMS hostname to
cms.<domain>when the CMS is enabled; - provisions authentication through an installed Ipseity Deployment and Ingress
when no explicit reference is supplied.
cms.enabled: falseopts out. A requested CMS without explicit or automatic authentication is blocked; empty authentication references are rejected by the schema; - rejects a same-Website conflict: the CMS hostname must not equal any
public hostname of the same
Website(domain + expanded aliases) — two Ingress rules for the same hostname would collide. Subdomain relationships are permitted because ingress hostname matching is exact (e.g.cms.example.comunderdomain: example.comis a distinct hostname that matches a different rule and is accepted).
Ownership and deletion
Generated resources use Kubernetes owner references where appropriate.
Persistent data is not destroyed automatically when a Website is deleted:
the retention policy is Retain and PVCs must be removed manually. This describes
retention of the claim objects;
removing a claim can still delete its backing volume according to the PV/storage
class reclaim policy. Ipseity namespace realm labels and all Ipseity ConfigMaps are also retained for manual
administrator cleanup; these ConfigMaps have no Website owner references.
Generated automatic authentication Secrets are removed, while explicitly
referenced Secrets are untouched. Finalizers
coordinate safe cleanup. See Security and
Operations.
Permissions
RBAC follows least privilege; the operator must not
require cluster-admin during normal operation. See deploy/02-operator-rbac.yaml
(kept in sync with controller/config/rbac/cluster_role.yaml):
websites:get/list/watch/update(updates persist finalizers).websites/status:get/patch/update, including the permissions delegated to CMS Roles. Kubernetes prevents creating Roles with permissions the controller does not hold. CMS Roles are limited to their own Website name.websites/finalizers:update, required by admission to setblockOwnerDeletionin generated owner references. The finalizer value itself is persisted through an ordinary Website update.- deployments, services, ingresses, persistentvolumeclaims, serviceaccounts,
roles, rolebindings:
get/list/watch/create/update/delete(nopatch— the operator reconciles these into existence). - networkpolicies:
get/list/watch/deletefor legacy cleanup only. - namespaces:
get/patchto merge the realm label. - configmaps:
get/list/create/update/deletefor retained Ipseity identities and legacy Viking cleanup, using the uncached API reader (no watch). - secrets:
get/list/watch/create/update/delete; only Website-owned automatic authentication Secrets are written or deleted. - events:
create/patch(EventRecorder). coordination.k8s.io/leases: full (leader-election lock for--leader-elect).
Status
The operator reports observed state through status conditions (Ready,
FrontendReady, CMSReady, StorageReady, Published) and currentRelease,
reflecting observed state rather than merely successful resource creation.
Published=True is only reported when the published PVC is bound, the frontend
Deployment is available and status.currentRelease is non-empty. The CMS
writes the active release into status.currentRelease by PATCHing the
websites/status subresource (granted by the generated <site>-cms-status
Role); the operator reads that field. The legacy annotation
cms.tlspu.com/current-release is no longer written by the CMS and is only read
as a fallback when the status field is empty.
Operator runtime
controller/cmd/controller/main.go accepts:
--metrics-addr(default:8080, serves/healthz,/readyz,/metrics),--health-probe-addr(default0= disabled).--leader-elect/--leader-election-id(defaultviking-cms-controller).--frontend-image/--cms-image(defaultsviking-cms-frontend:latest/viking-cms-cms:latest) for generated Deployments.--dns-namespace(defaultkube-system) /--dns-label(defaultk8s-app=kube-dns) are deprecated and ignored while NetworkPolicies are disabled; each also settable viaVIKING_DNS_NAMESPACE/VIKING_DNS_LABEL.--ipseity-namespace,--ipseity-deployment,--ipseity-ingressselect the provider;--ipseity-admin-realm/--ipseity-admin-subjectdefault toipseity/adrian. Each accepts aVIKING_IPSEITY_*fallback. See Ipseity authentication.--zap-devel(defaultfalse) /VIKING_ZAP_DEVELfor development logging; production defaults to JSON, Info level.
Namespace model
The full flag/environment table is in configuration.
Website resources are namespace-scoped. Generated workloads live in the same
namespace as their PVCs; cross-namespace PVC mounting is not supported. The
initial implementation may host multiple websites in one namespace but must not
assume all websites share a namespace.
With Ipseity integration, Websites sharing a namespace share its realm and administrator membership. Use separate namespaces for separate client realms.
