Skip to content
Open
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
1 change: 1 addition & 0 deletions tests/bdd/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ original order. Repeated options and empty values are preserved.
| `Then deployment {string} in namespace {string} using context {string} should complete rollout within {string}` | Runs `kubectl rollout status` for the named deployment with the explicit namespace, context, and timeout. Failure messages name the deployment without printing command output. |
| `Then NVCFBackend {string} in namespace {string} using context {string} should report agent status {string} within {string}` | Waits for the named backend's `status.agentStatus` to equal the visible value using the explicit namespace, context, and timeout. Failure messages name the backend without printing resource output. |
| `Then these Gateway API routes should be accepted and resolved using context {string} within {string}:` (table) | Requires `kind`, `name`, `namespace`, and `parent` headers. Waits for every named route to report both `Accepted=True` and `ResolvedRefs=True` for the named Gateway parent using the explicit context and timeout. The route kind is passed through without an allowlist. Failures name the table row, route, namespace, parent, and unmet condition without printing resource output. |
| `Then DNS name {string} should resolve within {string} seconds` | Waits for the explicit DNS name to resolve through the host resolver within the explicit timeout. `${VAR}` interpolation applies to the name and timeout. Resolution must remain successful for three consecutive checks. Failures report the unresolved name and timeout without printing resolver output. |

#### YAML comparison semantics

Expand Down
51 changes: 51 additions & 0 deletions tests/bdd/dsl/dns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
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"
"strconv"
"strings"
)

const waitForDNSScript = "tests/bdd/scripts/wait-for-dns.sh"

// DNSResolutionCommand builds the existing host-resolver polling command after
// resolving interpolation and validating its visible inputs.
func DNSResolutionCommand(hostname, timeout string) (string, error) {
hostname = strings.TrimSpace(Interpolate(hostname))
timeout = strings.TrimSpace(Interpolate(timeout))
if hostname == "" {
return "", fmt.Errorf("DNS name is empty")
}
if timeout == "" {
return "", fmt.Errorf("DNS resolution timeout is empty")
}
for _, char := range timeout {
if char < '0' || char > '9' {
return "", fmt.Errorf("DNS resolution timeout %q is not a non-negative integer", timeout)
}
}
timeoutSeconds, err := strconv.ParseInt(timeout, 10, 64)
if err != nil {
return "", fmt.Errorf("DNS resolution timeout %q is invalid: %w", timeout, err)
}
timeout = strconv.FormatInt(timeoutSeconds, 10)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return BuildCommand(waitForDNSScript, hostname, timeout), nil
}
84 changes: 84 additions & 0 deletions tests/bdd/dsl/dns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
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"
)

func TestDNSResolutionCommandInterpolatesAndBuildsScriptCommand(t *testing.T) {
t.Setenv("BDD_DNS_NAME", "api.192-0-2-10.nip.io")
t.Setenv("BDD_DNS_TIMEOUT", "180")

got, err := DNSResolutionCommand(" ${BDD_DNS_NAME} ", " ${BDD_DNS_TIMEOUT} ")
if err != nil {
t.Fatalf("build DNS resolution command: %v", err)
}
want := "tests/bdd/scripts/wait-for-dns.sh api.192-0-2-10.nip.io 180"
if got != want {
t.Fatalf("command = %q, want %q", got, want)
}
}

func TestDNSResolutionCommandNormalizesAndQuotesArguments(t *testing.T) {
got, err := DNSResolutionCommand("gateway name", "0180")
if err != nil {
t.Fatalf("build DNS resolution command: %v", err)
}
want := "tests/bdd/scripts/wait-for-dns.sh 'gateway name' 180"
if got != want {
t.Fatalf("command = %q, want %q", got, want)
}
}

