{{- if and .Values.ingress .Values.ingress.marketplace .Values.ingress.marketplace.enabled -}}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: provisioning
  namespace: sme
---
# Cluster-wide permissions for the provisioning SA.
#
# After #75 this is strictly READ ONLY across all non-namespaced and cross-NS
# reads. The only destructive verbs granted cluster-wide are patch+delete on
# Namespaces — and that is deliberate because tenant-<slug> namespaces are
# only cluster-scoped things the teardown flow owns end-to-end.
#
# Every per-tenant destructive operation (deleting HelmRelease finalizers,
# deleting legacy in-NS Kustomization CRs, reading DB-password secrets) is
# granted by a namespaced Role that ships INSIDE each tenant's manifest
# bundle (see services/provisioning/gitops:generateProvisioningTenantRBAC).
# So a bug in teardown that points at the wrong slug can't wipe flux-system
# Kustomization CRs or tenant CRs for tenants this SA wasn't invited into.
#
# Permissions to touch flux-system's own kustomizations + the kubeconfig
# mirror secrets live in a Role bound in flux-system (below).
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: sme-provisioning
rules:
  # Read-only status endpoints.
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["helm.toolkit.fluxcd.io"]
    resources: ["helmreleases"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["kustomize.toolkit.fluxcd.io"]
    resources: ["kustomizations"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["cert-manager.io"]
    resources: ["certificates", "certificaterequests"]
    # patch is needed to strip finalizers at teardown — cert-manager attaches
    # finalizer.cert-manager.io/certificate-secret-binding to every Cert CR
    # and once the tenant NS is Terminating the controller can't reconcile
    # the delete (can't write back into a Terminating NS), so the finalizer
    # is never removed and the NS stays Terminating forever. Same pattern as
    # the cluster-wide namespace delete below: cluster-wide is justified
    # because the GC target is cluster-scoped (the NS itself) and the
    # per-tenant Role generated in gitops.go could already be pruned by
    # Flux by the time we need to strip. At worst a buggy teardown can only
    # drop the finalizer off a Certificate — cert-manager will re-add it on
    # the next reconcile loop when the NS isn't Terminating. Issue #86.
    verbs: ["get", "list", "watch", "patch"]
  # Namespaces: tenant NS lifecycle. Cluster-wide because namespaces are a
  # cluster-scoped resource. Scoping by naming convention (tenant-<slug>) is
  # enforced in code; this rule trusts that discipline, but at worst a buggy
  # teardown can only delete a NAMESPACE — not CRs in other namespaces. The
  # previous version of this role could also delete Flux Kustomizations in
  # flux-system, which would take the whole cluster offline; that verb is
  # now gone from cluster scope.
  - apiGroups: [""]
    resources: ["namespaces"]
    verbs: ["get", "list", "watch", "patch", "delete"]
  # Secrets: cluster-wide read is needed so the provisioning service can
  # fetch the source `vc-vcluster` Secret from tenant-<slug> at mirror time
  # AND look up a DB password that lives in a tenant NS on day-2 installs.
  # Write is scoped to flux-system only (see Role below) so a bug can't
  # rewrite in-tenant secrets.
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: sme-provisioning
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: sme-provisioning
subjects:
  - kind: ServiceAccount
    name: provisioning
    namespace: sme
---
# flux-system-scoped Role: patch/delete the per-tenant Kustomization CRs that
# live here (see #97 for why they live here and not in the tenant NS), and
# create/update/delete the mirrored kubeconfig Secrets those CRs reference.
#
# Previously these rights were cluster-wide — a bug that resolved the wrong
# CR name could have deleted flux-system/flux-system itself and taken all
# reconciliation with it. Confining them to this one namespace means the
# absolute worst a teardown bug can do is break one tenant's sync, not the
# cluster.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sme-provisioning-fluxsystem
  namespace: flux-system
rules:
  - apiGroups: ["kustomize.toolkit.fluxcd.io"]
    resources: ["kustomizations"]
    verbs: ["get", "list", "watch", "patch", "delete"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sme-provisioning-fluxsystem
  namespace: flux-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: sme-provisioning-fluxsystem
subjects:
  - kind: ServiceAccount
    name: provisioning
    namespace: sme
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: provisioning
  namespace: sme
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sme-provisioning
  template:
    metadata:
      labels:
        app: sme-provisioning
    spec:
      serviceAccountName: provisioning
      imagePullSecrets:
        - name: ghcr-pull
      containers:
        - name: provisioning
          image: "{{ if .Values.global.imageRegistry }}{{ .Values.global.imageRegistry }}{{ else }}{{ .Values.images.registry }}{{ end }}/{{ .Values.images.organization }}/services-provisioning:{{ .Values.images.smeTag }}"
          imagePullPolicy: Always
          ports:
            - containerPort: 8084
          env:
            - name: MONGODB_URI
              valueFrom:
                configMapKeyRef:
                  name: sme-services-config
                  key: MONGODB_URI
            - name: MONGODB_DB
              value: "provisioning"
            - name: REDPANDA_BROKERS
              valueFrom:
                configMapKeyRef:
                  name: sme-services-config
                  key: REDPANDA_BROKERS
            - name: EVENT_BUS_PROTOCOL
              valueFrom:
                configMapKeyRef:
                  name: sme-services-config
                  key: EVENT_BUS_PROTOCOL
                  optional: true
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: sme-secrets
                  key: JWT_SECRET
            - name: CORS_ORIGIN
              valueFrom:
                configMapKeyRef:
                  name: sme-services-config
                  key: CORS_ORIGIN_PUBLIC
            # ── GitOps target path — issue #944 ───────────────────────────
            # Pre-#944 GIT_BASE_PATH was hardcoded to
            # `clusters/contabo-mkt/tenants` here. On a freshly franchised
            # Sovereign (the SME marketplace runs on the Sovereign too) that
            # path points at CONTABO's clusters/ tree — every alice tenant
            # commit landed in upstream openova/openova's contabo overlay,
            # which contabo Flux would then install on the contabo cluster.
            # C5-final caught + reverted the alice2 incident at commit
            # 20a0884a (revert visible on main as 5715db04, 2026-05-05).
            #
            # Fix: data-driven via .Values.smeServices.provisioning.gitBasePath
            # with a topology-aware default. Sovereign installs default to
            # clusters/<sovereignFQDN>/sme-tenants/ (matches the existing
            # catalyst-api SME-tenant orchestrator path in
            # sme_tenant_gitops.go and the Flux Kustomization spec.path the
            # chart 1.4.13 sme-tenants-kustomization.yaml watches). Catalyst-
            # Zero (contabo) keeps the legacy `clusters/contabo-mkt/tenants`
            # path so existing contabo-mkt tenants continue to reconcile.
            #
            # Source-side cross-cluster pollution guard: the provisioning
            # binary ALSO validates at startup + before every commit that
            # the path begins with `clusters/<self-FQDN>/` so a malicious
            # or misconfigured value pointing at a foreign cluster's tree
            # is rejected at runtime even if a per-Sovereign overlay sets
            # the value wrong. See main.go validateGitBasePath.
            - name: GIT_BASE_PATH
            {{- if .Values.smeServices.provisioning.gitBasePath }}
              value: {{ .Values.smeServices.provisioning.gitBasePath | quote }}
            {{- else if .Values.global.sovereignFQDN }}
              value: "clusters/{{ .Values.global.sovereignFQDN }}/sme-tenants"
            {{- else }}
              value: "clusters/contabo-mkt/tenants"
            {{- end }}
            # ── Sovereign FQDN env (cross-cluster pollution guard) ────────
            # Provisioning service uses this to validate that GIT_BASE_PATH
            # starts with `clusters/<SOVEREIGN_FQDN>/` before any git
            # commit. Empty on Catalyst-Zero (contabo) where the legacy
            # `clusters/contabo-mkt/tenants` path is the canonical write
            # target — the binary's guard recognises that special case.
            - name: SOVEREIGN_FQDN
              value: {{ .Values.global.sovereignFQDN | default "" | quote }}
            - name: CATALOG_URL
              valueFrom:
                configMapKeyRef:
                  name: sme-services-config
                  key: CATALOG_URL
            # ── GitOps token — issue #940 ─────────────────────────────────
            # On contabo this Secret is hand-rolled (clusters/contabo-mkt/
            # apps/sme/data/...). On franchised Sovereigns it is rendered by
            # templates/sme-services/provisioning-github-token.yaml (issue
            # #866) which mirrors the local Gitea admin password into
            # `sme/provisioning-github-token`. The Secret name + key are
            # operator-overridable via
            # .Values.smeServices.provisioning.githubToken.* without
            # forking the chart.
            #
            # Provisioning service rejects placeholder values at startup
            # (`<placeholder>`, `<changeme>`, empty) — a leaked Secret with
            # a placeholder string in its bytes is just as broken as no
            # Secret, and silent acceptance previously failed every per-
            # tenant commit on Sovereigns with "401 Bad credentials" buried
            # in service logs.
            - name: GITHUB_TOKEN
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.smeServices.provisioning.githubToken.secretName | default "provisioning-github-token" }}
                  key: {{ .Values.smeServices.provisioning.githubToken.secretKey | default "GITHUB_TOKEN" }}
            # ── Git host coordinates — issue #940 ─────────────────────────
            # Pre-#940 GITHUB_OWNER + GITHUB_REPO were chart-level literals
            # ("openova-io" / "openova"). On a Sovereign the orchestrator
            # MUST commit to the LOCAL Gitea (the cutover step flipped the
            # GitRepository CR to point there), so writes to upstream
            # github.com would (a) leak per-tenant overlays into the public
            # repo's main branch and (b) fail authentication because the
            # GITHUB_TOKEN Secret is the local Gitea admin password, not a
            # GitHub PAT.
            #
            # Defaults match the catalyst-api gitops env contract:
            #   - GITHUB_API_URL: in-cluster Gitea on Sovereign,
            #     https://api.github.com on contabo.
            #   - GITHUB_OWNER:   `openova` (the local Gitea org bp-gitea
            #     ships) on Sovereign, `openova-io` on contabo.
            #   - GITHUB_REPO:    `openova` on both — bp-gitea seeds a
            #     bare-name `openova/openova` repo on Sovereign install to
            #     match the upstream path so Kustomization paths stay
            #     stable across the cutover boundary.
            # Per Inviolable Principle #4, every value is operator-
            # overridable via .Values.smeServices.provisioning.git.*.
            - name: GITHUB_API_URL
            {{- if .Values.smeServices.provisioning.git.apiURL }}
              value: {{ .Values.smeServices.provisioning.git.apiURL | quote }}
            {{- else if .Values.global.sovereignFQDN }}
              value: "http://gitea-http.gitea.svc.cluster.local:3000/api/v1"
            {{- else }}
              value: "https://api.github.com"
            {{- end }}
            - name: GITHUB_OWNER
            {{- if .Values.smeServices.provisioning.git.owner }}
              value: {{ .Values.smeServices.provisioning.git.owner | quote }}
            {{- else if .Values.global.sovereignFQDN }}
              value: "openova"
            {{- else }}
              value: "openova-io"
            {{- end }}
            - name: GITHUB_REPO
              value: {{ .Values.smeServices.provisioning.git.repo | default "openova" | quote }}
            - name: GITHUB_BRANCH
              value: {{ .Values.smeServices.provisioning.git.branch | default "main" | quote }}
          resources:
            requests:
              cpu: 25m
              memory: 32Mi
            limits:
              cpu: 500m
              memory: 128Mi
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8084
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8084
            initialDelaySeconds: 3
            periodSeconds: 5
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 65534
---
apiVersion: v1
kind: Service
metadata:
  name: provisioning
  namespace: sme
spec:
  selector:
    app: sme-provisioning
  ports:
    - port: 8084
      targetPort: 8084
{{- end }}
