Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions tests/bdd/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ refactor in every consumer; that is a feature.
| `Then the rendered manifests in {string} should contain:` (table) | Requires a `text` header and one or more fixed strings. Recursively inspects regular files under the repo-relative directory and fails if any listed string is absent. `${VAR}` expansion applies to the path and table values. |
| `Then the rendered manifests in {string} under directories matching {string} should contain:` (table) | Positive rendered-manifest assertion scoped to files below a directory whose name matches the supplied shell pattern, such as `*-nats`. The render directory, directory-name pattern, and table values support `${VAR}` expansion. |
| `Then the rendered manifests in {string} should not contain:` (table) | Requires a `text` header and one or more fixed strings. Recursively inspects regular files under the repo-relative directory and fails if any listed string appears. `${VAR}` expansion applies to the path and table values. |
| `Then these Helm releases should be deployed using context {string}:` (table) | Requires `name` and `namespace` headers, with an optional `revision` header. Runs one explicit-context, all-namespaces `helm list` and asserts that every listed release has status `deployed`; non-empty revision cells are also matched. |
| `Then these ServiceMonitors should exist in namespace {string} using context {string}:` (table) | Requires a `name` header and one or more names. Runs one `kubectl get` with every named ServiceMonitor; exit code 0 proves every listed resource exists. |

#### YAML comparison semantics
Expand Down Expand Up @@ -256,8 +257,9 @@ Going away in `tests/bdd`:
`Template`, `HelmfileTemplate`, `RegisterCluster`,
`InstallNvcaOperator`). Replaced by `When I run command "make ..."`.
- The Helm release readback methods (`HelmReleaseDeployed`,
`NVCAOperatorReady`, `NVCAAgentReady`). Replaced by `helm list -o json`
+ `kubectl rollout status` / `kubectl wait` directly in Gherkin.
`NVCAOperatorReady`, `NVCAAgentReady`). Release deployment checks use the
table-driven Helm assertion; readiness remains explicit through
`kubectl rollout status` / `kubectl wait` in Gherkin.
- The `harness.CLIHarness` interface and its five domain methods
(`SelfHostedUp`, `SelfHostedInstallControlPlane`,
`SelfHostedComputePlaneRegister`, `SelfHostedComputePlaneInstall`,
Expand Down Expand Up @@ -460,6 +462,21 @@ func FilesDoNotContain(root string, needles []string) error
// to files below a directory whose name matches the shell pattern.
func FilesContain(root, directoryNamePattern string, needles []string) error

// HelmListCommand builds an explicit-context, all-namespaces Helm list command.
func HelmListCommand(kubeContext string) (string, error)

// HelmReleaseExpectation identifies a deployed Helm release and optionally
// pins the expected revision.
type HelmReleaseExpectation struct {
Name string
Namespace string
Revision string
}

// HelmReleasesDeployed asserts that every expected release exists in Helm's
// JSON output with status deployed and, when provided, the expected revision.
func HelmReleasesDeployed(raw string, expected []HelmReleaseExpectation) error

// ServiceMonitorExistenceCommand builds one kubectl get command whose
// successful exit proves every named ServiceMonitor exists.
func ServiceMonitorExistenceCommand(namespace, kubeContext string, names []string) (string, error)
Expand Down
104 changes: 104 additions & 0 deletions tests/bdd/dsl/helm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
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 (
"encoding/json"
"fmt"
"strings"
)

// HelmReleaseExpectation identifies a deployed Helm release and optionally
// pins the expected revision.
type HelmReleaseExpectation struct {
Name string
Namespace string
Revision string
}

type helmRelease struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Revision any `json:"revision"`
Status string `json:"status"`
}

// HelmListCommand builds an explicit-context, all-namespaces Helm list command.
func HelmListCommand(kubeContext string) (string, error) {
kubeContext = strings.TrimSpace(Interpolate(kubeContext))
if kubeContext == "" {
return "", fmt.Errorf("kube context is empty")
}
return "helm list --all-namespaces --kube-context " + quoteCommandArg(kubeContext) + " -o json", nil
}