func TestDNSResolutionCommandRejectsInvalidInputs(t *testing.T) {
tests := []struct {
name string
hostname string
timeout string
want string
}{
{name: "empty hostname", hostname: " ", timeout: "180", want: "DNS name is empty"},
{name: "missing hostname variable", hostname: "${BDD_DNS_NAME_MISSING}", timeout: "180", want: "DNS name is empty"},
{name: "empty timeout", hostname: "gateway.example.com", timeout: " ", want: "timeout is empty"},
{name: "negative timeout", hostname: "gateway.example.com", timeout: "-1", want: "not a non-negative integer"},
{name: "duration timeout", hostname: "gateway.example.com", timeout: "3m", want: "not a non-negative integer"},
{name: "overflowing timeout", hostname: "gateway.example.com", timeout: "9223372036854775808", want: "timeout \"9223372036854775808\" is invalid"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := DNSResolutionCommand(test.hostname, test.timeout)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error = %v, want substring %q", err, test.want)
}
})
}
}

func TestDNSResolutionCommandAllowsImmediateTimeout(t *testing.T) {
got, err := DNSResolutionCommand("gateway.example.com", "0")
if err != nil {
t.Fatalf("build DNS resolution command: %v", err)
}
want := "tests/bdd/scripts/wait-for-dns.sh gateway.example.com 0"
if got != want {
t.Fatalf("command = %q, want %q", got, want)
}
}
12 changes: 4 additions & 8 deletions tests/bdd/features/multi-cluster-eks-helmfile.feature
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,15 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust

# 5. Wait for the ELB hostname to be resolvable from the host's
# DNS resolver before installing.
When I run command "tests/bdd/scripts/wait-for-dns.sh ${EKS_GATEWAY_ADDR} 180"
Then the command exit code should be 0
Then DNS name "${EKS_GATEWAY_ADDR}" should resolve within "180" seconds

# Route hostnames such as api.<domain> must resolve independently. Derive
# a nip.io wildcard domain from one NLB address so worker pods on the
# compute cluster can resolve every control-plane route hostname.
When I run command "tests/bdd/scripts/resolve-gateway-domain.sh ${EKS_GATEWAY_ADDR}"
Then the command exit code should be 0
When I export command output to environment variable "EKS_GATEWAY_DOMAIN"
When I run command "tests/bdd/scripts/wait-for-dns.sh api.${EKS_GATEWAY_DOMAIN} 180"
Then the command exit code should be 0
Then DNS name "api.${EKS_GATEWAY_DOMAIN}" should resolve within "180" seconds

# 6. Copy base.yaml -> eks-bdd-multi.yaml and patch with the EKS
# knobs, including the resolvable Gateway domain.
Expand Down Expand Up @@ -316,8 +314,7 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust

# Register the compute cluster and write the returned Helm values under
# registration/.
When I run command "tests/bdd/scripts/wait-for-dns.sh ${EKS_GATEWAY_ADDR} 180"
Then the command exit code should be 0
Then DNS name "${EKS_GATEWAY_ADDR}" should resolve within "180" seconds

When I run command:
"""
Expand Down Expand Up @@ -406,8 +403,7 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust
# AWS can briefly return NXDOMAIN for a newly provisioned ELB even after
# earlier successful lookups. Reconfirm system-resolver stability before
# the CLI performs its function-details lookup and invocation.
When I run command "tests/bdd/scripts/wait-for-dns.sh ${EKS_GATEWAY_ADDR} 180"
Then the command exit code should be 0
Then DNS name "${EKS_GATEWAY_ADDR}" should resolve within "180" seconds

When I successfully invoke the function selected by NVCF CLI over HTTP with timeout "120" seconds and poll duration "5" seconds:
"""
Expand Down
3 changes: 1 addition & 2 deletions tests/bdd/features/single-cluster-eks-helmfile.feature
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ Feature: Install a single-cluster NVCF stack on a pre-provisioned EKS cluster wi
# DNS resolver. AWS DNS propagation typically lags ~30-90s
# behind NLB programming; without this wait, subsequent
# in-pod connections can fail intermittently.
When I run command "tests/bdd/scripts/wait-for-dns.sh ${EKS_GATEWAY_ADDR} 180"
Then the command exit code should be 0
Then DNS name "${EKS_GATEWAY_ADDR}" should resolve within "180" seconds

# 6. Copy base.yaml -> eks-bdd.yaml and patch with the EKS
# knobs (including global.domain from the just-exported
Expand Down
41 changes: 26 additions & 15 deletions tests/bdd/fixtures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,6 @@ printf '%s has address 192.0.2.10\n' "$1"

