Skip to content

fix: Phase 0 — Critical bug fixes + provisioner resilience + E2E tests#622

Merged
ArangoGutierrez merged 1 commit into
NVIDIA:mainfrom
ArangoGutierrez:fix/phase0-critical-bugs
Feb 9, 2026
Merged

fix: Phase 0 — Critical bug fixes + provisioner resilience + E2E tests#622
ArangoGutierrez merged 1 commit into
NVIDIA:mainfrom
ArangoGutierrez:fix/phase0-critical-bugs

Conversation

@ArangoGutierrez

Copy link
Copy Markdown
Collaborator

Summary

Phase 0 of the 360-degree codebase evaluation. Fixes critical bugs and hardens provisioning infrastructure.

Critical Bug Fixes

  • storageSizeGB global mutation — Use local variable to eliminate data race and persistent state corruption
  • log.Fatalf in goroutine — Replace with error propagation to prevent process crash during provisioning
  • File permission race — Remove redundant os.Create(0666) before os.WriteFile(0600) in status.go
  • Kubeconfig exposure — Use os.OpenFile with 0600 instead of os.Create(0666)

Provisioner Resilience

  • install_packages_with_retry — Retry on all failures (not just NO_PUBKEY), refresh apt cache, fix broken dpkg state between retries
  • NVIDIA driver — Update cuda-keyring to 1.1-1, add architecture detection (amd64/arm64), add 4-stage DKMS fallback chain (cuda-drivers → ubuntu-drivers → nvidia-driver-open → nvidia-headless-server)
  • Container toolkit — Fix duplicate Execute() bug (template ran twice), add retry wrappers
  • Docker template — Add with_retry for apt-get update calls
  • Containerd template — Default to latest version (matching docker pattern), handle "latest" in template
  • AWS delete — Increase instance termination waiter from 5min to 10min

E2E Test Suite (NEW)

  • Ginkgo/Gomega-based E2E tests against real AWS infrastructure (g4dn.xlarge)
  • Two test entries: containerd + kubeadm (K8s v1.35.0) and docker + NVIDIA runtime
  • Full lifecycle: create VPC/subnet/SG → launch EC2 → SSH provisioning → K8s validation → cleanup

Test Plan

  • go vet ./... passes
  • Unit tests pass
  • E2E containerd+kubeadm+K8s v1.35.0: PASS (660s)
  • E2E docker+NVIDIA runtime: PASS (649s)
  • Commits signed with -s -S (DCO + SSH)

Made with Cursor

Copilot AI review requested due to automatic review settings February 7, 2026 16:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Phase 0 hardening pass: fixes a few critical provisioning/AWS bugs, improves apt/package/driver installation resilience, and introduces a real AWS-backed E2E suite.

Changes:

  • Add Ginkgo/Gomega AWS E2E tests + test environment fixtures.
  • Improve provisioner robustness (retry logic, driver install fallback chain, fix duplicate template execution, safer SSH output handling).
  • Tighten file permissions for cached state and kubeconfig output; address AWS storage size mutation and extend termination waiter.

Reviewed changes

Copilot reviewed 17 out of 632 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/e2e_test.go Adds Ginkgo suite bootstrap + env initialization for E2E runs
tests/aws_test.go Implements real AWS lifecycle E2E coverage (create → provision → validate → cleanup)
tests/common/common.go Adds helper for generating unique IDs for E2E resource names
tests/data/test_aws.yml E2E environment fixture for containerd + kubeadm + K8s
tests/data/test_aws_docker.yml E2E environment fixture for Docker + NVIDIA runtime stack
pkg/provisioner/templates/nv-driver.go Adds arch detection + multi-stage driver fallback strategy
pkg/provisioner/templates/docker.go Wraps apt-get update with retry
pkg/provisioner/templates/containerd.go Aligns to “latest” default behavior + improves install steps
pkg/provisioner/templates/container-toolkit.go Adds retry install + fixes duplicate template execution
pkg/provisioner/templates/common.go Makes apt installs resilient via broader retries + dpkg repair steps
pkg/provisioner/provisioner.go Replaces log.Fatalf in goroutine with error propagation + sync
pkg/provider/aws/status.go Removes redundant file creation; tightens cache file permissions
pkg/provider/aws/delete.go Extends EC2 termination wait timeout
pkg/provider/aws/create.go Removes global storageSizeGB pointer mutation by using local variable
cmd/cli/create/create.go Writes kubeconfig with 0600 permissions
cmd/action/ci/entrypoint.go Writes kubeconfig with 0600 permissions
go.mod Updates Go version + adds E2E test dependencies