// HelmReleasesDeployed asserts that every expected release exists in Helm's
// JSON output with status deployed and, when provided, the expected revision.
func HelmReleasesDeployed(raw string, expected []HelmReleaseExpectation) error {
var actual []helmRelease
if err := json.Unmarshal([]byte(raw), &actual); err != nil {
return fmt.Errorf("parse helm list json: %w", err)
}
if len(expected) == 0 {
return fmt.Errorf("expected Helm releases are empty")
}

for _, rawExpectation := range expected {
expectation := HelmReleaseExpectation{
Name: strings.TrimSpace(Interpolate(rawExpectation.Name)),
Namespace: strings.TrimSpace(Interpolate(rawExpectation.Namespace)),
Revision: strings.TrimSpace(Interpolate(rawExpectation.Revision)),
}
if expectation.Name == "" {
return fmt.Errorf("helm release name is empty")
}
if expectation.Namespace == "" {
return fmt.Errorf("helm release %q namespace is empty", expectation.Name)
}

release, found := findHelmRelease(actual, expectation.Name, expectation.Namespace)
if !found {
return describeMissingHelmRelease(actual, expectation)
}
if release.Status != "deployed" {
return fmt.Errorf("helm release %q in namespace %q status = %q, want %q", expectation.Name, expectation.Namespace, release.Status, "deployed")
}
if expectation.Revision != "" && fmt.Sprint(release.Revision) != expectation.Revision {
return fmt.Errorf("helm release %q in namespace %q revision = %q, want %q", expectation.Name, expectation.Namespace, fmt.Sprint(release.Revision), expectation.Revision)
}
}
return nil
}

func findHelmRelease(releases []helmRelease, name, namespace string) (helmRelease, bool) {
for _, release := range releases {
if release.Name == name && release.Namespace == namespace {
return release, true
}
}
return helmRelease{}, false
}

func describeMissingHelmRelease(releases []helmRelease, expected HelmReleaseExpectation) error {
for _, release := range releases {
if release.Name == expected.Name {
return fmt.Errorf("helm release %q namespace = %q, want %q", expected.Name, release.Namespace, expected.Namespace)
}
}
return fmt.Errorf("helm release %q in namespace %q is missing", expected.Name, expected.Namespace)
}
92 changes: 92 additions & 0 deletions tests/bdd/dsl/helm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
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 (
"strings"
"testing"
)

const deployedHelmReleases = `[
{"name":"nats","namespace":"nats-system","revision":"2","status":"deployed"},
{"name":"api","namespace":"nvcf","revision":3,"status":"failed"}
]`

func TestHelmListCommandUsesExplicitContext(t *testing.T) {
t.Setenv("BDD_TMP_CONTEXT", "k3d-ncp-local")
got, err := HelmListCommand("${BDD_TMP_CONTEXT}")
if err != nil {
t.Fatalf("build command: %v", err)
}
want := "helm list --all-namespaces --kube-context k3d-ncp-local -o json"
if got != want {
t.Fatalf("command = %q, want %q", got, want)
}
}

func TestHelmListCommandRejectsEmptyContext(t *testing.T) {
if _, err := HelmListCommand(""); err == nil {
t.Fatal("expected empty context error")
}
}

func TestHelmReleasesDeployedMatchesOptionalRevision(t *testing.T) {
expected := []HelmReleaseExpectation{{Name: "nats", Namespace: "nats-system", Revision: "2"}}
if err := HelmReleasesDeployed(deployedHelmReleases, expected); err != nil {
t.Fatalf("assert releases: %v", err)
}

expected[0].Revision = ""
if err := HelmReleasesDeployed(deployedHelmReleases, expected); err != nil {
t.Fatalf("assert release without revision: %v", err)
}
}