func TestWaitForDNSRequiresStableSystemResolution(t *testing.T) {
binDir := t.TempDir()
countPath := filepath.Join(binDir, "resolver-count")
resolverScript := `#!/usr/bin/env bash
set -euo pipefail
count=0
Expand All @@ -410,21 +409,33 @@ fi
t.Fatalf("write fake sleep: %v", err)
}

cmd := exec.Command("bash", "scripts/wait-for-dns.sh", "gateway.example.invalid", "30")
cmd.Env = append(os.Environ(), "FAKE_RESOLVER_COUNT="+countPath, "PATH="+binDir+":"+os.Getenv("PATH"))
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("wait for stable DNS: %v\n%s", err, out)
tests := []struct {
name string
timeout string
}{
{name: "normal timeout", timeout: "30"},
{name: "maximum int64 timeout", timeout: "9223372036854775807"},
}
if got := string(out); !strings.Contains(got, "3 consecutive system-resolver checks after 5 attempts") {
t.Fatalf("wait output did not report stable resolution: %q", got)
}
count, err := os.ReadFile(countPath)
if err != nil {
t.Fatalf("read resolver attempt count: %v", err)
}
if got, want := strings.TrimSpace(string(count)), "5"; got != want {
t.Fatalf("resolver attempts = %s, want %s", got, want)
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
countPath := filepath.Join(t.TempDir(), "resolver-count")
cmd := exec.Command("bash", "scripts/wait-for-dns.sh", "gateway.example.invalid", tc.timeout)
cmd.Env = append(os.Environ(), "FAKE_RESOLVER_COUNT="+countPath, "PATH="+binDir+":"+os.Getenv("PATH"))
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("wait for stable DNS: %v\n%s", err, out)
}
if got := string(out); !strings.Contains(got, "3 consecutive system-resolver checks after 5 attempts") {
t.Fatalf("wait output did not report stable resolution: %q", got)
}
count, err := os.ReadFile(countPath)
if err != nil {
t.Fatalf("read resolver attempt count: %v", err)
}
if got, want := strings.TrimSpace(string(count)), "5"; got != want {
t.Fatalf("resolver attempts = %s, want %s", got, want)
}
})
}
}

Expand Down
6 changes: 4 additions & 2 deletions tests/bdd/scripts/wait-for-dns.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ if ! command -v python3 >/dev/null 2>&1; then
exit 69
fi

deadline=$(( $(date +%s) + TIMEOUT_SECONDS ))
start_time=$(date +%s)
attempt=0
consecutive_successes=0
while [[ "$consecutive_successes" -lt 3 ]]; do
Expand All @@ -60,7 +60,9 @@ PY
if [[ "$consecutive_successes" -ge 3 ]]; then
break
fi
if [[ $(date +%s) -ge "$deadline" ]]; then
current_time=$(date +%s)
elapsed_seconds=$(( current_time - start_time ))
if (( elapsed_seconds >= TIMEOUT_SECONDS )); then
echo "timed out after ${TIMEOUT_SECONDS}s waiting for DNS for $HOSTNAME (attempts=$attempt)" >&2
exit 2
fi
Expand Down
17 changes: 17 additions & 0 deletions tests/bdd/steps/assertion_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func registerAssertionSteps(ctx *godog.ScenarioContext, sc *ScenarioContext) {
ctx.Step(`^these Kubernetes resources should not exist in namespace "([^"]*)" using context "([^"]*)":$`, sc.kubernetesResourcesShouldNotExist)
ctx.Step(`^Kubernetes resource "([^"/]+)/([^"]+)" in namespace "([^"]*)" using context "([^"]*)" should contain:$`, sc.kubernetesResourceShouldContain)
ctx.Step(`^deployment "([^"]*)" in namespace "([^"]*)" using context "([^"]*)" should complete rollout within "([^"]*)"$`, sc.deploymentShouldCompleteRollout)
ctx.Step(`^DNS name "([^"]*)" should resolve within "([^"]*)" seconds$`, sc.dnsNameShouldResolve)
ctx.Step(`^NVCFBackend "([^"]*)" in namespace "([^"]*)" using context "([^"]*)" should report agent status "([^"]*)" within "([^"]*)"$`, sc.nvcfBackendShouldReportAgentStatus)
ctx.Step(`^these Gateway API routes should be accepted and resolved using context "([^"]*)" within "([^"]*)":$`, sc.gatewayAPIRoutesShouldBeAcceptedAndResolved)
}
Expand Down Expand Up @@ -359,6 +360,22 @@ func (sc *ScenarioContext) deploymentShouldCompleteRollout(ctx context.Context,
return nil
}

