Skip to content

Commit a4d8a28

Browse files
Merge pull request #445 from NVIDIA/main
Prepare for v0.2.15
2 parents bf47914 + dc50a47 commit a4d8a28

File tree

104 files changed

+1398
-57463
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+1398
-57463
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

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

5+
[![CI Pipeline](https://github.com/NVIDIA/holodeck/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/NVIDIA/holodeck/actions/workflows/ci.yaml)
6+
57
A tool for creating and managing GPU-ready Cloud test environments.
68

79
---

cmd/cli/create/create.go

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"bufio"
2121
"fmt"
2222
"os"
23+
"path/filepath"
2324
"strings"
2425

2526
"github.com/NVIDIA/holodeck/api/holodeck/v1alpha1"
@@ -38,6 +39,7 @@ import (
3839
type options struct {
3940
provision bool
4041
cachePath string
42+
cacheFile string
4143
envFile string
4244
kubeconfig string
4345

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

125127
// Add instance ID to environment metadata
126128
if opts.cfg.Labels == nil {
@@ -142,7 +144,7 @@ func (m command) run(c *cli.Context, opts *options) error {
142144
// SUSE: ec2-user
143145
opts.cfg.Spec.Username = "ubuntu"
144146
}
145-
provider, err = aws.New(m.log, opts.cfg, opts.cachePath)
147+
provider, err = aws.New(m.log, opts.cfg, opts.cacheFile)
146148
if err != nil {
147149
return err
148150
}
@@ -161,7 +163,7 @@ func (m command) run(c *cli.Context, opts *options) error {
161163
}
162164

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

177-
m.log.Info("\nCreated instance %s", instanceID)
179+
// Show helpful success message with connection instructions
180+
m.showSuccessMessage(instanceID, opts)
178181
return nil
179182
}
180183

181-
func (m *command) handleProvisionFailure(instanceID, cachePath string, provisionErr error) error {
184+
func (m *command) showSuccessMessage(instanceID string, opts *options) {
185+
m.log.Info("\n✅ Successfully created instance: %s\n", instanceID)
186+
187+
// Get public DNS name for AWS instances
188+
var publicDnsName string
189+
if opts.cfg.Spec.Provider == v1alpha1.ProviderAWS {
190+
for _, p := range opts.cache.Status.Properties {
191+
if p.Name == aws.PublicDnsName {
192+
publicDnsName = p.Value
193+
break
194+
}
195+
}
196+
} else if opts.cfg.Spec.Provider == v1alpha1.ProviderSSH {
197+
publicDnsName = opts.cfg.Spec.HostUrl
198+
}
199+
200+
// Show SSH connection instructions if we have a public DNS name
201+
if publicDnsName != "" && opts.cfg.Spec.Username != "" && opts.cfg.Spec.PrivateKey != "" {
202+
m.log.Info("📋 SSH Connection:")
203+
m.log.Info(" ssh -i %s %s@%s", opts.cfg.Spec.PrivateKey, opts.cfg.Spec.Username, publicDnsName)
204+
m.log.Info(" (If you get permission denied, run: chmod 600 %s)\n", opts.cfg.Spec.PrivateKey)
205+
}
206+
207+
// Show kubeconfig instructions if Kubernetes was installed
208+
switch {
209+
case opts.cfg.Spec.Kubernetes.Install && opts.provision && opts.kubeconfig != "":
210+
// Only show kubeconfig instructions if provisioning was done and kubeconfig was requested
211+
absPath, err := filepath.Abs(opts.kubeconfig)
212+
if err != nil {
213+
absPath = opts.kubeconfig
214+
}
215+
216+
// Check if the kubeconfig file actually exists
217+
if _, err := os.Stat(absPath); err == nil {
218+
m.log.Info("📋 Kubernetes Access:")
219+
m.log.Info(" Kubeconfig saved to: %s\n", absPath)
220+
m.log.Info(" Option 1 - Copy to default location:")
221+
m.log.Info(" cp %s ~/.kube/config\n", absPath)
222+
m.log.Info(" Option 2 - Set KUBECONFIG environment variable:")
223+
m.log.Info(" export KUBECONFIG=%s\n", absPath)
224+
m.log.Info(" Option 3 - Use with kubectl directly:")
225+
m.log.Info(" kubectl --kubeconfig=%s get nodes\n", absPath)
226+
}
227+
case opts.cfg.Spec.Kubernetes.Install && opts.provision && (opts.cfg.Spec.Kubernetes.KubernetesInstaller == "microk8s" || opts.cfg.Spec.Kubernetes.KubernetesInstaller == "kind"):
228+
m.log.Info("📋 Kubernetes Access:")
229+
m.log.Info(" Note: For %s, access kubeconfig on the instance after SSH\n", opts.cfg.Spec.Kubernetes.KubernetesInstaller)
230+
case opts.cfg.Spec.Kubernetes.Install && !opts.provision:
231+
m.log.Info("📋 Kubernetes Access:")
232+
m.log.Info(" Note: Run with --provision flag to install Kubernetes and download kubeconfig\n")
233+
}
234+
235+
// Show next steps
236+
m.log.Info("📋 Next Steps:")
237+
m.log.Info(" - List instances: holodeck list")
238+
m.log.Info(" - Get instance status: holodeck status %s\n", instanceID)
239+
m.log.Info(" - Delete instance: holodeck delete %s", instanceID)
240+
}
241+
242+
func (m *command) handleProvisionFailure(instanceID, cacheFile string, provisionErr error) error {
182243
m.log.Info("\n❌ Provisioning failed: %v\n", provisionErr)
183244

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

205266
if response == "y" || response == "yes" {
206267
// Delete the instance
207-
manager := instances.NewManager(m.log, cachePath)
268+
// Extract the directory path from the cache file path
269+
cacheDir := filepath.Dir(cacheFile)
270+
manager := instances.NewManager(m.log, cacheDir)
208271
if err := manager.DeleteInstance(instanceID); err != nil {
209272
m.log.Info("Failed to delete instance: %v", err)
210273
return m.provideCleanupInstructions(instanceID, provisionErr)
@@ -275,7 +338,7 @@ func runProvision(log *logger.FunLogger, opts *options) error {
275338
if err != nil {
276339
return fmt.Errorf("failed to marshal environment: %v", err)
277340
}
278-
if err := os.WriteFile(opts.cachePath, data, 0600); err != nil {
341+
if err := os.WriteFile(opts.cacheFile, data, 0600); err != nil {
279342
return fmt.Errorf("failed to update cache file with provisioning status: %v", err)
280343
}
281344
return fmt.Errorf("failed to run provisioner: %v", err)
@@ -287,7 +350,7 @@ func runProvision(log *logger.FunLogger, opts *options) error {
287350
if err != nil {
288351
return fmt.Errorf("failed to marshal environment: %v", err)
289352
}
290-
if err := os.WriteFile(opts.cachePath, data, 0600); err != nil {
353+
if err := os.WriteFile(opts.cacheFile, data, 0600); err != nil {
291354
return fmt.Errorf("failed to update cache file with provisioning status: %v", err)
292355
}
293356

0 commit comments

Comments
 (0)