A grab-bag of fixes the OKD staging install surfaced. Each landed as a diagnosable failure during real deploys: * URL parametrization. ZitadelSetupScore was hardcoded to `http://127.0.0.1:{port}` with a `Host:` header — fine for k3d port-forward, broken everywhere else. Adds `scheme: ZitadelScheme` (Http/Https), `port: Option<u16>` (None → scheme default), and `endpoint: Option<String>` for the rare port-forward case. The `Host:` header is now only injected when `endpoint` is set. * HTTP readiness gate. Helm reports SUCCESS when pods are Ready but on OKD the Route + cert-manager Certificate reconcile asynchronously — the first management call after install was dying with `CaUsedAsEndEntity` (rustls rejecting OKD's bootstrap CA cert served while cert-manager was still issuing). Score now polls `/debug/ready` with retry; treats connect / TLS errors as transient. * Admin password persistence. ZitadelScore was generating a fresh random password on every run, then printing it in the success banner — but Zitadel's chart only honors FirstInstance.* on the first install, so the printed password didn't match what was live in the DB. Now persisted via harmony_secret (LocalFile by default). * Login banner shows full SSO loginName. Default Zitadel org name is ZITADEL → org primary domain is `zitadel.<ExternalDomain>` → admin preferredLoginName is `admin@zitadel.<host>`. Print the full string so the operator pastes the right value. * Shared TLS Secret across Zitadel + login Ingresses. Two cert-manager-annotated Ingresses on the same host create two Certificates → two ACME Orders → competing HTTP01 challenges; the loser's Secret never lands and on OKD the second Ingress's Route is silently never admitted because the controller inlines TLS material into the Route at creation time. Login Ingress now references `zitadel-tls` (same as main) and drops its cert-manager.io annotation. Documented in docs/guides/kubernetes-ingress.md as the canonical pattern with the diagnostic signature so this doesn't get rediscovered. * fleet_staging_deploy namespaces. The OLDER staging deploy example hardcoded `fleet-system` / `zitadel`; renamed to `fleet-staging` / `zitadel-staging` to match `fleet_staging_install`'s convention. Five example call sites updated for the new ZitadelSetupScore shape; fleet_e2e_demo / fleet_auth_callout / harmony_sso pass the k3d port-forward as `endpoint: Some("http://127.0.0.1:8080")`, the staging examples take the defaults (direct https on 443). Tests: 8 new unit tests in setup.rs lock the URL builder, Host-header conditional, scheme serde, and minimal-fields deserialization. One new test in setup_score covers render_toml.
203 lines
6.7 KiB
Markdown
203 lines
6.7 KiB
Markdown
# Ingress Resources in Harmony
|
|
|
|
Harmony generates standard Kubernetes `networking.k8s.io/v1` Ingress resources. This ensures your deployments are portable across any Kubernetes distribution (vanilla K8s, OKD/OpenShift, K3s, etc.) without requiring vendor-specific configurations.
|
|
|
|
By default, Harmony does **not** set `spec.ingressClassName`. This allows the cluster's default ingress controller to automatically claim the resource, which is the correct approach for most single-controller clusters.
|
|
|
|
---
|
|
|
|
## TLS Configurations
|
|
|
|
There are two portable TLS modes for Ingress resources. Use only these in your Harmony deployments.
|
|
|
|
### 1. Plain HTTP (No TLS)
|
|
|
|
Omit the `tls` block entirely. The Ingress serves traffic over plain HTTP. Use this for local development or when TLS is terminated elsewhere (e.g., by a service mesh or external load balancer).
|
|
|
|
```yaml
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: my-app
|
|
namespace: my-ns
|
|
spec:
|
|
rules:
|
|
- host: app.example.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: my-app
|
|
port:
|
|
number: 8080
|
|
```
|
|
|
|
### 2. HTTPS with a Named TLS Secret
|
|
|
|
Provide a `tls` block with both `hosts` and a `secretName`. The ingress controller will use that Secret for TLS termination. The Secret must be a `kubernetes.io/tls` type in the same namespace as the Ingress.
|
|
|
|
There are two ways to provide this Secret.
|
|
|
|
#### Option A: Manual Secret
|
|
|
|
Create the TLS Secret yourself before deploying the Ingress. This is suitable when certificates are issued outside the cluster or managed by another system.
|
|
|
|
```yaml
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: my-app
|
|
namespace: my-ns
|
|
spec:
|
|
rules:
|
|
- host: app.example.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: my-app
|
|
port:
|
|
number: 8080
|
|
tls:
|
|
- hosts:
|
|
- app.example.com
|
|
secretName: app-example-com-tls
|
|
```
|
|
|
|
#### Option B: Automated via cert-manager (Recommended)
|
|
|
|
Add the `cert-manager.io/cluster-issuer` annotation to the Ingress. cert-manager will automatically perform the ACME challenge, generate the certificate, store it in the named Secret, and handle renewal. You do not create the Secret yourself.
|
|
|
|
```yaml
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: my-app
|
|
namespace: my-ns
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
spec:
|
|
rules:
|
|
- host: app.example.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: my-app
|
|
port:
|
|
number: 8080
|
|
tls:
|
|
- hosts:
|
|
- app.example.com
|
|
secretName: app-example-com-tls
|
|
```
|
|
|
|
If you use a namespace-scoped `Issuer` instead of a `ClusterIssuer`, replace the annotation with `cert-manager.io/issuer: <name>`.
|
|
|
|
---
|
|
|
|
## Do Not Use: TLS Without `secretName`
|
|
|
|
Avoid TLS entries that omit `secretName`:
|
|
|
|
```yaml
|
|
# ⚠️ Non-portable — do not use
|
|
tls:
|
|
- hosts:
|
|
- app.example.com
|
|
```
|
|
|
|
Behavior for this pattern is **controller-specific and not portable**. On OKD/OpenShift, the ingress-to-route translation rejects it as incomplete. On other controllers, it may silently serve a self-signed fallback or fail in unpredictable ways. Harmony does not support this pattern.
|
|
|
|
---
|
|
|
|
## Prerequisites for cert-manager
|
|
|
|
To use automated certificates (Option B above):
|
|
|
|
1. **cert-manager** must be installed on the cluster.
|
|
2. A `ClusterIssuer` or `Issuer` must exist. A typical Let's Encrypt production issuer:
|
|
|
|
```yaml
|
|
apiVersion: cert-manager.io/v1
|
|
kind: ClusterIssuer
|
|
metadata:
|
|
name: letsencrypt-prod
|
|
spec:
|
|
acme:
|
|
server: https://acme-v02.api.letsencrypt.org/directory
|
|
email: team@example.com
|
|
privateKeySecretRef:
|
|
name: letsencrypt-prod-account-key
|
|
solvers:
|
|
- http01:
|
|
ingress: {}
|
|
```
|
|
|
|
3. **DNS must already resolve** to the cluster's ingress endpoint before the Ingress is created. The HTTP01 challenge requires this routing to be active.
|
|
|
|
For wildcard certificates (e.g. `*.example.com`), HTTP01 cannot be used — configure a DNS01 solver with credentials for your DNS provider instead.
|
|
|
|
---
|
|
|
|
## Multiple Ingresses on the Same Host
|
|
|
|
When a single host is fronted by more than one Ingress (e.g. a Helm chart that ships separate Ingresses for an API and a UI under the same hostname), **all of them must reference the same TLS Secret, and only one of them should trigger cert-manager**.
|
|
|
|
```yaml
|
|
# Ingress 1 — owns the certificate request
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: app-api
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
spec:
|
|
rules:
|
|
- host: app.example.com
|
|
http: { paths: [{ path: /, pathType: Prefix, backend: { service: { name: app-api, port: { number: 8080 } } } }] }
|
|
tls:
|
|
- hosts: [app.example.com]
|
|
secretName: app-example-com-tls # cert-manager will populate this
|
|
|
|
---
|
|
# Ingress 2 — references the same Secret, no cert-manager annotation
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: app-ui
|
|
spec:
|
|
rules:
|
|
- host: app.example.com
|
|
http: { paths: [{ path: /ui, pathType: Prefix, backend: { service: { name: app-ui, port: { number: 3000 } } } }] }
|
|
tls:
|
|
- hosts: [app.example.com]
|
|
secretName: app-example-com-tls # reuses the cert above
|
|
```
|
|
|
|
Why this matters — and the failure mode if you don't:
|
|
|
|
- Two cert-manager-annotated Ingresses on the same host create **two `Certificate` resources** and **two ACME `Order`s** for the same domain.
|
|
- Both Orders launch HTTP01 challenges concurrently; the ingress controller sees two competing challenge Ingresses for `/.well-known/acme-challenge/...` with different tokens — one wins, the other fails.
|
|
- The loser's Certificate stays `Pending`, its Secret is never created.
|
|
- On OKD specifically, the ingress-to-route controller **inlines the TLS cert/key into the generated Route** at creation time. With no Secret it cannot inline anything, and the Route for the second Ingress is silently never admitted — the path becomes unreachable, while the first Ingress's path works fine.
|
|
|
|
The diagnostic signature: `kubectl get ingress` shows both Ingresses, `kubectl get route` shows only one, the second Ingress's `status.loadBalancer` is `{}`, and the second Certificate is stuck in `Pending`.
|
|
|
|
## OKD / OpenShift Notes
|
|
|
|
On OKD, standard Ingress resources are automatically translated into OpenShift `Route` objects. The default TLS termination mode is `edge`, which is correct for most HTTP applications. To control this explicitly, add:
|
|
|
|
```yaml
|
|
annotations:
|
|
route.openshift.io/termination: edge # or passthrough / reencrypt
|
|
```
|
|
|
|
This annotation is ignored on non-OpenShift clusters and is safe to include unconditionally.
|