Skip to content

Commit 7d031d8

Browse files
feat(instance): add VNC session management commands (#552)
* feat(instance): add VNC session management commands * refactor * docs: update README to reflect changes in VNC command to console command * add aliases * address change request removing an unexisting Console output param * update civogo * vnc differentiate the create console DTO with the get console status DTO --------- Co-authored-by: alessandroargentieri <[email protected]>
1 parent acc29ff commit 7d031d8

File tree

7 files changed

+174
-44
lines changed

7 files changed

+174
-44
lines changed

README.md

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -844,16 +844,16 @@ The recovery-status command supports custom output formats with the following fi
844844
* status - Current recovery mode status
845845
846846
847-
### VNC Access
847+
### VNC/Console Access
848848
849-
The VNC command allows you to access your instance through a browser-based VNC console.
849+
The console command allows you to access your instance through a browser-based VNC console.
850850
851851
```sh
852852
# Open VNC console (default duration)
853-
civo instance vnc INSTANCE_ID/HOSTNAME
853+
civo instance console INSTANCE_ID/HOSTNAME
854854
855855
# Open VNC console with custom duration
856-
civo instance vnc INSTANCE_ID/HOSTNAME --duration 2h
856+
civo instance console INSTANCE_ID/HOSTNAME --duration 2h
857857
```
858858
859859
The `--duration` flag accepts Go's duration format:
@@ -867,6 +867,13 @@ When executed, the command will:
867867
3. Automatically open the console in your default browser
868868
4. Attempt to connect for up to 35 seconds before timing out
869869
870+
```sh
871+
# Check the status of a VNC/console session
872+
civo instance console status INSTANCE_ID/HOSTNAME
873+
874+
# Stop an active VNC/console session
875+
civo instance console stop INSTANCE_ID/HOSTNAME
876+
```
870877
871878
## Kubernetes clusters
872879
@@ -1992,21 +1999,21 @@ $ civo sizes list --filter kubernetes
19921999
| g4g.kube.xlarge | Extra Large - Nvidia A100 80GB | Kubernetes | 96 | 1048576 | 100 |
19932000
+--------------------+--------------------------------+------------+-----+---------+-----+
19942001
| g4g.40.kube.small | Small - Nvidia A100 40GB | Kubernetes | 8 | 65536 | 200 |
1995-
+--------------------+--------------------------------+------------+-----+---------+-----+
2002+
+--------------------+--------------------------------+------------+-----------+---------+--------+------------+
19962003
| g4g.40.kube.medium | Medium - Nvidia A100 40GB | Kubernetes | 16 | 131072 | 400 |
1997-
+--------------------+--------------------------------+------------+-----+---------+-----+
2004+
+--------------------+--------------------------------+------------+-----------+---------+--------+------------+
19982005
| g4g.40.kube.large | Large - Nvidia A100 40GB | Kubernetes | 32 | 262133 | 400 |
1999-
+--------------------+--------------------------------+------------+-----+---------+-----+
2006+
+--------------------+--------------------------------+------------+-----------+---------+--------+------------+
20002007
| g4g.40.kube.xlarge | Extra Large - Nvidia A100 40GB | Kubernetes | 64 | 524288 | 400 |
2001-
+--------------------+--------------------------------+------------+-----+---------+-----+
2002-
| an.g1.l40s.kube.x1 | Small - Nvidia L40S 40GB | Kubernetes | 12 | 131072 | 200 |
2003-
+--------------------+--------------------------------+------------+-----+---------+-----+
2004-
| an.g1.l40s.kube.x2 | Medium - Nvidia L40S 40GB | Kubernetes | 24 | 262133 | 200 |
2005-
+--------------------+--------------------------------+------------+-----+---------+-----+
2006-
| an.g1.l40s.kube.x4 | Large - Nvidia L40S 40GB | Kubernetes | 48 | 524288 | 400 |
2007-
+--------------------+--------------------------------+------------+-----+---------+-----+
2008-
| an.g1.l40s.kube.x8 | Extra Large - Nvidia L40S 40GB | Kubernetes | 96 | 1048576 | 400 |
2009-
+--------------------+--------------------------------+------------+-----+---------+-----+
2008+
+--------------------+--------------------------------+------------+-----------+---------+--------+------------+
2009+
| an.g1.l40s.kube.x1 | Small - Nvidia L40S 40GB | Kubernetes | 12 | 131072 | 200 |
2010+
+--------------------+--------------------------------+------------+-----------+---------+--------+------------+
2011+
| an.g1.l40s.kube.x2 | Medium - Nvidia L40S 40GB | Kubernetes | 24 | 262133 | 200 |
2012+
+--------------------+--------------------------------+------------+-----------+---------+--------+------------+
2013+
| an.g1.l40s.kube.x4 | Large - Nvidia L40S 40GB | Kubernetes | 48 | 524288 | 400 |
2014+
+--------------------+--------------------------------+------------+-----------+---------+--------+------------+
2015+
| an.g1.l40s.kube.x8 | Extra Large - Nvidia L40S 40GB | Kubernetes | 96 | 1048576 | 400 |
2016+
+--------------------+--------------------------------+------------+-----------+---------+--------+------------+
20102017
```
20112018
20122019
## SSH Keys

cmd/instance/instance_vnc.go renamed to cmd/instance/console.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,39 @@ import (
1414
"github.com/spf13/cobra"
1515
)
1616

17-
// maxAttempts represents the max number of attempts (each one every 7s) to connect to the vnc URL
17+
// maxAttempts represents the max number of attempts (each one every 7s) to connect to the console URL
1818
const maxAttempts = 5
1919

2020
var duration string
2121

22-
var instanceVncCmd = &cobra.Command{
22+
var instanceConsoleCmd = &cobra.Command{
2323
Use: "console",
24-
Aliases: []string{"vnc", "access"},
25-
Example: "civo instance vnc INSTANCE-ID/NAME [--duration 2h]",
24+
Aliases: []string{"vnc", "access", "connect"},
25+
Example: "civo instance console INSTANCE-ID/NAME [--duration 2h]",
2626
Args: cobra.MinimumNArgs(1),
27-
Short: "Enable and access the noVNC console on an instance",
28-
Long: `Enable and access the console (through the VNC protocol via the default browser) on an instance with optional duration.
27+
Short: "Connect to the console of an instance",
28+
Long: `Enable and access the console (through the default browser) on an instance with optional duration.
2929
Duration follows Go's duration format (e.g. "30m", "1h", "24h")`,
3030
Run: func(cmd *cobra.Command, args []string) {
3131
utility.EnsureCurrentRegion()
3232

33-
// Create the API client
3433
client, err := config.CivoAPIClient()
3534
if err != nil {
3635
utility.Error("Failed to connect to Civo's API: %s", err)
3736
os.Exit(1)
3837
}
3938

40-
// Set the region if specified by the user
4139
if common.RegionSet != "" {
4240
client.Region = common.RegionSet
4341
}
4442

45-
// Locate the instance
4643
instance, err := client.FindInstance(args[0])
4744
if err != nil {
4845
utility.Error("Unable to find instance with ID/Name '%s': %s", args[0], err)
4946
os.Exit(1)
5047
}
5148

52-
// Enable VNC for the instance with optional duration
53-
var vnc civogo.InstanceVnc
49+
var vnc civogo.CreateInstanceVncResp
5450
if duration != "" {
5551
vnc, err = client.GetInstanceVnc(instance.ID, duration)
5652
} else {
@@ -61,19 +57,16 @@ Duration follows Go's duration format (e.g. "30m", "1h", "24h")`,
6157
os.Exit(1)
6258
}
6359

64-
// Display VNC details
6560
utility.Info("Console access successfully enabled for instance: %s", instance.Hostname)
6661
utility.Info("Console access URL: %s", vnc.URI)
6762
utility.Info("We're preparing console access. This may take a while...")
6863

69-
// exchange apikey with a valid JWT (accessing the VNC url is allowed only via JWTs)
7064
exchangeTokenResp, err := client.ExchangeAuthToken(&civogo.ExchangeAuthTokenRequest{})
7165
if err != nil {
7266
utility.Error("Failed to exchange your apikey with a valid Civo JWT '%s': %s", instance.Hostname, err)
7367
os.Exit(1)
7468
}
7569

76-
// chain the bearer token to the URI to let it be opened via the default browser:
7770
vnc.URI = fmt.Sprintf("%s&token=%s", vnc.URI, exchangeTokenResp.AccessToken)
7871

7972
err = waitEndpointReady(vnc.URI)
@@ -85,7 +78,6 @@ Duration follows Go's duration format (e.g. "30m", "1h", "24h")`,
8578
utility.Info("Opening the console in your default browser...")
8679
time.Sleep(3 * time.Second)
8780

88-
// Open VNC in the browser
8981
err = browser.OpenInBrowser(vnc.URI)
9082
if err != nil {
9183
utility.Error("Failed to open the console access URL in the browser: %s", err)
@@ -95,8 +87,6 @@ Duration follows Go's duration format (e.g. "30m", "1h", "24h")`,
9587
},
9688
}
9789

98-
// endpointReady checks if the given URL endpoint is ready by sending a GET request
99-
// and returning true if the HTTP status code is 200 OK.
10090
func endpointReady(url string) bool {
10191
utility.Info("New attempt to reach the console URL...")
10292
resp, err := http.Get(url)
@@ -108,8 +98,6 @@ func endpointReady(url string) bool {
10898
return resp.StatusCode == http.StatusOK
10999
}
110100

111-
// waitEndpointReady continuously checks the given URL endpoint every 5 seconds
112-
// until it becomes ready (i.e., it does not return an HTTP 503 status).
113101
func waitEndpointReady(url string) error {
114102
var attempt int
115103
for {

cmd/instance/console_status.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package instance
2+
3+
import (
4+
"os"
5+
"strings"
6+
7+
"github.com/civo/cli/common"
8+
"github.com/civo/cli/config"
9+
"github.com/civo/cli/utility"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var instanceConsoleStatusCmd = &cobra.Command{
14+
Use: "status",
15+
Short: "Get the status of the console for an instance",
16+
Example: "civo instance console status my-instance",
17+
Args: cobra.ExactArgs(1),
18+
Run: func(cmd *cobra.Command, args []string) {
19+
utility.EnsureCurrentRegion()
20+
21+
client, err := config.CivoAPIClient()
22+
if common.RegionSet != "" {
23+
client.Region = common.RegionSet
24+
}
25+
if err != nil {
26+
utility.Error("Creating the connection to Civo's API failed with %s", err)
27+
os.Exit(1)
28+
}
29+
30+
instance, err := client.FindInstance(args[0])
31+
if err != nil {
32+
utility.Error("Finding instance %s: %s", args[0], err)
33+
os.Exit(1)
34+
}
35+
36+
vnc, err := client.GetInstanceVncStatus(instance.ID)
37+
if err != nil {
38+
if strings.Contains(err.Error(), "404") {
39+
utility.Info("The console session for instance %s (%s) does not exist or has expired.", instance.Hostname, instance.ID)
40+
os.Exit(0)
41+
}
42+
utility.Error("Getting console status for instance %s: %s", instance.ID, err)
43+
os.Exit(1)
44+
}
45+
46+
ow := utility.NewOutputWriter()
47+
ow.StartLine()
48+
ow.AppendDataWithLabel("instance_id", instance.ID, "Instance ID")
49+
ow.AppendDataWithLabel("instance_hostname", instance.Hostname, "Instance Hostname")
50+
ow.AppendDataWithLabel("uri", vnc.URI, "URI")
51+
ow.AppendDataWithLabel("expiry", vnc.Expiration, "Expiry")
52+
53+
if common.OutputFormat == "json" {
54+
ow.WriteSingleObjectJSON(common.PrettySet)
55+
} else {
56+
ow.WriteKeyValues()
57+
}
58+
},
59+
}
60+
61+
func init() {
62+
instanceConsoleCmd.AddCommand(instanceConsoleStatusCmd)
63+
}

cmd/instance/console_stop.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package instance
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/civo/cli/common"
9+
"github.com/civo/cli/config"
10+
"github.com/civo/cli/utility"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var instanceConsoleStopCmd = &cobra.Command{
15+
Use: "stop",
16+
Short: "Stop the console session for an instance",
17+
Example: "civo instance console stop my-instance",
18+
Args: cobra.ExactArgs(1),
19+
Run: func(cmd *cobra.Command, args []string) {
20+
utility.EnsureCurrentRegion()
21+
22+
client, err := config.CivoAPIClient()
23+
if common.RegionSet != "" {
24+
client.Region = common.RegionSet
25+
}
26+
if err != nil {
27+
utility.Error("Creating the connection to Civo's API failed with %s", err)
28+
os.Exit(1)
29+
}
30+
31+
instance, err := client.FindInstance(args[0])
32+
if err != nil {
33+
utility.Error("Finding instance %s: %s", args[0], err)
34+
os.Exit(1)
35+
}
36+
37+
resp, err := client.DeleteInstanceVncSession(instance.ID)
38+
if err != nil {
39+
if strings.Contains(err.Error(), "404") {
40+
utility.Info("There is no active console session for instance %s (%s) to stop.", instance.Hostname, instance.ID)
41+
os.Exit(0)
42+
}
43+
utility.Error("Stopping console session for instance %s: %s", instance.ID, err)
44+
os.Exit(1)
45+
}
46+
47+
ow := utility.NewOutputWriter()
48+
ow.StartLine()
49+
ow.AppendDataWithLabel("instance_id", instance.ID, "Instance ID")
50+
ow.AppendDataWithLabel("instance_hostname", instance.Hostname, "Instance Hostname")
51+
ow.AppendDataWithLabel("result", string(resp.Result), "Result")
52+
53+
if common.OutputFormat == "human" {
54+
if string(resp.Result) == "ok" {
55+
fmt.Printf("Console session for instance %s (%s) was stopped successfully.\n",
56+
utility.Green(instance.Hostname), instance.ID)
57+
} else {
58+
fmt.Printf("Failed to stop console session for instance %s (%s). Result: %s\n",
59+
utility.Red(instance.Hostname), instance.ID, string(resp.Result))
60+
}
61+
} else {
62+
ow.WriteSingleObjectJSON(common.PrettySet)
63+
}
64+
},
65+
}
66+
67+
func init() {
68+
instanceConsoleCmd.AddCommand(instanceConsoleStopCmd)
69+
}

cmd/instance/instance.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func init() {
3838
InstanceCmd.AddCommand(instancePublicIPCmd)
3939
InstanceCmd.AddCommand(instancePasswordCmd)
4040
InstanceCmd.AddCommand(instanceTagCmd)
41-
InstanceCmd.AddCommand(instanceVncCmd)
41+
InstanceCmd.AddCommand(instanceConsoleCmd)
4242
InstanceCmd.AddCommand(instanceRecoveryCmd)
4343
InstanceCmd.AddCommand(instanceRecoveryStatusCmd)
4444
InstanceCmd.AddCommand(snapshotCmd)
@@ -69,10 +69,10 @@ func init() {
6969
instanceCreateCmd.Flags().StringArrayVar(&allowedIPs, "allowed-ips", []string{}, "A comma separated list of IP addresses that the instance is allowed to use")
7070
instanceCreateCmd.Flags().IntVar(&networkBandwidthLimit, "network-bandwidth-limit", 0, "The network bandwidth limit for the instance in Mbps (0 for unlimited)")
7171

72-
instanceVncCmd.Flags().StringVarP(&duration, "duration", "d", "", "Duration for VNC access (e.g. 30m, 1h, 24h)")
73-
7472
instanceStopCmd.Flags().BoolVarP(&waitStop, "wait", "w", false, "wait until the instance's is stoped")
7573

74+
instanceConsoleCmd.Flags().StringVarP(&duration, "duration", "", "", "The duration for the console session (e.g., '30m', '1h'). Default is provider-dependent.")
75+
7676
instanceAllowedIPsUpdateCmd.Flags().StringSliceVarP(&allowedIPsUpdate, "ips", "", []string{}, "Comma-separated list of IP addresses to allow (e.g., --ips 1.2.3.4,5.6.7.8). To clear all IPs, provide an empty string.")
7777

7878
instanceBandwidthUpdateCmd.Flags().IntVarP(&bandwidthLimitUpdate, "limit", "l", 0, "Network bandwidth limit in Mbps (e.g., 1000). Use 0 for unlimited")

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/adhocore/gronx v1.19.5
88
github.com/alejandrojnm/go-pluralize v0.1.0
99
github.com/briandowns/spinner v1.23.2
10-
github.com/civo/civogo v0.5.4
10+
github.com/civo/civogo v0.6.1
1111
github.com/google/go-github/v57 v57.0.0
1212
github.com/google/uuid v1.6.0
1313
github.com/gookit/color v1.5.4
@@ -37,7 +37,7 @@ require (
3737
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
3838
github.com/go-logr/logr v1.4.3 // indirect
3939
github.com/gogo/protobuf v1.3.2 // indirect
40-
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 // indirect
40+
github.com/gomarkdown/markdown v0.0.0-20240729212818-a2a9c4f76ef5 // indirect
4141
github.com/google/go-github v17.0.0+incompatible // indirect
4242
github.com/google/go-querystring v1.1.0 // indirect
4343
github.com/gosuri/uilive v0.0.4 // indirect
@@ -61,7 +61,7 @@ require (
6161
github.com/ulikunitz/xz v0.5.12 // indirect
6262
github.com/x448/float16 v0.8.4 // indirect
6363
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
64-
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect
64+
golang.org/x/image v0.18.0 // indirect
6565
golang.org/x/mod v0.25.0 // indirect
6666
golang.org/x/net v0.41.0 // indirect
6767
golang.org/x/sys v0.33.0 // indirect

go.sum

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c h1:aprLqMn7gSPT+vd
3030
github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c/go.mod h1:Ie6SubJv/NTO9Q0UBH0QCl3Ve50lu9hjbi5YJUw03TE=
3131
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
3232
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
33-
github.com/civo/civogo v0.5.4 h1:atR+zfqEEp9qvLa/DRr8eYWYPySi5Mh9uaMNbZ6b4fE=
34-
github.com/civo/civogo v0.5.4/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
33+
github.com/civo/civogo v0.6.1 h1:PFOh7rBU0vmj7LTDIv3z7l9uXG4SZyyzScCl3wyTFSc=
34+
github.com/civo/civogo v0.6.1/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
3535
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
3636
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
3737
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
@@ -62,6 +62,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
6262
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
6363
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 h1:Qxs3bNRWe8GTcKMxYOSXm0jx6j0de8XUtb/fsP3GZ0I=
6464
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
65+
github.com/gomarkdown/markdown v0.0.0-20240729212818-a2a9c4f76ef5 h1:8QWUW69MXlNdZXnDnD9vEQ1BL8/mm1FTiSesKKHYivk=
66+
github.com/gomarkdown/markdown v0.0.0-20240729212818-a2a9c4f76ef5/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
6567
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
6668
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
6769
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -203,8 +205,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
203205
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
204206
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
205207
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
206-
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s=
207208
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
209+
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
210+
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
208211
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
209212
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
210213
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=

0 commit comments

Comments
 (0)