Skip to content

Commit 4bd3cf5

Browse files
committed
ci: chart tests and real helm e2e on k3d, cache all CI downloads
- scripts/chart-tests.sh: lint all charts, render assertions, all validation failure paths (runs in CI as chart-tests job) - scripts/chart-e2e.sh: k3d cluster + RustFS container; installs the CRDs chart, the operator chart with a bootstrapped ClusterConnection running the image under test, and the rustfs-resources chart; asserts CR convergence and finalizer cleanup on helm uninstall. All images are preloaded into the node (in-cluster docker.io pulls don't work on proxied hosts) with single-platform saves (containerd image store attestations break ctr import otherwise). - CI: chart-tests and chart-e2e jobs; composite action caches pulled test images in the Actions cache; k3d binary and pinned rust toolchain (1.97.0) cached too.
1 parent 8b52810 commit 4bd3cf5

7 files changed

Lines changed: 334 additions & 4 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Cached container images
2+
description: Pull container images through the GitHub Actions cache instead of the network
3+
inputs:
4+
images:
5+
description: Whitespace-separated image references
6+
required: true
7+
cache-key:
8+
description: Actions cache key for the image tarballs
9+
required: true
10+
runs:
11+
using: composite
12+
steps:
13+
- uses: actions/cache@v4
14+
id: cache
15+
with:
16+
path: ~/.image-cache
17+
key: ${{ inputs.cache-key }}
18+
- name: Load images from cache
19+
if: steps.cache.outputs.cache-hit == 'true'
20+
shell: bash
21+
run: for f in ~/.image-cache/*.tar; do docker load -q -i "$f"; done
22+
- name: Pull and save images (cache miss)
23+
if: steps.cache.outputs.cache-hit != 'true'
24+
shell: bash
25+
run: |
26+
mkdir -p ~/.image-cache
27+
i=0
28+
for img in ${{ inputs.images }}; do
29+
docker pull -q "$img"
30+
docker save -o ~/.image-cache/img-$i.tar "$img"
31+
i=$((i+1))
32+
done

.github/workflows/ci.yml

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,29 @@ permissions:
1010

1111
env:
1212
CARGO_TERM_COLOR: always
13+
RUST_TOOLCHAIN: "1.97.0"
14+
K3D_VERSION: v5.9.0
15+
TEST_IMAGES: >-
16+
ghcr.io/openprojectx/dockerhub/rustfs/rustfs:1.0.0-beta.8
17+
ghcr.io/openprojectx/dockerhub/rancher/k3s:v1.34.9-k3s1
18+
K3S_SYSTEM_IMAGES: >-
19+
docker.io/rancher/mirrored-pause:3.6
20+
docker.io/rancher/mirrored-coredns-coredns:1.14.4
21+
docker.io/rancher/local-path-provisioner:v0.0.36
1322
1423
jobs:
1524
lint-and-unit:
1625
runs-on: ubuntu-latest
1726
steps:
1827
- uses: actions/checkout@v4
28+
- name: Cache rust toolchain
29+
uses: actions/cache@v4
30+
with:
31+
path: ~/.rustup/toolchains
32+
key: rustup-${{ runner.os }}-1.97.0
1933
- uses: dtolnay/rust-toolchain@stable
2034
with:
35+
toolchain: "1.97.0"
2136
components: rustfmt, clippy
2237
- uses: Swatinem/rust-cache@v2
2338
- name: Format
@@ -26,15 +41,31 @@ jobs:
2641
run: cargo clippy --all-targets --features integration,e2e -- -D warnings
2742
- name: Unit tests
2843
run: cargo test
29-
- name: Helm lint
30-
run: helm lint charts/rustfs-operator
44+
45+
chart-tests:
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v4
49+
- name: Chart lint, render and validation tests
50+
run: bash scripts/chart-tests.sh
3151

3252
container-tests:
3353
runs-on: ubuntu-latest
3454
steps:
3555
- uses: actions/checkout@v4
56+
- name: Cache rust toolchain
57+
uses: actions/cache@v4
58+
with:
59+
path: ~/.rustup/toolchains
60+
key: rustup-${{ runner.os }}-1.97.0
3661
- uses: dtolnay/rust-toolchain@stable
62+
with:
63+
toolchain: "1.97.0"
3764
- uses: Swatinem/rust-cache@v2
65+
- uses: ./.github/actions/cached-images
66+
with:
67+
images: ${{ env.TEST_IMAGES }}
68+
cache-key: test-images-rustfs-beta8-k3s-1.34.9
3869
- name: Integration tests (RustFS container)
3970
run: cargo test --features integration --test integration_rustfs
4071
- name: E2E tests (k3s + RustFS containers)
@@ -54,3 +85,37 @@ jobs:
5485
push: false
5586
cache-from: type=gha
5687
cache-to: type=gha,mode=max
88+
89+
# Real Helm flow against a k3d cluster: CRDs chart, operator chart with a
90+
# values-bootstrapped ClusterConnection, resources chart, convergence and
91+
# finalizer cleanup — using the image built from this commit.
92+
chart-e2e:
93+
runs-on: ubuntu-latest
94+
steps:
95+
- uses: actions/checkout@v4
96+
- uses: docker/setup-buildx-action@v3
97+
- name: Build operator image
98+
uses: docker/build-push-action@v6
99+
with:
100+
context: .
101+
load: true
102+
tags: rustfs-operator:ci
103+
cache-from: type=gha
104+
- name: Cache k3d binary
105+
id: k3d-cache
106+
uses: actions/cache@v4
107+
with:
108+
path: ~/.local/bin/k3d
109+
key: k3d-${{ env.K3D_VERSION }}-linux-amd64
110+
- name: Install k3d (cache miss)
111+
if: steps.k3d-cache.outputs.cache-hit != 'true'
112+
run: |
113+
mkdir -p ~/.local/bin
114+
curl -fsSL "https://github.com/k3d-io/k3d/releases/download/${K3D_VERSION}/k3d-linux-amd64" -o ~/.local/bin/k3d
115+
chmod +x ~/.local/bin/k3d
116+
- uses: ./.github/actions/cached-images
117+
with:
118+
images: ${{ env.TEST_IMAGES }} ${{ env.K3S_SYSTEM_IMAGES }}
119+
cache-key: chart-e2e-images-v1
120+
- name: Run chart e2e
121+
run: IMAGE=rustfs-operator:ci bash scripts/chart-e2e.sh

.github/workflows/release.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,16 @@ jobs:
4646
- uses: actions/checkout@v4
4747
# Binary artifact is opt-in: set the repository variable
4848
# RELEASE_BINARY=true to build and attach it.
49+
- name: Cache rust toolchain
50+
if: vars.RELEASE_BINARY == 'true'
51+
uses: actions/cache@v4
52+
with:
53+
path: ~/.rustup/toolchains
54+
key: rustup-${{ runner.os }}-1.97.0
4955
- uses: dtolnay/rust-toolchain@stable
5056
if: vars.RELEASE_BINARY == 'true'
57+
with:
58+
toolchain: "1.97.0"
5159
- uses: Swatinem/rust-cache@v2
5260
if: vars.RELEASE_BINARY == 'true'
5361
- name: Build release binary

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,20 @@ build and attach a linux-amd64 binary.
103103
| Layer | Command | Needs |
104104
|-------|---------|-------|
105105
| Unit (mocked provider) | `cargo test` | – |
106-
| Integration (real RustFS) | `cargo test --features integration --test integration_rustfs` | Docker, `rustfs/rustfs:1.0.0-beta.8` |
107-
| E2E (real k3s + RustFS, controllers in-process) | `cargo test --features e2e --test e2e_k3s` | Docker, `rancher/k3s:v1.34.9-k3s1` |
106+
| Integration (real RustFS) | `cargo test --features integration --test integration_rustfs` | Docker |
107+
| E2E (real k3s + RustFS, controllers in-process) | `cargo test --features e2e --test e2e_k3s` | Docker |
108+
| Chart tests (lint, render, validation) | `bash scripts/chart-tests.sh` | helm |
109+
| Chart e2e (helm install on k3d, real operator image) | `IMAGE=<image> bash scripts/chart-e2e.sh` | Docker, k3d, helm, kubectl |
108110

109111
The e2e test boots a k3s cluster and a RustFS server in containers, installs
110112
the CRDs, runs the controllers inside the test process, applies
111113
Bucket/User/Policy CRs and asserts both convergence in RustFS and finalizer
112114
cleanup on deletion.
115+
116+
The chart e2e goes one layer further: it creates a k3d cluster, installs the
117+
CRDs chart, the operator chart (with a values-bootstrapped ClusterConnection)
118+
running the given image, and the `rustfs-resources` chart, then asserts the
119+
declared resources converge and that `helm uninstall` cleans up remote state
120+
via finalizers. All required images are preloaded into the cluster, so
121+
nothing is pulled from inside it. CI runs every layer; test images, the k3d
122+
binary and the rust toolchain are cached in the GitHub Actions cache.

scripts/chart-e2e.sh

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env bash
2+
# Real-cluster chart e2e: k3d cluster + RustFS container, operator installed
3+
# from the Helm charts (CRDs chart first), resources declared via the
4+
# rustfs-resources chart, convergence and finalizer cleanup asserted.
5+
#
6+
# Required: IMAGE=<operator image present in the local docker daemon>
7+
# Uses: docker, k3d, helm, kubectl
8+
set -euo pipefail
9+
cd "$(dirname "$0")/.."
10+
11+
IMAGE="${IMAGE:?set IMAGE to the operator image to test (must exist locally)}"
12+
K3S_IMAGE="${K3S_IMAGE:-ghcr.io/openprojectx/dockerhub/rancher/k3s:v1.34.9-k3s1}"
13+
RUSTFS_IMAGE="${RUSTFS_IMAGE:-ghcr.io/openprojectx/dockerhub/rustfs/rustfs:1.0.0-beta.8}"
14+
# k3s system images (exact tags k3s v1.34.9 wants). Imported into the
15+
# cluster nodes so nothing is pulled from inside the cluster at runtime —
16+
# in-cluster docker.io pulls don't work on proxied/firewalled hosts.
17+
K3S_SYSTEM_IMAGES=(
18+
docker.io/rancher/mirrored-pause:3.6
19+
docker.io/rancher/mirrored-coredns-coredns:1.14.4
20+
docker.io/rancher/local-path-provisioner:v0.0.36
21+
)
22+
CLUSTER=rfo-chart-e2e
23+
RUSTFS_NAME=${CLUSTER}-rustfs
24+
25+
WORK=$(mktemp -d)
26+
export KUBECONFIG=$WORK/kubeconfig
27+
28+
cleanup() {
29+
if [ "${KEEP:-}" = "1" ]; then
30+
echo "KEEP=1: leaving cluster '$CLUSTER' and container '$RUSTFS_NAME' running (kubeconfig: $KUBECONFIG)"
31+
return
32+
fi
33+
k3d cluster delete $CLUSTER >/dev/null 2>&1 || true
34+
docker rm -f $RUSTFS_NAME >/dev/null 2>&1 || true
35+
rm -rf "$WORK"
36+
}
37+
trap cleanup EXIT
38+
39+
# Relaxed kubelet eviction thresholds so the cluster survives nearly-full
40+
# host disks (k3s ignores --kubelet-arg for these; a config drop-in works).
41+
cat > "$WORK/99-eviction.conf" <<EOF
42+
apiVersion: kubelet.config.k8s.io/v1beta1
43+
kind: KubeletConfiguration
44+
evictionHard:
45+
nodefs.available: 100Mi
46+
imagefs.available: 100Mi
47+
nodefs.inodesFree: 1%
48+
EOF
49+
50+
echo "== create k3d cluster"
51+
k3d cluster create $CLUSTER \
52+
--image "$K3S_IMAGE" \
53+
--no-lb \
54+
--k3s-arg '--disable=traefik@server:0' \
55+
--k3s-arg '--disable=metrics-server@server:0' \
56+
--volume "$WORK/99-eviction.conf:/var/lib/rancher/k3s/agent/etc/kubelet.conf.d/99-eviction.conf@server:0" \
57+
--kubeconfig-update-default=false \
58+
--kubeconfig-switch-context=false \
59+
--wait --timeout 180s
60+
k3d kubeconfig get $CLUSTER > "$KUBECONFIG"
61+
62+
echo "== start RustFS on the cluster network"
63+
docker run -d --rm --name $RUSTFS_NAME --network k3d-$CLUSTER \
64+
-e RUSTFS_ACCESS_KEY=rustfsadmin -e RUSTFS_SECRET_KEY=rustfsadmin \
65+
"$RUSTFS_IMAGE" >/dev/null
66+
67+
echo "== import operator + system images (no in-cluster registry pulls)"
68+
# `k3d image import` (and plain `docker save`) trips over multi-arch
69+
# attestation manifests when docker uses the containerd image store, so
70+
# save each image single-platform and stream it into the node's containerd.
71+
# The --platform flag only exists (and is only needed) on the containerd
72+
# store; the classic overlay2 store (GitHub runners) saves single-platform.
73+
save_args=()
74+
if [ "$(docker info -f '{{.Driver}}')" = "overlayfs" ]; then
75+
save_args=(--platform "linux/$(docker version --format '{{.Server.Arch}}')")
76+
fi
77+
for img in "$IMAGE" "${K3S_SYSTEM_IMAGES[@]}"; do
78+
docker image inspect "$img" >/dev/null 2>&1 || docker pull -q "$img"
79+
docker save "${save_args[@]}" "$img" \
80+
| docker exec -i k3d-$CLUSTER-server-0 ctr --namespace k8s.io images import - >/dev/null
81+
done
82+
83+
echo "== install CRDs chart"
84+
helm install rustfs-crds charts/rustfs-operator-crds --wait
85+
86+
echo "== install operator chart (skips existing CRDs) with bootstrapped ClusterConnection"
87+
repo=${IMAGE%:*}; tag=${IMAGE##*:}
88+
helm install rustfs-operator charts/rustfs-operator \
89+
-n rustfs-operator --create-namespace \
90+
--set image.repository="$repo" --set image.tag="$tag" --set image.pullPolicy=Never \
91+
--set-json "clusterConnections=[{\"name\":\"prod\",\"endpoint\":\"http://$RUSTFS_NAME:9000\",\"credentials\":{\"accessKey\":\"rustfsadmin\",\"secretKey\":\"rustfsadmin\"}}]" \
92+
--wait --timeout 300s
93+
94+
echo "== install resources chart"
95+
kubectl create namespace team-a
96+
helm install app-storage charts/rustfs-resources -n team-a \
97+
-f scripts/testdata/resources-values.yaml
98+
99+
wait_ready() { # <kind> <name>
100+
for _ in $(seq 1 60); do
101+
[ "$(kubectl -n team-a get "$1" "$2" -o jsonpath='{.status.ready}' 2>/dev/null)" = "true" ] && {
102+
echo " $1/$2 ready"; return 0; }
103+
sleep 3
104+
done
105+
echo "FAIL: timeout waiting for $1/$2:" >&2
106+
kubectl -n team-a get "$1" "$2" -o yaml 2>/dev/null | tail -15 >&2
107+
return 1
108+
}
109+
wait_ready bucket app-data
110+
wait_ready policy app-data-rw
111+
wait_ready user ci-user
112+
113+
echo "== uninstall resources release; finalizers must clean up remote state"
114+
helm uninstall app-storage -n team-a --wait
115+
for _ in $(seq 1 60); do
116+
[ -z "$(kubectl -n team-a get buckets,users,policies -o name 2>/dev/null)" ] && break
117+
sleep 3
118+
done
119+
remaining=$(kubectl -n team-a get buckets,users,policies -o name 2>/dev/null)
120+
[ -z "$remaining" ] || { echo "FAIL: CRs not cleaned up: $remaining" >&2; exit 1; }
121+
122+
echo "chart-e2e OK"

scripts/chart-tests.sh

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bash
2+
# Static chart tests: lint, render assertions, validation failure paths.
3+
# No cluster or docker needed.
4+
set -euo pipefail
5+
cd "$(dirname "$0")/.."
6+
7+
fail() { echo "FAIL: $*" >&2; exit 1; }
8+
9+
echo "== helm lint (all charts)"
10+
helm lint charts/* >/dev/null
11+
12+
echo "== main chart: default render has no ClusterConnection/Secret"
13+
render=$(helm template rel charts/rustfs-operator --namespace op-ns)
14+
grep -q 'kind: ClusterConnection' <<<"$render" && fail "unexpected ClusterConnection in default render"
15+
grep -q 'kind: Secret' <<<"$render" && fail "unexpected Secret in default render"
16+
17+
echo "== main chart: clusterConnections rendering"
18+
render=$(helm template rel charts/rustfs-operator --namespace op-ns --set-json 'clusterConnections=[
19+
{"name":"prod","endpoint":"http://e:9000","allowedNamespaces":["a"],"credentials":{"accessKey":"k","secretKey":"s"}},
20+
{"name":"stg","endpoint":"http://e2:9000","insecure":true,"credentials":{"existingSecret":"ext"}}]')
21+
grep -q 'credentialsSecretRef: "rel-rustfs-operator-prod-admin"' <<<"$render" || fail "inline creds secret not referenced"
22+
grep -q 'credentialsSecretRef: "ext"' <<<"$render" || fail "existingSecret not referenced"
23+
[ "$(grep -c 'kind: ClusterConnection' <<<"$render")" = 2 ] || fail "expected 2 ClusterConnections"
24+
[ "$(grep -c 'kind: Secret' <<<"$render")" = 1 ] || fail "expected exactly 1 Secret"
25+
grep -q 'insecure: true' <<<"$render" || fail "insecure not rendered"
26+
grep -q 'allowedNamespaces:' <<<"$render" || fail "allowedNamespaces not rendered"
27+
28+
echo "== crds chart: defaults, keep policy, toggles"
29+
render=$(helm template crds charts/rustfs-operator-crds)
30+
[ "$(grep -c 'kind: CustomResourceDefinition' <<<"$render")" = 4 ] || fail "expected 4 CRDs"
31+
[ "$(grep -c 'helm.sh/resource-policy: keep' <<<"$render")" = 4 ] || fail "expected keep policy on all CRDs"
32+
render=$(helm template crds charts/rustfs-operator-crds --set keep=false --set crds.user=false)
33+
[ "$(grep -c 'kind: CustomResourceDefinition' <<<"$render")" = 3 ] || fail "crds.user=false should drop one CRD"
34+
grep -q 'resource-policy' <<<"$render" && fail "keep=false should drop the resource-policy annotation"
35+
36+
echo "== resources chart: rendering"
37+
render=$(helm template rel charts/rustfs-resources -n team-a -f scripts/testdata/resources-values.yaml)
38+
grep -q 'kind: Bucket' <<<"$render" || fail "no Bucket rendered"
39+
grep -q 'quotaBytes: 10485760' <<<"$render" || fail "quotaBytes wrong or missing"
40+
grep -q 'clusterRef: "prod"' <<<"$render" || fail "default connection not applied"
41+
grep -q 'name: rel-user-ci-user' <<<"$render" || fail "chart-created user secret not rendered"
42+
render=$(helm template rel charts/rustfs-resources --set-json 'buckets=[{"name":"b","connection":{"secretRef":"local"}}]')
43+
grep -q 'secretRef: "local"' <<<"$render" || fail "per-entry connection override not applied"
44+
grep -q 'versioning' <<<"$render" && fail "omitted versioning must not render"
45+
46+
echo "== validation failures"
47+
expect_fail() { # <chart> <set-json> <message fragment>
48+
local out
49+
if out=$(helm template rel "$1" --set-json "$2" 2>&1); then
50+
fail "expected template failure for: $2"
51+
fi
52+
grep -q "$3" <<<"$out" || fail "wrong error for [$2]: $out"
53+
}
54+
C=charts/rustfs-operator
55+
expect_fail $C 'clusterConnections=[{"endpoint":"x"}]' "non-empty 'name'"
56+
expect_fail $C 'clusterConnections=[{"name":"a"}]' "'endpoint' is required"
57+
expect_fail $C 'clusterConnections=[{"name":"a","endpoint":"x","credentials":{"existingSecret":"s","accessKey":"k"}}]' "not both"
58+
expect_fail $C 'clusterConnections=[{"name":"a","endpoint":"x"}]' "must both be set"
59+
expect_fail $C 'clusterConnections=[{"name":"a","endpoint":"x","credentials":{"create":false}}]' "no credentials source"
60+
expect_fail $C 'clusterConnections=[{"name":"a","endpoint":"x","credentials":{"accessKey":"k","secretKey":"s"}},{"name":"a","endpoint":"y","credentials":{"existingSecret":"e"}}]' "duplicate name"
61+
R=charts/rustfs-resources
62+
expect_fail $R 'buckets=[{"name":"a"}]' "no connection"
63+
expect_fail $R 'buckets=[{"name":"a","connection":{"clusterRef":"p","secretRef":"s"}}]' "mutually exclusive"
64+
expect_fail $R 'buckets=[{"name":"a","connection":{"clusterRef":"p"},"deletionPolicy":"Del"}]' "deletionPolicy must be"
65+
expect_fail $R 'policies=[{"name":"a","connection":{"clusterRef":"p"}}]' "'document' is required"
66+
expect_fail $R 'users=[{"name":"a","connection":{"clusterRef":"p"}}]' "secret key source is required"
67+
expect_fail $R 'users=[{"name":"a","connection":{"clusterRef":"p"},"secretKey":"x","secretKeyRef":{"name":"y"}}]' "not both"
68+
69+
echo "chart-tests OK"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Values used by chart-tests.sh (render assertions) and chart-e2e.sh
2+
# (real-cluster reconciliation). Every entry must be able to reach
3+
# ready=true against the bootstrapped "prod" ClusterConnection.
4+
connection:
5+
clusterRef: prod
6+
7+
buckets:
8+
- name: app-data
9+
versioning: true
10+
quotaBytes: 10485760
11+
12+
policies:
13+
- name: app-data-rw
14+
document:
15+
Version: "2012-10-17"
16+
Statement:
17+
- Effect: Allow
18+
Action: ["s3:GetObject", "s3:PutObject"]
19+
Resource: ["arn:aws:s3:::app-data/*"]
20+
21+
users:
22+
- name: ci-user
23+
policies: ["app-data-rw"]
24+
secretKey: chart-e2e-secret-123

0 commit comments

Comments
 (0)