mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-27 06:18:43 +00:00
d457378c02
* fix(scanner): treat replication status writebacks as status-class hits A replication status writeback (persist_replication_state_if_current in Update mode, including the heals the scanner queues during its own walk) marked the bucket dirty on the namespace path: it advanced the dirty-usage generation, woke the scanner, reached the durable dirty journal and entered the activity snapshot. Any replicated bucket therefore made every scanner cycle Superseded, the authoritative usage baseline was never certified and usage freshness stayed stale for as long as replication ran (#8375). The writeback is now status class: record_status_class_mutation records only the process-local producer coverage bit and nothing else. There is no rescan queue for it, because a deployment with an active replication rule already walks every bucket on every cycle (ScannerMaintenanceFeatures::needs_regular_cycle) and that walk refreshes replication_stats. The routing lives in a named, unit-tested function in rustfs/src/storage/storage_api.rs; tier-expiration cleanup removes data and stays on the namespace path. No persisted format, protocol, admin API or configuration changes. This is phase 1 of docs/architecture/scanner-per-bucket-convergence.md, which this change adds together with its index entry. Refs #8375, #8494, rustfs/backlog#2816. * fix(scanner): refresh replication statistics during maintenance Refresh compacted prefixes and completed bucket caches during replication maintenance. Bind set execution reuse to the observed replication statistics so a same-cycle retry cannot restore older counters. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(scanner): avoid expanding the legacy S3 DTO surface Decode the existing replication configuration type from a JSON fixture instead of adding direct S3 DTO references to a new file. Preserve the compacted-prefix and same-cycle assertions and the footprint ratchet. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * perf(scanner): keep raw enumeration bookkeeping linear per entry The page writer inserted into its building page with a push-and-sort on every entry and committed each page through a clone. Insert in sorted position and move the committed page into the index instead. Add a regression test that records 20,000 entries in one prefix and pins the cost to one hash per entry and well under a second, so the quadratic re-normalization removed by #8114 (rustfs#8113) cannot return once pacing builds on this walker. * feat(scanner): decouple bucket walkers from disk count Add the scanner key walkers_per_disk (RUSTFS_SCANNER_WALKERS_PER_DISK, 1 through 16, default 1). Each local coordinator disk of a set spawns that many bucket walkers; remote disks keep one. The walker count is what max_concurrent_disk_scans caps and the disk count stays the floor. The walkers of one disk share its scanner admission through LocalDiskAdmissionShare: the first walker to start a bucket takes the per-disk permit and the last to finish releases it, so a peer's remote request still sees one scan session per physical disk. Without the share the second walker was rejected as busy and retired. The disk_concurrency_budget log line reports walkers_per_disk, walkers and the reason for the effective limit. The default stays 1 because no local disk reports rotational media today; the design keeps a higher flash default as the intent. * docs(scanner): document walkers_per_disk in the Helm chart and configuration manuals RUSTFS_SCANNER_WALKERS_PER_DISK was only described in the scanner runtime controls table. Add it to the Helm chart (values, configmap rendering, README table, template test), the configuration crate's environment reference with operator guidance, and a short section in the operations manual on when to raise it and how to confirm it took effect. --------- Co-authored-by: Hauser <housemecn@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com>
854 lines
39 KiB
Bash
Executable File
854 lines
39 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
CHART_DIR="$ROOT_DIR/helm/rustfs"
|
|
|
|
render_chart() {
|
|
helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set mode.distributed.enabled=false \
|
|
--set mode.standalone.enabled=true \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key \
|
|
"$@"
|
|
}
|
|
|
|
render_standalone_deployment() {
|
|
render_chart "$@" |
|
|
awk '
|
|
/^# Source: rustfs\/templates\/deployment.yaml$/ { in_deployment = 1 }
|
|
in_deployment && /^---$/ { exit }
|
|
in_deployment { print }
|
|
'
|
|
}
|
|
|
|
render_distributed_statefulset() {
|
|
helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key \
|
|
"$@" |
|
|
awk '
|
|
/^# Source: rustfs\/templates\/statefulset.yaml$/ { in_statefulset = 1 }
|
|
in_statefulset && /^---$/ { exit }
|
|
in_statefulset { print }
|
|
'
|
|
}
|
|
|
|
statefulset_env_names() {
|
|
yq eval '[(.spec.template.spec.containers[0].env // [])[] | .name] | join(" ")' -
|
|
}
|
|
|
|
statefulset_env_value() {
|
|
local rustfs_test_env_name=$1
|
|
RUSTFS_TEST_ENV_NAME="$rustfs_test_env_name" \
|
|
yq eval '.spec.template.spec.containers[0].env[] | select(.name == strenv(RUSTFS_TEST_ENV_NAME)) | .value' -
|
|
}
|
|
|
|
render_distributed_configmap() {
|
|
helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key \
|
|
"$@" |
|
|
awk '
|
|
/^# Source: rustfs\/templates\/configmap.yaml$/ { in_configmap = 1 }
|
|
in_configmap && /^---$/ { exit }
|
|
in_configmap { print }
|
|
'
|
|
}
|
|
|
|
render_server_cert() {
|
|
helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key \
|
|
--set mtls.enabled=true \
|
|
"$@" |
|
|
awk '
|
|
/^# Source: rustfs\/templates\/cert-manager-mtls\/04-server-cert.yaml$/ { in_cert = 1 }
|
|
in_cert && /^---$/ { exit }
|
|
in_cert { print }
|
|
'
|
|
}
|
|
|
|
# Gateway routes must resolve to the rendered Kubernetes Service without Traefik CRDs.
|
|
for gateway_class in traefik contour istio; do
|
|
for distributed in false true; do
|
|
standalone=true
|
|
if [[ "$distributed" == true ]]; then standalone=false; fi
|
|
gateway_output=$(render_chart \
|
|
--set "mode.distributed.enabled=$distributed" \
|
|
--set "mode.standalone.enabled=$standalone" \
|
|
--set gatewayApi.enabled=true \
|
|
--set "gatewayApi.gatewayClass=$gateway_class" \
|
|
--set fullnameOverride=gateway-test \
|
|
--set service.console.port=19001 \
|
|
--set service.endpoint.port=19000 \
|
|
--set gatewayApi.hostname=console.example.com \
|
|
--set gatewayApi.endpointHostname=s3.example.com \
|
|
--set gatewayApi.existingGateway.name=shared-gateway \
|
|
--set gatewayApi.existingGateway.namespace=gateway-system \
|
|
--set gatewayApi.listeners.https.name=https)
|
|
yq eval -e 'select(.kind == "Service" and .metadata.name == "gateway-test-svc") |
|
|
.metadata.namespace == "rustfs" and
|
|
.spec.ports[0].port == 19000 and .spec.ports[1].port == 19001' - <<<"$gateway_output" >/dev/null
|
|
for route in route endpoint-route; do
|
|
expected_port=19001
|
|
expected_host=console.example.com
|
|
if [[ "$route" == endpoint-route ]]; then
|
|
expected_port=19000
|
|
expected_host=s3.example.com
|
|
fi
|
|
ROUTE_NAME="gateway-test-$route" EXPECTED_PORT="$expected_port" EXPECTED_HOST="$expected_host" \
|
|
yq eval -e 'select(.kind == "HTTPRoute" and .metadata.name == strenv(ROUTE_NAME)) |
|
|
.metadata.namespace == "rustfs" and
|
|
.spec.hostnames[0] == strenv(EXPECTED_HOST) and
|
|
.spec.parentRefs[0].name == "shared-gateway" and
|
|
.spec.parentRefs[0].namespace == "gateway-system" and
|
|
.spec.parentRefs[0].sectionName == "https" and
|
|
.spec.rules[0].matches[0].path.type == "PathPrefix" and
|
|
.spec.rules[0].matches[0].path.value == "/" and
|
|
(.spec.rules[0].backendRefs | length) == 1 and
|
|
.spec.rules[0].backendRefs[0].name == "gateway-test-svc" and
|
|
.spec.rules[0].backendRefs[0].port == env(EXPECTED_PORT) and
|
|
(.spec.rules[0].backendRefs[0].kind // "Service") == "Service" and
|
|
(.spec.rules[0].backendRefs[0].group // "") == ""' - <<<"$gateway_output" >/dev/null
|
|
done
|
|
if grep -q 'kind: TraefikService' <<<"$gateway_output"; then
|
|
echo "Gateway API must not require a TraefikService CRD" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
done
|
|
|
|
gateway_default=$(render_chart --set gatewayApi.enabled=true)
|
|
yq eval -e 'select(.kind == "HTTPRoute" and .metadata.name == "rustfs-route") |
|
|
.spec.rules[0].backendRefs[0].name == "rustfs-svc" and
|
|
.spec.rules[0].backendRefs[0].port == 9001 and
|
|
.spec.parentRefs[0].name == "rustfs-gateway" and
|
|
.spec.parentRefs[0].sectionName == "websecure"' - <<<"$gateway_default" >/dev/null
|
|
if grep -q 'name: rustfs-endpoint-route' <<<"$gateway_default"; then
|
|
echo "The S3 route must be opt-in" >&2
|
|
exit 1
|
|
fi
|
|
|
|
gateway_no_redirect=$(render_chart --set gatewayApi.enabled=true --set gatewayApi.endpointHostname=s3.example.com --set gatewayApi.httpToHttpsRedirect=false)
|
|
if grep -q 'type: RequestRedirect' <<<"$gateway_no_redirect"; then
|
|
echo "Disabling HTTPS redirects must suppress the redirect route" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if render_chart --set gatewayApi.enabled=true --set gatewayApi.endpointHostname=example.rustfs.com >/dev/null 2>&1; then
|
|
echo "Console and S3 routes must not share a hostname" >&2
|
|
exit 1
|
|
fi
|
|
|
|
gateway_disabled=$(render_chart --set gatewayApi.endpointHostname=s3.example.com)
|
|
if grep -q 'kind: HTTPRoute' <<<"$gateway_disabled"; then
|
|
echo "Disabling Gateway API must suppress all HTTP routes" >&2
|
|
exit 1
|
|
fi
|
|
|
|
recreate_output=$(render_standalone_deployment --set mode.standalone.strategy.type=Recreate)
|
|
grep -q "type: Recreate" <<<"$recreate_output"
|
|
if grep -q "rollingUpdate:" <<<"$recreate_output"; then
|
|
echo "Recreate strategy must not render rollingUpdate fields" >&2
|
|
exit 1
|
|
fi
|
|
|
|
rolling_output=$(render_standalone_deployment)
|
|
grep -q "type: RollingUpdate" <<<"$rolling_output"
|
|
grep -q "rollingUpdate:" <<<"$rolling_output"
|
|
grep -Eq '^[[:space:]]*replicas:[[:space:]]*1[[:space:]]*$' <<<"$rolling_output"
|
|
|
|
scaled_to_zero_output=$(render_standalone_deployment --set replicaCount=0)
|
|
grep -Eq '^[[:space:]]*replicas:[[:space:]]*0[[:space:]]*$' <<<"$scaled_to_zero_output"
|
|
|
|
scanner_config_output=$(render_chart \
|
|
--set config.rustfs.scanner.speed=slow \
|
|
--set config.rustfs.scanner.delay=30 \
|
|
--set config.rustfs.scanner.max_wait_secs=15 \
|
|
--set config.rustfs.scanner.cycle_secs=3600 \
|
|
--set config.rustfs.scanner.start_delay_secs=60 \
|
|
--set config.rustfs.scanner.cycle_max_duration_secs=1800 \
|
|
--set config.rustfs.scanner.cycle_max_objects=0 \
|
|
--set config.rustfs.scanner.cycle_max_directories=100000 \
|
|
--set config.rustfs.scanner.bitrot_cycle_secs=0 \
|
|
--set config.rustfs.scanner.idle_mode=false \
|
|
--set config.rustfs.scanner.cache_save_timeout_secs=30 \
|
|
--set config.rustfs.scanner.max_concurrent_set_scans=2 \
|
|
--set config.rustfs.scanner.max_concurrent_disk_scans=1 \
|
|
--set config.rustfs.scanner.walkers_per_disk=2 \
|
|
--set config.rustfs.scanner.yield_every_n_objects=128 \
|
|
--set config.rustfs.scanner.alert_excess_versions=100 \
|
|
--set config.rustfs.scanner.alert_excess_version_size=1099511627776 \
|
|
--set config.rustfs.scanner.alert_excess_folders=65538)
|
|
grep -q 'RUSTFS_SCANNER_SPEED: "slow"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_DELAY: "30"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_MAX_WAIT_SECS: "15"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_CYCLE: "3600"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_START_DELAY_SECS: "60"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_CYCLE_MAX_DURATION_SECS: "1800"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_CYCLE_MAX_OBJECTS: "0"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_CYCLE_MAX_DIRECTORIES: "100000"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_BITROT_CYCLE_SECS: "0"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_IDLE_MODE: "false"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_CACHE_SAVE_TIMEOUT_SECS: "30"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_MAX_CONCURRENT_SET_SCANS: "2"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_MAX_CONCURRENT_DISK_SCANS: "1"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_WALKERS_PER_DISK: "2"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_YIELD_EVERY_N_OBJECTS: "128"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_ALERT_EXCESS_VERSIONS: "100"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_ALERT_EXCESS_VERSION_SIZE: "1099511627776"' <<<"$scanner_config_output"
|
|
grep -q 'RUSTFS_SCANNER_ALERT_EXCESS_FOLDERS: "65538"' <<<"$scanner_config_output"
|
|
|
|
# Fail-closed credential checks. Rendering must fail when no credentials,
|
|
# existingSecret, or allowInsecureDefaults override is supplied.
|
|
default_render_status=0
|
|
helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set mode.distributed.enabled=false \
|
|
--set mode.standalone.enabled=true \
|
|
>/dev/null 2>&1 || default_render_status=$?
|
|
if [[ $default_render_status -eq 0 ]]; then
|
|
echo "Default credentials must fail to render without an explicit override" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Rendering must also fail if someone re-supplies the well-known defaults.
|
|
default_creds_status=0
|
|
helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set mode.distributed.enabled=false \
|
|
--set mode.standalone.enabled=true \
|
|
--set secret.rustfs.access_key=rustfsadmin \
|
|
--set secret.rustfs.secret_key=rustfsadmin \
|
|
>/dev/null 2>&1 || default_creds_status=$?
|
|
if [[ $default_creds_status -eq 0 ]]; then
|
|
echo "Setting the well-known defaults must fail without allowInsecureDefaults" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# allowInsecureDefaults=true must succeed and emit the dev creds.
|
|
insecure_output=$(helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set mode.distributed.enabled=false \
|
|
--set mode.standalone.enabled=true \
|
|
--set secret.allowInsecureDefaults=true)
|
|
expected_b64=$(printf 'rustfsadmin' | base64)
|
|
if ! grep -q "RUSTFS_ACCESS_KEY: \"$expected_b64\"" <<<"$insecure_output"; then
|
|
echo "allowInsecureDefaults=true must emit the well-known dev access key" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# existingSecret must skip rendering the chart-managed Secret entirely.
|
|
existing_output=$(helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set mode.distributed.enabled=false \
|
|
--set mode.standalone.enabled=true \
|
|
--set secret.existingSecret=my-existing-secret)
|
|
if grep -q "RUSTFS_ACCESS_KEY:" <<<"$existing_output"; then
|
|
echo "existingSecret must suppress chart-managed Secret rendering" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Partial-default credentials (one key set to the well-known default) must
|
|
# fail rendering even when the other key is non-default.
|
|
partial_default_status=0
|
|
helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set mode.distributed.enabled=false \
|
|
--set mode.standalone.enabled=true \
|
|
--set secret.rustfs.access_key=rustfsadmin \
|
|
--set secret.rustfs.secret_key=some-other-secret \
|
|
>/dev/null 2>&1 || partial_default_status=$?
|
|
if [[ $partial_default_status -eq 0 ]]; then
|
|
echo "Partial-default credentials (access_key=rustfsadmin) must fail rendering" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Partial-empty credentials (only one of the two keys set) must fail rendering
|
|
# even when allowInsecureDefaults=true — never silently auto-fill a single
|
|
# missing key with the well-known default.
|
|
partial_empty_status=0
|
|
helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set mode.distributed.enabled=false \
|
|
--set mode.standalone.enabled=true \
|
|
--set secret.allowInsecureDefaults=true \
|
|
--set secret.rustfs.access_key=user-supplied-access-key \
|
|
>/dev/null 2>&1 || partial_empty_status=$?
|
|
if [[ $partial_empty_status -eq 0 ]]; then
|
|
echo "Partial-empty credentials (only access_key set) must fail rendering" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The Vault KMS token is a credential: it must be rendered into a Secret and must never
|
|
# reach the config ConfigMap, which is readable by anyone allowed to get ConfigMaps.
|
|
kms_token="CS_TEST_SECRET_DO_NOT_LOG"
|
|
kms_values=(
|
|
--set config.rustfs.kms.enabled=true
|
|
--set config.rustfs.kms.type=vault
|
|
--set config.rustfs.kms.vault.vault_backend=vault-kv2
|
|
--set config.rustfs.kms.vault.vault_address=http://vault.rustfs.svc:8200
|
|
--set "config.rustfs.kms.vault.vault_token=$kms_token"
|
|
--set config.rustfs.kms.vault.default_key=test-key
|
|
)
|
|
|
|
kms_configmap=$(render_distributed_configmap "${kms_values[@]}")
|
|
if grep -q 'RUSTFS_KMS_VAULT_TOKEN' <<<"$kms_configmap"; then
|
|
echo "The Vault KMS token must not be rendered into the ConfigMap" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -q 'RUSTFS_KMS_VAULT_ADDRESS' <<<"$kms_configmap"; then
|
|
echo "Non-secret KMS settings must stay in the ConfigMap" >&2
|
|
exit 1
|
|
fi
|
|
|
|
kms_full=$(helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key \
|
|
"${kms_values[@]}")
|
|
if grep -q "$kms_token" <<<"$kms_full"; then
|
|
echo "The Vault KMS token must never appear in plaintext in any rendered manifest" >&2
|
|
exit 1
|
|
fi
|
|
expected_kms_token_b64=$(printf '%s' "$kms_token" | base64)
|
|
if ! grep -q "RUSTFS_KMS_VAULT_TOKEN: \"$expected_kms_token_b64\"" <<<"$kms_full"; then
|
|
echo "The Vault KMS token must be rendered into a Secret" >&2
|
|
exit 1
|
|
fi
|
|
|
|
kms_statefulset=$(render_distributed_statefulset "${kms_values[@]}")
|
|
if ! grep -q 'name: rustfs-kms-secret' <<<"$kms_statefulset"; then
|
|
echo "The distributed StatefulSet must consume the KMS Secret via envFrom" >&2
|
|
exit 1
|
|
fi
|
|
|
|
kms_deployment=$(render_standalone_deployment "${kms_values[@]}")
|
|
if ! grep -q 'name: rustfs-kms-secret' <<<"$kms_deployment"; then
|
|
echo "The standalone Deployment must consume the KMS Secret via envFrom" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Without a configured token no KMS Secret is rendered and nothing references it.
|
|
no_kms_output=$(helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key)
|
|
if grep -q 'kms-secret' <<<"$no_kms_output"; then
|
|
echo "No KMS Secret must be rendered when no Vault token is configured" >&2
|
|
exit 1
|
|
fi
|
|
|
|
command -v yq >/dev/null 2>&1 || { echo "yq is required for extra-volumes structural tests" >&2; exit 1; }
|
|
|
|
# Structural helpers: verify wiring at the right YAML paths, not just string presence.
|
|
assert_extra_volumes_wired() {
|
|
local output="$1" label="$2"
|
|
|
|
if ! yq eval '.spec.template.spec.volumes[].name' - <<<"$output" | grep "^ca-bundle$" >/dev/null; then
|
|
echo "ca-bundle not found in spec.template.spec.volumes of $label" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local mount_path
|
|
mount_path=$(yq eval '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "ca-bundle") | .mountPath' - <<<"$output")
|
|
if [[ "$mount_path" != "/etc/ssl/certs/ca.crt" ]]; then
|
|
echo "ca-bundle mountPath in containers[0] is '$mount_path', expected /etc/ssl/certs/ca.crt in $label" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if yq eval '.spec.template.spec.initContainers[].volumeMounts[].name' - <<<"$output" | grep "^ca-bundle$" >/dev/null; then
|
|
echo "ca-bundle must not appear in initContainers volumeMounts of $label" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
assert_extra_volumes_absent() {
|
|
local output="$1" label="$2"
|
|
if yq eval '.spec.template.spec.volumes[].name' - <<<"$output" | grep "^ca-bundle$" >/dev/null; then
|
|
echo "ca-bundle must not appear in spec.template.spec.volumes of $label with empty extraVolumes" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# extraVolumes/extraVolumeMounts: structural placement in standalone Deployment.
|
|
standalone_extra_output=$(render_standalone_deployment \
|
|
--set 'extraVolumes[0].name=ca-bundle' \
|
|
--set 'extraVolumes[0].configMap.name=ca-bundle' \
|
|
--set 'extraVolumeMounts[0].name=ca-bundle' \
|
|
--set 'extraVolumeMounts[0].mountPath=/etc/ssl/certs/ca.crt' \
|
|
--set 'extraVolumeMounts[0].subPath=ca.crt')
|
|
assert_extra_volumes_wired "$standalone_extra_output" "standalone Deployment"
|
|
|
|
# Empty extraVolumes must not inject ca-bundle into standalone Deployment volumes.
|
|
standalone_default_output=$(render_standalone_deployment)
|
|
assert_extra_volumes_absent "$standalone_default_output" "standalone Deployment"
|
|
|
|
# extraVolumes/extraVolumeMounts: structural placement in distributed StatefulSet.
|
|
distributed_extra_output=$(render_distributed_statefulset \
|
|
--set 'extraVolumes[0].name=ca-bundle' \
|
|
--set 'extraVolumes[0].configMap.name=ca-bundle' \
|
|
--set 'extraVolumeMounts[0].name=ca-bundle' \
|
|
--set 'extraVolumeMounts[0].mountPath=/etc/ssl/certs/ca.crt' \
|
|
--set 'extraVolumeMounts[0].subPath=ca.crt')
|
|
assert_extra_volumes_wired "$distributed_extra_output" "distributed StatefulSet"
|
|
|
|
# Empty extraVolumes must not inject ca-bundle into distributed StatefulSet volumes.
|
|
distributed_default_output=$(render_distributed_statefulset)
|
|
assert_extra_volumes_absent "$distributed_default_output" "distributed StatefulSet"
|
|
|
|
# clusterDomain (issue #3857): a custom Kubernetes cluster domain must flow into
|
|
# the RUSTFS_VOLUMES FQDN and mTLS server cert SANs, defaulting to cluster.local.
|
|
volumes_default=$(render_distributed_configmap | grep 'RUSTFS_VOLUMES:' || true)
|
|
if ! grep -q 'svc\.cluster\.local' <<<"$volumes_default"; then
|
|
echo "Default RUSTFS_VOLUMES must use svc.cluster.local" >&2
|
|
exit 1
|
|
fi
|
|
|
|
volumes_custom=$(render_distributed_configmap --set clusterDomain=cluster.internal | grep 'RUSTFS_VOLUMES:' || true)
|
|
if ! grep -q 'svc\.cluster\.internal' <<<"$volumes_custom"; then
|
|
echo "Custom clusterDomain must appear in the RUSTFS_VOLUMES FQDN" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'cluster\.local' <<<"$volumes_custom"; then
|
|
echo "Custom clusterDomain must fully replace cluster.local in RUSTFS_VOLUMES" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# An explicit config.rustfs.volumes stays authoritative regardless of clusterDomain.
|
|
volumes_explicit=$(render_distributed_configmap \
|
|
--set config.rustfs.volumes=http://example.test/data \
|
|
--set clusterDomain=cluster.internal | grep 'RUSTFS_VOLUMES:' || true)
|
|
if ! grep -q 'RUSTFS_VOLUMES: "http://example.test/data"' <<<"$volumes_explicit"; then
|
|
echo "Explicit config.rustfs.volumes must remain authoritative regardless of clusterDomain" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# A dot-only clusterDomain must fall back to cluster.local instead of an empty domain.
|
|
volumes_dots=$(render_distributed_configmap --set clusterDomain=. | grep 'RUSTFS_VOLUMES:' || true)
|
|
if ! grep -q 'svc\.cluster\.local' <<<"$volumes_dots"; then
|
|
echo "Dot-only clusterDomain must fall back to cluster.local in RUSTFS_VOLUMES" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# mTLS server certificate SANs must honor clusterDomain too.
|
|
cert_default=$(render_server_cert)
|
|
if ! grep -q 'svc\.cluster\.local"' <<<"$cert_default"; then
|
|
echo "Default mTLS server cert SANs must use svc.cluster.local" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'example\.rustfs\.com' <<<"$cert_default"; then
|
|
echo "mTLS server cert SANs must not include the default external ingress host" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cert_external_host=$(render_server_cert --set ingress.hosts[0].host=storage.example.test)
|
|
if grep -q 'storage\.example\.test' <<<"$cert_external_host"; then
|
|
echo "mTLS server cert SANs must not include custom external ingress hosts" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cert_custom=$(render_server_cert --set clusterDomain=cluster.internal)
|
|
if ! grep -q 'svc\.cluster\.internal"' <<<"$cert_custom"; then
|
|
echo "Custom clusterDomain must appear in mTLS server cert SANs" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'svc\.cluster\.local"' <<<"$cert_custom"; then
|
|
echo "Custom clusterDomain must fully replace cluster.local in mTLS server cert SANs" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Legacy topology compatibility: default replicaCount=4 (no drivesPerNode set)
|
|
# must render the old 4x4 PVC names (data-rustfs-0 .. data-rustfs-3).
|
|
legacy_four_by_four=$(render_distributed_statefulset)
|
|
for i in 0 1 2 3; do
|
|
if ! grep -q "name: data-rustfs-${i}" <<<"$legacy_four_by_four"; then
|
|
echo "Legacy 4x4 topology must contain PVC data-rustfs-${i}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if grep -q "name: data$" <<<"$legacy_four_by_four"; then
|
|
echo "Legacy 4x4 topology must NOT contain a single 'data' PVC" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Legacy topology compatibility: replicaCount=16 (no drivesPerNode set)
|
|
# must render a single 'data' PVC (old 16x1 behaviour).
|
|
legacy_sixteen_by_one=$(render_distributed_statefulset --set replicaCount=16)
|
|
if ! grep -q "name: data$" <<<"$legacy_sixteen_by_one"; then
|
|
echo "Legacy 16x1 topology must contain a single 'data' PVC" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q "name: data-rustfs-" <<<"$legacy_sixteen_by_one"; then
|
|
echo "Legacy 16x1 topology must NOT contain data-rustfs-* PVCs" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Generic topology: explicit replicaCount=8 drivesPerNode=2 must render
|
|
# exactly two PVCs per pod.
|
|
generic_eight_by_two=$(render_distributed_statefulset --set replicaCount=8 --set drivesPerNode=2)
|
|
for i in 0 1; do
|
|
if ! grep -q "name: data-rustfs-${i}" <<<"$generic_eight_by_two"; then
|
|
echo "Generic 8x2 topology must contain PVC data-rustfs-${i}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if grep -q "name: data$" <<<"$generic_eight_by_two"; then
|
|
echo "Generic 8x2 topology must NOT contain a single 'data' PVC" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Issue #5416's exact three-node, one-drive topology must render all three
|
|
# ordinal hosts while each StatefulSet pod receives its own explicit anchor.
|
|
three_by_one_configmap=$(render_distributed_configmap --set replicaCount=3 --set drivesPerNode=1)
|
|
if ! grep -Fq 'RUSTFS_VOLUMES: "http://rustfs-{0...2}.rustfs-headless.rustfs.svc.cluster.local:9000/data"' <<<"$three_by_one_configmap"; then
|
|
echo "Three-node topology must render ordinals 0 through 2 in RUSTFS_VOLUMES" >&2
|
|
exit 1
|
|
fi
|
|
# The distributed init container only prepares directories. Peer DNS or TCP
|
|
# availability must not gate the RustFS process from applying its own quorum
|
|
# checks. The deprecated timeout remains accepted but is not rendered.
|
|
distributed_startup=$(render_distributed_statefulset \
|
|
--set replicaCount=3 \
|
|
--set drivesPerNode=1 \
|
|
--set startupWaitTimeoutSeconds=0)
|
|
for removed_gate in nslookup 'nc -z' wait_for_peer STARTUP_WAIT_TIMEOUT_SECONDS ENDPOINT_PORT; do
|
|
if grep -Fq "$removed_gate" <<<"$distributed_startup"; then
|
|
echo "Distributed init must not contain the removed peer gate: $removed_gate" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if ! grep -Fq 'mkdir -p /data/rustfs$i' <<<"$distributed_startup"; then
|
|
echo "Distributed init must keep per-drive directory initialization" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -Eq '^[[:space:]]+mkdir -p /data$' <<<"$distributed_startup"; then
|
|
echo "Distributed init must keep single-drive directory initialization" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Chart-generated volumes use the Downward API pod name to construct the exact
|
|
# endpoint hostname. Order is load-bearing because Kubernetes only expands
|
|
# environment references that were defined earlier in the env list.
|
|
generated_env_names=$(statefulset_env_names <<<"$distributed_startup")
|
|
if [[ "$generated_env_names" != "RUSTFS_CHART_POD_NAME RUSTFS_LOCAL_ENDPOINT_HOST" ]]; then
|
|
echo "Unexpected generated topology env order: $generated_env_names" >&2
|
|
exit 1
|
|
fi
|
|
|
|
pod_name_field=$(yq eval '.spec.template.spec.containers[0].env[] | select(.name == "RUSTFS_CHART_POD_NAME") | .valueFrom.fieldRef.fieldPath' - <<<"$distributed_startup")
|
|
local_endpoint_host=$(statefulset_env_value RUSTFS_LOCAL_ENDPOINT_HOST <<<"$distributed_startup")
|
|
if [[ "$pod_name_field" != "metadata.name" ]]; then
|
|
echo "Generated topology must source RUSTFS_CHART_POD_NAME from the Downward API" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$local_endpoint_host" != '$(RUSTFS_CHART_POD_NAME).rustfs-headless.rustfs.svc.cluster.local' ]]; then
|
|
echo "Unexpected generated RUSTFS_LOCAL_ENDPOINT_HOST: $local_endpoint_host" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Non-default release identity, namespace, cluster domain, mTLS, and endpoint
|
|
# port must compose into one generated topology without restoring peer gates.
|
|
nondefault_generated=$(helm template tenant "$CHART_DIR" \
|
|
--namespace object-storage \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key \
|
|
--set clusterDomain=cluster.corp \
|
|
--set mtls.enabled=true \
|
|
--set service.endpoint.port=9443 \
|
|
--set config.rustfs.address=:9443)
|
|
nondefault_statefulset=$(awk '
|
|
/^# Source: rustfs\/templates\/statefulset.yaml$/ { in_statefulset = 1 }
|
|
in_statefulset && /^---$/ { exit }
|
|
in_statefulset { print }
|
|
' <<<"$nondefault_generated")
|
|
nondefault_local_host=$(statefulset_env_value RUSTFS_LOCAL_ENDPOINT_HOST <<<"$nondefault_statefulset")
|
|
nondefault_namespace=$(yq eval '.metadata.namespace' - <<<"$nondefault_statefulset")
|
|
nondefault_endpoint_port=$(yq eval '.spec.template.spec.containers[0].ports[] | select(.name == "endpoint") | .containerPort' - <<<"$nondefault_statefulset")
|
|
mtls_liveness_port=$(yq eval '.spec.template.spec.containers[0].livenessProbe.tcpSocket.port' - <<<"$nondefault_statefulset")
|
|
mtls_readiness_command=$(yq eval '.spec.template.spec.containers[0].readinessProbe.exec.command[-1]' - <<<"$nondefault_statefulset")
|
|
if [[ "$nondefault_local_host" != '$(RUSTFS_CHART_POD_NAME).tenant-rustfs-headless.object-storage.svc.cluster.corp' ]]; then
|
|
echo "Combined generated topology produced an unexpected local endpoint host: $nondefault_local_host" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$nondefault_namespace" != "object-storage" || "$nondefault_endpoint_port" != "9443" ]]; then
|
|
echo "Combined generated topology must retain its namespace and custom endpoint port" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$mtls_liveness_port" != 9443 ]]; then
|
|
echo "mTLS liveness must check the listener independently of certificate validity" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -Fq 'https://127.0.0.1:9443/health/ready' <<<"$mtls_readiness_command"; then
|
|
echo "mTLS readiness must use the ready endpoint" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -Fq 'RUSTFS_VOLUMES: "https://tenant-rustfs-{0...3}.tenant-rustfs-headless.object-storage.svc.cluster.corp:9443/data/rustfs{0...3}"' <<<"$nondefault_generated"; then
|
|
echo "Combined generated topology must use its release fullname, namespace, domain, mTLS scheme, and endpoint port" >&2
|
|
exit 1
|
|
fi
|
|
if grep -Eq 'nslookup|nc -z|wait_for_peer' <<<"$nondefault_generated"; then
|
|
echo "Combined generated topology must not render peer DNS or TCP gates" >&2
|
|
exit 1
|
|
fi
|
|
|
|
default_scheme_port_generated=$(render_distributed_configmap \
|
|
--set service.endpoint.port=80 \
|
|
--set config.rustfs.address=:80)
|
|
if ! grep -Fq 'rustfs-headless.rustfs.svc.cluster.local:80/' <<<"$default_scheme_port_generated"; then
|
|
echo "Chart-generated topology must retain an explicit HTTP default port" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Unrelated extraEnv entries follow the generated entries so dependent
|
|
# environment expansion keeps working.
|
|
generated_with_extra_env=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=CUSTOM_ENV' \
|
|
--set 'extraEnv[0].value=custom-value')
|
|
generated_with_extra_names=$(statefulset_env_names <<<"$generated_with_extra_env")
|
|
if [[ "$generated_with_extra_names" != "RUSTFS_CHART_POD_NAME RUSTFS_LOCAL_ENDPOINT_HOST CUSTOM_ENV" ]]; then
|
|
echo "extraEnv must be appended after generated topology env entries" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Explicit bounded/fail-fast modes retain their legacy DNS locality semantics,
|
|
# so the generated anchor must not make the runtime reject the configuration.
|
|
for legacy_wait_mode in bounded ' Strict '; do
|
|
bounded_wait_mode=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE' \
|
|
--set-string "extraEnv[0].value=$legacy_wait_mode")
|
|
bounded_wait_env_names=$(statefulset_env_names <<<"$bounded_wait_mode")
|
|
bounded_wait_value=$(yq eval '.spec.template.spec.containers[0].env[0].value' - <<<"$bounded_wait_mode")
|
|
if [[ "$bounded_wait_env_names" != "RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE" || "$bounded_wait_value" != "$legacy_wait_mode" ]]; then
|
|
echo "Startup mode $legacy_wait_mode must opt out of the generated local endpoint anchor" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
unknown_wait_mode=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE' \
|
|
--set 'extraEnv[0].value=invalid-mode')
|
|
unknown_wait_env_names=$(statefulset_env_names <<<"$unknown_wait_mode")
|
|
if [[ "$unknown_wait_env_names" != "RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE" ]]; then
|
|
echo "An unknown explicit startup mode must not receive a generated local endpoint anchor" >&2
|
|
exit 1
|
|
fi
|
|
|
|
dynamic_wait_mode=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE' \
|
|
--set 'extraEnv[0].valueFrom.configMapKeyRef.name=startup-policy' \
|
|
--set 'extraEnv[0].valueFrom.configMapKeyRef.key=mode')
|
|
dynamic_wait_env_names=$(statefulset_env_names <<<"$dynamic_wait_mode")
|
|
if [[ "$dynamic_wait_env_names" != "RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE" ]]; then
|
|
echo "Dynamically sourced startup mode must opt out of the generated local endpoint anchor" >&2
|
|
exit 1
|
|
fi
|
|
|
|
orchestrated_wait_mode=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE' \
|
|
--set 'extraEnv[0].value=orchestrated')
|
|
orchestrated_wait_env_names=$(statefulset_env_names <<<"$orchestrated_wait_mode")
|
|
if [[ "$orchestrated_wait_env_names" != "RUSTFS_CHART_POD_NAME RUSTFS_LOCAL_ENDPOINT_HOST RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE" ]]; then
|
|
echo "Explicit orchestrated startup mode must retain one generated local endpoint anchor" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Existing POD_NAME remains independent from the chart-private Downward API
|
|
# variable, while an explicit anchor disables automatic anchor injection.
|
|
existing_pod_name=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=POD_NAME' \
|
|
--set 'extraEnv[0].valueFrom.fieldRef.fieldPath=metadata.name')
|
|
existing_pod_name_names=$(statefulset_env_names <<<"$existing_pod_name")
|
|
existing_pod_name_count=$(yq eval '[.spec.template.spec.containers[0].env[] | select(.name == "POD_NAME")] | length' - <<<"$existing_pod_name")
|
|
existing_pod_name_field=$(yq eval '.spec.template.spec.containers[0].env[] | select(.name == "POD_NAME") | .valueFrom.fieldRef.fieldPath' - <<<"$existing_pod_name")
|
|
existing_chart_pod_name_field=$(yq eval '.spec.template.spec.containers[0].env[] | select(.name == "RUSTFS_CHART_POD_NAME") | .valueFrom.fieldRef.fieldPath' - <<<"$existing_pod_name")
|
|
existing_pod_name_anchor=$(statefulset_env_value RUSTFS_LOCAL_ENDPOINT_HOST <<<"$existing_pod_name")
|
|
if [[ "$existing_pod_name_names" != "RUSTFS_CHART_POD_NAME RUSTFS_LOCAL_ENDPOINT_HOST POD_NAME" ||
|
|
"$existing_pod_name_count" != "1" ||
|
|
"$existing_pod_name_field" != "metadata.name" ||
|
|
"$existing_chart_pod_name_field" != "metadata.name" ||
|
|
"$existing_pod_name_anchor" != '$(RUSTFS_CHART_POD_NAME).rustfs-headless.rustfs.svc.cluster.local' ]]; then
|
|
echo "An existing extraEnv POD_NAME must be preserved without interfering with the automatic anchor" >&2
|
|
exit 1
|
|
fi
|
|
|
|
existing_local_anchor=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=RUSTFS_LOCAL_ENDPOINT_HOST' \
|
|
--set-string 'extraEnv[0].value=rustfs-0.rustfs-headless.rustfs.svc.cluster.local')
|
|
existing_local_anchor_names=$(statefulset_env_names <<<"$existing_local_anchor")
|
|
existing_local_anchor_value=$(statefulset_env_value RUSTFS_LOCAL_ENDPOINT_HOST <<<"$existing_local_anchor")
|
|
if [[ "$existing_local_anchor_names" != "RUSTFS_LOCAL_ENDPOINT_HOST" || "$existing_local_anchor_value" != "rustfs-0.rustfs-headless.rustfs.svc.cluster.local" ]]; then
|
|
echo "An existing explicit local endpoint anchor must be preserved without a generated duplicate" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# `helm upgrade --reuse-values` can omit keys introduced by the new chart.
|
|
# A missing localEndpointHost map keeps the new safe default instead of
|
|
# failing template evaluation.
|
|
missing_local_endpoint_host_values=$(render_distributed_statefulset \
|
|
--set-json 'localEndpointHost=null')
|
|
missing_local_endpoint_host_names=$(statefulset_env_names <<<"$missing_local_endpoint_host_values")
|
|
if [[ "$missing_local_endpoint_host_names" != "RUSTFS_CHART_POD_NAME RUSTFS_LOCAL_ENDPOINT_HOST" ]]; then
|
|
echo "Missing localEndpointHost values from a reused release must default to automatic anchor injection" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# An opaque existing Secret keeps the historical DNS path by default because
|
|
# it may hide runtime topology or startup-mode overrides. A credentials-only
|
|
# Secret can explicitly opt in without changing envFrom precedence.
|
|
existing_secret_statefulset=$(render_distributed_statefulset \
|
|
--set secret.existingSecret=legacy-rustfs-config)
|
|
existing_secret_env_names=$(statefulset_env_names <<<"$existing_secret_statefulset")
|
|
existing_secret_env_from=$(yq eval '[.spec.template.spec.containers[0].envFrom[] | (.configMapRef.name // .secretRef.name)] | join(" ")' - <<<"$existing_secret_statefulset")
|
|
if [[ -n "$existing_secret_env_names" || "$existing_secret_env_from" != "rustfs-config legacy-rustfs-config" ]]; then
|
|
echo "An opaque existingSecret must default to the legacy environment path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
existing_secret_opt_in=$(render_distributed_statefulset \
|
|
--set secret.existingSecret=legacy-rustfs-config \
|
|
--set localEndpointHost.autoInject=true)
|
|
existing_secret_opt_in_names=$(statefulset_env_names <<<"$existing_secret_opt_in")
|
|
existing_secret_opt_in_env_from=$(yq eval '[.spec.template.spec.containers[0].envFrom[] | (.configMapRef.name // .secretRef.name)] | join(" ")' - <<<"$existing_secret_opt_in")
|
|
if [[ "$existing_secret_opt_in_names" != "RUSTFS_CHART_POD_NAME RUSTFS_LOCAL_ENDPOINT_HOST" ||
|
|
"$existing_secret_opt_in_env_from" != "rustfs-config legacy-rustfs-config" ]]; then
|
|
echo "A credentials-only existingSecret must support explicit automatic anchor injection" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# A Secret that hides a custom topology can explicitly preserve the historical
|
|
# DNS path. No explicit env entries are added, and envFrom ordering is unchanged.
|
|
existing_secret_opt_out=$(render_distributed_statefulset \
|
|
--set secret.existingSecret=legacy-rustfs-config \
|
|
--set localEndpointHost.autoInject=false)
|
|
existing_secret_opt_out_env_names=$(statefulset_env_names <<<"$existing_secret_opt_out")
|
|
existing_secret_opt_out_env_from=$(yq eval '[.spec.template.spec.containers[0].envFrom[] | (.configMapRef.name // .secretRef.name)] | join(" ")' - <<<"$existing_secret_opt_out")
|
|
if [[ -n "$existing_secret_opt_out_env_names" || "$existing_secret_opt_out_env_from" != "rustfs-config legacy-rustfs-config" ]]; then
|
|
echo "localEndpointHost.autoInject=false must preserve the legacy existingSecret environment path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
quoted_local_endpoint_host_status=0
|
|
quoted_local_endpoint_host_error=$(render_distributed_statefulset \
|
|
--set secret.existingSecret=legacy-rustfs-config \
|
|
--set-string localEndpointHost.autoInject=false 2>&1) || quoted_local_endpoint_host_status=$?
|
|
if [[ $quoted_local_endpoint_host_status -eq 0 ||
|
|
"$quoted_local_endpoint_host_error" != *"localEndpointHost.autoInject must be a boolean or null"* ]]; then
|
|
echo "A quoted localEndpointHost.autoInject value must fail closed instead of becoming truthy" >&2
|
|
exit 1
|
|
fi
|
|
|
|
missing_local_endpoint_host_with_secret=$(render_distributed_statefulset \
|
|
--set secret.existingSecret=legacy-rustfs-config \
|
|
--set-json 'localEndpointHost=null')
|
|
if [[ -n "$(statefulset_env_names <<<"$missing_local_endpoint_host_with_secret")" ]]; then
|
|
echo "A reused release without localEndpointHost values must not override an opaque existingSecret" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# An explicit topology disables inference, but an address override alone keeps
|
|
# the generated anchor so RustFS can validate the effective port before format
|
|
# initialization.
|
|
volumes_override_statefulset=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=RUSTFS_VOLUMES' \
|
|
--set-string 'extraEnv[0].value=http://legacy.example/data')
|
|
volumes_override_names=$(statefulset_env_names <<<"$volumes_override_statefulset")
|
|
volumes_override_value=$(yq eval '.spec.template.spec.containers[0].env[0].value' - <<<"$volumes_override_statefulset")
|
|
if [[ "$volumes_override_names" != "RUSTFS_VOLUMES" || "$volumes_override_value" != "http://legacy.example/data" ]]; then
|
|
echo "extraEnv RUSTFS_VOLUMES must retain precedence without a generated anchor" >&2
|
|
exit 1
|
|
fi
|
|
|
|
address_override_statefulset=$(render_distributed_statefulset \
|
|
--set 'extraEnv[0].name=RUSTFS_ADDRESS' \
|
|
--set-string 'extraEnv[0].value=:9443')
|
|
address_override_names=$(statefulset_env_names <<<"$address_override_statefulset")
|
|
address_override_value=$(statefulset_env_value RUSTFS_ADDRESS <<<"$address_override_statefulset")
|
|
address_override_anchor_count=$(yq eval '[.spec.template.spec.containers[0].env[] | select(.name == "RUSTFS_LOCAL_ENDPOINT_HOST")] | length' - <<<"$address_override_statefulset")
|
|
if [[ "$address_override_names" != "RUSTFS_CHART_POD_NAME RUSTFS_LOCAL_ENDPOINT_HOST RUSTFS_ADDRESS" ||
|
|
"$address_override_value" != ":9443" ||
|
|
"$address_override_anchor_count" != "1" ]]; then
|
|
echo "extraEnv RUSTFS_ADDRESS must retain one generated local endpoint anchor" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Explicit volumes remain authoritative: the chart must not infer any identity,
|
|
# while a user-supplied local endpoint anchor remains allowed.
|
|
custom_volumes_statefulset=$(render_distributed_statefulset \
|
|
--set config.rustfs.volumes=http://example.test/data)
|
|
custom_volumes_env_names=$(statefulset_env_names <<<"$custom_volumes_statefulset")
|
|
if [[ -n "$custom_volumes_env_names" ]]; then
|
|
echo "Custom volumes must not receive generated topology env entries" >&2
|
|
exit 1
|
|
fi
|
|
|
|
custom_volumes_with_anchor=$(render_distributed_statefulset \
|
|
--set config.rustfs.volumes=http://example.test/data \
|
|
--set 'extraEnv[0].name=RUSTFS_LOCAL_ENDPOINT_HOST' \
|
|
--set 'extraEnv[0].value=example.test')
|
|
custom_anchor=$(statefulset_env_value RUSTFS_LOCAL_ENDPOINT_HOST <<<"$custom_volumes_with_anchor")
|
|
if [[ "$custom_anchor" != "example.test" ]]; then
|
|
echo "Custom volumes must allow a user-supplied local endpoint anchor" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if grep -q 'RUSTFS_LOCAL_ENDPOINT_HOST' <<<"$standalone_default_output"; then
|
|
echo "Standalone mode must not receive a distributed local endpoint anchor" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Every generated server pool uses its runtime pod name with the shared root
|
|
# headless service. No pool waits for another pool's DNS or TCP listener.
|
|
multi_pool_startup=$(helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key \
|
|
--set pools.enabled=true \
|
|
--set 'pools.list[0].replicaCount=2' \
|
|
--set 'pools.list[1].replicaCount=2')
|
|
multi_pool_anchor_count=$(grep -c 'name: RUSTFS_LOCAL_ENDPOINT_HOST' <<<"$multi_pool_startup" || true)
|
|
if [[ $multi_pool_anchor_count -ne 2 ]]; then
|
|
echo "Every generated server pool must receive RUSTFS_LOCAL_ENDPOINT_HOST" >&2
|
|
exit 1
|
|
fi
|
|
if grep -Eq 'nslookup|nc -z|wait_for_peer' <<<"$multi_pool_startup"; then
|
|
echo "Multi-pool init must not wait for peer DNS or TCP" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'rustfs-pool1-headless' <<<"$multi_pool_startup"; then
|
|
echo "Multi-pool local endpoint identity must use the shared root headless service" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# volumeClaimTemplates must not contain empty annotations when pvcAnnotations are unset,
|
|
# because Kubernetes treats annotations: {} as a mutation of the immutable field.
|
|
no_ann_output=$(render_distributed_statefulset)
|
|
if grep -A1 'kind: PersistentVolumeClaim' <<<"$no_ann_output" | grep -q 'annotations:'; then
|
|
echo "Empty pvcAnnotations must not render an annotations key in volumeClaimTemplates" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Distributed mode with replicaCount < 2 must fail rendering.
|
|
low_replica_status=0
|
|
render_distributed_statefulset --set replicaCount=1 >/dev/null 2>&1 || low_replica_status=$?
|
|
if [[ $low_replica_status -eq 0 ]]; then
|
|
echo "Distributed mode with replicaCount=1 must fail rendering" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# service.externalIPs must render correctly when supplied.
|
|
external_ips_output=$(helm template rustfs "$CHART_DIR" \
|
|
--namespace rustfs \
|
|
--set secret.rustfs.access_key=test-access-key \
|
|
--set secret.rustfs.secret_key=test-secret-key \
|
|
--set 'service.externalIPs[0]=203.0.113.1')
|
|
if ! grep -A2 'externalIPs:' <<<"$external_ips_output" | grep -q '203.0.113.1'; then
|
|
echo "service.externalIPs must contain 203.0.113.1" >&2
|
|
exit 1
|
|
fi
|