func (sc *ScenarioContext) dnsNameShouldResolve(ctx context.Context, hostname, timeout string) error {
command, err := dsl.DNSResolutionCommand(hostname, timeout)
if err != nil {
return err
}
if err := sc.runResolvedSuccessfully(ctx, command); err != nil {
return fmt.Errorf(
"DNS name %q did not resolve within %s seconds: %w",
strings.TrimSpace(dsl.Interpolate(hostname)),
strings.TrimSpace(dsl.Interpolate(timeout)),
err,
)
}
return nil
}

func (sc *ScenarioContext) nvcfBackendShouldReportAgentStatus(ctx context.Context, name, namespace, kubeContext, agentStatus, timeout string) error {
command, err := dsl.NVCFBackendAgentStatusCommand(name, namespace, kubeContext, agentStatus, timeout)
if err != nil {
Expand Down
69 changes: 69 additions & 0 deletions tests/bdd/steps/steps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,75 @@ func TestDeploymentShouldCompleteRolloutRunsExplicitWait(t *testing.T) {
}
}

func TestDNSNameShouldResolveRunsExplicitWait(t *testing.T) {
sc, fake := newScenarioContext(t)
fake.result = harness.Result{ExitCode: 0}
t.Setenv("BDD_DNS_NAME", "api.192-0-2-10.nip.io")
t.Setenv("BDD_DNS_TIMEOUT", "180")

if err := sc.dnsNameShouldResolve(context.Background(), "${BDD_DNS_NAME}", "${BDD_DNS_TIMEOUT}"); err != nil {
t.Fatalf("wait for DNS resolution: %v", err)
}
want := "tests/bdd/scripts/wait-for-dns.sh api.192-0-2-10.nip.io 180"
if len(fake.runs) != 1 || fake.runs[0].command != want {
t.Fatalf("runs = %#v, want %q", fake.runs, want)
}
}

func TestDNSNameShouldResolveRejectsInvalidInputsBeforeRunning(t *testing.T) {
tests := []struct {
name string
hostname string
timeout string
want string
}{
{
name: "empty hostname",
hostname: " ",
timeout: "180",
want: "DNS name is empty",
},
{
name: "invalid timeout",
hostname: "gateway.example.com",
timeout: "-1",
want: "not a non-negative integer",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
sc, fake := newScenarioContext(t)
err := sc.dnsNameShouldResolve(context.Background(), tc.hostname, tc.timeout)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("err = %v, want error containing %q", err, tc.want)
}
if len(fake.runs) != 0 {
t.Fatalf("runs = %d, want 0", len(fake.runs))
}
})
}
}

func TestDNSNameShouldResolveFailureNamesTargetWithoutResolverOutput(t *testing.T) {
sc, fake := newScenarioContext(t)
secretOutput := "unrelated-resolver-output"
fake.result = harness.Result{ExitCode: 2, Stdout: secretOutput, Stderr: secretOutput}

err := sc.dnsNameShouldResolve(context.Background(), "gateway.example.com", "180")
if err == nil {
t.Fatal("expected DNS resolution failure")
}
for _, want := range []string{`DNS name "gateway.example.com"`, "within 180 seconds"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("error = %q, want %q", err, want)
}
}
if strings.Contains(err.Error(), secretOutput) {
t.Fatalf("error leaked resolver output: %v", err)
}
}

func TestKubernetesResourceShouldContainFailureDoesNotExposeResourceValues(t *testing.T) {
sc, fake := newScenarioContext(t)
fake.result = harness.Result{ExitCode: 0, Stdout: `data:
Expand Down