From 6f5bb48698930d06bba88b1b3fcb0701f4d15be3 Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sun, 23 Aug 2026 11:03:37 -0700 Subject: [PATCH] test(bdd): prepare named Helmfile environments Replace repeated environment fixture copy and YAML update pairs with a table-driven step that derives and snapshots known Helmfile destinations without hiding test inputs. Closes #1080 Signed-off-by: Stephanie Baum --- tests/bdd/PLAN.md | 1 + tests/bdd/dsl/helmfile_environment.go | 48 ++++++++ tests/bdd/dsl/helmfile_environment_test.go | 65 +++++++++++ .../multi-cluster-eks-helmfile.feature | 6 +- .../features/multi-cluster-helmfile.feature | 6 +- tests/bdd/features/multi-cluster-up.feature | 6 +- tests/bdd/features/observability-all.feature | 9 +- .../features/observability-compute.feature | 9 +- .../features/observability-control.feature | 6 +- .../features/observability-disabled.feature | 6 +- .../single-cluster-eks-helmfile.feature | 6 +- ...e-cluster-helmfile-upstream-images.feature | 3 +- .../features/single-cluster-helmfile.feature | 6 +- .../single-cluster-up-oneclick.feature | 6 +- tests/bdd/features/single-cluster-up.feature | 6 +- tests/bdd/steps/file_steps.go | 29 +++++ tests/bdd/steps/steps_test.go | 103 ++++++++++++++++++ 17 files changed, 271 insertions(+), 50 deletions(-) create mode 100644 tests/bdd/dsl/helmfile_environment.go create mode 100644 tests/bdd/dsl/helmfile_environment_test.go diff --git a/tests/bdd/PLAN.md b/tests/bdd/PLAN.md index 95e930777..dd11d50a0 100644 --- a/tests/bdd/PLAN.md +++ b/tests/bdd/PLAN.md @@ -105,6 +105,7 @@ refactor in every consumer; that is a feature. |------|-------| | `And I copy the file {string} to {string}` | Both paths are repo-relative. | | `And I update yaml file {string} with keys:` (two-column table of dotted-path and value) | Path supports dotted notation and `[n]` indices (e.g. `global.imagePullSecrets[0].name`). Missing intermediate maps and missing list indices are upserted: writing `global.imagePullSecrets[0].name` against a file that has neither `global.imagePullSecrets` nor any list entry creates both. Existing scalars at intermediate positions cause the step to fail rather than silently overwrite a non-map. Value cells expand `${VAR}` from `os.Environ`. | +| `And I prepare Helmfile environment {string} for stack {string} from fixture {string} with values:` (two-column table of dotted-path and value) | Validates the stack and environment names, derives `deploy/stacks//environments/.yaml` from the absolute repository root, copies the explicit fixture, and applies the visible values table with the same YAML update and `${VAR}` interpolation behavior. Supported stacks are `self-managed`, `observability`, and `nvcf-compute-plane`. The destination is ledger-backed. | | `And I prepare self-managed secrets file {string} from template {string} using the current NGC registry credential` | The destination and template are explicit repo-relative paths with `${VAR}` interpolation. Replaces the template's registry credential placeholder with base64 of the current `$oauthtoken:` credential and writes the destination with mode `0600`. The destination is ledger-backed, and secret material never enters Gherkin, command logs, or failure messages. | | `And I substitute a block in file {string}:` (docstring) | The docstring contains an old block and replacement block separated by exactly one `---` line. `${VAR}` interpolation applies before an exact, ledger-backed replacement. Missing or malformed old blocks fail. | diff --git a/tests/bdd/dsl/helmfile_environment.go b/tests/bdd/dsl/helmfile_environment.go new file mode 100644 index 000000000..5f6dca100 --- /dev/null +++ b/tests/bdd/dsl/helmfile_environment.go @@ -0,0 +1,48 @@ +/* +SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dsl + +import ( + "fmt" + "path/filepath" + "regexp" +) + +var helmfileEnvironmentName = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`) + +var helmfileStacks = map[string]struct{}{ + "nvcf-compute-plane": {}, + "observability": {}, + "self-managed": {}, +} + +// HelmfileEnvironmentPath validates a named stack environment and returns its +// destination beneath an absolute repository root. It never depends on the +// process working directory. +func HelmfileEnvironmentPath(repoRoot, stack, environment string) (string, error) { + if !filepath.IsAbs(repoRoot) { + return "", fmt.Errorf("repository root must be absolute") + } + if _, ok := helmfileStacks[stack]; !ok { + return "", fmt.Errorf("unsupported Helmfile stack %q", stack) + } + if !helmfileEnvironmentName.MatchString(environment) { + return "", fmt.Errorf("invalid Helmfile environment name %q", environment) + } + return filepath.Join(repoRoot, "deploy", "stacks", stack, "environments", environment+".yaml"), nil +} diff --git a/tests/bdd/dsl/helmfile_environment_test.go b/tests/bdd/dsl/helmfile_environment_test.go new file mode 100644 index 000000000..963ef7b51 --- /dev/null +++ b/tests/bdd/dsl/helmfile_environment_test.go @@ -0,0 +1,65 @@ +/* +SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dsl + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestHelmfileEnvironmentPathSupportsKnownStacks(t *testing.T) { + repoRoot := t.TempDir() + for _, stack := range []string{"self-managed", "observability", "nvcf-compute-plane"} { + t.Run(stack, func(t *testing.T) { + got, err := HelmfileEnvironmentPath(repoRoot, stack, "local-bdd") + if err != nil { + t.Fatalf("environment path: %v", err) + } + want := filepath.Join(repoRoot, "deploy", "stacks", stack, "environments", "local-bdd.yaml") + if got != want { + t.Fatalf("path = %q, want %q", got, want) + } + }) + } +} + +func TestHelmfileEnvironmentPathRejectsInvalidInput(t *testing.T) { + repoRoot := t.TempDir() + tests := []struct { + name string + root string + stack string + environment string + want string + }{ + {name: "relative root", root: "repo", stack: "self-managed", environment: "local", want: "repository root must be absolute"}, + {name: "unknown stack", root: repoRoot, stack: "other", environment: "local", want: `unsupported Helmfile stack "other"`}, + {name: "empty environment", root: repoRoot, stack: "self-managed", environment: "", want: `invalid Helmfile environment name ""`}, + {name: "path traversal", root: repoRoot, stack: "self-managed", environment: "../local", want: `invalid Helmfile environment name "../local"`}, + {name: "path separator", root: repoRoot, stack: "self-managed", environment: "team/local", want: `invalid Helmfile environment name "team/local"`}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := HelmfileEnvironmentPath(tc.root, tc.stack, tc.environment) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("err = %v, want containing %q", err, tc.want) + } + }) + } +} diff --git a/tests/bdd/features/multi-cluster-eks-helmfile.feature b/tests/bdd/features/multi-cluster-eks-helmfile.feature index 3d3de57e7..60b0d8636 100644 --- a/tests/bdd/features/multi-cluster-eks-helmfile.feature +++ b/tests/bdd/features/multi-cluster-eks-helmfile.feature @@ -132,8 +132,7 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust # the agent config. The agent dials the bare-ELB service URLs # (which DNS-resolve) and sends these hostnames as the HTTP Host # header so the control-plane gateway HTTPRoutes match. - When I copy the file "deploy/stacks/self-managed/environments/base.yaml" to "deploy/stacks/self-managed/environments/eks-bdd-multi.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/eks-bdd-multi.yaml" with keys: + When I prepare Helmfile environment "eks-bdd-multi" for stack "self-managed" from fixture "deploy/stacks/self-managed/environments/base.yaml" with values: | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.imagePullSecrets[0].name | nvcr-pull-secret | @@ -164,8 +163,7 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust | openbao.migrations.issuerDiscovery.enabled | true | Then yaml file "deploy/stacks/self-managed/environments/eks-bdd-multi.yaml" key "global.domain" should equal "${EKS_GATEWAY_DOMAIN}" - When I copy the file "deploy/stacks/nvcf-compute-plane/environments/base.yaml" to "deploy/stacks/nvcf-compute-plane/environments/eks-bdd-multi.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/eks-bdd-multi.yaml" with keys: + When I prepare Helmfile environment "eks-bdd-multi" for stack "nvcf-compute-plane" from fixture "deploy/stacks/nvcf-compute-plane/environments/base.yaml" with values: | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.imagePullSecrets[0].name | nvcr-pull-secret | diff --git a/tests/bdd/features/multi-cluster-helmfile.feature b/tests/bdd/features/multi-cluster-helmfile.feature index 02cf36bd9..20aaa65d0 100644 --- a/tests/bdd/features/multi-cluster-helmfile.feature +++ b/tests/bdd/features/multi-cluster-helmfile.feature @@ -37,15 +37,13 @@ Feature: Install a local multi-cluster NVCF stack with Helmfile # operator-specific registry values before the first Helmfile # install. Later scenarios reuse that install instead of # reinstalling with different secrets or URLs. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd-multi.yaml" to "deploy/stacks/self-managed/environments/local-bdd.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local-bdd.yaml" with keys: + And I prepare Helmfile environment "local-bdd" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd-multi.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | api.env.NVCF_SIDECARS_LLM_ROUTER_CLIENT_IMAGE | nvcr.io/${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM}/stargate-client:0.2.0 | | observability.profile | disabled | - And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local-bdd.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local-bdd.yaml" with keys: + And I prepare Helmfile environment "local-bdd" for stack "nvcf-compute-plane" from fixture "tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/multi-cluster-up.feature b/tests/bdd/features/multi-cluster-up.feature index 7eb9cf761..c37c95411 100644 --- a/tests/bdd/features/multi-cluster-up.feature +++ b/tests/bdd/features/multi-cluster-up.feature @@ -38,14 +38,12 @@ Feature: Bring up a local multi-cluster NVCF stack with the CLI # on a fresh cluster the ServiceMonitor CRDs do not exist yet and # the diff fails before anything installs. The Helmfile workflow # (helmfile sync) has no diff phase and keeps the default profile. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd-multi.yaml" to "deploy/stacks/self-managed/environments/local.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local.yaml" with keys: + And I prepare Helmfile environment "local" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd-multi.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | observability.profile | disabled | - And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local.yaml" with keys: + And I prepare Helmfile environment "local" for stack "nvcf-compute-plane" from fixture "tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/observability-all.feature b/tests/bdd/features/observability-all.feature index de987bcdc..6e36fbff1 100644 --- a/tests/bdd/features/observability-all.feature +++ b/tests/bdd/features/observability-all.feature @@ -20,23 +20,20 @@ Feature: Install local Helmfile observability for both planes bash -c 'set -eo pipefail; printf %s "$NGC_API_KEY" | helm registry login nvcr.io --username "\$oauthtoken" --password-stdin' """ # Configure the control-plane stack and its shared observability child. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/self-managed/environments/local-bdd-observability-all.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local-bdd-observability-all.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-all" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | observability.profile | all | | functionAutoscaler.image.tag | 1.18.10 | # Give the shared observability Helmfile the same named environment. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/observability/environments/local-bdd-observability-all.yaml" - And I update yaml file "deploy/stacks/observability/environments/local-bdd-observability-all.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-all" for stack "observability" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | observability.profile | all | # Configure NVCA to join the same cluster and enable its collector. - And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local-bdd-observability-all.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local-bdd-observability-all.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-all" for stack "nvcf-compute-plane" from fixture "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/observability-compute.feature b/tests/bdd/features/observability-compute.feature index 8c8a83f38..5846430ea 100644 --- a/tests/bdd/features/observability-compute.feature +++ b/tests/bdd/features/observability-compute.feature @@ -22,23 +22,20 @@ Feature: Install local Helmfile observability with the compute profile """ # Install only control-plane prerequisites on ncp-local-cp. Shared # observability is installed separately on the compute cluster below. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd-multi.yaml" to "deploy/stacks/self-managed/environments/local-bdd-observability-compute.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local-bdd-observability-compute.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-compute" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd-multi.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | addons.llm.enabled | false | | observability.profile | disabled | # Configure the shared observability stack for compute-plane monitors. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd-multi.yaml" to "deploy/stacks/observability/environments/local-bdd-observability-compute.yaml" - And I update yaml file "deploy/stacks/observability/environments/local-bdd-observability-compute.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-compute" for stack "observability" from fixture "tests/bdd/fixtures/self-managed-local-bdd-multi.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | observability.profile | compute | # Configure NVCA to use the same compute observability profile. - And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local-bdd-observability-compute.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local-bdd-observability-compute.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-compute" for stack "nvcf-compute-plane" from fixture "tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/observability-control.feature b/tests/bdd/features/observability-control.feature index d85834e9a..e9d27842f 100644 --- a/tests/bdd/features/observability-control.feature +++ b/tests/bdd/features/observability-control.feature @@ -17,16 +17,14 @@ Feature: Install local Helmfile observability with the control profile bash -c 'set -eo pipefail; printf %s "$NGC_API_KEY" | helm registry login nvcr.io --username "\$oauthtoken" --password-stdin' """ # Set the self-managed stack environment. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/self-managed/environments/local-bdd-observability-control.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local-bdd-observability-control.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-control" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | observability.profile | control | | functionAutoscaler.image.tag | 1.18.10 | # Set the shared observability stack environment. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/observability/environments/local-bdd-observability-control.yaml" - And I update yaml file "deploy/stacks/observability/environments/local-bdd-observability-control.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-control" for stack "observability" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/observability-disabled.feature b/tests/bdd/features/observability-disabled.feature index 5dcb0bcf4..ee0039ca7 100644 --- a/tests/bdd/features/observability-disabled.feature +++ b/tests/bdd/features/observability-disabled.feature @@ -18,16 +18,14 @@ Feature: Render local Helmfile stacks with observability disabled bash -c 'set -eo pipefail; printf %s "$NGC_API_KEY" | helm registry login nvcr.io --username "\$oauthtoken" --password-stdin' """ # Create the self-managed stack environment used by the control-plane render. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/self-managed/environments/local-bdd-observability-disabled.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local-bdd-observability-disabled.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-disabled" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | observability.profile | disabled | And I prepare self-managed secrets file "deploy/stacks/self-managed/secrets/local-bdd-observability-disabled-secrets.yaml" from template "deploy/stacks/self-managed/secrets/secrets.yaml.template" using the current NGC registry credential # Create the compute-plane stack environment used by the worker render. - And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local-bdd-observability-disabled.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local-bdd-observability-disabled.yaml" with keys: + And I prepare Helmfile environment "local-bdd-observability-disabled" for stack "nvcf-compute-plane" from fixture "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/single-cluster-eks-helmfile.feature b/tests/bdd/features/single-cluster-eks-helmfile.feature index 2cccc24ff..93d079563 100644 --- a/tests/bdd/features/single-cluster-eks-helmfile.feature +++ b/tests/bdd/features/single-cluster-eks-helmfile.feature @@ -125,8 +125,7 @@ Feature: Install a single-cluster NVCF stack on a pre-provisioned EKS cluster wi # hostnames as the HTTP Host header so the gateway HTTPRoutes match. # This replaces the former @nvca-registration URL-rewrite + hostAliases # workaround. - When I copy the file "deploy/stacks/self-managed/environments/base.yaml" to "deploy/stacks/self-managed/environments/eks-bdd.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/eks-bdd.yaml" with keys: + When I prepare Helmfile environment "eks-bdd" for stack "self-managed" from fixture "deploy/stacks/self-managed/environments/base.yaml" with values: | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.imagePullSecrets[0].name | nvcr-pull-secret | @@ -148,8 +147,7 @@ Feature: Install a single-cluster NVCF stack on a pre-provisioned EKS cluster wi # The compute-plane Helmfile is a separate bundle, so give it an # environment file with the same registry and control-plane endpoints. - When I copy the file "deploy/stacks/nvcf-compute-plane/environments/base.yaml" to "deploy/stacks/nvcf-compute-plane/environments/eks-bdd.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/eks-bdd.yaml" with keys: + When I prepare Helmfile environment "eks-bdd" for stack "nvcf-compute-plane" from fixture "deploy/stacks/nvcf-compute-plane/environments/base.yaml" with values: | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.imagePullSecrets[0].name | nvcr-pull-secret | diff --git a/tests/bdd/features/single-cluster-helmfile-upstream-images.feature b/tests/bdd/features/single-cluster-helmfile-upstream-images.feature index 1baa80dda..89ec524be 100644 --- a/tests/bdd/features/single-cluster-helmfile-upstream-images.feature +++ b/tests/bdd/features/single-cluster-helmfile-upstream-images.feature @@ -20,8 +20,7 @@ Feature: Install a local single-cluster stack with upstream supporting images When I run command "k3d cluster get ncp-local-cp" Then the command exit code should be 1 Given I copy the file "deploy/stacks/self-managed/Makefile.dist" to "deploy/stacks/self-managed/Makefile" - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/self-managed/environments/local-bdd.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local-bdd.yaml" with keys: + And I prepare Helmfile environment "local-bdd" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/single-cluster-helmfile.feature b/tests/bdd/features/single-cluster-helmfile.feature index 2473ef166..5352fedc8 100644 --- a/tests/bdd/features/single-cluster-helmfile.feature +++ b/tests/bdd/features/single-cluster-helmfile.feature @@ -13,20 +13,18 @@ Feature: Install a local single-cluster NVCF stack with Helmfile | NGC_API_KEY | | SAMPLE_NGC_ORG | | SAMPLE_NGC_TEAM | - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/self-managed/environments/local-bdd.yaml" # The fixture is a copy of deploy/stacks/self-managed/environments/local.yaml, # which already carries every ncp-local local-mode override (storageClass, # replica counts, NVCA self-managed endpoints, addons.llm.*, agentConfig, # ingress.gatewayApi.*). The Background only overlays the operator-specific # values that vary per NGC org and pull-secret name. - And I update yaml file "deploy/stacks/self-managed/environments/local-bdd.yaml" with keys: + And I prepare Helmfile environment "local-bdd" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | api.env.NVCF_SIDECARS_LLM_ROUTER_CLIENT_IMAGE | nvcr.io/${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM}/stargate-client:0.2.0 | | observability.profile | disabled | - And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local-bdd.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local-bdd.yaml" with keys: + And I prepare Helmfile environment "local-bdd" for stack "nvcf-compute-plane" from fixture "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/single-cluster-up-oneclick.feature b/tests/bdd/features/single-cluster-up-oneclick.feature index 8c4e60aaf..e5c72e191 100644 --- a/tests/bdd/features/single-cluster-up-oneclick.feature +++ b/tests/bdd/features/single-cluster-up-oneclick.feature @@ -33,15 +33,13 @@ Feature: Bring up a local single-cluster NVCF stack with the self-hosted up one- # values from both split stacks. Author both local.yaml files from # the tracked BDD fixtures; the Ledger restores or removes them at # suite teardown. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/self-managed/environments/local.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local.yaml" with keys: + And I prepare Helmfile environment "local" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | api.env.NVCF_SIDECARS_LLM_ROUTER_CLIENT_IMAGE | nvcr.io/${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM}/stargate-client:0.2.0 | | observability.profile | disabled | - And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local.yaml" with keys: + And I prepare Helmfile environment "local" for stack "nvcf-compute-plane" from fixture "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/features/single-cluster-up.feature b/tests/bdd/features/single-cluster-up.feature index f26aea571..cb980f20c 100644 --- a/tests/bdd/features/single-cluster-up.feature +++ b/tests/bdd/features/single-cluster-up.feature @@ -37,14 +37,12 @@ Feature: Bring up a local single-cluster NVCF stack with the CLI # the ServiceMonitor CRDs do not exist yet and the diff fails # before anything installs. The Helmfile workflow (helmfile sync) # has no diff phase and keeps the default profile. - And I copy the file "tests/bdd/fixtures/self-managed-local-bdd.yaml" to "deploy/stacks/self-managed/environments/local.yaml" - And I update yaml file "deploy/stacks/self-managed/environments/local.yaml" with keys: + And I prepare Helmfile environment "local" for stack "self-managed" from fixture "tests/bdd/fixtures/self-managed-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | observability.profile | disabled | - And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local.yaml" - And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local.yaml" with keys: + And I prepare Helmfile environment "local" for stack "nvcf-compute-plane" from fixture "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" with values: | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | diff --git a/tests/bdd/steps/file_steps.go b/tests/bdd/steps/file_steps.go index 45361f901..9903b43df 100644 --- a/tests/bdd/steps/file_steps.go +++ b/tests/bdd/steps/file_steps.go @@ -34,6 +34,7 @@ import ( func registerFileSteps(ctx *godog.ScenarioContext, sc *ScenarioContext) { ctx.Step(`^I copy the file "([^"]*)" to "([^"]*)"$`, sc.iCopyFile) ctx.Step(`^I update yaml file "([^"]*)" with keys:$`, sc.iUpdateYAMLFile) + ctx.Step(`^I prepare Helmfile environment "([^"]*)" for stack "([^"]*)" from fixture "([^"]*)" with values:$`, sc.iPrepareHelmfileEnvironment) ctx.Step(`^I prepare self-managed secrets file "([^"]*)" from template "([^"]*)" using the current NGC registry credential$`, sc.iPrepareSelfManagedSecretsFile) ctx.Step(`^I substitute a block in file "([^"]*)":$`, sc.iSubstituteBlock) ctx.Step(`^environment variable "([^"]*)" is set$`, sc.environmentVariableIsSet) @@ -41,6 +42,34 @@ func registerFileSteps(ctx *godog.ScenarioContext, sc *ScenarioContext) { ctx.Step(`^file "([^"]*)" exists$`, sc.fileShouldExist) } +// iPrepareHelmfileEnvironment delegates named environment preparation so the +// registered Godog handler remains declarative. +func (sc *ScenarioContext) iPrepareHelmfileEnvironment(environment, stack, fixture string, table *godog.Table) error { + return sc.prepareHelmfileEnvironment(environment, stack, fixture, table) +} + +// prepareHelmfileEnvironment validates and derives the destination from the +// suite repository root, then snapshots, copies, and updates it. +func (sc *ScenarioContext) prepareHelmfileEnvironment(environment, stack, fixture string, table *godog.Table) error { + resolvedEnvironment := dsl.Interpolate(environment) + resolvedStack := dsl.Interpolate(stack) + dest, err := dsl.HelmfileEnvironmentPath(sc.Suite.Config.RepoRoot, resolvedStack, resolvedEnvironment) + if err != nil { + return err + } + keys, err := tableToKeyValuePairs(table) + if err != nil { + return err + } + if err := sc.Suite.Ledger.Snapshot(dest); err != nil { + return err + } + if err := copyFile(sc.resolvePath(dsl.Interpolate(fixture)), dest); err != nil { + return err + } + return dsl.UpdateYAMLKeys(dest, keys) +} + // iCopyFile copies src to dest, recording dest with the Ledger before // the write so suite teardown can restore. func (sc *ScenarioContext) iCopyFile(src, dest string) error { diff --git a/tests/bdd/steps/steps_test.go b/tests/bdd/steps/steps_test.go index 09c69e7f4..463a82f66 100644 --- a/tests/bdd/steps/steps_test.go +++ b/tests/bdd/steps/steps_test.go @@ -94,6 +94,109 @@ func TestICopyFileSnapshotsAndCopies(t *testing.T) { } } +func TestIPrepareHelmfileEnvironmentCopiesUpdatesAndRestoresAbsentDestination(t *testing.T) { + sc, _ := newScenarioContext(t) + t.Setenv("BDD_TMP_ENV_FIXTURE", "fixtures/base.yaml") + t.Setenv("SAMPLE_NGC_ORG", "test-org") + t.Setenv("SAMPLE_NGC_TEAM", "test-team") + fixtureAbs := filepath.Join(sc.Suite.Config.RepoRoot, "fixtures", "base.yaml") + if err := os.MkdirAll(filepath.Dir(fixtureAbs), 0o755); err != nil { + t.Fatalf("mkdir fixture: %v", err) + } + if err := os.WriteFile(fixtureAbs, []byte("global:\n storageClass: local-path\n"), 0o644); err != nil { + t.Fatalf("seed fixture: %v", err) + } + table := docTable(t, [][]string{ + {"global.imagePullSecrets[0].name", "nvcr-pull-secret"}, + {"global.image.repository", "${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM}"}, + }) + + if err := sc.iPrepareHelmfileEnvironment("local-bdd", "self-managed", "${BDD_TMP_ENV_FIXTURE}", table); err != nil { + t.Fatalf("prepare environment: %v", err) + } + dest := filepath.Join(sc.Suite.Config.RepoRoot, "deploy", "stacks", "self-managed", "environments", "local-bdd.yaml") + got, err := os.ReadFile(dest) + if err != nil { + t.Fatalf("read destination: %v", err) + } + for _, want := range []string{"storageClass: local-path", "name: nvcr-pull-secret", "repository: test-org/test-team"} { + if !strings.Contains(string(got), want) { + t.Fatalf("destination missing %q:\n%s", want, got) + } + } + + if err := sc.Suite.Ledger.RestoreAll(); err != nil { + t.Fatalf("restore: %v", err) + } + if _, err := os.Stat(dest); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("generated destination should be removed: %v", err) + } +} + +func TestIPrepareHelmfileEnvironmentRestoresExistingDestination(t *testing.T) { + sc, _ := newScenarioContext(t) + fixture := "fixtures/base.yaml" + fixtureAbs := filepath.Join(sc.Suite.Config.RepoRoot, fixture) + if err := os.MkdirAll(filepath.Dir(fixtureAbs), 0o755); err != nil { + t.Fatalf("mkdir fixture: %v", err) + } + if err := os.WriteFile(fixtureAbs, []byte("global: {}\n"), 0o644); err != nil { + t.Fatalf("seed fixture: %v", err) + } + dest := filepath.Join(sc.Suite.Config.RepoRoot, "deploy", "stacks", "observability", "environments", "existing.yaml") + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + t.Fatalf("mkdir destination: %v", err) + } + original := []byte("operatorAuthored: true\n") + if err := os.WriteFile(dest, original, 0o640); err != nil { + t.Fatalf("seed destination: %v", err) + } + table := docTable(t, [][]string{{"observability.mode", "install"}}) + + if err := sc.iPrepareHelmfileEnvironment("existing", "observability", fixture, table); err != nil { + t.Fatalf("prepare environment: %v", err) + } + if err := sc.Suite.Ledger.RestoreAll(); err != nil { + t.Fatalf("restore: %v", err) + } + got, err := os.ReadFile(dest) + if err != nil { + t.Fatalf("read restored destination: %v", err) + } + if string(got) != string(original) { + t.Fatalf("restored body = %q, want %q", got, original) + } + info, err := os.Stat(dest) + if err != nil { + t.Fatalf("stat restored destination: %v", err) + } + if info.Mode().Perm() != 0o640 { + t.Fatalf("restored mode = %o, want 640", info.Mode().Perm()) + } +} + +func TestIPrepareHelmfileEnvironmentRejectsInvalidNamesBeforeWriting(t *testing.T) { + sc, _ := newScenarioContext(t) + table := docTable(t, [][]string{{"global.image.registry", "nvcr.io"}}) + for _, tc := range []struct { + name string + environment string + stack string + }{ + {name: "unsupported stack", environment: "local", stack: "unknown"}, + {name: "unsafe environment", environment: "../local", stack: "self-managed"}, + } { + t.Run(tc.name, func(t *testing.T) { + if err := sc.iPrepareHelmfileEnvironment(tc.environment, tc.stack, "missing.yaml", table); err == nil { + t.Fatal("expected validation error") + } + }) + } + if _, err := os.Stat(filepath.Join(sc.Suite.Config.RepoRoot, "deploy")); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("validation failure should not create deploy tree: %v", err) + } +} + func TestIPrepareSelfManagedSecretsFileRendersInterpolatedPaths(t *testing.T) { sc, fake := newScenarioContext(t) t.Setenv("NGC_API_KEY", "test-api-key")