Comment thread tests/e2e_test.go
Comment on lines +36 to +41
func TestMain(t *testing.T) {
suiteName := "E2E Holodeck"

RegisterFailHandler(Fail)
RunSpecs(t, suiteName)
}

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestMain has a special meaning in Go (func TestMain(m *testing.M)), and using that name for a normal test function is confusing. Consider renaming this to something like TestE2E/TestE2ESuite to avoid ambiguity and make intent clearer.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e3a4ec1

Comment thread tests/aws_test.go
GinkgoWriter.Println("=== Starting test:", config.name, "===")

uniqueID := common.GenerateUID()
artifactDir := filepath.Join(LogArtifactDir, config.name+"-"+uniqueID)

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If LOG_ARTIFACT_DIR is unset/empty, filepath.Join("", ...) will create artifacts relative to the repo working directory, which can dirty the workspace in CI/dev runs. Consider defaulting LogArtifactDir to a temp dir (e.g., os.MkdirTemp("", "holodeck-e2e-*")) when the env var is empty, and cleaning it up on success.

Suggested change
artifactDir := filepath.Join(LogArtifactDir, config.name+"-"+uniqueID)
logArtifactDir := LogArtifactDir
if logArtifactDir == "" {
tmpDir, err := os.MkdirTemp("", "holodeck-e2e-*")
Expect(err).NotTo(HaveOccurred(), "Failed to create temporary artifact directory")
logArtifactDir = tmpDir
defer func() {
// only clean up temporary artifacts on successful test runs
if CurrentSpecReport().Failed() {
return
}
err := os.RemoveAll(tmpDir)
Expect(err).NotTo(HaveOccurred(), "Failed to clean up temporary artifact directory")
}()
}
artifactDir := filepath.Join(logArtifactDir, config.name+"-"+uniqueID)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In CI, LOG_ARTIFACT_DIR is always set. For local runs, the relative path behavior is intentional — it keeps artifacts visible for debugging. Adding temp dir logic would hide artifacts by default, making local E2E debugging harder.

Comment thread tests/common/common.go
Comment on lines +22 to +31
"log"
)

func GenerateUID() string {
b := make([]byte, 4)
_, err := rand.Read(b)
if err != nil {
log.Fatalf("failed to generate secure random UID: %v", err)
}
return hex.EncodeToString(b)

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling log.Fatalf in a helper makes failures unrecoverable and can skip Ginkgo cleanup (leaking AWS resources). Prefer returning (string, error) and failing the spec at the call site (e.g., with Expect(err).NotTo(HaveOccurred())), so defers/cleanup can still run.

Suggested change
"log"
)
func GenerateUID() string {
b := make([]byte, 4)
_, err := rand.Read(b)
if err != nil {
log.Fatalf("failed to generate secure random UID: %v", err)
}
return hex.EncodeToString(b)
"fmt"
)
func GenerateUID() (string, error) {
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("failed to generate secure random UID: %w", err)
}
return hex.EncodeToString(b), nil

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e3a4ec1

Comment thread tests/data/test_aws.yml Outdated
type: g4dn.xlarge
region: us-west-1
ingressIpRanges:
- "0.0.0.0/0"

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opening ingress to 0.0.0.0/0 in E2E fixtures is risky for real AWS runs (exposes SSH to the internet). Consider restricting this to the CI runner’s public IP (passed via env/CI metadata) or making the allowed CIDR configurable with a safe default.

Suggested change
- "0.0.0.0/0"
- "203.0.113.0/24"

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are ephemeral E2E security groups created and destroyed within a single test run (~10 min). The instances use short-lived, scoped credentials in isolated test accounts. Restricting to runner IP adds complexity (runner IP detection varies by CI provider) for minimal security gain given the short-lived nature. Added a comment documenting this rationale in e3a4ec1.

Comment thread pkg/provider/aws/status.go Outdated
}

return nil
return os.WriteFile(cachePath, data, 0600)

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.WriteFile(..., 0600) only applies the permission bits on file creation; if the cache file already exists with broader permissions from previous runs, its mode may remain unchanged. Consider explicitly enforcing file mode after writing (e.g., os.Chmod(cachePath, 0600)) or writing to a temp file with 0600 and atomically renaming it.

Suggested change
return os.WriteFile(cachePath, data, 0600)
if err := os.WriteFile(cachePath, data, 0600); err != nil {
return err
}
return os.Chmod(cachePath, 0600)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e3a4ec1

Comment thread pkg/provider/aws/delete.go Outdated
Comment on lines 68 to 71
}, 10*time.Minute, waiterOptions...); err != nil {
a.fail()
return fmt.Errorf("error waiting for instance to be terminated: %v", err)
}

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns a newly formatted error but drops the original error for callers that want to unwrap/inspect it. Prefer wrapping with %w (and keep error strings consistent with the rest of the PR’s failed to ...: %w style).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e3a4ec1

Comment thread cmd/cli/create/create.go Outdated
Comment on lines 243 to 246
localFile, err := os.OpenFile(opts.kubeconfig, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("error creating local file: %v", err)
}

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operation is no longer strictly “creating” the file (it may also truncate an existing file). Consider updating the message to reflect open/write intent (and wrap with %w for consistency).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e3a4ec1

ArangoGutierrez added a commit to ArangoGutierrez/holodeck that referenced this pull request Feb 7, 2026
- Rename TestMain to TestE2E to avoid Go testing convention conflict
- Change GenerateUID to return (string, error) with 16 bytes entropy,
  replacing log.Fatalf that bypassed Ginkgo cleanup
- Add os.Chmod after os.WriteFile for cache file permission enforcement
- Replace %v with %w in error wrapping for proper error chain propagation
- Update error message in create.go to reflect OpenFile intent
- Add documentation comments to E2E test fixtures explaining 0.0.0.0/0
  ingress rationale (ephemeral security groups, isolated test accounts)

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ArangoGutierrez
ArangoGutierrez force-pushed the fix/phase0-critical-bugs branch from e3a4ec1 to cd6f624 Compare February 9, 2026 13:11
@coveralls

coveralls commented Feb 9, 2026

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 21830413696

Details

  • 5 of 27 (18.52%) changed or added relevant lines in 3 files are covered.
  • 5 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.2%) to 46.362%

Changes Missing Coverage Covered Lines Changed/Added Lines %
pkg/provider/aws/create.go 2 3 66.67%
pkg/provider/aws/status.go 3 4 75.0%
pkg/provisioner/provisioner.go 0 20 0.0%
Files with Coverage Reduction New Missed Lines %
pkg/provider/aws/status.go 2 71.89%
pkg/provisioner/provisioner.go 3 2.32%
Totals Coverage Status
Change from base Build 21756354230: -0.2%
Covered Lines: 2313
Relevant Lines: 4989

💛 - Coveralls

@ArangoGutierrez
ArangoGutierrez enabled auto-merge (squash) February 9, 2026 13:29
@ArangoGutierrez
ArangoGutierrez force-pushed the fix/phase0-critical-bugs branch 3 times, most recently from 964f55d to 9dc7a41 Compare February 9, 2026 14:34
…e permissions

- Use local variable for storageSizeGB instead of taking address of
  package-level var, preventing data race and pointer aliasing with AWS SDK
- Replace log.Fatalf in provisioner goroutine with proper error propagation
  via channel and sync.WaitGroup, preventing process crash and goroutine leaks
- Simplify status.go update() to MkdirAll + WriteFile + Chmod(0600),
  removing redundant os.Create(0666) that left cache files world-readable

Re-implemented against current upstream/main (a19ea84).

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ArangoGutierrez
ArangoGutierrez force-pushed the fix/phase0-critical-bugs branch from 9dc7a41 to 4f550ee Compare February 9, 2026 15:07
@ArangoGutierrez
ArangoGutierrez merged commit 8405b1f into NVIDIA:main Feb 9, 2026
19 checks passed
ArangoGutierrez added a commit to ArangoGutierrez/holodeck that referenced this pull request Feb 10, 2026
…e permissions (NVIDIA#622)

- Use local variable for storageSizeGB instead of taking address of
  package-level var, preventing data race and pointer aliasing with AWS SDK
- Replace log.Fatalf in provisioner goroutine with proper error propagation
  via channel and sync.WaitGroup, preventing process crash and goroutine leaks
- Simplify status.go update() to MkdirAll + WriteFile + Chmod(0600),
  removing redundant os.Create(0666) that left cache files world-readable

Re-implemented against current upstream/main (a19ea84).

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ArangoGutierrez added a commit to ArangoGutierrez/holodeck that referenced this pull request Feb 10, 2026
…e permissions (NVIDIA#622)

- Use local variable for storageSizeGB instead of taking address of
  package-level var, preventing data race and pointer aliasing with AWS SDK
- Replace log.Fatalf in provisioner goroutine with proper error propagation
  via channel and sync.WaitGroup, preventing process crash and goroutine leaks
- Simplify status.go update() to MkdirAll + WriteFile + Chmod(0600),
  removing redundant os.Create(0666) that left cache files world-readable

Re-implemented against current upstream/main (a19ea84).

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ArangoGutierrez added a commit to ArangoGutierrez/holodeck that referenced this pull request Feb 10, 2026
…e permissions (NVIDIA#622)

- Use local variable for storageSizeGB instead of taking address of
  package-level var, preventing data race and pointer aliasing with AWS SDK
- Replace log.Fatalf in provisioner goroutine with proper error propagation
  via channel and sync.WaitGroup, preventing process crash and goroutine leaks
- Simplify status.go update() to MkdirAll + WriteFile + Chmod(0600),
  removing redundant os.Create(0666) that left cache files world-readable

Re-implemented against current upstream/main (a19ea84).

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ArangoGutierrez added a commit to ArangoGutierrez/holodeck that referenced this pull request Feb 12, 2026
…e permissions (NVIDIA#622)

- Use local variable for storageSizeGB instead of taking address of
  package-level var, preventing data race and pointer aliasing with AWS SDK
- Replace log.Fatalf in provisioner goroutine with proper error propagation
  via channel and sync.WaitGroup, preventing process crash and goroutine leaks
- Simplify status.go update() to MkdirAll + WriteFile + Chmod(0600),
  removing redundant os.Create(0666) that left cache files world-readable

Re-implemented against current upstream/main (a19ea84).

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ArangoGutierrez added a commit to ArangoGutierrez/holodeck that referenced this pull request Feb 13, 2026
…e permissions (NVIDIA#622)

- Use local variable for storageSizeGB instead of taking address of
  package-level var, preventing data race and pointer aliasing with AWS SDK
- Replace log.Fatalf in provisioner goroutine with proper error propagation
  via channel and sync.WaitGroup, preventing process crash and goroutine leaks
- Simplify status.go update() to MkdirAll + WriteFile + Chmod(0600),
  removing redundant os.Create(0666) that left cache files world-readable

Re-implemented against current upstream/main (a19ea84).

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ArangoGutierrez added a commit to ArangoGutierrez/holodeck that referenced this pull request Feb 13, 2026
…e permissions (NVIDIA#622)

- Use local variable for storageSizeGB instead of taking address of
  package-level var, preventing data race and pointer aliasing with AWS SDK
- Replace log.Fatalf in provisioner goroutine with proper error propagation
  via channel and sync.WaitGroup, preventing process crash and goroutine leaks
- Simplify status.go update() to MkdirAll + WriteFile + Chmod(0600),
  removing redundant os.Create(0666) that left cache files world-readable

Re-implemented against current upstream/main (a19ea84).

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants