Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/on-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ jobs:
{ "example": "examples/gitopsstacks/eso.yaml" },
{ "example": "examples/gitopsstacks/eso.yaml", "observed_resources": "examples/test/mocks/observed-resources/eso/steps/1/" },
{ "example": "examples/gitopsstacks/eso.yaml", "observed_resources": "examples/test/mocks/observed-resources/eso/steps/2/" },
{ "example": "examples/gitopsstacks/eso.yaml", "observed_resources": "examples/test/mocks/observed-resources/eso/steps/3/" }
{ "example": "examples/gitopsstacks/eso.yaml", "observed_resources": "examples/test/mocks/observed-resources/eso/steps/3/" },
{ "example": "examples/gitopsstacks/zitadel-oidc.yaml" },
{ "example": "examples/gitopsstacks/zitadel-oidc.yaml", "observed_resources": "examples/test/mocks/observed-resources/zitadel-oidc/steps/1/" }
]
api_path: apis/gitopsstacks
error_on_missing_schemas: true
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/on-push-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ jobs:
{ "example": "examples/gitopsstacks/eso.yaml" },
{ "example": "examples/gitopsstacks/eso.yaml", "observed_resources": "examples/test/mocks/observed-resources/eso/steps/1/" },
{ "example": "examples/gitopsstacks/eso.yaml", "observed_resources": "examples/test/mocks/observed-resources/eso/steps/2/" },
{ "example": "examples/gitopsstacks/eso.yaml", "observed_resources": "examples/test/mocks/observed-resources/eso/steps/3/" }
{ "example": "examples/gitopsstacks/eso.yaml", "observed_resources": "examples/test/mocks/observed-resources/eso/steps/3/" },
{ "example": "examples/gitopsstacks/zitadel-oidc.yaml" },
{ "example": "examples/gitopsstacks/zitadel-oidc.yaml", "observed_resources": "examples/test/mocks/observed-resources/zitadel-oidc/steps/1/" }
]
api_path: apis/gitopsstacks
error_on_missing_schemas: true
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ EXAMPLES := \
examples/gitopsstacks/eso.yaml:: \
examples/gitopsstacks/eso.yaml::examples/test/mocks/observed-resources/eso/steps/1/ \
examples/gitopsstacks/eso.yaml::examples/test/mocks/observed-resources/eso/steps/2/ \
examples/gitopsstacks/eso.yaml::examples/test/mocks/observed-resources/eso/steps/3/
examples/gitopsstacks/eso.yaml::examples/test/mocks/observed-resources/eso/steps/3/ \
examples/gitopsstacks/zitadel-oidc.yaml:: \
examples/gitopsstacks/zitadel-oidc.yaml::examples/test/mocks/observed-resources/zitadel-oidc/steps/1/

# Render all examples (parallel execution, output shown per-job when complete)
render\:all:
Expand Down
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ A single Crossplane resource that provisions a complete GitOps foundation: ArgoC
└── only if crossplane.enabled ──┘
```

**Up to 7 composed resources:** 1 Helm Release + 1 GitHub Repository + up to 4 Kubernetes Objects + 1 Usage protection
The base stack composes a Helm Release, GitHub Repository, ArgoCD Applications,
and deletion-ordering Usages. Optional OIDC adds target-cluster objects for the
Zitadel credentials, ProviderConfig, Project, OIDC application, and labeled
client Secret.

| Resource | Type | Purpose |
|----------|------|---------|
| ArgoCD | Helm Release (`argo-cd` v9.4.3) | Continuous delivery platform |
| ArgoCD | Helm Release (`argo-cd` v9.7.1) | Continuous delivery platform |
| GitHub Repository | `repo.github.m.upbound.io` | GitOps source repository |
| Projects Application | Kubernetes Object (ArgoCD Application) | Syncs ArgoCD projects from the repo |
| Deletion Usage | Usage | Ensures projects app deletes before ArgoCD |
Expand Down Expand Up @@ -171,6 +174,45 @@ spec:

`overrideAllValues` replaces **all** defaults — chart defaults, monitoring config, everything. Use `values` for additive changes instead.

### Stage 6: Gateway exposure and Zitadel OIDC

Expose ArgoCD through an existing Gateway listener and configure ArgoCD's
native OIDC support. GitopsStack creates a dedicated Zitadel Project and OIDC
application in the target cluster. The generated client secret stays in
Kubernetes and is referenced by name from `argocd-cm`; it is never stored in
the XR or Helm values.

```yaml
spec:
exposure:
enabled: true
hostname: argocd.example.com
gatewayRef:
name: platform
namespace: istio-ingress
sectionName: https
auth:
oidc:
enabled: true
issuerURL: https://auth.example.com
rbac:
adminEmails:
- platform-admin@example.com
zitadel:
# AuthStack status.providerConfig.awsSecretsManagerPath
awsSecretsManagerPath: push/my-cluster/zitadel-credentials
```

Prerequisites are Gateway API, External Secrets Operator with the configured
ClusterSecretStore, Crossplane's namespaced Zitadel provider in the target
cluster, and an AuthStack-published management access token. TLS terminates at
the Gateway, so the generated HTTPRoute sends cleartext HTTP to ArgoCD inside
the cluster. ArgoCD CLI users should use `--grpc-web` through this HTTPRoute.

When enabled, typed exposure and OIDC settings take precedence over conflicting
raw Helm values so the public URL, callback, and client remain consistent.
Local ArgoCD admin login remains enabled as a break-glass path.

## Creation Order

Resources are created as their dependencies become ready:
Expand Down Expand Up @@ -232,15 +274,26 @@ The Usage ensures ArgoCD CRDs stay alive until all ArgoCD Application CRs are cl
| `argocd.namespace` | string | no | `namespace` | Per-component namespace override |
| `argocd.values` | object | no | `{}` | Helm values merged with defaults |
| `argocd.overrideAllValues` | object | no | — | Helm values replacing all defaults |
| `exposure.enabled` | boolean | no | `false` | Create an ArgoCD HTTPRoute |
| `exposure.hostname` | string | with exposure | — | Public ArgoCD hostname |
| `exposure.gatewayRef.*` | object | no | `platform` / `istio-ingress` / `https` | Existing Gateway listener |
| `auth.oidc.enabled` | boolean | no | `false` | Enable native ArgoCD OIDC with Zitadel |
| `auth.oidc.issuerURL` | string | with OIDC | — | Zitadel issuer URL |
| `auth.oidc.rbac.adminEmails` | []string | no | `[]` | Explicit email-to-admin mappings |
| `auth.oidc.zitadel.awsSecretsManagerPath` | string | with OIDC | — | AuthStack-published PAT path |
| `repository.org` | string | yes | — | GitHub organization |
| `repository.name` | string | no | `{clusterName}-gitops` | Repository name |
| `repository.externalName` | string | no | — | Existing repository name to import instead of creating a new repository |
| `repository.description` | string | no | auto-generated | Repository description |
| `repository.visibility` | string | no | `private` | `public`, `private`, or `internal` |
| `repository.autoInit` | boolean | no | `true` | Create initial commit (ignored when template is set) |
| `repository.template.owner` | string | no | — | Template repo owner |
| `repository.template.repository` | string | no | — | Template repo name |
| `repository.topics` | []string | no | `[]` | Repository topics |
| `repository.deleteBranchOnMerge` | boolean | no | `true` | Auto-delete head branches on merge |
| `externalSecrets.githubToken.secretPath` | string | no | — | AWS Secrets Manager secret containing a GitHub token |
| `externalSecrets.githubToken.tokenKey` | string | no | `token` | JSON key containing the token |
| `externalSecrets.githubToken.username` | string | no | `x-access-token` | HTTPS username for ArgoCD repository credentials |
| `applications.apps.enabled` | boolean | no | `true` | Deploy the root app-of-apps Application |
| `applications.apps.path` | string | no | `apps` | Path in repo to sync |
| `applications.crossplane.enabled` | boolean | no | `false` | Deploy Crossplane integration apps |
Expand All @@ -262,7 +315,7 @@ global:
| Field | Type | Description |
|-------|------|-------------|
| `status.ready` | boolean | `true` when all composed resources report Ready |
| `status.repository.url` | string | Full URL of the created GitHub repository |
| `status.repository.url` | string | Full URL of the managed GitHub repository |

## Dependencies

Expand Down
118 changes: 117 additions & 1 deletion apis/gitopsstacks/definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,94 @@ spec:
description: Helm values that replace all defaults.
type: object
x-kubernetes-preserve-unknown-fields: true
exposure:
description: Optional public exposure of the ArgoCD server through an existing Gateway API Gateway.
type: object
properties:
enabled:
description: Enable an HTTPRoute for the ArgoCD server. Defaults to false.
type: boolean
default: false
hostname:
description: Public DNS hostname for ArgoCD.
type: string
minLength: 1
gatewayRef:
description: Existing Gateway listener that accepts the ArgoCD HTTPRoute.
type: object
properties:
name:
description: Gateway name. Defaults to platform.
type: string
namespace:
description: Gateway namespace. Defaults to istio-ingress.
type: string
sectionName:
description: Gateway listener section name. Defaults to https.
type: string
x-kubernetes-validations:
- rule: "!self.enabled || has(self.hostname)"
message: spec.exposure.hostname is required when exposure is enabled
auth:
description: Authentication configuration for ArgoCD.
type: object
properties:
oidc:
description: Native ArgoCD OIDC backed by a Zitadel application managed in the target cluster.
type: object
properties:
enabled:
description: Enable native ArgoCD OIDC with Zitadel. Defaults to false.
type: boolean
default: false
name:
description: Identity provider display name. Defaults to Zitadel.
type: string
issuerURL:
description: Public Zitadel OIDC issuer URL.
type: string
pattern: '^https://[^/?#[:space:]]+(?:/[^?#[:space:]]*)?$'
requestedScopes:
description: OIDC scopes requested by ArgoCD. Defaults to openid, profile, and email.
type: array
minItems: 1
items:
type: string
rbac:
description: Minimal ArgoCD RBAC mappings for OIDC identities.
type: object
properties:
adminEmails:
description: Email addresses explicitly mapped to ArgoCD's built-in admin role.
type: array
items:
type: string
format: email
zitadel:
description: Zitadel management inputs used to create the Project and OIDC application.
type: object
properties:
awsSecretsManagerPath:
description: AWS Secrets Manager path containing the AuthStack access token.
type: string
minLength: 1
accessTokenProperty:
description: JSON property containing the access token. Defaults to access_token.
type: string
secretStoreName:
description: ClusterSecretStore used to read the access token. Defaults to default.
type: string
projectName:
description: Zitadel Project name. Defaults to {clusterName}-argocd.
type: string
applicationName:
description: Zitadel OIDC application name. Defaults to {clusterName}-argocd.
type: string
required:
- awsSecretsManagerPath
x-kubernetes-validations:
- rule: "!self.enabled || (has(self.issuerURL) && has(self.zitadel))"
message: spec.auth.oidc.issuerURL and spec.auth.oidc.zitadel are required when OIDC is enabled
repository:
description: Configuration for the GitOps GitHub repository.
type: object
Expand All @@ -135,6 +223,9 @@ spec:
name:
description: Repository name. Defaults to {clusterName}-gitops.
type: string
externalName:
description: Existing GitHub repository name to import instead of creating a new repository.
type: string
description:
description: Repository description.
type: string
Expand Down Expand Up @@ -177,7 +268,7 @@ spec:
type: object
properties:
enabled:
description: Enable ESO integration. Creates an ExternalSecret to pull GitHub App credentials from AWS Secrets Manager into ArgoCD repo-creds.
description: Enable ESO integration. Creates an ExternalSecret to pull GitHub App or token credentials from AWS Secrets Manager into ArgoCD repo-creds.
type: boolean
secretStoreName:
description: Name of the ClusterSecretStore created by SecretStack. Defaults to "default".
Expand All @@ -200,6 +291,21 @@ spec:
type: string
required:
- secretPath
githubToken:
description: GitHub token credential reference in AWS Secrets Manager. Used instead of githubApp when configured.
type: object
properties:
secretPath:
description: AWS Secrets Manager secret name containing the GitHub token.
type: string
tokenKey:
description: JSON key for the GitHub token within the secret. Defaults to "token".
type: string
username:
description: HTTPS username written to the ArgoCD repo-creds Secret. Defaults to "x-access-token".
type: string
required:
- secretPath
applications:
description: ArgoCD Application configuration.
type: object
Expand All @@ -223,6 +329,9 @@ spec:
type: boolean
required:
- clusterName
x-kubernetes-validations:
- rule: "!has(self.auth) || !has(self.auth.oidc) || !self.auth.oidc.enabled || (has(self.exposure) && self.exposure.enabled)"
message: spec.exposure.enabled must be true when native OIDC is enabled
status:
description: GitopsStackStatus defines the observed state.
type: object
Expand All @@ -237,5 +346,12 @@ spec:
url:
description: Full URL of the gitops repository.
type: string
oidc:
description: Observed ArgoCD OIDC client state.
type: object
properties:
clientId:
description: Zitadel client ID observed from the managed OIDC application.
type: string
required:
- spec
31 changes: 31 additions & 0 deletions examples/gitopsstacks/zitadel-oidc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: hops.ops.com.ai/v1alpha1
kind: GitopsStack
metadata:
name: gitops
namespace: example-env
spec:
clusterName: example-cluster
labels:
team: platform
repository:
org: hops-ops
name: example-cluster-gitops
exposure:
enabled: true
hostname: argocd.example.com
gatewayRef:
name: platform
namespace: istio-ingress
sectionName: https
auth:
oidc:
enabled: true
issuerURL: https://auth.example.com
rbac:
adminEmails:
- platform-admin@example.com
zitadel:
awsSecretsManagerPath: push/example-cluster/zitadel-credentials
applications:
apps:
enabled: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: kubernetes.m.crossplane.io/v1alpha1
kind: Object
metadata:
name: gitops-zitadel-oidc-client
namespace: example-env
annotations:
crossplane.io/composition-resource-name: zitadel-oidc-client
gotemplating.fn.crossplane.io/composition-resource-name: zitadel-oidc-client
status:
conditions:
- type: Ready
status: "True"
- type: Synced
status: "True"
Loading
Loading