Skip to content

Commit 7a10a9e

Browse files
committed
feat(chart): bootstrap ClusterConnections and admin Secrets from values
Optional clusterConnections values list renders cluster-scoped ClusterConnection resources with either a chart-created admin Secret (inline accessKey/secretKey) or a referenced existingSecret. Off by default; default rendered output is unchanged. Entries are validated with actionable template errors (required fields, exactly one credentials source, duplicate names). Verified CRD-before-templates ordering on a fresh helm install against k3s.
1 parent c3f9d43 commit 7a10a9e

7 files changed

Lines changed: 263 additions & 3 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rustfs-operator"
3-
version = "0.2.2"
3+
version = "0.3.0"
44
edition = "2024"
55
rust-version = "1.92"
66
license = "MIT OR Apache-2.0"

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ helm install rustfs-operator rustfs-operator/rustfs-operator \
5353
--namespace rustfs-operator --create-namespace
5454
```
5555

56+
The chart can also bootstrap `ClusterConnection` resources (and their admin
57+
credentials Secrets) from values — see
58+
[`charts/rustfs-operator/README.md`](charts/rustfs-operator/README.md).
59+
5660
Or run from source against the current kubeconfig:
5761

5862
```sh

charts/rustfs-operator/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ keywords:
1010
- s3
1111
- operator
1212
# version and appVersion are overwritten from the git tag by the release workflow
13-
version: 0.1.0
14-
appVersion: "0.1.0"
13+
version: 0.3.0
14+
appVersion: "0.3.0"

