Skip to content

Commit 105af9e

Browse files
committed
Route buckets per backend + verify multi-backend functional test end-to-end
The multi-backend functional test configured s3 + azureblob + filesystem but only ever exercised one backend: the chart had no way to route a bucket to a specific backend, so S3Proxy served every bucket from the first (default) backend. The leg passed without touching azureblob or s3 — a false green. Chart: - Per-backend bucket-locator. Move s3proxy.bucket-locator.N out of the shared config block (which copied one global list into every backend file, so S3Proxy's first-file-wins de-dup meant only the first backend honored it) into each backend's own properties, driven by config.backends.<name>.bucketLocators. - Per-backend credentials. Replace the single shared secret.properties (which emitted only the first-enabled backend's jclouds.credential and appended it to every backend file) with secret-common.properties + per-backend secret-<name>.properties; the merge initContainer appends common + the matching backend secret, so each backend gets only its own credential. Also fixes a latent GCS double-"jclouds.credential=" prefix. - azureblob: emit the provider-agnostic jclouds.endpoint (was the jclouds.azureblob.endpoint typo) and render the computed default endpoint when only account is set. Test: - ci/functional/values/multi-backend.yaml routes smoke-s3-*/smoke-az-*/smoke-fs-* to their backends; azureblob uses the azureblob-sdk provider, which signs correctly against Azurite (the legacy azureblob provider mis-signs -> 403). - ci/functional/assert-routing.sh writes one bucket per backend through s3proxy and verifies each object physically lands on its intended backend (MinIO for s3, Azurite for azureblob via az CLI, filesystem by elimination) and that no bucket leaks into another backend. - functional-test.yaml: multi-backend and azureblob legs are now hard gates (all soft_fail removed). No Chart.yaml version bump (handled separately), so verify-version fails by design for now. Verified on kind: multi-backend routing exclusive (s3->MinIO, az->Azurite, fs->filesystem); azureblob-only, s3-only, filesystem-only all pass.
1 parent 3c252ff commit 105af9e

9 files changed

Lines changed: 304 additions & 116 deletions

File tree

.github/workflows/functional-test.yaml

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,20 @@ name: Functional Test (kind)
55
# s3proxy. Public repo → GitHub-hosted runner + kind, no cluster credentials, so
66
# it is safe to run on every PR (including forks).
77
#
8-
# Backend coverage:
9-
# s3 -> MinIO mock, HARD GATE (full S3 round-trip).
10-
# filesystem / transient -> HARD GATE (install + round-trip). The chart now
11-
# emits placeholder jclouds.identity/jclouds.credential
12-
# for local backends (DND-1442), so s3proxy starts and
13-
# serves. These legs guard that fix against regression.
14-
# azureblob -> Azurite mock, NON-BLOCKING (soft_fail) until DND-1416
15-
# fixes the endpoint property (jclouds.azureblob.endpoint
16-
# -> jclouds.endpoint); the failing round-trip is the
17-
# functional test demonstrably catching that regression.
8+
# Backend coverage (all HARD GATES):
9+
# s3 -> MinIO mock, full S3 round-trip.
10+
# filesystem / transient -> install + round-trip. The chart emits placeholder
11+
# jclouds.identity/jclouds.credential for local
12+
# backends, so s3proxy starts and serves.
13+
# azureblob -> Azurite mock, full round-trip through the fixed
14+
# jclouds.endpoint (provider-agnostic).
1815
# multi-backend -> s3 (MinIO) + azureblob (Azurite) + filesystem (PVC)
19-
# in ONE release, round-tripping one bucket per backend
20-
# (smoke-s3-*/smoke-az-*/smoke-fs-*). NON-BLOCKING:
21-
# cannot pass on current main — blocked by DND-1442 and
22-
# DND-1416, plus two multi-backend chart gaps (secret.yaml
23-
# emits only the first enabled backend's jclouds.credential
24-
# into the shared secret.properties, and there is no
25-
# per-backend bucket-locator to route buckets between
26-
# backends on the shared endpoint). See the notes in
27-
# ci/functional/values/multi-backend.yaml.
28-
# Each soft_fail leg auto-goes-green once its bug is fixed.
16+
# in ONE release, with per-backend bucket-locator
17+
# routing. assert-routing.sh writes one bucket per
18+
# backend through s3proxy and verifies each object
19+
# physically lands on its intended backend (MinIO for
20+
# s3, Azurite for azureblob, filesystem by elimination)
21+
# and that no bucket leaks into another backend.
2922
# gcs/b2/openstack-swift/rackspace are render-only (covered by lint-render + helm-diff):
3023
# gcs has no jclouds.endpoint override in the chart, and the others have no
3124
# lightweight in-cluster emulator.
@@ -48,7 +41,6 @@ jobs:
4841
functional:
4942
name: functional (${{ matrix.backend }})
5043
runs-on: ubuntu-latest
51-
continue-on-error: ${{ matrix.soft_fail == true }}
5244
strategy:
5345
fail-fast: false
5446
matrix:
@@ -63,12 +55,10 @@ jobs:
6355
- backend: azureblob
6456
values: ci/functional/values/azureblob.yaml
6557
mock: ci/functional/mocks/azurite.yaml
66-
soft_fail: true
6758
- backend: multi-backend
6859
values: ci/functional/values/multi-backend.yaml
6960
mock: ci/functional/mocks/minio.yaml ci/functional/mocks/azurite.yaml
70-
buckets: smoke-s3 smoke-az smoke-fs
71-
soft_fail: true
61+
verify: routing
7262
env:
7363
NAMESPACE: s3proxy-test
7464
RELEASE: s3proxy
@@ -105,15 +95,19 @@ jobs:
10595
--values "${{ matrix.values }}" \
10696
--wait --timeout 5m
10797
108-
- name: Smoke test (S3 round-trip)
109-
# matrix.buckets is a space-separated list of bucket prefixes — one
110-
# round-trip per prefix (multi-backend scenarios use one per backend).
98+
- name: Verify (${{ matrix.verify || 'round-trip' }})
99+
# Single-backend legs: one S3 round-trip through s3proxy.
100+
# multi-backend leg (verify=routing): assert each bucket physically lands
101+
# on its intended backend (needs aws + az; both are on ubuntu-latest).
111102
run: |
112-
chmod +x ci/functional/smoke-test.sh
113-
for prefix in ${{ matrix.buckets || 'smoke' }}
114-
do
115-
ci/functional/smoke-test.sh "${RELEASE}" "${NAMESPACE}" 9000 test-access-key test-secret-key "${prefix}"
116-
done
103+
if [ "${{ matrix.verify }}" = "routing" ]
104+
then
105+
chmod +x ci/functional/assert-routing.sh
106+
ci/functional/assert-routing.sh "${RELEASE}" "${NAMESPACE}"
107+
else
108+
chmod +x ci/functional/smoke-test.sh
109+
ci/functional/smoke-test.sh "${RELEASE}" "${NAMESPACE}" 9000 test-access-key test-secret-key
110+
fi
117111
118112
- name: Diagnostics on failure
119113
if: failure()

charts/s3proxy/README.md.gotmpl

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -257,17 +257,27 @@ config:
257257
another-bucket: "actual-bucket-name"
258258
```
259259

260-
### Bucket Locator
260+
### Bucket Locator (routing buckets to backends)
261261

262-
Assign specific buckets to different backends:
262+
When more than one backend is enabled, assign buckets to a specific backend with
263+
that backend's own `bucketLocators` list. S3Proxy reads bucket-locators per
264+
backend, so each list is emitted only into that backend's properties file. Glob
265+
patterns are supported. A bucket that matches no backend's list falls through to
266+
the first-enabled backend (the default).
263267

264268
```yaml
265269
config:
266-
buckets:
267-
locator:
268-
- "bucket1"
269-
- "bucket2"
270-
- "*.test" # Glob patterns supported
270+
backends:
271+
s3:
272+
enabled: true
273+
# ... credentials ...
274+
bucketLocators:
275+
- "prod-*"
276+
- "customer-data"
277+
filesystem:
278+
enabled: true
279+
bucketLocators:
280+
- "scratch-*" # everything else also lands here (first-enabled default)
271281
```
272282

273283
## Monitoring

charts/s3proxy/templates/configmap.yaml

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,11 @@ s3proxy.alias.{{ $key }}={{ $value }}
3232
{{- end }}
3333
{{- end }}
3434

35-
{{- if .Values.config.buckets.locator }}
36-
# Bucket locator
37-
{{- range $index, $bucket := .Values.config.buckets.locator }}
38-
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
39-
{{- end }}
40-
{{- end }}
35+
{{- /* Bucket locators are per-backend: each backend's properties file lists
36+
the buckets it owns (see config.backends.<name>.bucketLocators). They are
37+
intentionally NOT emitted here in the shared config, because S3Proxy
38+
de-duplicates locators across --properties files (first file wins), so a
39+
shared list would route every bucket to only the first backend. */}}
4140

4241
{{- if .Values.config.middlewares.readOnly }}
4342
# Read-only middleware
@@ -83,6 +82,9 @@ data:
8382
# properties file, even for local backends where the values are unused.
8483
jclouds.identity={{ .Values.config.backends.filesystem.identity | default "local" }}
8584
jclouds.credential={{ .Values.config.backends.filesystem.credential | default "local" }}
85+
{{- range $index, $bucket := .Values.config.backends.filesystem.bucketLocators }}
86+
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
87+
{{- end }}
8688
{{- end }}
8789

8890
{{- if .Values.config.backends.transient.enabled }}
@@ -99,6 +101,9 @@ data:
99101
# properties file, even for local backends where the values are unused.
100102
jclouds.identity={{ .Values.config.backends.transient.identity | default "local" }}
101103
jclouds.credential={{ .Values.config.backends.transient.credential | default "local" }}
104+
{{- range $index, $bucket := .Values.config.backends.transient.bucketLocators }}
105+
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
106+
{{- end }}
102107
{{- end }}
103108

104109
{{- if .Values.config.backends.s3.enabled }}
@@ -123,6 +128,9 @@ data:
123128
{{- if .Values.config.backends.s3.secretAccessKey.value }}
124129
# Credential will be merged from the secret properties file
125130
{{- end }}
131+
{{- range $index, $bucket := .Values.config.backends.s3.bucketLocators }}
132+
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
133+
{{- end }}
126134
{{- end }}
127135

128136
{{- if .Values.config.backends.azureblob.enabled }}
@@ -131,15 +139,18 @@ data:
131139

132140
# Azure Blob backend configuration
133141
jclouds.provider={{ .Values.config.backends.azureblob.provider }}
134-
{{- if .Values.config.backends.azureblob.endpoint }}
135-
jclouds.azureblob.endpoint={{ .Values.config.backends.azureblob.endpoint | default (printf "https://%s.blob.core.windows.net" .Values.config.backends.azureblob.account) }}
142+
{{- if or .Values.config.backends.azureblob.endpoint .Values.config.backends.azureblob.account }}
143+
jclouds.endpoint={{ .Values.config.backends.azureblob.endpoint | default (printf "https://%s.blob.core.windows.net" .Values.config.backends.azureblob.account) }}
136144
{{- end }}
137145
{{- if .Values.config.backends.azureblob.account }}
138146
jclouds.identity={{ .Values.config.backends.azureblob.account }}
139147
{{- end }}
140148
{{- if or .Values.config.backends.azureblob.key.value .Values.config.backends.azureblob.sasToken.value }}
141149
# Credentials will be merged from the secret properties file
142150
{{- end }}
151+
{{- range $index, $bucket := .Values.config.backends.azureblob.bucketLocators }}
152+
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
153+
{{- end }}
143154
{{- end }}
144155

145156
{{- if .Values.config.backends.googleCloudStorage.enabled }}
@@ -161,6 +172,9 @@ data:
161172
# Private key credential will be merged from the secret properties file
162173
# jclouds.credential will be provided by the secret
163174
{{- end }}
175+
{{- range $index, $bucket := .Values.config.backends.googleCloudStorage.bucketLocators }}
176+
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
177+
{{- end }}
164178
{{- end }}
165179

166180
{{- if .Values.config.backends.b2.enabled }}
@@ -175,6 +189,9 @@ data:
175189
{{- if .Values.config.backends.b2.applicationKey.value }}
176190
# Credential will be merged from the secret properties file
177191
{{- end }}
192+
{{- range $index, $bucket := .Values.config.backends.b2.bucketLocators }}
193+
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
194+
{{- end }}
178195
{{- end }}
179196

180197
{{- if .Values.config.backends.openstackSwift.enabled }}
@@ -195,6 +212,9 @@ data:
195212
{{- if .Values.config.backends.openstackSwift.password.value }}
196213
# Credential will be merged from the secret properties file
197214
{{- end }}
215+
{{- range $index, $bucket := .Values.config.backends.openstackSwift.bucketLocators }}
216+
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
217+
{{- end }}
198218
{{- end }}
199219

200220
{{- if .Values.config.backends.rackspaceCloudfiles.enabled }}
@@ -213,4 +233,7 @@ data:
213233
{{- if .Values.config.backends.rackspaceCloudfiles.apiKey.value }}
214234
# Credential will be merged from the secret properties file
215235
{{- end }}
236+
{{- range $index, $bucket := .Values.config.backends.rackspaceCloudfiles.bucketLocators }}
237+
s3proxy.bucket-locator.{{ add $index 1 }}={{ $bucket }}
238+
{{- end }}
216239
{{- end }}

charts/s3proxy/templates/deployment.yaml

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,18 @@ spec:
4444
set -e
4545
echo "Merging configuration files..."
4646
47-
# Check if secret properties file exists
48-
SECRET_FILE="/secret/secret.properties"
47+
# secret-common.properties holds the client-facing auth and is
48+
# appended to every backend file. Each backend additionally gets
49+
# its own secret-<backend>.properties (its jclouds.credential), so
50+
# one backend's credential never leaks into another's file.
51+
COMMON_SECRET="/secret/secret-common.properties"
4952
5053
# Loop through all properties files in the config directory
5154
for config_file in /config/*.properties
5255
do
5356
if [ -f "$config_file" ]
5457
then
55-
# Get the filename
58+
# Get the filename, e.g. backend-s3.properties
5659
filename=$(basename "$config_file")
5760
output_file="/merged-config/$filename"
5861
@@ -61,11 +64,22 @@ spec:
6164
# Copy base config file to output
6265
cp "$config_file" "$output_file"
6366
64-
# If secret file exists, append its contents (overriding duplicates)
65-
if [ -f "$SECRET_FILE" ]
67+
# Append the shared client-auth secret, if present
68+
if [ -f "$COMMON_SECRET" ]
6669
then
6770
echo "" >> "$output_file" # Add newline separator
68-
cat "$SECRET_FILE" >> "$output_file"
71+
cat "$COMMON_SECRET" >> "$output_file"
72+
fi
73+
74+
# Append this backend's own secret: backend-<name>.properties
75+
# pairs with secret-<name>.properties
76+
backend="${filename#backend-}"
77+
backend="${backend%.properties}"
78+
backend_secret="/secret/secret-${backend}.properties"
79+
if [ -f "$backend_secret" ]
80+
then
81+
echo "" >> "$output_file" # Add newline separator
82+
cat "$backend_secret" >> "$output_file"
6983
fi
7084
fi
7185
done

charts/s3proxy/templates/secret.yaml

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,53 @@ metadata:
66
{{- include "s3proxy.labels" . | nindent 4 }}
77
type: Opaque
88
stringData:
9-
# Properties file containing sensitive configuration values
10-
# This will be merged with the backend properties files by the init container
11-
secret.properties: |
9+
# Sensitive properties merged into the backend properties files by the
10+
# merge-configs initContainer:
11+
# * secret-common.properties -> appended to EVERY backend file (client auth).
12+
# * secret-<backend>.properties -> appended ONLY to that backend's file, so
13+
# each backend gets its own jclouds.credential and never another backend's.
14+
# Local backends (filesystem/transient) need no secret file; their placeholder
15+
# jclouds.identity/credential come from the ConfigMap.
1216
{{- if and .Values.config.auth.identity .Values.config.auth.secret }}
13-
# S3Proxy authentication credentials (for clients connecting to s3proxy)
17+
secret-common.properties: |
18+
# S3Proxy client authentication credentials (shared by all backends)
1419
s3proxy.identity={{ .Values.config.auth.identity }}
1520
s3proxy.credential={{ .Values.config.auth.secret }}
1621
{{- end }}
17-
18-
{{- if .Values.config.backends.s3.enabled }}
19-
{{- if .Values.config.backends.s3.secretAccessKey.value }}
20-
# S3 backend credentials
22+
{{- if and .Values.config.backends.s3.enabled .Values.config.backends.s3.secretAccessKey.value }}
23+
secret-s3.properties: |
24+
# S3 backend credential
2125
jclouds.credential={{ .Values.config.backends.s3.secretAccessKey.value }}
22-
{{- end }}
23-
{{- else if .Values.config.backends.azureblob.enabled }}
24-
{{- if or .Values.config.backends.azureblob.key.value .Values.config.backends.azureblob.sasToken.value }}
26+
{{- end }}
27+
{{- if .Values.config.backends.azureblob.enabled }}
28+
{{- if or .Values.config.backends.azureblob.key.value .Values.config.backends.azureblob.sasToken.value }}
29+
secret-azureblob.properties: |
2530
# Azure Blob backend credentials
26-
{{- end }}
27-
{{- if .Values.config.backends.azureblob.key.value }}
31+
{{- if .Values.config.backends.azureblob.key.value }}
2832
jclouds.credential={{ .Values.config.backends.azureblob.key.value }}
29-
{{- end }}
30-
{{- if .Values.config.backends.azureblob.sasToken.value }}
33+
{{- end }}
34+
{{- if .Values.config.backends.azureblob.sasToken.value }}
3135
jclouds.azureblob.sas={{ .Values.config.backends.azureblob.sasToken.value }}
32-
{{- end }}
33-
{{- else if .Values.config.backends.googleCloudStorage.enabled }}
34-
{{- if .Values.config.backends.googleCloudStorage.privateKey.value }}
35-
# Google Cloud Storage backend credentials (privateKey stored in secret)
36-
jclouds.credential=jclouds.credential={{ .Values.config.backends.googleCloudStorage.privateKey.value | trim | replace "\n" "\\n\\" }}
37-
{{- end }}
38-
{{- else if .Values.config.backends.b2.enabled }}
39-
{{- if .Values.config.backends.b2.applicationKey.value }}
40-
# Backblaze B2 backend credentials
36+
{{- end }}
37+
{{- end }}
38+
{{- end }}
39+
{{- if and .Values.config.backends.googleCloudStorage.enabled .Values.config.backends.googleCloudStorage.privateKey.value }}
40+
secret-google-cloud-storage.properties: |
41+
# Google Cloud Storage backend credential (PEM private key)
42+
jclouds.credential={{ .Values.config.backends.googleCloudStorage.privateKey.value | trim | replace "\n" "\\n\\" }}
43+
{{- end }}
44+
{{- if and .Values.config.backends.b2.enabled .Values.config.backends.b2.applicationKey.value }}
45+
secret-b2.properties: |
46+
# Backblaze B2 backend credential
4147
jclouds.credential={{ .Values.config.backends.b2.applicationKey.value }}
42-
{{- end }}
43-
{{- else if .Values.config.backends.openstackSwift.enabled }}
44-
{{- if .Values.config.backends.openstackSwift.password.value }}
45-
# OpenStack Swift backend credentials
48+
{{- end }}
49+
{{- if and .Values.config.backends.openstackSwift.enabled .Values.config.backends.openstackSwift.password.value }}
50+
secret-openstack-swift.properties: |
51+
# OpenStack Swift backend credential
4652
jclouds.credential={{ .Values.config.backends.openstackSwift.password.value }}
47-
{{- end }}
48-
{{- else if .Values.config.backends.rackspaceCloudfiles.enabled }}
49-
{{- if .Values.config.backends.rackspaceCloudfiles.apiKey.value }}
50-
# Rackspace Cloud Files backend credentials
53+
{{- end }}
54+
{{- if and .Values.config.backends.rackspaceCloudfiles.enabled .Values.config.backends.rackspaceCloudfiles.apiKey.value }}
55+
secret-rackspace-cloudfiles.properties: |
56+
# Rackspace Cloud Files backend credential
5157
jclouds.credential={{ .Values.config.backends.rackspaceCloudfiles.apiKey.value }}
52-
{{- end }}
5358
{{- end }}

0 commit comments

Comments
 (0)