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
33 changes: 15 additions & 18 deletions docs/domains.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The Helm chart exposes every hostname-dependent value. The values that matter:
| `ingress.tls` | `[]` | TLS hosts + secret name |
| `ingress.className` | `""` | e.g. `traefik`, `nginx` |
| `ingress.annotations` | `{}` | e.g. `cert-manager.io/cluster-issuer` or traefik middleware |
| `ingress.middlewares` | `[]` | Optional Traefik `Middleware` CRs (`traefik.io/v1alpha1`), each `{name, spec}`, rendered into the release namespace |

The recommended deployment model is **single-host**: frontend, API, and proxy
are all served from one domain. The frontend defaults to calling the API at
Expand All @@ -36,21 +37,9 @@ The API sets a host-only `kw-session` cookie — this means single-host keeps th
cookie on one origin and avoids cross-origin cookie issues.

**Traefik ingress controllers** need a `StripPrefix` middleware to strip `/api`
before the API service handles the request:

```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: kube-workspaces-strip-api
namespace: kube-workspaces-system
spec:
stripPrefix:
prefixes:
- /api
```

With the middleware in place, configure the Helm values:
before the API service handles the request. The Helm chart renders it from
`ingress.middlewares` — the entry below produces a `Middleware` named
`kube-workspaces-strip-api` in the release namespace, matched by the annotation:

