fix(catalyst-api,bp-catalyst-platform): SME tenant gitops auth + git binary (#878) (#880)

Three-part fix that unblocks the SME tenant pipeline post-Day-2-
Independence cutover. Live-reproduced on otech103 — POST /api/v1/sme/
tenants succeeds (HTTP 202) but the first reconcile fails with
"gitops token unconfigured" → after wiring the env, fails with
`exec: "git": executable file not found in $PATH` → after fixing
the URL hardcoding, would still 401 against local Gitea because
the basic-auth username is hardcoded "x-access-token".

Part A — code (marketplace_settings.go + sme_tenant_gitops.go):
- Add gitOpsConfig.User (loaded from CATALYST_GITOPS_USER env,
  default "x-access-token" for back-compat with GitHub PATs).
- New injectTokenIntoURLWithUser(rawURL, user, token) — variant of
  injectTokenIntoURL that takes a configurable basic-auth username.
- Update all 3 call sites in marketplace_settings.go +
  sme_tenant_gitops.go to use the new variant with cfg.User.

Part B — Containerfile:
- apk add git in the runtime stage. The SME tenant pipeline (#804)
  and marketplace-settings GitOps writer both shell out to git
  clone/commit/push; without the binary every first reconcile fails.

Part C — chart (api-deployment.yaml):
- Wire CATALYST_GITOPS_USER + CATALYST_GITOPS_TOKEN envs on
  catalyst-api Deployment, sourced from the local `gitea-admin-secret`
  (already mirrored into catalyst-system via bp-reflector annotation
  per #866). optional=true so Catalyst-Zero (contabo) keeps using
  its existing GitHub PAT path.

Bump bp-catalyst-platform 1.4.10 -> 1.4.11 + lockstep slot 13 pin.

Closes #878

Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
e3mrah 2026-05-05 08:45:45 +04:00 committed by GitHub
parent 8e4c88fd28
commit 7bdd14fcb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 89 additions and 17 deletions

View File

@ -104,7 +104,7 @@ spec:
# /auth/send-pin → SendMagicLink (and /auth/verify-pin →
# VerifyMagicLink) so the UI's PIN-naming reaches the existing
# backend handler.
version: 1.4.10
version: 1.4.11
sourceRef:
kind: HelmRepository
name: bp-catalyst-platform

View File

@ -77,13 +77,18 @@ FROM docker.io/library/alpine:3.20
ARG KUBECTL_VERSION=v1.31.4
ARG HELM_VERSION=v3.16.3
RUN apk add --no-cache ca-certificates curl bash \
RUN apk add --no-cache ca-certificates curl bash git \
&& curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" -o /usr/local/bin/kubectl \
&& chmod +x /usr/local/bin/kubectl \
&& curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" | tar xz -C /tmp \
&& mv /tmp/linux-amd64/helm /usr/local/bin/helm \
&& rm -rf /tmp/linux-amd64 \
&& chmod +x /usr/local/bin/helm
# git is required by the SME tenant provisioning pipeline (#804) and the
# marketplace-settings GitOps writer — both shell out to `git clone /
# commit / push` against the catalyst-zero / Sovereign GitOps repo.
# Without git on PATH the pipeline fails with `exec: "git": executable
# file not found` at the first reconcile (issue #878).
# tofu CLI from the verified-checksum builder stage. Installed mode 0755 so
# every user (including UID 65534 from runAsUser) can execute it.

View File

@ -172,12 +172,19 @@ func (h *Handler) HandleSetMarketplace(w http.ResponseWriter, r *http.Request) {
})
}
// gitOpsConfig captures the clone-target URL, branch, token, and committer
// identity for the GitOps push. Every field is runtime-configurable per
// INVIOLABLE-PRINCIPLES.md #4.
// gitOpsConfig captures the clone-target URL, branch, user/token, and
// committer identity for the GitOps push. Every field is runtime-
// configurable per INVIOLABLE-PRINCIPLES.md #4.
type gitOpsConfig struct {
RepoURL string
Branch string
RepoURL string
Branch string
// User is the basic-auth username embedded in the clone URL. GitHub
// PATs accept any username (canonical "x-access-token"); Gitea
// requires the real account name. Wired via CATALYST_GITOPS_USER so
// the SAME catalyst-api binary works against both GitHub (Catalyst-
// Zero pre-cutover) and the Sovereign-side local Gitea (post-Day-2-
// Independence). Issue #878.
User string
Token string
CommitterName string
CommitterMail string
@ -187,6 +194,7 @@ func loadGitOpsConfig() gitOpsConfig {
return gitOpsConfig{
RepoURL: envOr("CATALYST_GITOPS_REPO_URL", "https://github.com/openova-io/openova"),
Branch: envOr("CATALYST_GITOPS_BRANCH", "main"),
User: envOr("CATALYST_GITOPS_USER", "x-access-token"),
Token: os.Getenv("CATALYST_GITOPS_TOKEN"),
CommitterName: envOr("CATALYST_GITOPS_COMMITTER_NAME", "catalyst-api"),
CommitterMail: envOr("CATALYST_GITOPS_COMMITTER_EMAIL", "ops@openova.io"),
@ -208,7 +216,7 @@ func envOr(key, fallback string) string {
func writeMarketplaceOverlay(ctx context.Context, cfg gitOpsConfig, sovereignFQDN string, body SetMarketplaceRequest, log *slog.Logger) (string, error) {
// Build the authenticated clone URL once so the token is never echoed
// to a subprocess argument list (visible to /proc/<pid>/cmdline).
authURL, err := injectTokenIntoURL(cfg.RepoURL, cfg.Token)
authURL, err := injectTokenIntoURLWithUser(cfg.RepoURL, cfg.User, cfg.Token)
if err != nil {
return "", fmt.Errorf("rewrite repo URL: %w", err)
}
@ -501,27 +509,44 @@ func runGitOutput(ctx context.Context, dir string, args ...string) (string, erro
}
// injectTokenIntoURL rewrites https://github.com/foo into
// https://x-access-token:<TOKEN>@github.com/foo so `git clone` can
// authenticate against GitHub without an SSH key. Returns an error if
// the URL is not http/https.
// https://<USER>:<TOKEN>@github.com/foo so `git clone` can authenticate
// without an SSH key. The user is configurable so the same code path
// works for:
// - GitHub PATs — user="x-access-token" (default; GitHub ignores
// the username when the token is a PAT)
// - Local Gitea — user="gitea_admin" (Gitea checks the username on
// basic auth; "x-access-token" returns 401)
//
// Returns an error if the URL is not http/https.
func injectTokenIntoURL(rawURL, token string) (string, error) {
return injectTokenIntoURLWithUser(rawURL, "x-access-token", token)
}
// injectTokenIntoURLWithUser is the configurable variant. Issue #878 —
// post-cutover Sovereign uses local Gitea, which requires the real
// admin username (default GitHub PAT username "x-access-token" returns
// 401). Loaded from CATALYST_GITOPS_USER env via gitOpsConfig.User.
func injectTokenIntoURLWithUser(rawURL, user, token string) (string, error) {
if token == "" {
return rawURL, nil
}
if user == "" {
user = "x-access-token"
}
if strings.HasPrefix(rawURL, "https://") {
// Strip any pre-existing userinfo, then re-inject.
stripped := strings.TrimPrefix(rawURL, "https://")
if at := strings.IndexByte(stripped, '@'); at >= 0 {
stripped = stripped[at+1:]
}
return "https://x-access-token:" + token + "@" + stripped, nil
return "https://" + user + ":" + token + "@" + stripped, nil
}
if strings.HasPrefix(rawURL, "http://") {
stripped := strings.TrimPrefix(rawURL, "http://")
if at := strings.IndexByte(stripped, '@'); at >= 0 {
stripped = stripped[at+1:]
}
return "http://x-access-token:" + token + "@" + stripped, nil
return "http://" + user + ":" + token + "@" + stripped, nil
}
return "", fmt.Errorf("unsupported repo URL scheme: %s", rawURL)
}

View File

@ -95,7 +95,7 @@ func (w DefaultSMETenantGitOpsWriter) WriteTenantOverlay(ctx context.Context, re
}
}()
authURL, err := injectTokenIntoURL(cfg.RepoURL, cfg.Token)
authURL, err := injectTokenIntoURLWithUser(cfg.RepoURL, cfg.User, cfg.Token)
if err != nil {
return "", fmt.Errorf("rewrite repo URL: %w", err)
}
@ -174,7 +174,7 @@ func (w DefaultSMETenantGitOpsWriter) DeleteTenantOverlay(ctx context.Context, r
}
defer os.RemoveAll(scratch)
authURL, err := injectTokenIntoURL(cfg.RepoURL, cfg.Token)
authURL, err := injectTokenIntoURLWithUser(cfg.RepoURL, cfg.User, cfg.Token)
if err != nil {
return "", fmt.Errorf("rewrite repo URL: %w", err)
}

View File

@ -1,7 +1,7 @@
apiVersion: v2
name: bp-catalyst-platform
version: 1.4.10
appVersion: 1.4.10
version: 1.4.11
appVersion: 1.4.11
description: |
Catalyst Platform — the unified Catalyst control plane umbrella chart for Catalyst-Zero.
Composes the catalyst-{ui,api}, console, admin, marketplace UI modules and the marketplace-api backend.
@ -614,6 +614,24 @@ description: |
pipeline) but ultimately point at the Sovereign's public FQDN.
optional=true since Catalyst-Zero (contabo) doesn't run the SME
tenant pipeline. Lockstep slot 13 pin bumps to 1.4.10. 2026-05-05.
1.4.11 (issue #878): wire CATALYST_GITOPS_USER + CATALYST_GITOPS_TOKEN
env on the catalyst-api Deployment, sourced from the local Gitea
admin secret (`gitea-admin-secret`, keys `username` + `password`).
Without these, the SME tenant pipeline (#804) and the marketplace-
settings GitOps writer fail at the first reconcile with "gitops
token unconfigured" (post-cutover Sovereign has no GitHub PAT — the
GitOps target is the local Gitea). optional=true so Catalyst-Zero
(contabo) keeps using the existing GitHub PAT path. Pairs with a
catalyst-api code change (marketplace_settings.go +
sme_tenant_gitops.go): injectTokenIntoURL now takes a configurable
username (was hardcoded "x-access-token"; GitHub PAT-only) so the
same code path works for both GitHub and Gitea. Also adds `git` to
the catalyst-api Containerfile (Alpine 3.20 base + apk add git) —
the pipeline shells out to git clone/commit/push, and without the
binary the first reconcile fails with `exec: "git": executable
file not found in $PATH`. Lockstep slot 13 pin bumps to 1.4.11.
2026-05-05.
type: application
# Opt-out from the blueprint-release hollow-chart guard (issue #181 / #510).

View File

@ -524,6 +524,30 @@ spec:
name: sovereign-fqdn
key: fqdn
optional: true
# CATALYST_GITOPS_USER + CATALYST_GITOPS_TOKEN — basic-auth
# credentials embedded in the GitOps clone URL (issue #878).
# Pre-cutover (Catalyst-Zero): User=x-access-token, Token=GitHub
# PAT (already wired via separate CATALYST_GITOPS_TOKEN secret on
# contabo). Post-cutover (Sovereign): User=gitea_admin,
# Token=<gitea-admin-password> from the local Gitea admin secret.
# The same secret (`gitea-admin-secret`) is mirrored into
# catalyst-system via the bp-reflector annotation block on
# bp-gitea (issue #866), so this Sovereign-side wiring works
# post-Day-2-Independence without a manual mirror step.
# optional=true: Catalyst-Zero (contabo) does not run the SME
# tenant pipeline.
- name: CATALYST_GITOPS_USER
valueFrom:
secretKeyRef:
name: gitea-admin-secret
key: username
optional: true
- name: CATALYST_GITOPS_TOKEN
valueFrom:
secretKeyRef:
name: gitea-admin-secret
key: password
optional: true
# CATALYST_HANDOVER_KEY_PATH — path to the RS256 PRIVATE key
# catalyst-api uses to mint magic-link + handover JWTs. The
# signer auto-generates the keypair on first start if absent.