func TestHelmReleasesDeployedReportsMissingRelease(t *testing.T) {
err := HelmReleasesDeployed(deployedHelmReleases, []HelmReleaseExpectation{{Name: "missing", Namespace: "nvcf"}})
if err == nil || !strings.Contains(err.Error(), `helm release "missing" in namespace "nvcf" is missing`) {
t.Fatalf("error = %v", err)
}
}

func TestHelmReleasesDeployedReportsNamespaceMismatch(t *testing.T) {
err := HelmReleasesDeployed(deployedHelmReleases, []HelmReleaseExpectation{{Name: "nats", Namespace: "wrong"}})
if err == nil || !strings.Contains(err.Error(), `namespace = "nats-system", want "wrong"`) {
t.Fatalf("error = %v", err)
}
}

func TestHelmReleasesDeployedReportsStatusMismatch(t *testing.T) {
err := HelmReleasesDeployed(deployedHelmReleases, []HelmReleaseExpectation{{Name: "api", Namespace: "nvcf"}})
if err == nil || !strings.Contains(err.Error(), `status = "failed", want "deployed"`) {
t.Fatalf("error = %v", err)
}
}

func TestHelmReleasesDeployedReportsRevisionMismatch(t *testing.T) {
err := HelmReleasesDeployed(deployedHelmReleases, []HelmReleaseExpectation{{Name: "nats", Namespace: "nats-system", Revision: "1"}})
if err == nil || !strings.Contains(err.Error(), `revision = "2", want "1"`) {
t.Fatalf("error = %v", err)
}
}

func TestHelmReleasesDeployedRejectsMalformedJSON(t *testing.T) {
if err := HelmReleasesDeployed("not json", []HelmReleaseExpectation{{Name: "nats", Namespace: "nats-system"}}); err == nil {
t.Fatal("expected parse error")
}
}
44 changes: 21 additions & 23 deletions tests/bdd/features/multi-cluster-eks-helmfile.feature
Original file line number Diff line number Diff line change
Expand Up @@ -199,25 +199,24 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust
When I run command "make -C deploy/stacks/self-managed install HELMFILE_ENV=eks-bdd-multi"
Then the command exit code should be 0

When I run command "helm list --all-namespaces --kube-context ${EKS_CONTEXT} -o json"
Then the json output should contain rows:
| name | namespace | status |
| nats | nats-system | deployed |
| cert-manager | cert-manager | deployed |
| openbao-server | vault-system | deployed |
| cassandra | cassandra-system | deployed |
| api-keys | api-keys | deployed |
| sis | sis | deployed |
| api | nvcf | deployed |
| nvct-api | nvcf | deployed |
| invocation-service | nvcf | deployed |
| grpc-proxy | nvcf | deployed |
| ess-api | ess | deployed |
| notary-service | nvcf | deployed |
| admin-issuer-proxy | api-keys | deployed |
| reval | nvcf | deployed |
| nats-auth-callout-service | nats-system | deployed |
| ingress | envoy-gateway-system | deployed |
Then these Helm releases should be deployed using context "${EKS_CONTEXT}":
| name | namespace |
| nats | nats-system |
| cert-manager | cert-manager |
| openbao-server | vault-system |
| cassandra | cassandra-system |
| api-keys | api-keys |
| sis | sis |
| api | nvcf |
| nvct-api | nvcf |
| invocation-service | nvcf |
| grpc-proxy | nvcf |
| ess-api | ess |
| notary-service | nvcf |
| admin-issuer-proxy | api-keys |
| reval | nvcf |
| nats-auth-callout-service | nats-system |
| ingress | envoy-gateway-system |