```yaml
api:
Expand All @@ -63,6 +52,12 @@ ingress:
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.middlewares: kube-workspaces-system-kube-workspaces-strip-api@kubernetescrd
middlewares:
- name: kube-workspaces-strip-api
spec:
stripPrefix:
prefixes:
- /api
hosts:
- host: workspaces.example.com
paths:
Expand Down Expand Up @@ -213,9 +208,11 @@ Notes:
examples.
- ArgoCD renders the chart with `helm template` and applies the output as plain
manifests — it does not create a Helm release, so `helm list` won't show it.
- The middleware YAML should be applied before the Ingress references it, or
managed by a separate ArgoCD Application (the Ingress will still route
correctly even if the middleware is missing for a short time).
- The chart renders the strip-api `Middleware` itself from `ingress.middlewares`,
so it is applied together with the Ingress in one sync. Only if you manage the
middleware out-of-band (e.g. a separate ArgoCD Application) should you care
about ordering: apply the `Middleware` before the Ingress references it — the
Ingress still routes correctly even if the middleware is missing briefly.

### Two-host setup (alternative)

Expand Down
2 changes: 1 addition & 1 deletion helm/kube-workspaces/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: kube-workspaces
description: A Helm chart for deploying kube-workspaces (controller, API, proxy and frontend)
type: application
version: 0.3.3
version: 0.4.0
appVersion: "0.3.0"

dependencies:
Expand Down
18 changes: 18 additions & 0 deletions helm/kube-workspaces/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ images. The chart vendors a catalog from
| `ingress.enabled` | `false` | Render an `Ingress` |
| `ingress.className` | `""` | `ingressClassName` (e.g. `traefik`, `nginx`) |
| `ingress.annotations` | `{}` | e.g. `cert-manager.io/cluster-issuer` or ingress-controller-specific middleware |
| `ingress.middlewares` | `[]` | Optional Traefik `Middleware` CRs (`traefik.io/v1alpha1`), rendered into the release namespace. Each entry is `{name, spec}`; `spec` is passed through verbatim. Reference one from an annotation as `<release-namespace>-<name>@kubernetescrd` |
| `ingress.hosts` | one `workspaces.local` host, five paths | Host → paths mapping. **Overriding `hosts` replaces the whole list** — each host you supply needs its own complete `paths` list, or the chart fails the render rather than emit an Ingress rule with no paths |
| `ingress.tls` | `[]` | `[{hosts: [...], secretName: ...}]` entries |

Expand All @@ -227,6 +228,23 @@ traffic if misrouted. See [`docs/proxy.md`](../../docs/proxy.md) for the full
routing table and [`docs/domains.md`](../../docs/domains.md) for customizing
hosts/paths without hand-editing the whole list.

Traefik ingress controllers need a `stripPrefix` middleware to strip the `/api`
prefix before requests reach the API service. The chart can render it for you:

```yaml
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.middlewares: kube-workspaces-system-kube-workspaces-strip-api@kubernetescrd
middlewares:
- name: kube-workspaces-strip-api
spec:
stripPrefix:
prefixes:
- /api
```

### Authentication

Authentication is **opt-in**; with `auth.enabled: false` (the default), every
Expand Down
28 changes: 28 additions & 0 deletions helm/kube-workspaces/templates/middlewares.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{{- /*
Optional Traefik Middleware CRs (traefik.io/v1alpha1), declared in
.Values.ingress.middlewares. Each entry is {name, spec}; spec is passed
through verbatim so any middleware the Traefik CRD supports can be declared
from values.yaml without editing this template.

The Middleware is rendered into the release namespace (.Release.Namespace).
Traefik references a middleware on an Ingress router via a qualifying
annotation whose value is "<namespace>-<name>@kubernetescrd" — see
docs/domains.md for the strip-prefix example.
*/ -}}
{{- range .Values.ingress.middlewares }}
{{- $name := required "each entry in .Values.ingress.middlewares requires a 'name'" .name }}
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "kube-workspaces.labels" $ | nindent 4 }}
{{- with .annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- required "each entry in .Values.ingress.middlewares requires a 'spec'" .spec | toYaml | nindent 2 }}
{{- end }}
138 changes: 138 additions & 0 deletions helm/kube-workspaces/tests/middlewares_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Unit tests for the Traefik Middleware template
# (templates/middlewares.yaml, driven by .Values.ingress.middlewares).
#
# Traefik attaches middleware to Ingress routers via a qualifying annotation —
# "<namespace>-<name>@kubernetescrd" — so the Middleware must land in the same
# namespace as the Ingress (the release namespace) with a name the annotation
# can address. Asserting those two fields is the whole point of the template.
suite: middlewares
templates:
- middlewares.yaml
tests:
- it: renders nothing when no middlewares are configured
asserts:
- hasDocuments:
count: 0

- it: renders a middleware with the given name, namespace and spec
set:
ingress.middlewares:
- name: kube-workspaces-strip-api
spec:
stripPrefix:
prefixes:
- /api
asserts:
- hasDocuments:
count: 1
- isKind:
of: Middleware
- isAPIVersion:
of: traefik.io/v1alpha1
- equal:
path: metadata.name
value: kube-workspaces-strip-api
- equal:
path: metadata.namespace
value: NAMESPACE
- equal:
path: spec.stripPrefix.prefixes[0]
value: /api

- it: renders one document per configured middleware
set:
ingress.middlewares:
- name: strip-api
spec:
stripPrefix:
prefixes:
- /api
- name: rate-limit
spec:
rateLimit:
average: 100
burst: 50
asserts:
- hasDocuments:
count: 2

- it: renders the first middleware as documented
documentIndex: 0
set:
ingress.middlewares:
- name: strip-api
spec:
stripPrefix:
prefixes:
- /api
- name: rate-limit
spec:
rateLimit:
average: 100
burst: 50
asserts:
- equal:
path: metadata.name
value: strip-api
- equal:
path: spec.stripPrefix.prefixes[0]
value: /api

- it: renders the second middleware as documented
documentIndex: 1
set:
ingress.middlewares:
- name: strip-api
spec:
stripPrefix:
prefixes:
- /api
- name: rate-limit
spec:
rateLimit:
average: 100
burst: 50
asserts:
- equal:
path: metadata.name
value: rate-limit
- equal:
path: spec.rateLimit.average
value: 100

- it: applies labels and optional annotations
set:
ingress.middlewares:
- name: strip-api
annotations:
description: strips the /api prefix
spec:
stripPrefix:
prefixes:
- /api
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/managed-by"]
value: Helm
- equal:
path: metadata.annotations.description
value: strips the /api prefix

- it: fails when an entry has no name
set:
ingress.middlewares:
- spec:
stripPrefix:
prefixes:
- /api
asserts:
- failedTemplate:
errorMessage: "each entry in .Values.ingress.middlewares requires a 'name'"

- it: fails when an entry has no spec
set:
ingress.middlewares:
- name: strip-api
asserts:
- failedTemplate:
errorMessage: "each entry in .Values.ingress.middlewares requires a 'spec'"
46 changes: 38 additions & 8 deletions helm/kube-workspaces/tests/rbac_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,52 @@ tests:
path: rules[8].verbs
content: delete

- it: renders cross-namespace Role in kube-workspaces-system when local auth is enabled and release namespace differs
documentSelector:
path: metadata.namespace
value: kube-workspaces-system
- it: renders cross-namespace Role and RoleBinding in kube-workspaces-system when local auth is enabled and release namespace differs
set:
auth.localAuth.enabled: true
asserts:
- isKind:
# Existence, namespace-scoped. `containsDocument` with `any: true`
# asserts that some rendered document matches — pinning name + namespace
# rules out the identical release-namespace sibling.
- containsDocument:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
name: RELEASE-NAME-kube-workspaces-local-auth-secrets
namespace: kube-workspaces-system
any: true
- containsDocument:
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
name: RELEASE-NAME-kube-workspaces-local-auth-secrets
namespace: kube-workspaces-system
any: true
# Both Roles in the render (release namespace and kube-workspaces-system)
# carry identical name and rules, so select on kind for the verb checks.
- documentSelector:
path: kind
value: Role
matchMany: true
isKind:
of: Role
- equal:
- documentSelector:
path: kind
value: Role
matchMany: true
equal:
path: metadata.name
value: RELEASE-NAME-kube-workspaces-local-auth-secrets
- contains:
- documentSelector:
path: kind
value: Role
matchMany: true
contains:
path: rules[0].verbs
content: create
- contains:
- documentSelector:
path: kind
value: Role
matchMany: true
contains:
path: rules[0].verbs
content: update

Expand Down
15 changes: 15 additions & 0 deletions helm/kube-workspaces/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ ingress:
enabled: false
className: ""
annotations: {}
# Optional Traefik Middleware CRs (traefik.io/v1alpha1) rendered into the
# release namespace. Each entry is {name, spec}; spec is passed through
# verbatim, so any middleware the Traefik CRD supports can be declared here.
# Attach them to the Ingress via annotations, e.g. Traefik stripPrefix:
#
# ingress:
# annotations:
# traefik.ingress.kubernetes.io/router.middlewares: <release-namespace>-<name>@kubernetescrd
# middlewares:
# - name: kube-workspaces-strip-api
# spec:
# stripPrefix:
# prefixes:
# - /api
middlewares: []
# Each host entry needs its own `paths` list. If you override `hosts` you must
# supply `paths` too — a host with no paths renders an invalid Ingress.
hosts:
Expand Down
Loading