11package views
22
33import (
4+ "bytes"
45 "context"
6+ "encoding/json"
57 "fmt"
8+ "io"
9+ "net/http"
610 "os/exec"
711 "strings"
812
913 "github.com/render-oss/cli/pkg/client"
1014 "github.com/render-oss/cli/pkg/command"
15+ "github.com/render-oss/cli/pkg/config"
1116 "github.com/render-oss/cli/pkg/deploy"
1217 "github.com/render-oss/cli/pkg/service"
1318 "github.com/render-oss/cli/pkg/tui"
@@ -18,13 +23,18 @@ type SSHInput struct {
1823 InstanceID string // Set when a specific instance is selected or provided
1924 Project * client.Project
2025 EnvironmentIDs []string
26+ Ephemeral bool `cli:"ephemeral"`
2127
2228 Args []string
2329}
2430
2531// NewSSHView creates an SSH execution view - always returns ExecModel
2632func NewSSHView (ctx context.Context , input * SSHInput ) * tui.ExecModel {
27- return tui .NewExecModel ("ssh" , handleSSHError , command .LoadCmd (ctx , loadDataSSH , input ))
33+ loadingMsg := "%s Preparing SSH connection..."
34+ if input .Ephemeral {
35+ loadingMsg = "%s Creating ephemeral shell (this may take a moment)..."
36+ }
37+ return tui .NewExecModel ("ssh" , handleSSHError , command .LoadCmdWithLoadingMsg (ctx , loadDataSSH , input , loadingMsg ))
2838}
2939
3040func handleSSHError (err error ) error {
@@ -34,6 +44,46 @@ func handleSSHError(err error) error {
3444 }
3545}
3646
47+ // createEphemeralShell creates an ephemeral shell pod for the given service.
48+ // This must be called before SSH'ing into an ephemeral shell.
49+ func createEphemeralShell (ctx context.Context , serviceID string ) error {
50+ apiCfg , err := config .DefaultAPIConfig ()
51+ if err != nil {
52+ return fmt .Errorf ("failed to get API config: %w" , err )
53+ }
54+
55+ url := fmt .Sprintf ("%sservices/%s/ephemeral-shell" , apiCfg .Host , serviceID )
56+
57+ req , err := http .NewRequestWithContext (ctx , http .MethodPost , url , bytes .NewReader (nil ))
58+ if err != nil {
59+ return fmt .Errorf ("failed to create request: %w" , err )
60+ }
61+
62+ req .Header = client .AddHeaders (req .Header , apiCfg .Key )
63+ req .Header .Set ("Accept" , "application/json" )
64+
65+ resp , err := http .DefaultClient .Do (req )
66+ if err != nil {
67+ return fmt .Errorf ("failed to create ephemeral shell: %w" , err )
68+ }
69+ defer resp .Body .Close ()
70+
71+ body , _ := io .ReadAll (resp .Body )
72+
73+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
74+ // Try to extract error message from JSON response
75+ var apiErr struct {
76+ Message string `json:"message"`
77+ }
78+ if json .Unmarshal (body , & apiErr ) == nil && apiErr .Message != "" {
79+ return fmt .Errorf ("failed to create ephemeral shell: %s" , apiErr .Message )
80+ }
81+ return fmt .Errorf ("failed to create ephemeral shell: received status %d" , resp .StatusCode )
82+ }
83+
84+ return nil
85+ }
86+
3787func getServiceFromIDOrName (ctx context.Context , c * client.ClientWithResponses , idOrName string ) (* client.Service , error ) {
3888 serviceRepo := service .NewRepo (c )
3989
@@ -129,6 +179,22 @@ func loadDataSSH(ctx context.Context, in *SSHInput) (*exec.Cmd, error) {
129179 }
130180 }
131181
182+ if in .Ephemeral {
183+ // Create the ephemeral shell pod first
184+ if err := createEphemeralShell (ctx , serviceInfo .Id ); err != nil {
185+ return nil , tui.UserFacingError {
186+ Title : "Failed to create ephemeral shell" ,
187+ Message : err .Error (),
188+ }
189+ }
190+
191+ // Format: ephemeral.{service-id}@{ssh-host}
192+ parts := strings .SplitN (finalSSHAddress , "@" , 2 )
193+ if len (parts ) == 2 {
194+ finalSSHAddress = "ephemeral." + serviceInfo .Id + "@" + parts [1 ]
195+ }
196+ }
197+
132198 args := []string {finalSSHAddress }
133199 args = append (args , in .Args ... )
134200
0 commit comments