# Confirm gateway-routes templated global.domain into the api
# HTTPRoute hostname on the control-plane cluster.
Expand Down Expand Up @@ -339,10 +338,9 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust
"""
Then the command exit code should be 0

When I run command "helm list -n nvca-operator --kube-context ${EKS_COMPUTE_CONTEXT} -o json"
Then the json output should contain rows:
| name | namespace | status |
| nvca-operator | nvca-operator | deployed |
Then these Helm releases should be deployed using context "${EKS_COMPUTE_CONTEXT}":
| name | namespace |
| nvca-operator | nvca-operator |

When I run command "kubectl rollout status deployment/nvca-operator -n nvca-operator --context ${EKS_COMPUTE_CONTEXT} --timeout=10m"
Then the command exit code should be 0
Expand Down
56 changes: 28 additions & 28 deletions tests/bdd/features/multi-cluster-helmfile.feature
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ Feature: Install a local multi-cluster NVCF stack with Helmfile
| 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:
| 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} |
| 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 "deploy/stacks/self-managed/secrets/secrets.yaml.template" to "deploy/stacks/self-managed/secrets/local-bdd-secrets.yaml"
And I substitute "REPLACE_WITH_BASE64_DOCKER_CREDENTIAL" in file "deploy/stacks/self-managed/secrets/local-bdd-secrets.yaml" with base64 of "$oauthtoken:${NGC_API_KEY}"
# Conflict precheck: single-cluster ncp-local's k3d serverlb
Expand Down Expand Up @@ -84,27 +86,26 @@ Feature: Install a local multi-cluster NVCF stack with Helmfile
When I run command "make -C deploy/stacks/self-managed install HELMFILE_ENV=local-bdd"
Then the command exit code should be 0

When I run command "helm list --all-namespaces --kube-context k3d-ncp-local-cp -o json"
Then the json output should contain rows:
| name | namespace | status |
| nats | nats-system | deployed |
| cert-manager | cert-manager | deployed |
| openbao-server | vault-system | deployed |
| cassandra | cassandra-system | deployed |
| api-keys | api-keys | deployed |
| sis | sis | deployed |
| api | nvcf | deployed |
| nvct-api | nvcf | deployed |
| invocation-service | nvcf | deployed |
| grpc-proxy | nvcf | deployed |
| ess-api | ess | deployed |
| notary-service | nvcf | deployed |
| admin-issuer-proxy | api-keys | deployed |
| reval | nvcf | deployed |
| nats-auth-callout-service | nats-system | deployed |
| ingress | envoy-gateway-system | deployed |
| llm-request-router | nvcf | deployed |
| llm-api-gateway | nvcf | deployed |
Then these Helm releases should be deployed using context "k3d-ncp-local-cp":
| name | namespace |
| nats | nats-system |
| cert-manager | cert-manager |
| openbao-server | vault-system |
| cassandra | cassandra-system |
| api-keys | api-keys |
| sis | sis |
| api | nvcf |
| nvct-api | nvcf |
| invocation-service | nvcf |
| grpc-proxy | nvcf |
| ess-api | ess |
| notary-service | nvcf |
| admin-issuer-proxy | api-keys |
| reval | nvcf |
| nats-auth-callout-service | nats-system |
| ingress | envoy-gateway-system |
| llm-request-router | nvcf |
| llm-api-gateway | nvcf |

# These routes are installed by ncp-local before the Helmfile
# stack, then become fully resolved once the control-plane
Expand Down Expand Up @@ -213,10 +214,9 @@ Feature: Install a local multi-cluster NVCF stack with Helmfile
"""
Then the command exit code should be 0

When I run command "helm list -n nvca-operator --kube-context k3d-ncp-local-compute-1 -o json"
Then the json output should contain rows:
| name | namespace | status |
| nvca-operator | nvca-operator | deployed |
Then these Helm releases should be deployed using context "k3d-ncp-local-compute-1":
| name | namespace |
| nvca-operator | nvca-operator |

When I run command "kubectl rollout status deployment/nvca-operator -n nvca-operator --context k3d-ncp-local-compute-1 --timeout=10m"
Then the command exit code should be 0
Expand Down
Loading
Loading