charts/rustfs-operator/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# rustfs-operator Helm chart
2+
3+
Deploys the [rustfs-operator](https://github.com/OpenProjectX/rustfs-operator):
4+
a Kubernetes operator managing RustFS buckets, IAM users and policies via
5+
`Bucket`, `User`, `Policy` and `ClusterConnection` CRDs (installed from this
6+
chart's `crds/` directory).
7+
8+
```sh
9+
helm repo add rustfs-operator https://openprojectx.github.io/rustfs-operator
10+
helm install rustfs-operator rustfs-operator/rustfs-operator \
11+
--namespace rustfs-operator --create-namespace
12+
```
13+
14+
## Values
15+
16+
| Key | Default | Description |
17+
|-----|---------|-------------|
18+
| `image.repository` | `ghcr.io/openprojectx/rustfs-operator` | operator image |
19+
| `image.tag` | chart `appVersion` | image tag override |
20+
| `replicaCount` | `1` | operator replicas |
21+
| `logLevel` | `info` | `RUST_LOG` value |
22+
| `serviceAccount.create` | `true` | create the ServiceAccount |
23+
| `rbac.create` | `true` | create ClusterRole/Role and bindings |
24+
| `rbac.clusterWideSecrets` | `true` | see [RBAC](#rbac) |
25+
| `clusterConnections` | `[]` | bootstrap ClusterConnection resources, see below |
26+
27+
## Bootstrapping ClusterConnections
28+
29+
`clusterConnections` declares cluster-scoped `ClusterConnection` resources
30+
(and optionally their admin credentials Secrets) directly from values, so
31+
GitOps setups don't need a second mechanism. Off by default.
32+
33+
**Inline credentials** — the chart creates the Secret in the release
34+
namespace (named `<fullname>-<name>-admin` unless `secretName` is set):
35+
36+
```yaml
37+
clusterConnections:
38+
- name: prod
39+
endpoint: http://rustfs.storage.svc:9000
40+
allowedNamespaces: ["team-a", "team-b"] # omit = all namespaces
41+
credentials:
42+
accessKey: rustfsadmin
43+
secretKey: rustfsadmin
44+
```
45+
46+
**Existing Secret** (recommended for production) — reference a Secret in the
47+
release namespace holding `accessKey`/`secretKey`; the chart creates none:
48+
49+
```yaml
50+
clusterConnections:
51+
- name: prod
52+
endpoint: http://rustfs.storage.svc:9000
53+
credentials:
54+
existingSecret: rustfs-admin
55+
```
56+
57+
> **Security note:** inline `accessKey`/`secretKey` are stored in the Helm
58+
> release values (a Secret in the release namespace, readable by anyone who
59+
> can read Secrets there or run `helm get values`). Prefer `existingSecret`
60+
> combined with sealed-secrets / external-secrets in production.
61+
62+
## RBAC
63+
64+
`rbac.clusterWideSecrets: true` (default) lets the operator read Secrets in
65+
all namespaces, which `connection.secretRef` and per-namespace
66+
`User.spec.secretKeyRef` need. When you use **only** `ClusterConnection`
67+
(and keep `User` resources in the operator's namespace), set it to `false`
68+
for least privilege — the operator then reads Secrets only in its own
69+
namespace via a namespaced Role.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{{- $seen := dict }}
2+
{{- range .Values.clusterConnections }}
3+
{{- $name := .name | default "" }}
4+
{{- if eq $name "" }}
5+
{{- fail "clusterConnections: every entry needs a non-empty 'name'" }}
6+
{{- end }}
7+
{{- if hasKey $seen $name }}
8+
{{- fail (printf "clusterConnections: duplicate name %q (ClusterConnection is cluster-scoped; names must be unique)" $name) }}
9+
{{- end }}
10+
{{- $_ := set $seen $name true }}
11+
{{- $endpoint := .endpoint | default "" }}
12+
{{- if eq $endpoint "" }}
13+
{{- fail (printf "clusterConnections[%s]: 'endpoint' is required" $name) }}
14+
{{- end }}
15+
{{- $creds := .credentials | default dict }}
16+
{{- $existing := $creds.existingSecret | default "" }}
17+
{{- $accessKey := $creds.accessKey | default "" }}
18+
{{- $secretKey := $creds.secretKey | default "" }}
19+
{{- if and (ne $existing "") (or (ne $accessKey "") (ne $secretKey "")) }}
20+
{{- fail (printf "clusterConnections[%s]: set either credentials.existingSecret or inline credentials.accessKey/secretKey, not both" $name) }}
21+
{{- end }}
22+
{{- $create := true }}
23+
{{- if hasKey $creds "create" }}
24+
{{- $create = $creds.create }}
25+
{{- end }}
26+
{{- if ne $existing "" }}
27+
{{- $create = false }}
28+
{{- end }}
29+
{{- if and (eq $existing "") (not $create) }}
30+
{{- fail (printf "clusterConnections[%s]: no credentials source; set credentials.existingSecret or leave credentials.create enabled with accessKey/secretKey" $name) }}
31+
{{- end }}
32+
{{- if and $create (or (eq $accessKey "") (eq $secretKey "")) }}
33+
{{- fail (printf "clusterConnections[%s]: credentials.accessKey and credentials.secretKey must both be set when the chart creates the Secret" $name) }}
34+
{{- end }}
35+
{{- $secretName := ternary $existing ($creds.secretName | default (printf "%s-%s-admin" (include "rustfs-operator.fullname" $) $name)) (ne $existing "") }}
36+
---
37+
apiVersion: rustfs.com/v1alpha1
38+
kind: ClusterConnection
39+
metadata:
40+
name: {{ $name }}
41+
labels:
42+
{{- include "rustfs-operator.labels" $ | nindent 4 }}
43+
spec:
44+
endpoint: {{ $endpoint | quote }}
45+
credentialsSecretRef: {{ $secretName | quote }}
46+
{{- with .region }}
47+
region: {{ . | quote }}
48+
{{- end }}
49+
{{- if .insecure }}
50+
insecure: true
51+
{{- end }}
52+
{{- with .allowedNamespaces }}
53+
allowedNamespaces:
54+
{{- toYaml . | nindent 4 }}
55+
{{- end }}
56+
{{- if $create }}
57+
---
58+
apiVersion: v1
59+
kind: Secret
60+
metadata:
61+
name: {{ $secretName }}
62+
namespace: {{ $.Release.Namespace }}
63+
labels:
64+
{{- include "rustfs-operator.labels" $ | nindent 4 }}
65+
type: Opaque
66+
stringData:
67+
accessKey: {{ $accessKey | quote }}
68+
secretKey: {{ $secretKey | quote }}
69+
{{- end }}
70+
{{- end }}

charts/rustfs-operator/values.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ rbac:
2323
# operator's namespace; the operator then only reads Secrets there.
2424
clusterWideSecrets: true
2525

26+
# Optionally declare ClusterConnection resources (and their admin
27+
# credentials Secrets) inline. Off by default; see the chart README.
28+
# WARNING: inline accessKey/secretKey end up in the Helm release values —
29+
# prefer `existingSecret` (sealed/external secret) in production.
30+
clusterConnections: []
31+
# clusterConnections:
32+
# - name: prod # required, cluster-scoped resource name
33+
# endpoint: http://rustfs.storage.svc:9000 # required
34+
# region: "" # optional
35+
# insecure: false # optional
36+
# allowedNamespaces: [] # optional; empty/omitted = all namespaces
37+
# credentials:
38+
# # EITHER inline (chart creates the Secret in the release namespace):
39+
# create: true # default true
40+
# secretName: "" # default "<fullname>-<name>-admin"
41+
# accessKey: ""
42+
# secretKey: ""
43+
# # OR reference an existing Secret with accessKey/secretKey keys:
44+
# existingSecret: ""
45+
2646
# value for RUST_LOG
2747
logLevel: info
2848

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Feature request: optionally create ClusterConnection(s) + admin Secret from Helm values
2+
3+
## Context
4+
The `rustfs-operator` chart currently deploys only the operator Deployment + RBAC.
5+
The admin credentials Secret and the cluster-scoped `ClusterConnection` CR must be
6+
created by hand (see `deploy/example.yaml`). In GitOps/helmfile setups this forces a
7+
second, out-of-chart mechanism just to bootstrap the connection.
8+
9+
Add **optional** support to the chart so a `ClusterConnection` (and, optionally, its
10+
admin credentials Secret) can be declared inline in `values.yaml`. Feature must be
11+
**off by default** and must not change any existing rendered output when unset.
12+
13+
## Chart facts to respect
14+
- CRD: group `rustfs.com`, version `v1alpha1`, kind `ClusterConnection`, **cluster-scoped**.
15+
- `ClusterConnection.spec`: `endpoint` (required), `credentialsSecretRef` (required — name
16+
of a Secret **in the operator's namespace** with keys `accessKey`/`secretKey`),
17+
optional `region`, `insecure`, `allowedNamespaces` (string list; absent = all).
18+
- The credentials Secret must live in the operator's own namespace (`.Release.Namespace`);
19+
a namespaced Role already grants the operator read access there.
20+
- `rbac.clusterWideSecrets` already exists; when only `ClusterConnection` is used it can be
21+
`false`. Do not change its default.
22+
23+
## Values API (new)
24+
Add a top-level `clusterConnections` **list** (support zero or more servers):
25+
26+
```yaml
27+
clusterConnections: []
28+
# clusterConnections:
29+
# - name: prod # required; metadata.name of the ClusterConnection (cluster-scoped)
30+
# endpoint: http://rustfs-svc.storage.svc.cluster.local:9000 # required
31+
# allowedNamespaces: [] # optional; omit/empty -> render nothing (operator treats absent = all)
32+
# region: "" # optional
33+
# insecure: false # optional
34+
# credentials:
35+
# # Provide EITHER inline creds (chart creates the Secret in the operator ns) ...
36+
# create: true # default true
37+
# secretName: "" # optional override; default: "<fullname>-<name>-admin"
38+
# accessKey: ""
39+
# secretKey: ""
40+
# # ... OR reference an existing Secret (chart creates no Secret):
41+
# existingSecret: "" # if set, credentials.create is ignored and this name
42+
# # is used as credentialsSecretRef (must have accessKey/secretKey)
43+
```
44+
45+
### Behavior
46+
- `clusterConnections: []` (default) => render nothing new; byte-for-byte identical output to today.
47+
- For each entry:
48+
- Always render a `ClusterConnection` (cluster-scoped, no namespace) with `spec.endpoint`,
49+
`spec.credentialsSecretRef` = resolved secret name (see below), and — only when set —
50+
`region`, `insecure`, and `allowedNamespaces`.
51+
- Resolve `credentialsSecretRef`:
52+
- if `credentials.existingSecret` is non-empty => use it; render **no** Secret.
53+
- else if `credentials.create` (default true) => render a `Secret` of `type: Opaque` in
54+
`.Release.Namespace` named `credentials.secretName | default "<fullname>-<name>-admin"`,
55+
with `stringData.accessKey` / `stringData.secretKey`; use that name as `credentialsSecretRef`.
56+
- `allowedNamespaces`: omit the field entirely when the list is empty/unset (so operator's
57+
"absent = all" semantics apply); render the list when provided.
58+
59+
## Validation (fail template with a clear message)
60+
- `name` and `endpoint` are required per entry.
61+
- Exactly one credentials source: reject if `existingSecret` is set **and** inline
62+
`accessKey`/`secretKey` are also set.
63+
- When `credentials.create` is true and `existingSecret` is empty, both `accessKey` and
64+
`secretKey` must be non-empty (do not silently emit an empty-keyed Secret).
65+
- Duplicate `name` values across the list should fail (they'd collide on a cluster-scoped object).
66+
67+
## Templates / conventions
68+
- New file `templates/clusterconnection.yaml` (range over `.Values.clusterConnections`,
69+
`---` separated).
70+
- Use existing `_helpers.tpl` fullname/labels helpers; apply the same standard labels the
71+
other objects use.
72+
- CRDs in `crds/` are applied by Helm before `templates/`, so the CR renders fine on a fresh
73+
install — but please confirm ordering on first `helm install` (no CRD-not-found race).
74+
75+
## RBAC note
76+
No change to defaults. Please document that when a deployment uses **only** ClusterConnection
77+
(no per-namespace `secretRef`, no `User.secretKeyRef` in app namespaces), users can set
78+
`rbac.clusterWideSecrets: false` for least privilege.
79+
80+
## Docs
81+
- Document the new `clusterConnections` values block in the chart README with one inline-creds
82+
example and one `existingSecret` example.
83+
- Note the security trade-off: inline `accessKey`/`secretKey` land in the release values;
84+
recommend `existingSecret` (or a sealed/external secret) for production.
85+
86+
## Acceptance criteria
87+
1. `helm lint` passes; `helm template` with default values produces **no** ClusterConnection/Secret
88+
and is unchanged from current output.
89+
2. `helm template` with one inline-creds entry renders: a Secret in the release namespace
90+
(keys `accessKey`/`secretKey`) **and** a ClusterConnection whose `credentialsSecretRef`
91+
equals that Secret's name.
92+
3. Same with `existingSecret` set renders the ClusterConnection referencing that name and
93+
**no** Secret.
94+
4. Multiple entries render multiple independent ClusterConnections/Secrets.
95+
5. `allowedNamespaces` present only when provided.
96+
6. Validation failures produce actionable error messages.
97+
7. Bump chart `version`; update README.

0 commit comments

Comments
 (0)