Skip to content

Commit 2b8b6ce

Browse files
committed
cluster: fix describe resources
1 parent 9d5fcf5 commit 2b8b6ce

13 files changed

Lines changed: 145 additions & 129 deletions

Development.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,27 @@ Examples:
105105
./cloudstackctl apply -f application.yaml
106106
```
107107

108+
CLI flags: `--all / -A`
109+
------------------------
110+
111+
Two commands support an `--all` (short `-A`) flag in cluster mode to query CloudStack directly rather than the controller DB:
112+
113+
- `get VirtualMachine -A` — list all VMs from CloudStack (including unmanaged VMs); without `-A` the controller returns only VMs persisted in the DB (managed by cloudstackctl).
114+
- `describe <Kind> <name> -A` — describe the named resource by querying CloudStack directly rather than using controller-managed state.
115+
116+
Examples:
117+
118+
```bash
119+
# Cluster mode: list only managed VMs (default)
120+
./cloudstackctl get VirtualMachine
121+
122+
# Cluster mode: list all VMs from CloudStack (include unmanaged)
123+
./cloudstackctl get VirtualMachine -A
124+
125+
# Describe a VM from CloudStack directly
126+
./cloudstackctl describe VirtualMachine my-vm -A
127+
```
128+
108129
## PostgreSQL environment variables
109130

110131
`cloudstackctl` reads the database connection from `DATABASE_DSN` if provided, or

cmd/cli/describe.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"cloudstackctl/pkg/handlers"
55
"encoding/json"
6+
"fmt"
67
"log"
78
"net/url"
89

@@ -31,22 +32,57 @@ var describeCmd = &cobra.Command{
3132
// Standalone: use local describe wrapper
3233
payload := map[string]string{"kind": resourceType, "name": name}
3334
raw, _ := json.Marshal(payload)
34-
if err := handlers.DescribeCloudStackResource(raw); err != nil {
35+
if respAny, err := handlers.DescribeCloudStackResource(raw); err != nil {
3536
log.Fatalf("Local describe failed: %v", err)
37+
} else {
38+
// Try to marshal the returned object into pretty JSON
39+
if b, jerr := json.MarshalIndent(respAny, "", " "); jerr == nil {
40+
fmt.Println(string(b))
41+
} else {
42+
// Fallbacks: if it's a []byte, print string; otherwise use default fmt
43+
if bs, ok := respAny.([]byte); ok {
44+
fmt.Println(string(bs))
45+
} else {
46+
fmt.Printf("%v\n", respAny)
47+
}
48+
}
3649
}
3750
return
3851
}
3952

4053
// Cluster mode: query controller describe endpoint
41-
path := "/describe?kind=" + url.QueryEscape(resourceType) + "&name=" + url.QueryEscape(name)
54+
q := url.Values{}
55+
q.Set("kind", resourceType)
56+
q.Set("name", name)
57+
if describeAll {
58+
q.Set("all", "true")
59+
}
60+
path := "/describe?" + q.Encode()
4261
body, err := ControllerRequest("GET", path, nil)
4362
if err != nil {
4463
log.Fatalf("Failed to query controller: %v", err)
64+
} else {
65+
var obj any
66+
if uerr := json.Unmarshal(body, &obj); uerr != nil {
67+
// Not valid JSON? print raw.
68+
fmt.Println(string(body))
69+
} else {
70+
if b, merr := json.MarshalIndent(obj, "", " "); merr == nil {
71+
fmt.Println(string(b))
72+
} else {
73+
fmt.Println(string(body))
74+
}
75+
}
4576
}
46-
println(string(body))
4777
},
4878
}
4979

5080
func init() {
5181
rootCmd.AddCommand(describeCmd)
5282
}
83+
84+
var describeAll bool
85+
86+
func init() {
87+
describeCmd.Flags().BoolVarP(&describeAll, "all", "A", false, "Describe a VM from CloudStack (cluster mode only)")
88+
}

controller/controller.go

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -345,46 +345,19 @@ func (c *Controller) Start() {
345345
return
346346
}
347347
switch kind {
348-
case "Network":
349-
params := c.csClient.Network.NewListNetworksParams()
350-
params.SetName(name)
351-
resp, err := c.csClient.Network.ListNetworks(params)
352-
if err != nil || resp == nil || len(resp.Networks) == 0 {
353-
http.Error(w, "network not found", http.StatusNotFound)
354-
return
355-
}
356-
b, _ := json.Marshal(resp.Networks[0])
357-
w.Header().Set("Content-Type", "application/json")
358-
w.WriteHeader(http.StatusOK)
359-
w.Write(b)
360-
return
361-
case "Template":
362-
params := c.csClient.Template.NewListTemplatesParams("")
363-
params.SetName(name)
364-
params.SetTemplatefilter("all")
365-
resp, err := c.csClient.Template.ListTemplates(params)
366-
if err != nil || resp == nil || len(resp.Templates) == 0 {
367-
http.Error(w, "template not found", http.StatusNotFound)
368-
return
369-
}
370-
b, _ := json.Marshal(resp.Templates[0])
371-
w.Header().Set("Content-Type", "application/json")
372-
w.WriteHeader(http.StatusOK)
373-
w.Write(b)
374-
return
375-
case "Volume":
376-
params := c.csClient.Volume.NewListVolumesParams()
377-
params.SetName(name)
378-
resp, err := c.csClient.Volume.ListVolumes(params)
379-
if err != nil || resp == nil || len(resp.Volumes) == 0 {
380-
http.Error(w, "volume not found", http.StatusNotFound)
348+
case "Network", "Volume", "SSHKey", "SecurityGroup", "AffinityGroup", "UserData":
349+
// Standalone: use local describe wrapper
350+
payload := map[string]string{"kind": kind, "name": name}
351+
raw, _ := json.Marshal(payload)
352+
if resp, err := handlers.DescribeCloudStackResource(raw); err != nil {
353+
log.Fatalf("Local describe failed: %v", err)
354+
} else {
355+
b, _ := json.Marshal(resp)
356+
w.Header().Set("Content-Type", "application/json")
357+
w.WriteHeader(http.StatusOK)
358+
w.Write(b)
381359
return
382360
}
383-
b, _ := json.Marshal(resp.Volumes[0])
384-
w.Header().Set("Content-Type", "application/json")
385-
w.WriteHeader(http.StatusOK)
386-
w.Write(b)
387-
return
388361
case "Application":
389362
var app v1.Application
390363
if db.DB == nil || db.DB.Where("metadata_name = ?", name).First(&app).Error != nil {
@@ -408,6 +381,24 @@ func (c *Controller) Start() {
408381
w.Write(b)
409382
return
410383
case "VirtualMachine":
384+
// If client asked for all, delegate to handlers which query CloudStack
385+
if r.URL.Query().Get("all") == "true" {
386+
payload := map[string]string{"kind": kind}
387+
if name != "" {
388+
payload["name"] = name
389+
}
390+
raw, _ := json.Marshal(payload)
391+
obj, err := handlers.DescribeCloudStackResource(raw)
392+
if err != nil {
393+
http.Error(w, fmt.Sprintf("failed to describe %s: %v", kind, err), http.StatusInternalServerError)
394+
return
395+
}
396+
b, _ := json.Marshal(obj)
397+
w.Header().Set("Content-Type", "application/json")
398+
w.WriteHeader(http.StatusOK)
399+
w.Write(b)
400+
return
401+
}
411402
var vm v1.VirtualMachine
412403
if db.DB == nil || db.DB.Where("metadata_name = ?", name).First(&vm).Error != nil {
413404
http.Error(w, "virtualmachine not found", http.StatusNotFound)

pkg/handlers/affinitygroup.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package handlers
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"log"
76

@@ -56,24 +55,22 @@ func ListAffinityGroups(name string) (any, error) {
5655
return resp, err
5756
}
5857

59-
// DescribeAffinityGroup prints details for an affinity group by name.
60-
func DescribeAffinityGroup(name string) error {
58+
// DescribeAffinityGroup returns the affinity group object from CloudStack by name.
59+
func DescribeAffinityGroup(name string) (any, error) {
6160
client, err := cloudstack.NewClient()
6261
if err != nil {
63-
return fmt.Errorf("failed to create CloudStack client: %w", err)
62+
return nil, fmt.Errorf("failed to create CloudStack client: %w", err)
6463
}
6564
params := client.AffinityGroup.NewListAffinityGroupsParams()
6665
params.SetName(name)
6766
resp, err := client.AffinityGroup.ListAffinityGroups(params)
6867
if err != nil {
69-
return fmt.Errorf("cloudstack API error: %w", err)
68+
return nil, fmt.Errorf("cloudstack API error: %w", err)
7069
}
7170
if resp == nil || len(resp.AffinityGroups) == 0 {
72-
return fmt.Errorf("affinity group %s not found", name)
71+
return nil, fmt.Errorf("affinity group %s not found", name)
7372
}
74-
b, _ := json.MarshalIndent(resp.AffinityGroups[0], "", " ")
75-
fmt.Println(string(b))
76-
return nil
73+
return resp.AffinityGroups[0], nil
7774
}
7875

7976
// ResolveAffinityGroup returns the CloudStack affinity group ID for a given name.

pkg/handlers/network.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package handlers
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"log"
76
"strconv"
@@ -51,27 +50,22 @@ func ListNetworks(name string) (any, error) {
5150
return resp, err
5251
}
5352

54-
// PrintNetworks prints a table of networks from the SDK slice.
55-
// PrintNetworks moved to print.go
56-
5753
// DescribeNetwork prints JSON for a single network identified by name.
58-
func DescribeNetwork(name string) error {
54+
func DescribeNetwork(name string) (any, error) {
5955
client, err := cloudstack.NewClient()
6056
if err != nil {
61-
return fmt.Errorf("failed to create CloudStack client: %w", err)
57+
return nil, fmt.Errorf("failed to create CloudStack client: %w", err)
6258
}
6359
params := client.Network.NewListNetworksParams()
6460
params.SetName(name)
6561
resp, err := client.Network.ListNetworks(params)
6662
if err != nil {
67-
return fmt.Errorf("cloudstack API error: %w", err)
63+
return nil, fmt.Errorf("cloudstack API error: %w", err)
6864
}
6965
if resp == nil || len(resp.Networks) == 0 {
70-
return fmt.Errorf("network %s not found", name)
66+
return nil, fmt.Errorf("network %s not found", name)
7167
}
72-
data, _ := json.MarshalIndent(resp.Networks[0], "", " ")
73-
log.Println(string(data))
74-
return nil
68+
return resp.Networks[0], nil
7569
}
7670

7771
// DeleteNetwork deletes a network by name via CloudStack API.

pkg/handlers/securitygroup.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package handlers
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"log"
76

@@ -27,22 +26,20 @@ func ListSecurityGroups(name string) (any, error) {
2726
return resp, err
2827
}
2928

30-
// DescribeSecurityGroup prints JSON for a security group by name.
31-
func DescribeSecurityGroup(name string) error {
29+
// DescribeSecurityGroup returns the security group object from CloudStack by name.
30+
func DescribeSecurityGroup(name string) (any, error) {
3231
client, err := cloudstack.NewClient()
3332
if err != nil {
34-
return fmt.Errorf("failed to create CloudStack client: %w", err)
33+
return nil, fmt.Errorf("failed to create CloudStack client: %w", err)
3534
}
3635
sg, _, err := client.SecurityGroup.GetSecurityGroupByName(name)
3736
if err != nil {
38-
return fmt.Errorf("cloudstack API error: %w", err)
37+
return nil, fmt.Errorf("cloudstack API error: %w", err)
3938
}
4039
if sg == nil {
41-
return fmt.Errorf("security group %s not found", name)
40+
return nil, fmt.Errorf("security group %s not found", name)
4241
}
43-
data, _ := json.MarshalIndent(sg, "", " ")
44-
log.Println(string(data))
45-
return nil
42+
return sg, nil
4643
}
4744

4845
// DeleteSecurityGroup deletes a security group by name.

pkg/handlers/snapshot.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package handlers
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"log"
76
"os"
@@ -33,24 +32,22 @@ func ListSnapshots(name string) error {
3332
return nil
3433
}
3534

36-
// DescribeSnapshot prints JSON for a snapshot by name.
37-
func DescribeSnapshot(name string) error {
35+
// DescribeSnapshot returns the snapshot object from CloudStack by name.
36+
func DescribeSnapshot(name string) (any, error) {
3837
client, err := cloudstack.NewClient()
3938
if err != nil {
40-
return fmt.Errorf("failed to create CloudStack client: %w", err)
39+
return nil, fmt.Errorf("failed to create CloudStack client: %w", err)
4140
}
4241
params := client.Snapshot.NewListSnapshotsParams()
4342
params.SetName(name)
4443
resp, err := client.Snapshot.ListSnapshots(params)
4544
if err != nil {
46-
return fmt.Errorf("cloudstack API error: %w", err)
45+
return nil, fmt.Errorf("cloudstack API error: %w", err)
4746
}
4847
if resp == nil || len(resp.Snapshots) == 0 {
49-
return fmt.Errorf("snapshot %s not found", name)
48+
return nil, fmt.Errorf("snapshot %s not found", name)
5049
}
51-
data, _ := json.MarshalIndent(resp.Snapshots[0], "", " ")
52-
log.Println(string(data))
53-
return nil
50+
return resp.Snapshots[0], nil
5451
}
5552

5653
// DeleteSnapshot deletes a snapshot by name.

pkg/handlers/sshkey.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package handlers
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"log"
76

@@ -26,26 +25,24 @@ func ListSSHKeys(name string) (any, error) {
2625
return resp, err
2726
}
2827

29-
// DescribeSSHKey prints JSON for an SSH key by name.
30-
func DescribeSSHKey(name string) error {
28+
// DescribeSSHKey returns the SSH keypair object from CloudStack by name.
29+
func DescribeSSHKey(name string) (any, error) {
3130
client, err := cloudstack.NewClient()
3231
if err != nil {
33-
return fmt.Errorf("failed to create CloudStack client: %w", err)
32+
return nil, fmt.Errorf("failed to create CloudStack client: %w", err)
3433
}
3534
params := client.SSH.NewListSSHKeyPairsParams()
3635
if name != "" {
3736
params.SetName(name)
3837
}
3938
resp, err := client.SSH.ListSSHKeyPairs(params)
4039
if err != nil {
41-
return fmt.Errorf("cloudstack API error: %w", err)
40+
return nil, fmt.Errorf("cloudstack API error: %w", err)
4241
}
4342
if resp == nil || len(resp.SSHKeyPairs) == 0 {
44-
return fmt.Errorf("ssh key %s not found", name)
43+
return nil, fmt.Errorf("ssh key %s not found", name)
4544
}
46-
data, _ := json.MarshalIndent(resp.SSHKeyPairs[0], "", " ")
47-
log.Println(string(data))
48-
return nil
45+
return resp.SSHKeyPairs[0], nil
4946
}
5047

5148
// DeleteSSHKey deletes an SSH key by name.

0 commit comments

Comments
 (0)