fix(kubernetes): close admin-kubeconfig race on tenant cluster bootstrap (#2413)
## What this PR does Closes #2412. On a cold tenant-Kubernetes bootstrap, the parent HelmRelease raced the admin-kubeconfig Secret that Kamaji provisions asynchronously. Three CP-side Deployments (cluster-autoscaler, kccm, kcsi-controller) mounted that Secret as a hard volume, flux helm-controller's default wait budget was too short for Kamaji cold start, and `install.remediation { retries: -1 }` then uninstalled the Cluster CR and restarted the cycle forever. Implements a defense-in-depth fix: - `optional: true` on the admin-kubeconfig Secret volume in all three Deployments so kubelet no longer FailedMounts while Kamaji is still bootstrapping. - A shared `wait-for-kubeconfig` init container (in `templates/_helpers.tpl`) that polls for `super-admin.svc` with a 10m deadline, strictly below the HelmRelease Install.Timeout so a broken tenant falls into CrashLoopBackOff visibly instead of hanging forever. - Per-Application HelmRelease Install/Upgrade timeout, driven by a new `release.cozystack.io/helm-install-timeout` annotation on ApplicationDefinition. Kubernetes-rd sets it to `15m`; other kinds leave it unset and keep flux defaults, so their failed installs remediate on the normal cadence. Parser rejects ns/us/µs (accepted by `time.ParseDuration`, rejected by Flux's CRD pattern) at startup. - Soft-skip when `_namespace.etcd` is empty: the CP-side Deployments, the Cluster/KamajiControlPlane/KubevirtCluster/WorkloadMonitor CRs, and every child HelmRelease that references admin-kubeconfig now render only when an etcd DataStore exists for this tenant. An `awaiting-etcd` ConfigMap is emitted as a user-visible status beacon so `helm install` still succeeds and flux retries on its 5m interval until the Tenant chart catches up. - e2e remediation guard built on `.status.history[].status` (the Snapshot shape), not on `.status.installFailures` - `ClearFailures()` zeroes the latter on every successful reconciliation, which made the previous guard vacuous. Tests: - Go unit tests for the annotation parser (accepted/rejected units) and the HR builder (table-driven across kinds). - helm unittest for the per-template structure (optional volume, init container, dataStoreName, awaiting-etcd beacon). - bats unit tests for the shell guard (every combination of empty/zero/positive history entries, plus pinned HR v2 shape). - Chart-wide bats invariants: every Deployment mounting admin-kubeconfig has the guards; zero such Deployments and zero HelmReleases render when etcd is empty. All wired into the existing `make unit-tests` target (`go-unit-tests` added alongside `helm-unit-tests` and `bats-unit-tests`). Option 2 from the ticket (separate HelmRelease with `dependsOn`) was intentionally not taken: the combination above closes the same race without restructuring the chart's HelmRelease topology. ### Release note ```release-note fix(kubernetes): close admin-kubeconfig race on tenant Kubernetes bootstrap. The parent HelmRelease no longer enters an uninstall/retry cycle when Kamaji control-plane cold start exceeds flux's default wait budget. A Kubernetes tenant created before the parent Tenant application has etcd enabled now renders only an awaiting-etcd beacon ConfigMap and waits quietly for the DataStore to appear, instead of producing half-installed Deployments that CrashLoopBackOff forever. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Per-application Helm install/upgrade timeout via metadata annotation. * Init-container guards that wait for admin kubeconfig before workloads start. * Chart resources now render conditionally based on etcd presence. * **Tests** * Helm-template tests for admin-kubeconfig invariants and remediation-cycle detection. * New Go unit tests and CI Helm/unittest coverage plus test value files. * **Chores** * Added BusyBox image pin and new Makefile test targets (including Go unit-tests). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
commit
bdf23e66d1
42 changed files with 1059 additions and 48 deletions
10
Makefile
10
Makefile
|
|
@ -82,11 +82,19 @@ test:
|
|||
make -C packages/core/testing apply
|
||||
make -C packages/core/testing test
|
||||
|
||||
unit-tests: helm-unit-tests bats-unit-tests
|
||||
unit-tests: helm-unit-tests bats-unit-tests go-unit-tests
|
||||
|
||||
helm-unit-tests:
|
||||
hack/helm-unit-tests.sh
|
||||
|
||||
# Scoped go test over the cozystack-api surface that this repo owns. Kept
|
||||
# narrow intentionally - running `go test ./...` pulls in generated code
|
||||
# round-trip suites whose behavior depends on tool versions outside this
|
||||
# repo's control (kubebuilder, openapi-gen, etc.) and is better exercised
|
||||
# from their generator workflows.
|
||||
go-unit-tests:
|
||||
go test ./pkg/registry/... ./pkg/config/... ./pkg/cmd/server/...
|
||||
|
||||
# Discover every hack/*.bats file that is NOT an e2e test and run it
|
||||
# through cozytest.sh. Drop a new *.bats file in hack/ and it is picked
|
||||
# up automatically on the next `make unit-tests` run.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ type ConfigSpec struct {
|
|||
// Kubernetes control-plane configuration.
|
||||
// +kubebuilder:default:={}
|
||||
ControlPlane ControlPlane `json:"controlPlane"`
|
||||
// Optional image overrides for air-gapped or rate-limited registries.
|
||||
// +kubebuilder:default:={}
|
||||
Images Images `json:"images"`
|
||||
}
|
||||
|
||||
type APIServer struct {
|
||||
|
|
@ -157,6 +160,12 @@ type GatewayAPIAddon struct {
|
|||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type Images struct {
|
||||
// Image used by the wait-for-kubeconfig init container. Empty falls back to images/busybox.tag.
|
||||
// +kubebuilder:default:=""
|
||||
WaitForKubeconfig string `json:"waitForKubeconfig,omitempty"`
|
||||
}
|
||||
|
||||
type IngressNginxAddon struct {
|
||||
// Enable the controller (requires nodes labeled `ingress-nginx`).
|
||||
// +kubebuilder:default:=false
|
||||
|
|
|
|||
145
hack/admin-kubeconfig-invariant.bats
Normal file
145
hack/admin-kubeconfig-invariant.bats
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#!/usr/bin/env bats
|
||||
# -----------------------------------------------------------------------------
|
||||
# Chart-wide invariant for packages/apps/kubernetes:
|
||||
#
|
||||
# Every Deployment in this chart that mounts <release>-admin-kubeconfig as a
|
||||
# Secret volume MUST:
|
||||
# - declare that volume optional: true (so kubelet does not FailedMount
|
||||
# while Kamaji is still provisioning the Secret), AND
|
||||
# - include the wait-for-kubeconfig init container (so the pod becomes
|
||||
# Ready only after Kamaji publishes the Secret).
|
||||
#
|
||||
# The per-template unittests in packages/apps/kubernetes/tests/ lock in
|
||||
# today's three Deployments (cluster-autoscaler, kccm, csi controller) by
|
||||
# name. This invariant is stricter: any future Deployment added to this
|
||||
# chart that mounts the same Secret but forgets the guard will fail here.
|
||||
#
|
||||
# Requires: helm, yq (mikefarah v4+), jq. All three are available on the
|
||||
# project's CI runners and on the maintainer workstation.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@test "every Deployment mounting admin-kubeconfig has optional:true and wait-for-kubeconfig init" {
|
||||
values_file="packages/apps/kubernetes/tests/values-ci.yaml"
|
||||
[ -f "$values_file" ]
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
helm template invariant packages/apps/kubernetes \
|
||||
--namespace tenant-root \
|
||||
--values "$values_file" \
|
||||
2>/dev/null > "$tmp/rendered.yaml"
|
||||
|
||||
# yq streams one JSON object per input document. jq -s slurps the stream
|
||||
# into an array so we can treat all Deployments as a single collection.
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s --raw-output '
|
||||
map(select(.kind == "Deployment")) |
|
||||
map({
|
||||
name: .metadata.name,
|
||||
volumes: (.spec.template.spec.volumes // []),
|
||||
initNames: ((.spec.template.spec.initContainers // []) | map(.name)),
|
||||
}) |
|
||||
map(
|
||||
.name as $n |
|
||||
.initNames as $ins |
|
||||
(.volumes[] | select(.secret.secretName | test("-admin-kubeconfig$")?))
|
||||
| {
|
||||
name: $n,
|
||||
optional: (.secret.optional == true),
|
||||
hasInit: ($ins | index("wait-for-kubeconfig") != null),
|
||||
}
|
||||
)
|
||||
' > "$tmp/summary.json"
|
||||
|
||||
# At least one Deployment must match; if a refactor removes every
|
||||
# admin-kubeconfig volume from this chart, the test must be updated
|
||||
# deliberately rather than silently passing.
|
||||
matched=$(jq 'length' "$tmp/summary.json")
|
||||
[ "$matched" -ge 1 ]
|
||||
|
||||
offenders=$(jq --raw-output '.[] | select(.optional != true or .hasInit != true) | .name' "$tmp/summary.json")
|
||||
|
||||
if [ -n "$offenders" ]; then
|
||||
echo "Deployments mounting *-admin-kubeconfig without optional:true + wait-for-kubeconfig init:" >&2
|
||||
echo "$offenders" >&2
|
||||
echo "Full summary:" >&2
|
||||
cat "$tmp/summary.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Invariant holds for $matched Deployment(s)"
|
||||
}
|
||||
|
||||
@test "chart emits zero admin-kubeconfig Deployments when tenant has no etcd DataStore" {
|
||||
# Without a DataStore (parent Tenant has not populated _namespace.etcd yet)
|
||||
# the control-plane-side Deployments must NOT render at all. If they did,
|
||||
# the wait-for-kubeconfig init would CrashLoopBackOff indefinitely - there
|
||||
# would be no KamajiControlPlane to provision the Secret - consuming the
|
||||
# HelmRelease wait budget and triggering exactly the remediation cycle the
|
||||
# rest of this chart tries to avoid. This test renders the whole chart
|
||||
# with etcd empty and asserts no Deployment references the admin-kubeconfig
|
||||
# Secret.
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
helm template invariant packages/apps/kubernetes \
|
||||
--namespace tenant-root \
|
||||
--values packages/apps/kubernetes/tests/values-ci-no-etcd.yaml \
|
||||
2>/dev/null > "$tmp/rendered.yaml"
|
||||
|
||||
matched=$(
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s '
|
||||
map(select(.kind == "Deployment")) |
|
||||
map(select(
|
||||
(.spec.template.spec.volumes // [])
|
||||
| any(.secret.secretName | test("-admin-kubeconfig$")?)
|
||||
)) |
|
||||
length
|
||||
'
|
||||
)
|
||||
|
||||
if [ "$matched" -ne 0 ]; then
|
||||
echo "Expected zero Deployments mounting *-admin-kubeconfig when etcd is empty, got $matched:" >&2
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s 'map(select(.kind == "Deployment") | .metadata.name)' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No admin-kubeconfig Deployments rendered for empty etcd (as expected)"
|
||||
}
|
||||
|
||||
@test "chart emits zero admin-kubeconfig HelmReleases when tenant has no etcd DataStore" {
|
||||
# Same principle as the Deployment variant above, extended to every child
|
||||
# HelmRelease (cilium, coredns, csi, cert-manager, ...). They reference
|
||||
# *-admin-kubeconfig via kubeConfig.secretRef and would otherwise sit in
|
||||
# NotReady forever on an etcd-less tenant, polluting the HelmRelease list
|
||||
# the operator sees and contradicting the "awaiting-etcd beacon only"
|
||||
# contract of the soft-skip path.
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
helm template invariant packages/apps/kubernetes \
|
||||
--namespace tenant-root \
|
||||
--values packages/apps/kubernetes/tests/values-ci-no-etcd.yaml \
|
||||
2>/dev/null > "$tmp/rendered.yaml"
|
||||
|
||||
matched=$(
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s '
|
||||
map(select(.kind == "HelmRelease")) |
|
||||
map(select(.spec.kubeConfig.secretRef.name | test("-admin-kubeconfig$")?)) |
|
||||
length
|
||||
'
|
||||
)
|
||||
|
||||
if [ "$matched" -ne 0 ]; then
|
||||
echo "Expected zero HelmReleases referencing *-admin-kubeconfig when etcd is empty, got $matched:" >&2
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s 'map(select(.kind == "HelmRelease") | .metadata.name)' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No admin-kubeconfig HelmReleases rendered for empty etcd (as expected)"
|
||||
}
|
||||
39
hack/e2e-apps/remediation-guard.sh
Normal file
39
hack/e2e-apps/remediation-guard.sh
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Helpers for asserting that a Flux HelmRelease did not fall into an
|
||||
# install/upgrade remediation cycle during an e2e run.
|
||||
#
|
||||
# Background: Flux helm-controller's ClearFailures() zeroes
|
||||
# .status.installFailures / .status.upgradeFailures on every successful
|
||||
# reconciliation (see the upstream ClearFailures method on
|
||||
# HelmReleaseStatus). That makes those counters useless for a guard that
|
||||
# runs after the HelmRelease has reached Ready - the values are always 0.
|
||||
#
|
||||
# What survives a successful reconciliation is .status.history, a bounded
|
||||
# list of release Snapshots. Each Snapshot carries a status field that
|
||||
# tracks the Helm release state: deployed, superseded, failed, uninstalled,
|
||||
# and so on. A remediation cycle leaves the footprint behind: a snapshot
|
||||
# with status "uninstalled" (from install/upgrade remediation) or "failed"
|
||||
# (Helm release failure that remediation then uninstalled). Those stay in
|
||||
# history even after a subsequent successful reinstall.
|
||||
#
|
||||
# helmrelease_has_remediation_cycle takes a newline-delimited list of
|
||||
# snapshot statuses (whatever the caller extracted via kubectl -o jsonpath
|
||||
# or equivalent) and returns 0 (detected) when any entry is "failed" or
|
||||
# "uninstalled", 1 otherwise. Empty input is treated as "no history yet,
|
||||
# no cycle observed".
|
||||
|
||||
helmrelease_has_remediation_cycle() {
|
||||
statuses="$1"
|
||||
if [ -z "${statuses}" ]; then
|
||||
return 1
|
||||
fi
|
||||
# printf + grep over the pipe, rather than a heredoc plus while read.
|
||||
# printf %s treats the status string as a literal payload, so any stray
|
||||
# $ in a future caller's input does not trigger shell expansion. grep
|
||||
# returns 0 iff at least one line matches the allowlist, which is
|
||||
# exactly the contract the caller wants, so we can return its exit
|
||||
# status directly.
|
||||
if printf '%s\n' "${statuses}" | grep --extended-regexp --quiet '^(failed|uninstalled)$'; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
. hack/e2e-apps/remediation-guard.sh
|
||||
|
||||
run_kubernetes_test() {
|
||||
local version_expr="$1"
|
||||
local test_name="$2"
|
||||
|
|
@ -320,6 +322,35 @@ EOF
|
|||
done
|
||||
kubectl wait hr kubernetes-${test_name}-ingress-nginx -n tenant-test --timeout=5m --for=condition=ready
|
||||
|
||||
# Guard: parent HelmRelease must not have entered an install/upgrade remediation cycle.
|
||||
# A non-zero installFailures/upgradeFailures indicates the helm-wait budget expired while
|
||||
# admin-kubeconfig was still being provisioned, which would trigger uninstall remediation
|
||||
# and churn the Cluster CR.
|
||||
# Flux helm-controller v2 retains per-revision release Snapshots in
|
||||
# .status.history; each Snapshot's .status reflects the Helm release
|
||||
# state (deployed/superseded/failed/uninstalled). A remediation cycle
|
||||
# leaves a "failed" or "uninstalled" entry behind that survives a later
|
||||
# successful reinstall, unlike the installFailures/upgradeFailures
|
||||
# counters (which ClearFailures zeroes on every successful reconcile).
|
||||
# The shape is pinned by hack/remediation-guard.bats; the upstream
|
||||
# types are github.com/fluxcd/helm-controller/api v2 Snapshot.
|
||||
history_statuses=$(kubectl get hr -n tenant-test "kubernetes-${test_name}" \
|
||||
-ojsonpath='{range .status.history[*]}{.status}{"\n"}{end}')
|
||||
# Always emit the raw value so a silent future-Flux field rename shows
|
||||
# up as "empty history on a Ready HR" in CI logs rather than vanishing.
|
||||
echo "Parent HelmRelease history statuses:"
|
||||
printf '%s\n' "${history_statuses:-<empty>}"
|
||||
if [ -z "${history_statuses}" ]; then
|
||||
echo "Unexpected empty .status.history on a Ready HelmRelease - Flux API shape may have changed." >&2
|
||||
kubectl -n tenant-test describe hr "kubernetes-${test_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if helmrelease_has_remediation_cycle "${history_statuses}"; then
|
||||
echo "Parent HelmRelease entered remediation cycle." >&2
|
||||
kubectl -n tenant-test describe hr "kubernetes-${test_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
pkill -f "port-forward.*${port}:" 2>/dev/null || true
|
||||
rm -f "tenantkubeconfig-${test_name}"
|
||||
|
|
|
|||
127
hack/remediation-guard.bats
Normal file
127
hack/remediation-guard.bats
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env bats
|
||||
# -----------------------------------------------------------------------------
|
||||
# Unit tests for hack/e2e-apps/remediation-guard.sh
|
||||
#
|
||||
# helmrelease_has_remediation_cycle takes a newline-delimited list of
|
||||
# HelmRelease history snapshot status values (deployed/superseded/failed/
|
||||
# uninstalled/...) and returns 0 when any entry is "failed" or "uninstalled"
|
||||
# (meaning flux helm-controller performed install/upgrade remediation).
|
||||
#
|
||||
# This is used by the e2e script after the HelmRelease reaches Ready. The
|
||||
# failure/upgrade counters (.status.installFailures / .status.upgradeFailures)
|
||||
# are useless there because flux's ClearFailures zeroes them on successful
|
||||
# reconciliation; .status.history retains the snapshot trail.
|
||||
#
|
||||
# cozytest.sh's awk parser recognizes only @test blocks and a bare `}` on
|
||||
# its own line; there is no bats `run` or `$status`. Assertions are
|
||||
# expressed as direct shell tests that exit non-zero on failure.
|
||||
#
|
||||
# Run with: hack/cozytest.sh hack/remediation-guard.bats
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@test "empty history returns not-detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if helmrelease_has_remediation_cycle ""; then
|
||||
echo "expected not-detected for empty history" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "single deployed snapshot returns not-detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if helmrelease_has_remediation_cycle "deployed"; then
|
||||
echo "expected not-detected for deployed-only history" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "deployed then superseded returns not-detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
statuses=$(printf 'deployed\nsuperseded\n')
|
||||
if helmrelease_has_remediation_cycle "${statuses}"; then
|
||||
echo "expected not-detected for deployed+superseded history" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "single failed snapshot returns detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if ! helmrelease_has_remediation_cycle "failed"; then
|
||||
echo "expected detected when history contains failed snapshot" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "single uninstalled snapshot returns detected" {
|
||||
# The exact signature of the install-remediation race: the first install
|
||||
# exceeded flux's wait budget, remediation uninstalled, the next retry
|
||||
# eventually succeeded. History still carries the uninstalled snapshot.
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if ! helmrelease_has_remediation_cycle "uninstalled"; then
|
||||
echo "expected detected when history contains uninstalled snapshot" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "uninstalled then deployed still returns detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
statuses=$(printf 'uninstalled\ndeployed\n')
|
||||
if ! helmrelease_has_remediation_cycle "${statuses}"; then
|
||||
echo "expected detected despite later successful deploy" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "deployed then failed still returns detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
statuses=$(printf 'deployed\nfailed\n')
|
||||
if ! helmrelease_has_remediation_cycle "${statuses}"; then
|
||||
echo "expected detected when any entry is failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "status.history extraction pins HR v2 status.history shape" {
|
||||
# Pins the Flux HelmRelease v2 .status.history[].status shape that
|
||||
# run-kubernetes.sh relies on. If a future flux release renames the
|
||||
# field, the jsonpath returns nothing, the guard reports no cycle,
|
||||
# and real remediation loops slip past the e2e assertion. This test
|
||||
# uses yq to read the exact path used in the e2e script; the upstream
|
||||
# Snapshot type lives at
|
||||
# github.com/fluxcd/helm-controller/api/v2.Snapshot (via go.mod).
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
cat > "$tmp/hr.yaml" <<'YAML'
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: kubernetes-test
|
||||
namespace: tenant-test
|
||||
status:
|
||||
history:
|
||||
- name: kubernetes-test
|
||||
namespace: tenant-test
|
||||
version: 1
|
||||
status: uninstalled
|
||||
- name: kubernetes-test
|
||||
namespace: tenant-test
|
||||
version: 2
|
||||
status: deployed
|
||||
YAML
|
||||
|
||||
# Default yq output is yaml scalar format, which for string values emits
|
||||
# bare unquoted tokens - matching what kubectl -o jsonpath produces in
|
||||
# e2e. Do not switch to JSON output here; that would quote the values
|
||||
# and break the loop in helmrelease_has_remediation_cycle.
|
||||
statuses=$(yq '.status.history[].status' "$tmp/hr.yaml")
|
||||
|
||||
[ -n "$statuses" ]
|
||||
echo "$statuses" | grep --quiet '^uninstalled$'
|
||||
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if ! helmrelease_has_remediation_cycle "$statuses"; then
|
||||
echo "expected detected for pinned HR snippet with uninstalled + deployed history" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
|
@ -4,6 +4,9 @@ KUBERNETES_PKG_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
|
|||
include ../../../hack/common-envs.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
test:
|
||||
helm unittest .
|
||||
|
||||
generate:
|
||||
cozyvalues-gen -m 'kubernetes' -v values.yaml -s values.schema.json -r README.md -g ../../../api/apps/v1alpha1/kubernetes/types.go
|
||||
../../../hack/update-crd.sh
|
||||
|
|
@ -67,3 +70,4 @@ image-cluster-autoscaler:
|
|||
echo "$(REGISTRY)/cluster-autoscaler:$(call settag,$(KUBERNETES_PKG_TAG))@$$(yq e '."containerimage.digest"' images/cluster-autoscaler.json -o json -r)" \
|
||||
> images/cluster-autoscaler.tag
|
||||
rm -f images/cluster-autoscaler.json
|
||||
|
||||
|
|
|
|||
|
|
@ -145,31 +145,33 @@ See the reference for components utilized in this service:
|
|||
|
||||
### Kubernetes Control Plane Configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------------------------------- | ------------------------------------------------ | ---------- | ------- |
|
||||
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
|
||||
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
|
||||
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `large` |
|
||||
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------- | ------- |
|
||||
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
|
||||
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
|
||||
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `large` |
|
||||
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `images` | Optional image overrides for air-gapped or rate-limited registries. | `object` | `{}` |
|
||||
| `images.waitForKubeconfig` | Image used by the wait-for-kubeconfig init container. Empty falls back to images/busybox.tag. | `string` | `""` |
|
||||
|
||||
|
||||
## Parameter examples and reference
|
||||
|
|
|
|||
1
packages/apps/kubernetes/images/busybox.tag
Normal file
1
packages/apps/kubernetes/images/busybox.tag
Normal file
|
|
@ -0,0 +1 @@
|
|||
docker.io/library/busybox:1.37.0@sha256:1487d0af5f52b4ba31c7e465126ee2123fe3f2305d638e7827681e7cf6c83d5e
|
||||
|
|
@ -49,3 +49,52 @@ Selector labels
|
|||
app.kubernetes.io/name: {{ include "kubernetes.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
wait-for-kubeconfig init container shared by the control-plane-side
|
||||
Deployments (cluster-autoscaler, kccm, kcsi-controller) that mount the
|
||||
*-admin-kubeconfig Secret provisioned asynchronously by Kamaji. The
|
||||
Secret volume is declared optional so kubelet does not FailedMount while
|
||||
Kamaji is still bootstrapping; this container polls the mounted path and
|
||||
exits only when super-admin.svc appears, which happens after kubelet's
|
||||
optional-Secret refresh cycle.
|
||||
|
||||
The 10m deadline stays strictly below the 15m HelmRelease
|
||||
Install.Timeout set by cozystack-api for the Kubernetes kind (via the
|
||||
release.cozystack.io/helm-install-timeout annotation) so the
|
||||
CrashLoopBackOff surfaces before flux remediation fires and uninstalls
|
||||
the Cluster CR.
|
||||
|
||||
The default image lives in images/busybox.tag and points directly at
|
||||
docker.io by digest (not mirrored to ghcr.io like the other .tag files
|
||||
here): the payload is a one-shot sh loop and the digest pin makes the
|
||||
pull immutable. Operators in air-gapped or rate-limited environments
|
||||
can override it via .Values.images.waitForKubeconfig (any registry
|
||||
reference kubelet can pull). When the value is empty the chart falls
|
||||
back to the bundled digest pin, preserving the prior default.
|
||||
|
||||
Call site owns the surrounding volumes block; the kubeconfig volume
|
||||
must exist on the pod and mount at /etc/kubernetes/kubeconfig.
|
||||
*/}}
|
||||
{{- define "kubernetes.waitForAdminKubeconfig" -}}
|
||||
- name: wait-for-kubeconfig
|
||||
image: "{{ default (.Files.Get "images/busybox.tag" | trim) .Values.images.waitForKubeconfig }}"
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
deadline=$(( $(date +%s) + 600 ))
|
||||
until [ -s /etc/kubernetes/kubeconfig/super-admin.svc ]; do
|
||||
if [ "$(date +%s)" -ge "$deadline" ]; then
|
||||
echo "admin kubeconfig was not provisioned within 10m; exiting so the pod goes CrashLoopBackOff and surfaces in dashboards" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "waiting for admin kubeconfig (provisioned by Kamaji, visible after kubelet Secret refresh)..."
|
||||
sleep 5
|
||||
done
|
||||
volumeMounts:
|
||||
- name: kubeconfig
|
||||
mountPath: /etc/kubernetes/kubeconfig
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,14 @@
|
|||
{{- /*
|
||||
Gate the control-plane-side workloads on the parent tenant having an etcd
|
||||
DataStore. Without it no KamajiControlPlane is ever created, Kamaji never
|
||||
provisions -admin-kubeconfig, and rendering these Deployments would cause
|
||||
the wait-for-kubeconfig init to CrashLoopBackOff indefinitely, consuming
|
||||
the parent HelmRelease install timeout and triggering the very uninstall
|
||||
remediation cycle this chart is supposed to avoid. Rendering them only
|
||||
when $etcd is set keeps the HelmRelease Ready while flux retries on its
|
||||
interval and picks up the DataStore as soon as the Tenant chart finishes.
|
||||
*/}}
|
||||
{{- if .Values._namespace.etcd }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
|
@ -23,6 +34,8 @@ spec:
|
|||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: "NoSchedule"
|
||||
initContainers:
|
||||
{{- include "kubernetes.waitForAdminKubeconfig" $ | nindent 6 }}
|
||||
containers:
|
||||
- image: "{{ $.Files.Get "images/cluster-autoscaler.tag" | trim }}"
|
||||
name: cluster-autoscaler
|
||||
|
|
@ -56,6 +69,7 @@ spec:
|
|||
name: cloud-config
|
||||
- secret:
|
||||
secretName: {{ .Release.Name }}-admin-kubeconfig
|
||||
optional: true
|
||||
name: kubeconfig
|
||||
serviceAccountName: {{ .Release.Name }}-cluster-autoscaler
|
||||
terminationGracePeriodSeconds: 10
|
||||
|
|
@ -105,3 +119,4 @@ rules:
|
|||
- list
|
||||
- update
|
||||
- watch
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
{{- $etcd := .Values._namespace.etcd }}
|
||||
{{- /*
|
||||
When $etcd is empty, the parent Tenant application has not populated
|
||||
_namespace.etcd in cozystack-values yet - either the operator forgot to
|
||||
set etcd: true on an ancestor Tenant, or the Tenant HelmRelease is still
|
||||
reconciling. Either way, rendering a KamajiControlPlane with an empty
|
||||
dataStoreName would be rejected by Kamaji's admission webhook and the
|
||||
HelmRelease would fail to install, triggering remediation. Instead, emit
|
||||
a single ConfigMap as a user-visible status beacon and skip the rest so
|
||||
flux marks the HelmRelease Ready and retries its 5m reconcile loop until
|
||||
the Tenant chart catches up.
|
||||
*/}}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
{{- $kubevirtmachinetemplateNames := list }}
|
||||
|
|
@ -84,6 +95,26 @@ spec:
|
|||
- name: default
|
||||
pod: {}
|
||||
{{- end }}
|
||||
{{- if not $etcd }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-awaiting-etcd
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/name: kubernetes
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
data:
|
||||
status: "awaiting-etcd"
|
||||
message: |
|
||||
No DataStore is available for this tenant Kubernetes cluster. The parent
|
||||
Tenant application has not populated _namespace.etcd. Set spec.etcd: true
|
||||
on an ancestor Tenant (usually tenant-root) and wait for its HelmRelease
|
||||
to reconcile - this HelmRelease will pick up the DataStore on its next
|
||||
5m reconcile loop and provision the cluster.
|
||||
{{- else }}
|
||||
---
|
||||
apiVersion: cluster.x-k8s.io/v1beta1
|
||||
kind: Cluster
|
||||
|
|
@ -404,3 +435,4 @@ metadata:
|
|||
spec:
|
||||
{{- .spec | toYaml | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
|
|
@ -24,6 +25,8 @@ spec:
|
|||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: "NoSchedule"
|
||||
initContainers:
|
||||
{{- include "kubernetes.waitForAdminKubeconfig" $ | nindent 6 }}
|
||||
containers:
|
||||
- name: csi-driver
|
||||
imagePullPolicy: Always
|
||||
|
|
@ -234,4 +237,6 @@ spec:
|
|||
emptyDir: {}
|
||||
- secret:
|
||||
secretName: {{ .Release.Name }}-admin-kubeconfig
|
||||
optional: true
|
||||
name: kubeconfig
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if .Values.addons.certManager.enabled }}
|
||||
{{- if and .Values.addons.certManager.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ cert-manager:
|
|||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.addons.certManager.enabled }}
|
||||
{{- if and .Values.addons.certManager.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ cilium:
|
|||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -55,3 +56,4 @@ spec:
|
|||
- name: {{ .Release.Name }}-gateway-api-crds
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ coredns:
|
|||
clusterIP: "10.95.0.10"
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -42,3 +43,4 @@ spec:
|
|||
{{- end }}
|
||||
- name: {{ .Release.Name }}-cilium
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -39,3 +40,4 @@ spec:
|
|||
- name: {{ .Release.Name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if .Values.addons.fluxcd.enabled }}
|
||||
{{- if and .Values.addons.fluxcd.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if $.Values.addons.gatewayAPI.enabled }}
|
||||
{{- if and $.Values.addons.gatewayAPI.enabled $.Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if .Values.addons.gpuOperator.enabled }}
|
||||
{{- if and .Values.addons.gpuOperator.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ ingress-nginx:
|
|||
node-role.kubernetes.io/ingress-nginx: ""
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.addons.ingressNginx.enabled }}
|
||||
{{- if and .Values.addons.ingressNginx.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -36,3 +37,4 @@ spec:
|
|||
namespace: {{ .Release.Namespace }}
|
||||
- name: {{ .Release.Name }}-prometheus-operator-crds
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{{- $targetTenant := .Values._namespace.monitoring }}
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
{{- if .Values.addons.monitoringAgents.enabled }}
|
||||
{{- if and .Values.addons.monitoringAgents.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -31,3 +32,4 @@ spec:
|
|||
- name: {{ .Release.Name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if .Values.addons.velero.enabled }}
|
||||
{{- if and .Values.addons.velero.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if .Values.addons.monitoringAgents.enabled }}
|
||||
{{- if and .Values.addons.monitoringAgents.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ vertical-pod-autoscaler:
|
|||
memory: 1600Mi
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.addons.monitoringAgents.enabled }}
|
||||
{{- if and .Values.addons.monitoringAgents.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if .Values.addons.monitoringAgents.enabled }}
|
||||
{{- if and .Values.addons.monitoringAgents.enabled .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -33,3 +34,4 @@ spec:
|
|||
- name: {{ .Release.Name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
|
|
@ -22,6 +23,8 @@ spec:
|
|||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: "NoSchedule"
|
||||
initContainers:
|
||||
{{- include "kubernetes.waitForAdminKubeconfig" $ | nindent 6 }}
|
||||
containers:
|
||||
- name: kubevirt-cloud-controller-manager
|
||||
args:
|
||||
|
|
@ -55,5 +58,7 @@ spec:
|
|||
name: cloud-config
|
||||
- secret:
|
||||
secretName: {{ .Release.Name }}-admin-kubeconfig
|
||||
optional: true
|
||||
name: kubeconfig
|
||||
serviceAccountName: {{ .Release.Name }}-kccm
|
||||
{{- end }}
|
||||
|
|
|
|||
155
packages/apps/kubernetes/tests/admin_kubeconfig_wait_test.yaml
Normal file
155
packages/apps/kubernetes/tests/admin_kubeconfig_wait_test.yaml
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
suite: admin-kubeconfig wait guards
|
||||
|
||||
release:
|
||||
name: test
|
||||
namespace: tenant-root
|
||||
|
||||
values:
|
||||
- values-ci.yaml
|
||||
|
||||
tests:
|
||||
- it: cluster-autoscaler mounts admin-kubeconfig as optional
|
||||
template: templates/cluster-autoscaler/deployment.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: Deployment
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[?(@.name=="kubeconfig")].secret.secretName
|
||||
value: test-admin-kubeconfig
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[?(@.name=="kubeconfig")].secret.optional
|
||||
value: true
|
||||
|
||||
- it: cluster-autoscaler waits for admin-kubeconfig via initContainer
|
||||
template: templates/cluster-autoscaler/deployment.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: Deployment
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].name
|
||||
value: wait-for-kubeconfig
|
||||
- contains:
|
||||
path: spec.template.spec.initContainers[0].volumeMounts
|
||||
content:
|
||||
name: kubeconfig
|
||||
mountPath: /etc/kubernetes/kubeconfig
|
||||
readOnly: true
|
||||
|
||||
- it: kccm mounts admin-kubeconfig as optional
|
||||
template: templates/kccm/manager.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: Deployment
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[?(@.name=="kubeconfig")].secret.secretName
|
||||
value: test-admin-kubeconfig
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[?(@.name=="kubeconfig")].secret.optional
|
||||
value: true
|
||||
|
||||
- it: kccm waits for admin-kubeconfig via initContainer
|
||||
template: templates/kccm/manager.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: Deployment
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].name
|
||||
value: wait-for-kubeconfig
|
||||
- contains:
|
||||
path: spec.template.spec.initContainers[0].volumeMounts
|
||||
content:
|
||||
name: kubeconfig
|
||||
mountPath: /etc/kubernetes/kubeconfig
|
||||
readOnly: true
|
||||
|
||||
- it: csi controller mounts admin-kubeconfig as optional
|
||||
template: templates/csi/deploy.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: Deployment
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[?(@.name=="kubeconfig")].secret.secretName
|
||||
value: test-admin-kubeconfig
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[?(@.name=="kubeconfig")].secret.optional
|
||||
value: true
|
||||
|
||||
- it: csi controller waits for admin-kubeconfig via initContainer
|
||||
template: templates/csi/deploy.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: Deployment
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].name
|
||||
value: wait-for-kubeconfig
|
||||
- contains:
|
||||
path: spec.template.spec.initContainers[0].volumeMounts
|
||||
content:
|
||||
name: kubeconfig
|
||||
mountPath: /etc/kubernetes/kubeconfig
|
||||
readOnly: true
|
||||
|
||||
- it: wait-for-kubeconfig defaults to bundled busybox digest pin
|
||||
template: templates/cluster-autoscaler/deployment.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: Deployment
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: spec.template.spec.initContainers[0].image
|
||||
pattern: '^docker\.io/library/busybox:[^@]+@sha256:[0-9a-f]{64}$'
|
||||
|
||||
- it: wait-for-kubeconfig honours images.waitForKubeconfig override
|
||||
template: templates/cluster-autoscaler/deployment.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: Deployment
|
||||
set:
|
||||
images:
|
||||
waitForKubeconfig: "registry.example.test/library/busybox:1.37.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].image
|
||||
value: "registry.example.test/library/busybox:1.37.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
- it: cluster.yaml renders and wires dataStoreName when tenant has etcd
|
||||
template: templates/cluster.yaml
|
||||
documentSelector:
|
||||
path: kind
|
||||
value: KamajiControlPlane
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.dataStoreName
|
||||
value: tenant-root
|
||||
|
||||
- it: cluster.yaml skips Cluster resources when tenant has no etcd DataStore
|
||||
# Must NOT fail rendering - the parent Tenant chart populates
|
||||
# _namespace.etcd asynchronously, so rendering failures here would cause
|
||||
# flux install remediation on every cold bootstrap. Instead, emit only a
|
||||
# ConfigMap status beacon so the HelmRelease reports Ready while flux
|
||||
# retries on its interval until the DataStore appears.
|
||||
template: templates/cluster.yaml
|
||||
set:
|
||||
_namespace:
|
||||
etcd: ""
|
||||
monitoring: ""
|
||||
ingress: ""
|
||||
seaweedfs: ""
|
||||
host: ""
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: ConfigMap
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: test-awaiting-etcd
|
||||
- equal:
|
||||
path: data.status
|
||||
value: awaiting-etcd
|
||||
9
packages/apps/kubernetes/tests/values-ci-no-etcd.yaml
Normal file
9
packages/apps/kubernetes/tests/values-ci-no-etcd.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
_namespace:
|
||||
etcd: ""
|
||||
monitoring: ""
|
||||
ingress: ""
|
||||
seaweedfs: ""
|
||||
host: ""
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
nodeGroups: null
|
||||
9
packages/apps/kubernetes/tests/values-ci.yaml
Normal file
9
packages/apps/kubernetes/tests/values-ci.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
_namespace:
|
||||
etcd: tenant-root
|
||||
monitoring: ""
|
||||
ingress: ""
|
||||
seaweedfs: ""
|
||||
host: ""
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
nodeGroups: null
|
||||
|
|
@ -630,6 +630,18 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"description": "Optional image overrides for air-gapped or rate-limited registries.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"waitForKubeconfig": {
|
||||
"description": "Image used by the wait-for-kubeconfig init container. Empty falls back to images/busybox.tag.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,3 +197,10 @@ controlPlane:
|
|||
server:
|
||||
resources: {}
|
||||
resourcesPreset: "micro"
|
||||
|
||||
## @typedef {struct} Images - Optional image overrides for chart-internal helpers.
|
||||
## @field {string} [waitForKubeconfig] - Image used by the wait-for-kubeconfig init container. Empty falls back to images/busybox.tag.
|
||||
|
||||
## @param {Images} images - Optional image overrides for air-gapped or rate-limited registries.
|
||||
images:
|
||||
waitForKubeconfig: ""
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -160,6 +160,34 @@ func (o *CozyServerOptions) Complete() error {
|
|||
// Convert to ResourceConfig
|
||||
o.ResourceConfig = &config.ResourceConfig{}
|
||||
for _, crd := range crdList.Items {
|
||||
release := config.ReleaseConfig{
|
||||
Prefix: crd.Spec.Release.Prefix,
|
||||
Labels: crd.Spec.Release.Labels,
|
||||
ChartRef: config.ChartRefConfig{
|
||||
Kind: crd.Spec.Release.ChartRef.Kind,
|
||||
Name: crd.Spec.Release.ChartRef.Name,
|
||||
Namespace: crd.Spec.Release.ChartRef.Namespace,
|
||||
},
|
||||
}
|
||||
// Per-Application HelmRelease Install/Upgrade timeout. Applications
|
||||
// whose parent chart contains asynchronously-provisioned resources
|
||||
// the chart itself depends on (for example, the Kamaji-provisioned
|
||||
// admin-kubeconfig Secret for Kubernetes tenants) need a longer
|
||||
// wait budget than the Flux default. Consumed by the REST storage
|
||||
// layer when building the HelmRelease Spec. The parser rejects
|
||||
// units Flux would reject at webhook time, so a bad annotation
|
||||
// surfaces as a loud startup failure instead of a silent drop to
|
||||
// defaults.
|
||||
d, err := config.ParseHelmInstallTimeoutAnnotation(
|
||||
crd.Annotations[config.HelmInstallTimeoutAnnotation],
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"ApplicationDefinition %q has invalid %s annotation: %w",
|
||||
crd.Name, config.HelmInstallTimeoutAnnotation, err,
|
||||
)
|
||||
}
|
||||
release.HelmInstallTimeout = d
|
||||
resource := config.Resource{
|
||||
Application: config.ApplicationConfig{
|
||||
Kind: crd.Spec.Application.Kind,
|
||||
|
|
@ -168,15 +196,7 @@ func (o *CozyServerOptions) Complete() error {
|
|||
ShortNames: []string{}, // TODO: implement shortnames
|
||||
OpenAPISchema: crd.Spec.Application.OpenAPISchema,
|
||||
},
|
||||
Release: config.ReleaseConfig{
|
||||
Prefix: crd.Spec.Release.Prefix,
|
||||
Labels: crd.Spec.Release.Labels,
|
||||
ChartRef: config.ChartRefConfig{
|
||||
Kind: crd.Spec.Release.ChartRef.Kind,
|
||||
Name: crd.Spec.Release.ChartRef.Name,
|
||||
Namespace: crd.Spec.Release.ChartRef.Namespace,
|
||||
},
|
||||
},
|
||||
Release: release,
|
||||
}
|
||||
o.ResourceConfig.Resources = append(o.ResourceConfig.Resources, resource)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,48 @@ limitations under the License.
|
|||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HelmInstallTimeoutAnnotation is the ApplicationDefinition metadata
|
||||
// annotation key that overrides the Flux HelmRelease Install.Timeout and
|
||||
// Upgrade.Timeout for a given Application kind.
|
||||
const HelmInstallTimeoutAnnotation = "release.cozystack.io/helm-install-timeout"
|
||||
|
||||
// helmTimeoutPattern mirrors the CRD validation pattern used by Flux
|
||||
// helm-controller on HelmReleaseSpec.Install.Timeout (ms/s/m/h units only).
|
||||
// time.ParseDuration accepts ns/us/µs, but Flux rejects them - parsing here
|
||||
// with the same shape avoids feeding the controller a value it will later
|
||||
// reject at webhook time. See
|
||||
// github.com/fluxcd/helm-controller/api/v2 HelmReleaseSpec.Install.Timeout
|
||||
// in the go module cache.
|
||||
var helmTimeoutPattern = regexp.MustCompile(`^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$`)
|
||||
|
||||
// ParseHelmInstallTimeoutAnnotation parses the value of the
|
||||
// release.cozystack.io/helm-install-timeout annotation. The empty string is
|
||||
// treated as "unset" and returns (0, nil) so callers can leave
|
||||
// HelmInstallTimeout zeroed and let flux defaults apply. Values accepted by
|
||||
// time.ParseDuration but rejected by Flux (ns/us/µs) return a helpful
|
||||
// error instead of silently parsing and failing later at HelmRelease
|
||||
// admission.
|
||||
func ParseHelmInstallTimeoutAnnotation(raw string) (time.Duration, error) {
|
||||
if raw == "" {
|
||||
return 0, nil
|
||||
}
|
||||
if !helmTimeoutPattern.MatchString(raw) {
|
||||
return 0, fmt.Errorf("must match %s (Flux accepts ms/s/m/h units only), got %q",
|
||||
helmTimeoutPattern, raw)
|
||||
}
|
||||
d, err := time.ParseDuration(raw)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("time.ParseDuration(%q): %w", raw, err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ResourceConfig represents the structure of the configuration file.
|
||||
type ResourceConfig struct {
|
||||
Resources []Resource `yaml:"resources"`
|
||||
|
|
@ -41,6 +83,12 @@ type ReleaseConfig struct {
|
|||
Prefix string `yaml:"prefix"`
|
||||
Labels map[string]string `yaml:"labels"`
|
||||
ChartRef ChartRefConfig `yaml:"chartRef"`
|
||||
// HelmInstallTimeout overrides the Flux HelmRelease Install.Timeout and
|
||||
// Upgrade.Timeout for this Application kind. When zero, flux defaults
|
||||
// apply. Populated from the
|
||||
// release.cozystack.io/helm-install-timeout annotation on the
|
||||
// ApplicationDefinition at start-up.
|
||||
HelmInstallTimeout time.Duration `yaml:"helmInstallTimeout,omitempty"`
|
||||
}
|
||||
|
||||
// ChartRefConfig references a Flux source artifact for the Helm chart.
|
||||
|
|
|
|||
119
pkg/config/config_test.go
Normal file
119
pkg/config/config_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cover the annotation parser used by cozystack-api at startup. The parser
|
||||
// is consumed by pkg/cmd/server/start.go on every ApplicationDefinition; a
|
||||
// typo here silently drops back to flux defaults and the Kubernetes tenant
|
||||
// race described in cozystack#2412 reappears, so the table must exercise:
|
||||
// - the unset path (empty string treated as "no override"),
|
||||
// - every unit Flux accepts (ms, s, m, h),
|
||||
// - compound forms (the CRD pattern accepts repeats),
|
||||
// - units time.ParseDuration accepts but Flux rejects (ns, us, µs),
|
||||
// - outright garbage.
|
||||
func TestParseHelmInstallTimeoutAnnotation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
want time.Duration
|
||||
wantErr bool
|
||||
errMatch string
|
||||
}{
|
||||
{
|
||||
name: "empty string leaves flux defaults in place",
|
||||
input: "",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "minutes",
|
||||
input: "15m",
|
||||
want: 15 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "hours",
|
||||
input: "1h",
|
||||
want: time.Hour,
|
||||
},
|
||||
{
|
||||
name: "seconds",
|
||||
input: "45s",
|
||||
want: 45 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "milliseconds",
|
||||
input: "500ms",
|
||||
want: 500 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
name: "compound hour and minutes",
|
||||
input: "2h30m",
|
||||
want: 2*time.Hour + 30*time.Minute,
|
||||
},
|
||||
{
|
||||
name: "decimal minutes",
|
||||
input: "1.5m",
|
||||
want: 90 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "nanoseconds rejected - Flux CRD pattern excludes ns",
|
||||
input: "500ns",
|
||||
wantErr: true,
|
||||
errMatch: "Flux accepts ms/s/m/h units only",
|
||||
},
|
||||
{
|
||||
name: "microseconds rejected - Flux CRD pattern excludes us",
|
||||
input: "500us",
|
||||
wantErr: true,
|
||||
errMatch: "Flux accepts ms/s/m/h units only",
|
||||
},
|
||||
{
|
||||
name: "microseconds unicode rejected",
|
||||
input: "500µs",
|
||||
wantErr: true,
|
||||
errMatch: "Flux accepts ms/s/m/h units only",
|
||||
},
|
||||
{
|
||||
name: "bare digits rejected",
|
||||
input: "15",
|
||||
wantErr: true,
|
||||
errMatch: "Flux accepts ms/s/m/h units only",
|
||||
},
|
||||
{
|
||||
name: "garbage rejected",
|
||||
input: "abc",
|
||||
wantErr: true,
|
||||
errMatch: "Flux accepts ms/s/m/h units only",
|
||||
},
|
||||
{
|
||||
name: "negative rejected",
|
||||
input: "-15m",
|
||||
wantErr: true,
|
||||
errMatch: "Flux accepts ms/s/m/h units only",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ParseHelmInstallTimeoutAnnotation(tc.input)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got duration=%v", got)
|
||||
}
|
||||
if tc.errMatch != "" && !strings.Contains(err.Error(), tc.errMatch) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), tc.errMatch)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1528,6 +1528,24 @@ func (r *REST) convertApplicationToHelmRelease(app *appsv1alpha1.Application) (*
|
|||
},
|
||||
}
|
||||
|
||||
// Per-Application HelmRelease wait budget. The mechanism is generic:
|
||||
// an ApplicationDefinition that sets
|
||||
// release.cozystack.io/helm-install-timeout gets Install.Timeout and
|
||||
// Upgrade.Timeout populated from ReleaseConfig.HelmInstallTimeout
|
||||
// (parsed at startup). Applications that leave it unset keep flux
|
||||
// defaults so their failed installs remediate on the normal cadence.
|
||||
// Today only kubernetes-rd carries the annotation because the
|
||||
// Kubernetes Application's parent chart contains CAPI/Kamaji
|
||||
// resources whose admin-kubeconfig Secret is provisioned
|
||||
// asynchronously and Kamaji cold-start routinely exceeds flux's
|
||||
// default wait budget; any future kind with the same shape can opt
|
||||
// in by setting the same annotation.
|
||||
if r.releaseConfig.HelmInstallTimeout > 0 {
|
||||
timeout := metav1.Duration{Duration: r.releaseConfig.HelmInstallTimeout}
|
||||
helmRelease.Spec.Install.Timeout = &timeout
|
||||
helmRelease.Spec.Upgrade.Timeout = &timeout
|
||||
}
|
||||
|
||||
return helmRelease, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
116
pkg/registry/apps/application/rest_timeout_test.go
Normal file
116
pkg/registry/apps/application/rest_timeout_test.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/config"
|
||||
)
|
||||
|
||||
func newRESTForTimeout(kind, prefix string, helmInstallTimeout time.Duration) *REST {
|
||||
return &REST{
|
||||
kindName: kind,
|
||||
releaseConfig: config.ReleaseConfig{
|
||||
Prefix: prefix,
|
||||
ChartRef: config.ChartRefConfig{
|
||||
Kind: "HelmChart",
|
||||
Name: "x",
|
||||
Namespace: "cozy-system",
|
||||
},
|
||||
HelmInstallTimeout: helmInstallTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Table-driven: every Application kind carries a per-CRD HelmRelease wait
|
||||
// budget. The Kubernetes kind's parent chart contains CAPI/Kamaji resources
|
||||
// whose admin-kubeconfig Secret is provisioned asynchronously, so its
|
||||
// ApplicationDefinition sets release.cozystack.io/helm-install-timeout=15m
|
||||
// (or longer). Other kinds leave the annotation unset and keep flux defaults
|
||||
// so their failed installs remediate on the normal cadence. The test must
|
||||
// cover both paths: a kind with the timeout set and one without.
|
||||
func TestConvertApplicationToHelmRelease_AppliesReleaseConfigTimeout(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
kind string
|
||||
prefix string
|
||||
configured time.Duration
|
||||
wantSet bool
|
||||
}{
|
||||
{
|
||||
name: "Kubernetes kind with 15m configured gets Install and Upgrade Timeout",
|
||||
kind: "Kubernetes",
|
||||
prefix: "kubernetes-",
|
||||
configured: 15 * time.Minute,
|
||||
wantSet: true,
|
||||
},
|
||||
{
|
||||
// Fictional kind on purpose: the test is about the unset path
|
||||
// regardless of which real Application kind ends up needing a
|
||||
// timeout override. Using a real kind name would create false
|
||||
// coupling to that Application's ApplicationDefinition.
|
||||
name: "unrelated kind without configured timeout keeps flux defaults",
|
||||
kind: "PlaceholderKindForDefaults",
|
||||
prefix: "placeholder-",
|
||||
configured: 0,
|
||||
wantSet: false,
|
||||
},
|
||||
{
|
||||
name: "arbitrary future kind with 20m configured gets 20m",
|
||||
kind: "TalosCluster",
|
||||
prefix: "talos-",
|
||||
configured: 20 * time.Minute,
|
||||
wantSet: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := newRESTForTimeout(tc.kind, tc.prefix, tc.configured)
|
||||
app := &appsv1alpha1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "tenant-root"},
|
||||
}
|
||||
|
||||
hr, err := r.convertApplicationToHelmRelease(app)
|
||||
if err != nil {
|
||||
t.Fatalf("convertApplicationToHelmRelease returned error: %v", err)
|
||||
}
|
||||
|
||||
if hr.Spec.Install == nil || hr.Spec.Upgrade == nil {
|
||||
t.Fatalf("Spec.Install/Upgrade must be non-nil")
|
||||
}
|
||||
|
||||
if tc.wantSet {
|
||||
if hr.Spec.Install.Timeout == nil {
|
||||
t.Fatalf("Spec.Install.Timeout must be set when HelmInstallTimeout=%v", tc.configured)
|
||||
}
|
||||
if hr.Spec.Install.Timeout.Duration != tc.configured {
|
||||
t.Errorf("Spec.Install.Timeout = %v, want %v", hr.Spec.Install.Timeout.Duration, tc.configured)
|
||||
}
|
||||
if hr.Spec.Upgrade.Timeout == nil {
|
||||
t.Fatalf("Spec.Upgrade.Timeout must be set when HelmInstallTimeout=%v", tc.configured)
|
||||
}
|
||||
if hr.Spec.Upgrade.Timeout.Duration != tc.configured {
|
||||
t.Errorf("Spec.Upgrade.Timeout = %v, want %v", hr.Spec.Upgrade.Timeout.Duration, tc.configured)
|
||||
}
|
||||
} else {
|
||||
if hr.Spec.Install.Timeout != nil {
|
||||
t.Errorf("Spec.Install.Timeout must be nil when HelmInstallTimeout=0, got %v", hr.Spec.Install.Timeout.Duration)
|
||||
}
|
||||
if hr.Spec.Upgrade.Timeout != nil {
|
||||
t.Errorf("Spec.Upgrade.Timeout must be nil when HelmInstallTimeout=0, got %v", hr.Spec.Upgrade.Timeout.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
if hr.Spec.Install.Remediation == nil || hr.Spec.Install.Remediation.Retries != -1 {
|
||||
t.Errorf("Spec.Install.Remediation.Retries must remain -1, got %+v", hr.Spec.Install.Remediation)
|
||||
}
|
||||
if hr.Spec.Upgrade.Remediation == nil || hr.Spec.Upgrade.Remediation.Retries != -1 {
|
||||
t.Errorf("Spec.Upgrade.Remediation.Retries must remain -1, got %+v", hr.Spec.Upgrade.Remediation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue