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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[![Latest Release](https://img.shields.io/github/v/release/NVIDIA/holodeck?label=latest%20release)](https://github.com/NVIDIA/holodeck/releases/latest)

[![CI Pipeline](https://github.com/NVIDIA/holodeck/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/NVIDIA/holodeck/actions/workflows/ci.yaml)

A tool for creating and managing GPU-ready Cloud test environments.

---
Expand Down
81 changes: 72 additions & 9 deletions cmd/cli/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/NVIDIA/holodeck/api/holodeck/v1alpha1"
Expand All @@ -38,6 +39,7 @@ import (
type options struct {
provision bool
cachePath string
cacheFile string
envFile string
kubeconfig string

Expand Down Expand Up @@ -120,7 +122,7 @@ func (m command) run(c *cli.Context, opts *options) error {
// Create instance manager and generate unique ID
manager := instances.NewManager(m.log, opts.cachePath)
instanceID := manager.GenerateInstanceID()
opts.cachePath = manager.GetInstanceCacheFile(instanceID)
opts.cacheFile = manager.GetInstanceCacheFile(instanceID)

// Add instance ID to environment metadata
if opts.cfg.Labels == nil {
Expand All @@ -142,7 +144,7 @@ func (m command) run(c *cli.Context, opts *options) error {
// SUSE: ec2-user
opts.cfg.Spec.Username = "ubuntu"
}
provider, err = aws.New(m.log, opts.cfg, opts.cachePath)
provider, err = aws.New(m.log, opts.cfg, opts.cacheFile)
if err != nil {
return err
}
Expand All @@ -161,7 +163,7 @@ func (m command) run(c *cli.Context, opts *options) error {
}

// Read cache after creating the environment
opts.cache, err = jyaml.UnmarshalFromFile[v1alpha1.Environment](opts.cachePath)
opts.cache, err = jyaml.UnmarshalFromFile[v1alpha1.Environment](opts.cacheFile)
if err != nil {
return fmt.Errorf("failed to read cache file: %v", err)
}
Expand All @@ -170,15 +172,74 @@ func (m command) run(c *cli.Context, opts *options) error {
err := runProvision(m.log, opts)
if err != nil {
// Handle provisioning failure with user interaction
return m.handleProvisionFailure(instanceID, opts.cachePath, err)
return m.handleProvisionFailure(instanceID, opts.cacheFile, err)
}
}

m.log.Info("\nCreated instance %s", instanceID)
// Show helpful success message with connection instructions
m.showSuccessMessage(instanceID, opts)
return nil
}

func (m *command) handleProvisionFailure(instanceID, cachePath string, provisionErr error) error {
func (m *command) showSuccessMessage(instanceID string, opts *options) {
m.log.Info("\n✅ Successfully created instance: %s\n", instanceID)

// Get public DNS name for AWS instances
var publicDnsName string
if opts.cfg.Spec.Provider == v1alpha1.ProviderAWS {
for _, p := range opts.cache.Status.Properties {
if p.Name == aws.PublicDnsName {
publicDnsName = p.Value
break
}
}
} else if opts.cfg.Spec.Provider == v1alpha1.ProviderSSH {
publicDnsName = opts.cfg.Spec.HostUrl
}

// Show SSH connection instructions if we have a public DNS name
if publicDnsName != "" && opts.cfg.Spec.Username != "" && opts.cfg.Spec.PrivateKey != "" {
m.log.Info("📋 SSH Connection:")
m.log.Info(" ssh -i %s %s@%s", opts.cfg.Spec.PrivateKey, opts.cfg.Spec.Username, publicDnsName)
m.log.Info(" (If you get permission denied, run: chmod 600 %s)\n", opts.cfg.Spec.PrivateKey)
}

// Show kubeconfig instructions if Kubernetes was installed
switch {
case opts.cfg.Spec.Kubernetes.Install && opts.provision && opts.kubeconfig != "":
// Only show kubeconfig instructions if provisioning was done and kubeconfig was requested
absPath, err := filepath.Abs(opts.kubeconfig)
if err != nil {
absPath = opts.kubeconfig
}

// Check if the kubeconfig file actually exists
if _, err := os.Stat(absPath); err == nil {
m.log.Info("📋 Kubernetes Access:")
m.log.Info(" Kubeconfig saved to: %s\n", absPath)
m.log.Info(" Option 1 - Copy to default location:")
m.log.Info(" cp %s ~/.kube/config\n", absPath)
m.log.Info(" Option 2 - Set KUBECONFIG environment variable:")
m.log.Info(" export KUBECONFIG=%s\n", absPath)
m.log.Info(" Option 3 - Use with kubectl directly:")
m.log.Info(" kubectl --kubeconfig=%s get nodes\n", absPath)
}
case opts.cfg.Spec.Kubernetes.Install && opts.provision && (opts.cfg.Spec.Kubernetes.KubernetesInstaller == "microk8s" || opts.cfg.Spec.Kubernetes.KubernetesInstaller == "kind"):
m.log.Info("📋 Kubernetes Access:")
m.log.Info(" Note: For %s, access kubeconfig on the instance after SSH\n", opts.cfg.Spec.Kubernetes.KubernetesInstaller)
case opts.cfg.Spec.Kubernetes.Install && !opts.provision:
m.log.Info("📋 Kubernetes Access:")
m.log.Info(" Note: Run with --provision flag to install Kubernetes and download kubeconfig\n")
}

// Show next steps
m.log.Info("📋 Next Steps:")
m.log.Info(" - List instances: holodeck list")
m.log.Info(" - Get instance status: holodeck status %s\n", instanceID)
m.log.Info(" - Delete instance: holodeck delete %s", instanceID)
}

func (m *command) handleProvisionFailure(instanceID, cacheFile string, provisionErr error) error {
m.log.Info("\n❌ Provisioning failed: %v\n", provisionErr)

// Check if we're in a non-interactive environment
Expand All @@ -204,7 +265,9 @@ func (m *command) handleProvisionFailure(instanceID, cachePath string, provision

if response == "y" || response == "yes" {
// Delete the instance
manager := instances.NewManager(m.log, cachePath)
// Extract the directory path from the cache file path
cacheDir := filepath.Dir(cacheFile)
manager := instances.NewManager(m.log, cacheDir)
if err := manager.DeleteInstance(instanceID); err != nil {
m.log.Info("Failed to delete instance: %v", err)
return m.provideCleanupInstructions(instanceID, provisionErr)
Expand Down Expand Up @@ -275,7 +338,7 @@ func runProvision(log *logger.FunLogger, opts *options) error {
if err != nil {
return fmt.Errorf("failed to marshal environment: %v", err)
}
if err := os.WriteFile(opts.cachePath, data, 0600); err != nil {
if err := os.WriteFile(opts.cacheFile, data, 0600); err != nil {
return fmt.Errorf("failed to update cache file with provisioning status: %v", err)
}
return fmt.Errorf("failed to run provisioner: %v", err)
Expand All @@ -287,7 +350,7 @@ func runProvision(log *logger.FunLogger, opts *options) error {
if err != nil {
return fmt.Errorf("failed to marshal environment: %v", err)
}
if err := os.WriteFile(opts.cachePath, data, 0600); err != nil {
if err := os.WriteFile(opts.cacheFile, data, 0600); err != nil {
return fmt.Errorf("failed to update cache file with provisioning status: %v", err)
}

Expand Down
Loading
Loading