- Complete Keycloak OIDC SSO across all platform services backed by Vault + ExternalSecrets Operator on single-node K3s.
- Single-node K3s (compute-lcis-2, v1.35.5+k3s), Cilium CNI.
- ArgoCD auto-sync from git via AppSet → Helm apps + raw manifests.
- Secrets via Vault KV v2 + ExternalSecrets Operator.
- Traefik + Let's Encrypt OVH DNS-01 for Ingress TLS.
- Git-push-only workflow.
- Vault fully rebuilt: stale PVC deleted, StatefulSet recreated, re-initialized with Shamir (1 key).
- Vault admin token created (
admin-fullpolicy, 95 chars, based onhvs.CAES...) withvault-initsecret stored. - All 9 Keycloak client secrets generated in Platform realm and stored in Vault KV v2 at path
secret/data/keycloakand also stored in vault-init secret. - ExternalSecrets Operator AppRole auth configured with read-only policy on
secret/data/*. - ClusterSecretStore
vaultconfigured. All ExternalSecrets usingkey: keycloakpath (no double-nesting). - Helm values fixed: Vault replicas
3→1, all ExternalSecretkey: secret/data/keycloak→key: keycloak. - All Keycloak OIDC secrets restored to Vault and picked up by ExternalSecrets.
- ArgoCD mounted in kubectl to verify sync. All applications now show
Syncedstatus correctly. - Kustomize apps that were deprecated are removed from operands (now fully Helm-based).
- All Vault KV data restored:
secret/data/keycloak(admin credentials, user passwords), all 10 OIDC client secrets atsecret/data/oidc/<app>, and SAML certs (devopsRealmScalingCert,sonarqubePrivateKeySecured,sonarqubeCertificateSecured) - All 29 ExternalSecrets now Ready=True (keycloak-secrets + sonarqube-saml previously failed)
- ArgoCD platform 35/39 apps Healthy ✅; 4 Unknown: argocd-image-updater, kube-prometheus, link-shortener, open-telemetry (metadata only, not breaking)
- microcks MongoDB auth fixed:
userMpassword synced between secret and MongoDB admin user; microcks app 1/1 READY - Plane pods now scheduling after stuck pods cleanup (Too many pods limit no longer hit)
- Legacy cleanup: 24 stuck pods force-deleted (backstage, ci, default, gitea, keycloak, microcks, plane), 20 old microcks-mongodb legacy RS deleted
-
Keycloak crash loop fixed (commits b34fed6, ea51148): removed invalid
subComponents/devops import field that broke--import-realmJSON parsing on KC 26.7;keycloak-0Running, import clean. -
IGNORE_EXISTINGimport confirmed: existingplatformrealm is skipped on startup — live realm data is never touched by re-imports; startup still parses the file, so import-validity fixes are mandatory. -
C1 group RBAC activated live and verified end-to-end:
- Created missing realm roles (
platform-admin,platform-engineer,developer,qa-team,security-team,readonly) and group→realm-role mappings. - Created
microcks-appclient roles (admin,manager,user) and group→client-role mappings (platform-admins→admin, platform-engineers/developers→manager, readonly→user). - Token verification: bruno →
realm_access:['platform-admin'],resource_access.microcks-app:['admin']; ci-runner →developer+platform-engineer,microcks-app:['manager']; gitops-user →platform-engineer,microcks-app:['manager'].
- Created missing realm roles (
-
Root-caused KC 26 "Account is not fully set up" (invalid_grant on password login): default user profile requires
firstName/lastName; missing values triggerVERIFY_PROFILE. Fixedci-runner/gitops-userlive and in the realm file (commit aa09067). -
Setup job
keycloak-platform-realm-setup-20260818completed (passwords + groups). GitOps source of truth (platform-realm-configmap.yaml) is now complete for future rebuilds: realm roles, client roles (roles.clientmap), groups with mappings, user names. -
Devops realm confirmed absent from live Keycloak (404); Nexus/SonarQube auth is via Traefik ForwardAuth, unaffected. Devops realm/SAML assets are dormant (orphan
devops-realmCM in keycloak ns). -
Microcks H3 done: MongoDB 4.4.29 → 7.0.18, FCV chain 4.4→5.0→6.0→7.0 (commits 4aadf9b, 2cd1e36, 963e73a, b76d969, 12d8685).
- Instance is empty: 1 collection
serviceState, 0 documents, PVC 7 days old. Snapshot kept:/tmp/opencode/backups/microcks-mongo-4.4.29-pre-H3-2026-08-18.gz(346B, gzip-verified). - Direct 4.4→7.0 jump is impossible even with zero documents: mongod refuses to boot on a
featureCompatibilityVersion: 4.4document (7.0 only accepts 6.0/6.3/7.0). Mandatory path: 5.0.32 → setFCV 5.0 → 6.0.26 → setFCV 6.0 → 7.0.18 → setFCV 7.0 (confirm: truerequired for 7.0). FCV bumps run as root via pod env (MONGO_INITDB_ROOT_USERNAME/PASSWORD,adminUsername/adminPasswordkeys). - Official mongo 6.0/7.0 images ship mongosh only (no legacy
mongoshell — verified with one-shot pods on 5.0.32/6.0.26/7.0.18). Upstream chart 1.13.2 (and 1.14.0) readinessProbe hardcodes the legacy shell → 6.0+ pods would never go Ready. - ArgoCD
postRenderis unusable on this cluster:applications.argoproj.ioCRD (v1alpha1 only) has nopostRenderschema field; the API server silently prunes it from every Application object. - Chart 1.13.2 vendored to
helm/vendor/microcks; app source repointed to git (path: helm/vendor/microcks; note: adding achart:field makes ArgoCD treat the source as a helm index repo → 404). Vendored probe is shell-agnostic (mongosh … --eval pingfirst, legacymongofallback) — works 4.4→7.0. - Verified: 7.0.18 pod 1/1, FCV=7.0,
serviceState: 0(data intact), app root=200 and/api/services=401 (auth enforced), zero mongo errors after cutover (only transientInterruptedAtShutdownin the 13:51–13:58 Recreate window), ArgoCD Synced/Healthy.
- Instance is empty: 1 collection
-
Microcks M1 documented (report-only, not executed): Vault↔MongoDB credential rotation runbook below — the 1h ExternalSecret refresh cannot re-credential a running MongoDB user.
-
Microcks integration phase A (2026-08-18):
microcks-ciCI identity live and verified.- Dedicated Keycloak client
microcks-ci(confidential, no user flows,serviceAccountsEnabled) — reconcilerspec.json(bootstrap/keycloak-reconciler/configmap.yaml) + realm export entry (generated secret backfilled, same pattern as other clients). - Role: service-account-microcks-ci →
microcks-app:admin(CI creates import jobs/tests; Microcks readsresource_access["microcks-app"]regardless of azp). Applied + verified by git Jobkeycloak-realm-microcks-ci-setup-20260818-r4(kubernetes/keycloak-realm/platform-realm-setup-microcks-ci.yaml). Superseded for full access by the cumulative mapping inkeycloak-microcks-roles-cumulative-20260818-r7(1.13.2 requiresuseron read-detail paths). - KC 26 API gotcha:
POST /users/{id}/role-mappings/clients/{client}404 "Role not found" unless the payload RoleRepresentation includes the roleid(name+containerId alone is rejected). Payload used:[{id,name,clientRole,composite,containerId}]. - Vault:
secret/data/oidc/microcks-ci(clientSecret) written one-off with root token (reconciler has noVAULT_ADMIN_TOKEN, so its create-path Vault push is skipped). keycloak-secretsExternalSecret:clientSecretMicrocksCi+ templateCLIENT_SECRET_MICROCKS_CI(syncs on its 1h refresh).- Verified E2E:
client_credentialsgrant → tokenazp=microcks-ci,resource_access.microcks-app.roles=["admin"]. - Superseded jobs r1/r2/r3 (wrong endpoint/verify pattern) left for ArgoCD prune; mapping verified idempotent via r4.
- Phase plan (features first, app 1.14.0 upgrade last, in a separate window): A ✅ identity → B ✅ OpenAPI committed + contract import from Woodpecker (pipeline green #75, REST import path) → C: mock + test scenario + MCP endpoint demo → D: Woodpecker contract step (test live
/q/openapivs spec pre/post-deploy, fail-the-pipe on drift) + Woodpecker secrets for CI identity → E: Backstageresource:apicatalog entry (stockbackstage:1.53.1image — Microcks plugin needs a custom image build first; decide then) → F: app 1.13.2→1.14.0 (re-vendor chart, re-apply dual-shell readinessProbe patch, values diff,?vbump).
- Dedicated Keycloak client
- Phase B (A is done: r4 Complete 1/1, r1–r3 pruned):
- Topology corrected: live CI source is standalone public Gitea repo
bruno/link-shortener(noebproorg, no GitHub→Gitea mirror; Backstage entitygitea.ebruno.fr/ebpro/gitopsis stale). Decision: Phase B/D changes go to Gitea;openapi.yamlalso synced to the GitHubgitops/link-shortener/copy. Gitea PAT (bruno, read+write repository) provided — never committed. - Import proven via REST:
POST /api/artifact/upload?mainArtifact=true→ 201. CLI dead end documented:quay.io/microcks/microcks-cli:1.13.2doesn't exist (quay/Docker Hub publish nightly/sig tags only) — REST upload is the import path;/api/artifact/download(formurl=) is the pipeline-friendly variant. - RBAC gap found, fixed, live-verified (r7 Complete 1/1): Microcks 1.13.2
SecurityConfigurationis strict per-path, no hierarchy —GET /api/services/{id}and/api/services/searchrequireROLE_usereven for admins (403insufficient_scopewith an admin-only grant; hit both the CI token andplatform-adminsUI users). Fix = cumulative role ladder: realm export (platform-realm-configmap.yaml) defines themicrocks-appclient rolesadmin/manager/user(live UUIDs pinned) and group mappings are cumulative (admins: admin+manager+user; engineers/developers: manager+user; readonly: user). Jobkeycloak-microcks-roles-cumulative-20260818-r7(kubernetes/keycloak-realm/platform-realm-microcks-roles-job.yaml) applied to the live realm (4 groups +service-account-microcks-ci) — all 11 mappings verified [OK] (commits 7538334/89c0f55/363f687).- E2E verified: fresh CI token now carries
resource_access.microcks-app.roles=["manager","admin","user"];GET /api/services/{id}→ 200,GET /api/services→ 200,GET /api/services/search?name=link-shortener→ 200. Search param in 1.13.2 isname(criterion→ 500pattern can not be nullfromfindByNameLike(null)). CI token lifespan is short (few minutes) — refetch per pipeline run. - KC 26.7.0 kcadm gotchas (r5/r6 failures): generic
kcadm add <path> --payloaddoes not exist (Did you mean: kcadm.sh add-roles?, exit 2) — silent no-op; grants now go via REST POST over bash/dev/tcpwith the kcadm-session admin token (no curl in the image, no awk either). Master admin token is short-lived → r6's user-grant section 401'd until r7 re-logs in + re-extracts the token before the SA grants.
- E2E verified: fresh CI token now carries
- Service renamed to
link-shortener:info.titleupdated in/tmp/opencode/openapi.yaml→ re-upload 201 → servicelink-shortener:1.0.0-SNAPSHOT(id6a84d19dcc8d8f29c60e656f, type REST, 7 ops); oldlink-shortener API(6a84bf1ecc8d8f29c60e656c) deleted (200). - Mock endpoint finding (closed for our purposes):
/m/**is unrouted (Spring 404 from the app itself);/dynarest/**is theDynamicMockRestControllerwhosegetMockContextonly matchesServiceType.GENERIC_REST→ 400 for ourREST-type (OpenAPI-imported) service;/api/mocks*absent from the 1.13.2 spec. Consequence: the CI contract step is import-based (token →POST /api/artifact/upload→ 201 + service listed) — deterministic, no mock dependency. - Phase B done (2026-08-19): pipeline fully green — #75
8bfbe16success.clone→microcks-contract(Keycloakmicrocks-citoken →POST /api/artifact/upload→ 201) →build(Quarkus/Maven) →docker-build-push(harbor.ebruno.fr/link-shortener/link-shortener:latestpushed). Service live in Microcks aslink-shortener:1.0.0-SNAPSHOT.openapi.yaml+.woodpecker.yamlon Giteabruno/link-shortenermain; GitHub gitops copy synced. - Root cause 1 — Woodpecker v3 removed secret auto-injection (v3.0.0): secrets must be mapped per step via
environment:\n VAR:\n from_secret: NAME. This was the "parameter not set" failure. - Root cause 2 — forge config-fetch 401 (pipelines
error, no steps): per-user Gitea OAuth JWTs have 1h access-token lifetime (Gitea 1.26 default) and Woodpecker v3.17 does not refresh them (no TOKEN env option on the gitea provider — verified from binary). Fixed via GitOps:GITEA__oauth2__ACCESS_TOKEN_EXPIRATION_TIME=864000(10d) +REFRESH_TOKEN_EXPIRATION_TIME=2160(90h) inhelm/releases/gitea/values.yaml(the AppSet's real value source) + mirror inbootstrap/helm-values/gitea-official.yaml+?v=5→6bump (raw-URL valueFiles cache trap), commits a6030fb/3b11902. Bruno re-linked Gitea token in Woodpecker UI (one-time; new token = 240h).⚠️ ci-runner(non-interactive) has no Woodpecker user row → first push will hit the same wall; needs one manual link or a service account. - Root cause 3 — truncated Woodpecker secrets: UI paste mangled
MICROCKS_CLIENT_SECRET(86→17 chars) andMICROCKS_HOSThad a trailing newline. Woodpecker stores secretvalueplaintext in DBsecrets→ fixed with byte-exactUPDATE(k8skeycloak-secrets/CLIENT_SECRET_MICROCKS_CI→ secrets id 10, md5-verified; id 7 trimmed). UI re-entry is lossy; prefer this path for long secrets. - Woodpecker DB forensics:
pipelines.errors(config-fetch jsonb),log_entries(hex-encoded step logs perstep_id),users(per-user forge OAuth tokens).
- Topology corrected: live CI source is standalone public Gitea repo
- Microcks 1.13.2 API archaeology (complete — jar forensics): streamed
app.jar(132MB) viakubectl exec cat(no tar in image; podman store ≠ K3s containerd store) → extracted every controller route + DTO from class string pools and the bundled Angular app. Controllers live inio/github/microcks/web(NOTrest).- Confirmed routes:
PUT /api/services/{id}/operation(bodyOperationOverrideDTO= defaultDelay, defaultDelayStrategy, dispatcher, dispatcherRules, parameterConstraints — no message bodies),PUT /api/services/{id}/metadata,POST /api/tests(bodyTestRequestDTO= serviceId, testEndpoint, timeout, filteredOperations, operationsHeaders, runnerType, secretName, oAuth2Context),GET /api/tests/{id},GET /api/tests/service/{id},GET /api/tests/{id}/messages/{testCaseId},POST /api/tests/{id}/testCaseResult,POST /mcp/{service}/{version}(+/sse) — MCP endpoint exists in 1.13.2,/api/artifact/upload,/api/resources/{name}|/service/{id},/api/copilot/samples/*,/dynarest/{service}/{version}/{resource}(GENERIC_REST type only). - Definitive: no message-creation REST endpoint exists in 1.13.2 (verified: ServiceController + TestController string pools + all 10 JS chunks — UI only ever calls
PUT .../operation+POST /api/tests;messagesMapis read-only in UI). - Mock/test content path =
example/examplesin the OpenAPI spec at artifact import:GET /api/services/{id}returns{service, messagesMap}(keyed by operation name; empty when spec has no examples). No examples ⇒ empty mocks. - Mock invocation =
/api/rest/{service}/{version}/**(RestController) — e.g.GET /api/rest/link-shortener/1.0.0-SNAPSHOT/api/links/{code}. Messagedomain ={content, headers, operationId, testCaseId, name, sourceArtifact}; a test run replays each message's request againsttestEndpointand compares with the expected response.
- Confirmed routes:
- Contract-first SOTA 2026 scope (agreed with user):
- Phase C+ (NEW — Pact CDK loop):
quarkiverse/quarkus-pact(provider + consumer, test scope). Trust verified: official Quarkiverse, maintained by Holly Cummins (author of Pact-JVM), used in Quarkus Super Heroes, Apache-2.0, latest 1.6.0; config is via standard Pact JVM system properties (-Dpact.brokerUrl,-Dpact.provider.version, …) — no Quarkus config. In-cluster Pact Broker (pact-brokerns): CI must usehttp://pact-broker.pact-broker.svc:80+ basic auth (userpactbroker, k8s secret keyuser-password) — publicpactbroker.ebruno.frsits behind Traefikfwd-auth. Onemvn verify= consumer publish → provider verification (in-JVM Quarkus test, no separate server) → results publish to broker. - Phase D (expanded): drift check gains quality gates —
spectral lint openapi.yaml+oasdiff break origin/main openapi.yaml(fail on breaking change w/o version bump) + spec↔code drift (generate spec from Quarkus code viaquarkus-smallrye-openapi— already in pom — and diff vs committedopenapi.yaml). - Phase E: Backstage catalog links Microcks + Pact Broker on the
resource:apientry. Phase F: Microcks 1.13.2→1.14.0 (NOT a blocker: MCP + per-operation overrides already exist in 1.13.2). - Microcks↔Quarkus: no extension exists (verified: none in Quarkiverse; 1.13.2 jar has no OTLP traffic-capture receiver — its OTel config is self-observability only).
- Extensions to add:
quarkus-pact-provider,quarkus-pact-consumer(test),quarkus-opentelemetry(main — feeds platform OTel stack; standard bridge if future Microcks adds traffic capture). - Maven wrapper: already in repo (
mvnw3.3.4, only-script, pinned Maven 3.9.16 = exact CI-image version). Fixes:distributionUrl→ in-cluster Nexus (fresh CI containers must not reach repo.maven.apache.org);.woodpecker.yamlbuild →./mvnw -s /tmp/settings-nexus.xml clean verify, drop-DskipTests(Pact loop needs tests running in CI).
- Phase C+ (NEW — Pact CDK loop):
- Phase C unblocked: 7 realistic
examplepayloads added toopenapi.yaml(all ops + POST request body), pushed58c516f→ pipeline re-imports →messagesMapshould populate.- Remaining: mock curl via
/api/rest/...→POST /api/tests(testEndpoint = mock base for self-consistent demo; Phase D repoints at the live app) → MCP JSON-RPC demo (POST /mcp/link-shortener/1.0.0-SNAPSHOT: initialize → tools/list → tools/call) → Pact smoke in a cluster maven pod (no JDK on the gitops host).
- Remaining: mock curl via
- Pact CDK loop verified green locally (Quarkus 3.37.4 +
quarkiverse/quarkus-pact1.6.0, Java 25): consumer test 1/1, provider test 3/3 — provider runs the real app (Quarkus test) against the CNPG DB via local port15432(SCRAM bypass: JDBC URL with?user=&password=query params; local-only, CI needs env/secret DB config).- Pact DSL fix: response body is a flat JSON array →
like(Map.of(...))per element (list-shaped matchers failed); dropped unneeded provider state. - App bug fixed:
POST /api/links500 —LinkResource.create()now fillsshortcode(Link.randomCode()) +createdAtwhen absent (entity is bound directly from the request JSON; request field istargetUrl). Commits02911d9,a8ffba6on Giteabruno/link-shortenermain → pipeline #29 success (16:36 UTC) → Harbor:latestcarries the fix.
- Pact DSL fix: response body is a flat JSON array →
- Stale
:1.0.1image pin — full root cause, resolved (commit3a105ed):- Symptom: renders pinned
harbor.ebruno.fr/link-shortener/link-shortener:1.0.1despite gitdeployment.yaml=:latest. Survived: ImageUpdater CR removal (a4cae78), manifest-cache flush, app-controller restart, hard refreshes. App object verified surgically clean (speckustomize: nullunder 100 s of 2 s-interval monitoring during reconcile; 0 annotations/labels/managedFields; AppSet template clean; external image-updater pod inactiveimages_updated=0). - Root cause: ArgoCD is actually v3.5.1 (not v2.14 as AGENTS.md says). The repo-server's
mergeSourceParameters(reposerver/repository/repository.go~L1863) reads.argocd-source-<appName>.yamlfrom the app's path in the git repo and applies it as a JSON merge-patch onto theApplicationSourceat render time — this is the image-updater's git write-back target. The pin file (kustomize: images: […:1.0.1]) was committed on Aug 15 by the updater's write-back (eab8c76 "build: update of application link-shortener") and survived the CR deletion. Explains everything: clean app spec, clean AppSet, pin on every render, constant manifest cache-key FNV hash across revisions. - Fix (git-only):
git rm kubernetes/link-shortener/.argocd-source-link-shortener.yaml→3a105ed→ hard refresh → reva4cae78→3a105ed→ auto-sync → podlink-shortener-787678d48b:latestRunning, app Synced/Healthy. - Live verification:
POST /api/links {"targetUrl":"https://example.com"}→ 200 with server-generatedshortcode+createdAt;GET /api/links/{code}→ 200;DELETE→ 204 (test row removed). ⚠️ Operational note: any future ArgoCD image-updater (or.argocd-source*.yamlfile) in this repo is load-bearing render state — deleting the updater must also delete its write-back files (repo-wide check:find . -name ".argocd-source*" -not -path "./.git/*"— none remain).
- Symptom: renders pinned
- Woodpecker CI
pact-brokerstep end-to-end GREEN (pipelines #399f3c559+ #408552970, all 5 steps success): Phase 1a consumer pact generation (1/1) → Phase 1b local provider verification (3/3, Quarkus test vs CNPG DB) → Phase 2 publish to in-cluster broker (POST /contracts/publish, version=$CI_COMMIT_SHA) → Phase 3 verification from broker (LinkShortenerProviderPactBrokerTest3/3,-Dpact.verifier.publishResults=true). Test-scope only; build/docker steps untouched. - Fix 1 (SCRAM creds): canonical
quarkus.datasource.username/quarkus.datasource.passwordinsrc/test/resources/application.properties(thedatasource.jdbc.*keys are NOT loaded → pool built without credentials). CI injectsDB_TEST_USER/PASSWORD/URLfrom Woodpecker secretDB_TEST_PASSWORD(Vaultsecret/data/postgresql/link-shortener, app userlinkshortener); SCRAM query-param bypass stays local-only. - Fix 2 (ROOT CAUSE of the Phase 3 401) — broker
PACT_BROKER_BASE_URL: the public host was configured (https://pactbroker.ebruno.fr). pact-jvmPactBrokerClient.fetchConsumersWithSelectorsV2navigates the HAL home via its own client URL (in-cluster → OK) but then reads thepb:provider-pacts-for-verificationlink from the broker's HAL document and POSTs there — that link is generated absolute from the broker's configuredbase_url→ the POST re-hosted tohttps://pactbroker.ebruno.fr/...→ 401 from Traefikfwd-auth(broker auth is disabled; anonymous public traffic 401s). Fix (gitc02f504):broker.config.baseUrl→http://pact-broker.pact-broker.svc:80inhelm/releases/pact-broker/values.yaml(+ legacy copybootstrap/helm-values/pact-broker.yaml) +?v=3→4cache-bust inbootstrap/appset-helm.yaml. Verified:helm template6.1.0 render diff = that one env line; pod env in-cluster; HALfor-verificationnowhttp://pact-broker.pact-broker.svc:80/pacts/provider/{provider}/for-verification; public host still 302→Keycloak (SSO intact).- Broker 2.119.0 details:
SetBaseUrl— single base URL ⇒ always used; multiple (base_urls, space-split — the singular env is split too in this version) ⇒ per-request match (X-Forwarded → raw host → first). Stock chart (6.1.0 pins, 6.4.0 checked) renders only the singularPACT_BROKER_BASE_URL, no generic env splat ⇒ public+in-cluster multi-base impossible without a chart fork; chose single in-cluster URL. - Accepted trade-off: off-cluster consumers see in-cluster absolute HAL links (REST links, UI "view verification results" HAL-browser redirect, badges). Web UI navigation unaffected (relative links; chart docs: baseUrl "is not currently used for links in the UI").
⚠️ One non-reproducible transient: a single probe response (~23:56 UTC) mixed in-clusterpb:providerwith public-hostpb:pactshrefs; same pod/env returned 6/6 fully in-cluster immediately after; single listener in pod netns, 0 restarts, all old RS 0/0; no CI impact (both greens straddled it).
- Broker 2.119.0 details:
- Probe instrumentation removed (link-shortener
8552970): reflection pool probe@BeforeAll+md5()+ shellprintenv|md5sumlines. CI green after removal. - Broker state: consumer versions
6aa7d03/9f3c559/8552970published, verification results published for the last two.can-i-deploy= top-levelGET /can-i-deploy?pacticipant=&version=&to=|environment=route (legacy/pacts/.../can_i_deployis unrouted on this version) — natural hook for a Phase D deploy gate.
- Pipeline now 6 gates:
spec-gate→build(+code↔spec drift) →microcks-contract→pact-broker→deploy-gate→docker-build-push. Commits on Giteabruno/link-shortenermain:bbcc2e2(gates),4ae54d2(Spectral 6.16.3 has noannotatedformat — default formatter),7527837(push baseline viaCI_PREV_COMMIT_SHA, best-effort). Pipelines: #41 failed (bad spectral flag), #424ae54d2+ #437527837fully green (all 7 steps incl. clone). spec-gate(node:22-bookworm; push/pull_request/manual): pinned@stoplight/spectral-cli@6.16.3+ oasdiff 1.29.1 (sha256-pinned tarball). Lint (errors fail, warnings allowed — spec currently 0 errors/23 warnings: operation-description/operationId/tag-defined),oasdiff validate(3.1 conformance), breaking gate: baseline = PR →origin/HEAD, push →CI_PREV_COMMIT_SHA(best-effort fetch) elseHEAD~1; breaking change withoutinfo.versionbump → FATAL, with bump → WARN pass, no baseline → WARN skip.- Code↔spec drift gate (in
build, maven): boots the built jar against the CNPG DB (DB_TEST_PASSWORDsecret), health-loops/q/health/live(~8s), fetches/q/openapi(YAML),oasdiff diff openapi.yaml runtime --exclude-elements examples,extensions --fail-on-diff— code that drifts from the committed contract fails the pipe. - Runtime-spec pins (
application.properties):quarkus.smallrye-openapi.info-title=link-shortener+quarkus.smallrye-openapi.servers=https://link.ebruno.fr— runtime doc previously saidlink-shortener APIand omittedservers; drift diff now empty. deploy-gate(after pact-broker):GET /can-i-deploy?pacticipant=link-shortener&version=$CI_COMMIT_SHA&to=ci— HTTP≠200 or"deployable":falseblocks the image push. Contract live-verified:toparam mandatory (absent → 400); verified SHA →deployable:true; unverified version → 200 +deployable:false(fail-closed).- Known limitation (by design): push-event breaking baseline currently WARN-skips (depth-1 clone;
--deepen/fetch-by-SHA not available from Gitea). PR baseline (origin/main) works; the gate never blocks on missing baseline — it blocks on real breaking changes without a version bump.
- Re-vendored chart 1.14.0 into
helm/vendor/microcks/(tgz frommicrocks.io/helm, sha256 digest-verified). Upstream 1.13.2→1.14.0 delta (full 158-line diff audited): Chart.yaml version bump;configmap.yamlKafka re-key (spring.kafka.producer.*→kafka.producers.{service-changes,asyncapi-triggers}.*+ newmicrocks-asyncapi-triggerschannel) — all inside async sections, inert (features.async.enabled: false); values.yaml default image tags 1.13.2→1.14.0 (app + async-minion; we don't pin the app tag, chart default applies). deployment.yaml byte-identical upstream ⇒ dual-shell mongo readinessProbe patch carried over unchanged. helm lintclean;helm templaterender diff vs patched 1.13.2 = only: app image1.13.2→1.14.0, configmap Kafka re-key (+checksum), fresh self-signed gRPC cert per render (already covered byignoreDifferencesonmicrocks-microcks-grpc-secret→ no live churn).bootstrap/app-microcks.yamlvalueFiles bumped?v=7→8(cache-bust, values themselves unchanged). Commit48aa506pushed;gitops-platformhard-refresh annotated → chain propagated (gitops-platform rev48aa506→microcksapp Synced/Healthy, op Succeeded).- Verified live: pod
microcks-786df49c551/1 — Java 21.0.11, Mongo client CONNECTED tomicrocks-mongodb:27017, GRPC 9090 up, started 23.6s, zero errors/exceptions;/api/health200, UI/200, anonymous/api/services401 (auth enforced). Mongo pod untouched (samelibrary/mongo:7.0.18, no restart);microcks-postman-runtimerolled once purely from shared configmap checksum (benign). CI import path (POST /api/artifact/upload) unchanged by this release.
- Phase E (deferred by user for now): Backstage catalog
resource:apientry linking Microcks service + Pact Broker (stockbackstage:1.53.1image; Microcks plugin would need a custom image build — not required for plain catalog-info links).
- Nothing.
- Vault KV v2 base path is
secret→ ExternalSecretkeyonly needskeycloak(notsecret/data/keycloak). - AppRole auth used for ExternalSecrets instead of static token (more secure;
external-secretspolicy limits tosecret/data/*). - Vault rem合规 Shamir in Helm values — auto-unseal can be added later via
server.autoUnseal. vault-initsecret stores bothroot_token(28-char init token) andadmin_token(95-char full admin) to support Helm init container and manual admin.
- Rebuild all Vault KV data — postgresql/backstage, postgresql/harbor, postgresql/nexus, postgresql/gitea, postgresql/plane, postgresql/sonarqube, postgresql/keycloak, backstage
- Validate SSO OIDC: Verify ArgoCD, Gitea, Harbor, Plane, Microcks OIDC all authenticate against Keycloak
- Vault autoUnseal: Configure raft/wal for resilience (backwards compatible with Shamir fallback)
- Postgres cluster cleanup: Remove deprecated manifest-synced postgres resources (now CNPG managed)
- Update AGENTS.md: Corrected ArGoCD app statuses
Chain: Vault KV-v2 secret/data/microcks (keys username/password = app user userM in db microcks; adminUsername/adminPassword = root user in db admin) → ClusterSecretStore vault (AppRole) → ExternalSecret microcks/microcks-mongodb-connection (refreshInterval: 1h) → K8s secret → pod env at container start.
The gap: the 1h refresh only re-fetches from Vault. Rotating a password in Vault does NOT run alterUser in MongoDB — the running mongod keeps the old user password until it is changed in-DB, and running pods keep the old env until recreated. Rotating Vault first without the in-DB step means the next pod rollout boots with a password MongoDB rejects → crash-loop.
Safe procedure (one-off, maintenance window; DB is empty so risk is low):
- Generate new password X.
- In-DB change first (one-off exec, no value printed): as root —
db.getSiblingDB("microcks").updateUser("userM", {pwd: "X"})(and/ordb.getSiblingDB("admin").updateUser("<adminUsername>", {pwd: "X"})if rotating admin). - Update Vault
secret/data/microcks(password/adminPassword). ExternalSecret syncs the K8s secret within ≤1h (or sooner on next refresh). - Recreate both pods via git only (no
kubectl rollout restart): flip acommonAnnotationsvalue for app + mongodb inhelm/releases/microcks/values.yaml(chart supportscommonAnnotations), bump?v, push. Both workloads re-read env (X) and authenticate against mongod (X). - Verify: mongo pod 1/1, app pod 1/1,
/api/services= 401, no newMongoNodeIsRecovering/auth errors.
- Vault Token Access:
vault-initsecret invaultnamespace storesadmin_token(95-charhvs.CAES...full admin) androot_token(28 char) — this is key used by Helm init container and for all Vault CLI operations. - Vault Token State: Updated 2026-07-13. Token is fresh from init container. Will need to rotate if SHA rebuilt.
- ExternalSecrets key fix: The
key: keycloak(notsecret/data/keycloak) prevents the path from doubling. All 10 manifests now consistent. - vault-approle K8s secret: ExternalSecrets Operator detects
vault-approleinexternal-secretsns. needs proper role/secret-id. - Keycloak admin password: Raw base64-encoded value from
keycloak-secretssecret inkeycloaknamespace. Keycloak HTTP frontend URL and - Keycloak access:
admin-cliclient with password fromkeycloak-secretsinkeycloaknamespace. All 9 confidential clients exist inplatformrealm with secrets real. - ArgoCD SSO OIDC: Controlled by
helm/releases/argocd/oidc/credentials-secretsK8s secret. - Plane app: Last sync updated 2026-07-09. Target revision 1.6.0. Values file:
bootstrap/helm-values/plane.yaml.
kubernetes/external-secrets/cluster-secret-store.yaml— ClusterSecretStore for Vault (AppRole auth,path: secret,version: v2)bootstrap/appset-manifests.yaml— ArgoCD AppSet manifests (addedkubernetes/external-secretsdirectory)helm/releases/vault/values.yaml— Vault Helm (replicas: 1, admin_token, OIDC init container)bootstrap/helm-values/vault.yaml— Vault base Helm values (secret/data/keycloak)kubernetes/postgresql/keycloak-external-secret.yaml— Keycloak postgres secrets from Vaultkubernetes/postgresql/argocd-oidc-external-secret.yaml— ArgoCD OIDC secrets from Vaultkeycloakpathkubernetes/postgresql/argocd-server-oidc-credentials.yaml— ArgoCD server OIDC client secretkubernetes/postgresql/gitea-oidc-external-secret.yaml— Gitea OIDC secrets from Vaultkubernetes/postgresql/plane-oidc-external-secret.yaml— Plane OIDC secrets from Vaultkubernetes/postgresql/grafana-oidc-external-secret.yaml— Grafana OIDC secrets from Vaultkubernetes/postgresql/harbor-oidc-external-secret.yaml— Harbor OIDC secrets from Vaultkubernetes/postgresql/keycloak-external-secret.yaml— Keycloak secrets from Vaultkubernetes/oauth2-proxy/oauth2-proxy-external-secret.yaml— oauth2-proxy secrets from Vaultkubernetes/postgresql/proxy-external-secret.yaml— Vault OIDC client secret from Vaulthelm/releases/argocd/values.yaml— ArgoCD OIDC config (server.extraArgs.oidc.config)bootstrap/helm-values/harbor.yaml— Harbor OIDC (auth.oidc.loginPageUrl,oidc.issuer)bootstrap/helm-values/plane.yaml— Plane Helm values (OIDC, ingress, etc.)bootstrap/helm-values/kube-prometheus.yaml— Grafana/Alertmanager OIDC configbootstrap/k8s-apps/plane.yaml— Deprecated (helm apps stored in bootstrap)bootstrap/app-microcks.yaml— Microcks Application; git-hosted chart source (path: helm/vendor/microcks, nochartfield), valueFiles?v=7helm/vendor/microcks/— Vendored chart 1.13.2; onlytemplates/deployment.yamlmodified (dual-shell readinessProbe,timeoutSeconds: 1→5)helm/releases/microcks/values.yaml—mongodb.image.tag: 7.0.18kubernetes/postgresql/microcks-mongodb-external-secret.yaml— Vault→K8s MongoDB creds (M1 chain)