diff --git a/.github/workflows/on-pr.yaml b/.github/workflows/on-pr.yaml index cc74cc6..4aa3f65 100644 --- a/.github/workflows/on-pr.yaml +++ b/.github/workflows/on-pr.yaml @@ -27,28 +27,28 @@ permissions: jobs: validate: - uses: unbounded-tech/workflows-crossplane/.github/workflows/validate.yaml@v2.20.0 + uses: hops-ops/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0 with: examples: | [ - { "example": "examples/authstacks/minimal.yaml" }, - { "example": "examples/authstacks/standard.yaml" }, - { "example": "examples/authstacks/local-colima.yaml" } + { "example": "examples/authstacks/minimal.yaml", "api_path": "apis/authstacks" }, + { "example": "examples/authstacks/standard.yaml", "api_path": "apis/authstacks" }, + { "example": "examples/authstacks/local-colima.yaml", "api_path": "apis/authstacks" } ] - api_path: apis/authstacks error_on_missing_schemas: true test: - uses: unbounded-tech/workflows-crossplane/.github/workflows/test.yaml@v2.20.0 + uses: hops-ops/workflows-crossplane/.github/workflows/test.yaml@v3.0.0 e2e: - uses: unbounded-tech/workflows-crossplane/.github/workflows/e2e.yaml@v2.20.0 + uses: hops-ops/workflows-crossplane/.github/workflows/e2e.yaml@v3.0.0 publish: needs: - validate - test - uses: unbounded-tech/workflows-crossplane/.github/workflows/publish.yaml@v2.20.0 - secrets: inherit + uses: hops-ops/workflows-crossplane/.github/workflows/publish.yaml@v3.0.0 + secrets: + GH_PAT: ${{ secrets.GH_PAT }} with: tag: pr-${{ github.event.pull_request.number }}-${{ github.sha }} diff --git a/.github/workflows/on-push-main.yaml b/.github/workflows/on-push-main.yaml index c92c83e..4f7b49c 100644 --- a/.github/workflows/on-push-main.yaml +++ b/.github/workflows/on-push-main.yaml @@ -23,22 +23,21 @@ permissions: jobs: validate: - uses: unbounded-tech/workflows-crossplane/.github/workflows/validate.yaml@v2.20.0 + uses: hops-ops/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0 with: examples: | [ - { "example": "examples/authstacks/minimal.yaml" }, - { "example": "examples/authstacks/standard.yaml" }, - { "example": "examples/authstacks/local-colima.yaml" } + { "example": "examples/authstacks/minimal.yaml", "api_path": "apis/authstacks" }, + { "example": "examples/authstacks/standard.yaml", "api_path": "apis/authstacks" }, + { "example": "examples/authstacks/local-colima.yaml", "api_path": "apis/authstacks" } ] - api_path: apis/authstacks error_on_missing_schemas: true test: - uses: unbounded-tech/workflows-crossplane/.github/workflows/test.yaml@v2.20.0 + uses: hops-ops/workflows-crossplane/.github/workflows/test.yaml@v3.0.0 e2e: - uses: unbounded-tech/workflows-crossplane/.github/workflows/e2e.yaml@v2.20.0 + uses: hops-ops/workflows-crossplane/.github/workflows/e2e.yaml@v3.0.0 version-and-tag: name: Version and Tag diff --git a/Makefile b/Makefile index 53b68b6..1608799 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,28 @@ SHELL := /bin/bash PACKAGE ?= auth-stack +# Default XRD_DIR for legacy single-API targets; multi-API targets derive per-example. XRD_DIR := apis/authstacks COMPOSITION := $(XRD_DIR)/composition.yaml DEFINITION := $(XRD_DIR)/definition.yaml -CONFIGURATION := $(XRD_DIR)/configuration.yaml EXAMPLE_DEFAULT := examples/authstacks/standard.yaml RENDER_TESTS := $(wildcard tests/test-*) E2E_TESTS := $(wildcard tests/e2etest-*) +# Multi-API support: examples//.yaml maps to apis//. +# Helper macro: api_dir_for(example_path) → apis/ +api-dir = apis/$(word 2,$(subst /, ,$(1))) + clean: rm -rf _output rm -rf .up - rm -f $(CONFIGURATION) build: up project build -generate-configuration: - @set -euo pipefail; \ - hops validate generate-configuration --path . --api-path "$(XRD_DIR)" - # Examples list - mirrors GitHub Actions workflow # Format: example_path::observed_resources_path (observed_resources_path is optional) +# api_path is derived from example_path via the api-dir macro (examples//... → apis//). EXAMPLES := \ examples/authstacks/minimal.yaml:: \ examples/authstacks/standard.yaml:: \ @@ -35,14 +35,17 @@ render\:all: for entry in $(EXAMPLES); do \ example=$${entry%%::*}; \ observed=$${entry#*::}; \ + api_dir=$$(echo "$$example" | awk -F/ '{print "apis/" $$2}'); \ + composition="$$api_dir/composition.yaml"; \ + definition="$$api_dir/definition.yaml"; \ outfile="$$tmpdir/$$(echo $$entry | tr '/:' '__')"; \ ( \ if [ -n "$$observed" ]; then \ echo "=== Rendering $$example with observed-resources $$observed ==="; \ - up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example --observed-resources=$$observed; \ + up composition render --xrd=$$definition $$composition $$example --observed-resources=$$observed; \ else \ - echo "=== Rendering $$example ==="; \ - up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example; \ + echo "=== Rendering $$example (api=$$api_dir) ==="; \ + up composition render --xrd=$$definition $$composition $$example; \ fi; \ echo "" \ ) > "$$outfile" 2>&1 & \ @@ -59,24 +62,27 @@ render\:all: exit $$failed # Validate all examples (parallel execution, output shown per-job when complete) -validate\:all: generate-configuration +validate\:all: @tmpdir=$$(mktemp -d); \ pids=""; \ for entry in $(EXAMPLES); do \ example=$${entry%%::*}; \ observed=$${entry#*::}; \ + api_dir=$$(echo "$$example" | awk -F/ '{print "apis/" $$2}'); \ + composition="$$api_dir/composition.yaml"; \ + definition="$$api_dir/definition.yaml"; \ outfile="$$tmpdir/$$(echo $$entry | tr '/:' '__')"; \ ( \ if [ -n "$$observed" ]; then \ echo "=== Validating $$example with observed-resources $$observed ==="; \ - up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \ + up composition render --xrd=$$definition $$composition $$example \ --observed-resources=$$observed --include-full-xr --quiet | \ - crossplane beta validate $(XRD_DIR) --error-on-missing-schemas -; \ + crossplane beta validate $$api_dir --error-on-missing-schemas -; \ else \ - echo "=== Validating $$example ==="; \ - up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \ + echo "=== Validating $$example (api=$$api_dir) ==="; \ + up composition render --xrd=$$definition $$composition $$example \ --include-full-xr --quiet | \ - crossplane beta validate $(XRD_DIR) --error-on-missing-schemas -; \ + crossplane beta validate $$api_dir --error-on-missing-schemas -; \ fi; \ echo "" \ ) > "$$outfile" 2>&1 & \ @@ -93,16 +99,16 @@ validate\:all: generate-configuration exit $$failed # Shorthand aliases -.PHONY: render validate generate-configuration +.PHONY: clean build test e2e publish render validate render: ; @$(MAKE) 'render:all' -validate: ; @$(MAKE) generate-configuration 'validate:all' +validate: ; @$(MAKE) 'validate:all' -# Single example targets +# Single example targets (legacy — uses default XRD_DIR for examples/authstacks/.yaml) render\:%: @example="examples/authstacks/$*.yaml"; \ up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example -validate\:%: generate-configuration +validate\:%: @example="examples/authstacks/$*.yaml"; \ up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \ --include-full-xr --quiet | \ @@ -113,3 +119,7 @@ test: e2e: up test run $(E2E_TESTS) --e2e + +publish: + @if [ -z "$(tag)" ]; then echo "Error: tag is not set. Usage: make publish tag="; exit 1; fi + up project build --push --tag $(tag) diff --git a/README.md b/README.md index 519c49e..404cf9f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # auth-stack -Installs Zitadel into a Kubernetes cluster as the platform identity provider. Wraps the upstream `zitadel/zitadel` Helm chart with a typed XRD surface that handles the namespace, the database wiring, Gateway API routing, and re-projects the chart-managed bootstrap secrets (admin PAT + login-client PAT) into XR status for downstream consumers. +Installs Zitadel into a Kubernetes cluster as the platform identity provider, and hosts a focused set of `auth.hops.ops.com.ai` primitive XRDs (`HumanUser`, `MachineUser`, `Grant`, `IDP`) that compose against the installed Zitadel. + +The stack XRD (`AuthStack`) wraps the upstream `zitadel/zitadel` Helm chart — handling the namespace, database wiring, Gateway API routing, and re-projecting chart-managed bootstrap secrets (admin PAT + login-client PAT) into XR status for downstream consumers. + +The primitive XRDs let operators declaratively manage the identity model inside a running Zitadel — see [Auth-group primitives](#auth-group-primitives) below. ## Quick Start @@ -76,6 +80,21 @@ status: loginClientPatSecretRef: { name: login-client, namespace: zitadel, key: pat } ``` +## Auth-group primitives + +Per [[specs/identity-architecture]], the auth-group primitive XRDs that have substantive composition value-add — `HumanUser`, `MachineUser`, `Grant`, `IDP` — live in this repo alongside `AuthStack` under the `auth.hops.ops.com.ai` group. + +Status: + +| Kind | Plural | Composes | Status | +|---|---|---|---| +| `HumanUser` | `humanusers` | `HumanUser` + optional `UserIDPLink` MRs | TO WRITE | +| `MachineUser` | `machineusers` | `MachineUser` + `PAT` + AWS SM push pipeline | TO WRITE | +| `Grant` | `grants` | `ProjectMember` (same-Org) or `ProjectGrant + GrantMember` (cross-Org) | TO WRITE | +| `IDP` | `idps` | polymorphic over `GoogleIDP` / `GitHubIDP` / `OIDCIDP` / `SAMLIDP` | TO WRITE | + +Operators apply raw Zitadel `Org` / `Project` / `Role` MRs (`org.zitadel.m.crossplane.io`, `project.zitadel.m.crossplane.io`) directly when they need them — the `Tenant` business kind in [[tenant-stack]] composes the initial set during Tenant scaffolding. + ## Cross-Stack Integration The intent is for consumer stacks (gitops/ArgoCD, observe/Grafana, the-website) to wire to AuthStack's status surface rather than configuring OIDC manually. Today, those consumers still need a Zitadel OIDC application created out-of-band (via the Zitadel UI/API) and a client ID/secret provided to them. Once the Zitadel Crossplane provider lands, consumer stacks can declaratively create OIDC applications by referencing `status.bootstrap.iamAdminPatSecretRef`. diff --git a/apis/authstacks/configuration.yaml b/apis/authstacks/configuration.yaml deleted file mode 100644 index 40e33fd..0000000 --- a/apis/authstacks/configuration.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: meta.pkg.crossplane.io/v1alpha1 -kind: Configuration -metadata: - name: auth-stack - annotations: - meta.crossplane.io/maintainer: Patrick Lee Scott - meta.crossplane.io/source: github.com/hops-ops/auth-stack - meta.crossplane.io/description: AuthStack installs Zitadel as the platform identity provider via the upstream Helm chart, with typed integration points for PostgreSQL and Gateway API. -spec: - dependsOn: - - function: xpkg.crossplane.io/crossplane-contrib/function-auto-ready - version: '>=v0.6.3' - - provider: xpkg.crossplane.io/crossplane-contrib/provider-helm - version: '>=v1' - - provider: xpkg.crossplane.io/crossplane-contrib/provider-kubernetes - version: '>=v1' diff --git a/examples/authstacks/local-colima.yaml b/examples/authstacks/local-colima.yaml index fe1812e..c3774ae 100644 --- a/examples/authstacks/local-colima.yaml +++ b/examples/authstacks/local-colima.yaml @@ -1,7 +1,8 @@ # Local-dev manifest for colima. # Pairs with: a `PSQLCluster` named `zitadel-pg` whose `app.externalSecret` -# pulls credentials from AWS SM at `zitadel/app`, and a masterkey at -# `zitadel/masterkey` synced via `hops secrets sync aws`. +# pulls credentials from AWS SM at `zitadel/app`, and a masterkey + +# admin-password under a single AWS SM entry at `zitadel/credentials` +# synced via `hops secrets sync aws`. apiVersion: hops.ops.com.ai/v1alpha1 kind: AuthStack metadata: @@ -12,11 +13,13 @@ spec: domain: auth.localtest.me externalSecure: false + # PULL flow: AuthStack ExternalSecret pulls masterkey + admin-password + # from a single AWS SM JSON blob keyed by secretPath. Both values are + # seeded together by `hops auth bootstrap `. externalSecrets: enabled: true secretStoreName: hops-aws-secrets-manager - masterkey: - secretPath: zitadel/masterkey + secretPath: zitadel/credentials firstInstance: org: hops-ops diff --git a/tests/test-render/main.k b/tests/test-render/main.k index 8f7c88e..7e7f8ea 100644 --- a/tests/test-render/main.k +++ b/tests/test-render/main.k @@ -1,4 +1,3 @@ -import models.ai.com.ops.hops.v1alpha1 as stacksv1alpha1 import models.io.upbound.dev.meta.v1alpha1 as metav1alpha1 # ============================================================================== @@ -19,7 +18,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "mk-secret" spec = { clusterName = "test-cluster" @@ -52,7 +53,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "mk-inline" spec = { clusterName = "test-cluster" @@ -86,8 +89,11 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "pg-auth" + metadata.namespace = "zitadel" spec = { clusterName = "test-cluster" domain = "auth.example.com" @@ -128,7 +134,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "emb-auth" metadata.namespace = "default" spec = { @@ -188,7 +196,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "eso-auth" spec = { clusterName = "test-cluster" @@ -244,7 +254,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "ext-auth" spec = { clusterName = "test-cluster" @@ -285,7 +297,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "gw-auth" spec = { clusterName = "test-cluster" @@ -336,7 +350,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "labeled-auth" spec = { clusterName = "test-cluster" @@ -391,7 +407,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "no-push" spec = { clusterName = "test-cluster" @@ -425,7 +443,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "ps-defaults" spec = { clusterName = "test-cluster" @@ -508,7 +528,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "ps-explicit" spec = { clusterName = "test-cluster" @@ -559,7 +581,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "ps-off" spec = { clusterName = "test-cluster" @@ -598,7 +622,9 @@ _items = [ xrdPath = "apis/authstacks/definition.yaml" timeoutSeconds = 60 validate = False - xr = stacksv1alpha1.AuthStack { + xr = { + apiVersion = "hops.ops.com.ai/v1alpha1" + kind = "AuthStack" metadata.name = "fi-auth" spec = { clusterName = "test-cluster" diff --git a/tests/test-render/model b/tests/test-render/model new file mode 120000 index 0000000..faff6e4 --- /dev/null +++ b/tests/test-render/model @@ -0,0 +1 @@ +../../.up/kcl/models \ No newline at end of file diff --git a/upbound.yaml b/upbound.yaml index 8b50477..905810a 100644 --- a/upbound.yaml +++ b/upbound.yaml @@ -18,7 +18,8 @@ spec: version: '>=v1' description: AuthStack installs Zitadel as the platform identity provider via the upstream Helm chart, with typed integration points for PostgreSQL and Gateway - API. + API. Also hosts a focused set of auth.hops.ops.com.ai primitive XRDs (HumanUser, + MachineUser, Grant, IDP) that compose against the installed Zitadel. license: Apache-2.0 maintainer: Patrick Lee Scott readme: |