Skip to content

Commit c79380d

Browse files
committed
controller: fix issues and support multiple resource in a yaml file
1 parent 2caf94c commit c79380d

10 files changed

Lines changed: 500 additions & 362 deletions

File tree

README.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,24 @@ For local development configuration see [Development.md](Development.md).
1515

1616
There are two supported mode:
1717

18-
- **Standalone mode (`-s` / `--standalone`):** CLI-only mode that talks directly to CloudStack APIs and does not read or write the database. Use this for quick ad-hoc operations without running the controller. Cluster mode requires running the controller and Postgres (see Development.md).
18+
- **Standalone mode (`-s` / `--standalone`):** CLI-only mode that talks directly to CloudStack APIs and does not read or write the database. Use this for quick ad-hoc operations without running the controller. Controller mode requires running the controller and Postgres (see Development.md).
1919

20-
- **Cluster mode (default):** `cloudstackctl` operates with a PostgreSQL backing store and a controller process that reconciles desired state with CloudStack. Resources are managed via the database/controller.
20+
- **Controller mode (default):** `cloudstackctl` operates with a PostgreSQL backing store and a controller process that reconciles desired state with CloudStack. Resources are managed via the database/controller.
2121

2222
## Standalone mode
2323

2424
<img src="Architecture-standalone.png" width="50%" alt="Architecture of Standalone mode" />
2525

26-
## Cluster mode
26+
## Controller mode
2727

28-
<img src="Architecture.png" width="50%" alt="Architecture of Cluster mode" />
28+
<img src="Architecture.png" width="50%" alt="Architecture of Controller mode" />
2929

3030

3131
---
3232

3333
## Two Modes With YAML Support
3434

35-
| Feature | Standalone Mode | Cluster Mode |
35+
| Feature | Standalone Mode | Controller mode |
3636
|---|---|---|
3737
| Purpose | Direct CloudStack resource management using YAML | Declarative orchestration with controller and DB |
3838
| Architecture | CLI → CloudStack API | CLI → API Server → PostgreSQL → Controller → CloudStack API |
@@ -43,7 +43,7 @@ There are two supported mode:
4343

4444
### Resource Support Matrix
4545

46-
| Resource | Standalone Mode | Cluster Mode |
46+
| Resource | Standalone Mode | Controller mode |
4747
|---|:---:|:---:|
4848
| VirtualMachine |||
4949
| Network |||
@@ -257,13 +257,17 @@ export PGSSLMODE=disable
257257

258258
# Future Enhancements
259259

260-
* Rolling updates
261-
* Automatic load balancer creation
262-
* Advanced health checks
263-
* Dependency graph visualization
264-
* Drift detection and auto-healing
265-
* Multi-zone deployments
266-
* Self-healing of VMs and components
260+
* CLI: Rolling updates
261+
* CLI: Support resource update via YAML file
262+
* CLI: Multi-zone deployments
263+
* CLI: Security group improvements
264+
* CLI/Controller: Support reconciling resources
265+
* Controller: Support network services of isolated network
266+
* Controller: Advanced health checks
267+
* Controller: Dependency graph visualization
268+
* Controller: Self-healing of VMs and components
269+
* Controller: Scaling of components
270+
* Controller: Configurable timeout settings
267271

268272
---
269273

cmd/cli/apply.go

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package cli
22

33
import (
4+
"bytes"
45
"encoding/json"
5-
6+
"io"
67
"log"
78
"os"
89

910
"cloudstackctl/pkg/handlers"
1011

1112
"github.com/spf13/cobra"
12-
"sigs.k8s.io/yaml"
13+
"gopkg.in/yaml.v3"
1314
)
1415

1516
// applyCmd represents the apply command
@@ -30,43 +31,77 @@ var applyCmd = &cobra.Command{
3031
log.Fatalf("Failed to read file: %v", err)
3132
}
3233

33-
// Convert YAML to JSON for the API
34-
jsonData, err := yaml.YAMLToJSON(data)
35-
if err != nil {
36-
log.Fatalf("Failed to convert YAML to JSON: %v", err)
34+
// Determine kind from first document for logging and mode handling.
35+
dec := yaml.NewDecoder(bytes.NewReader(data))
36+
var first map[string]interface{}
37+
if err := dec.Decode(&first); err != nil && err != io.EOF {
38+
log.Fatalf("Failed to parse YAML: %v", err)
3739
}
38-
39-
// Inspect kind
40-
var meta map[string]interface{}
41-
if err := json.Unmarshal(jsonData, &meta); err != nil {
42-
log.Fatalf("Invalid resource JSON: %v", err)
40+
kind := ""
41+
if first != nil {
42+
if k, ok := first["kind"].(string); ok {
43+
kind = k
44+
}
4345
}
4446

45-
// Determine kind for logging and mode handling
46-
kind, _ := meta["kind"].(string)
47-
48-
// standalone mode: apply the resource directly via handlers
47+
// standalone mode: apply each document locally
4948
if standalone {
50-
// Application, Component, VirtualMachineSpec are only supported in controller mode
51-
if kind == "Application" || kind == "Component" || kind == "VirtualMachineSpec" {
52-
log.Fatalf("%s is not supported in standalone mode", kind)
53-
}
54-
if id, err := handlers.ApplyCloudStackResource(jsonData); err != nil {
55-
log.Fatalf("Local apply failed for %s: %v", kind, err)
56-
} else {
57-
if id != "" {
58-
log.Printf("Applied %s id=%s", kind, id)
49+
dec := yaml.NewDecoder(bytes.NewReader(data))
50+
for {
51+
var doc map[string]interface{}
52+
if err := dec.Decode(&doc); err != nil {
53+
if err == io.EOF {
54+
break
55+
}
56+
log.Fatalf("Failed to decode YAML: %v", err)
57+
}
58+
if doc == nil {
59+
continue
60+
}
61+
j, _ := json.Marshal(doc)
62+
// inspect kind and reject unsupported managed kinds
63+
kk, _ := doc["kind"].(string)
64+
if kk == "Application" || kk == "Component" || kk == "VirtualMachineSpec" {
65+
log.Fatalf("%s is not supported in standalone mode", kk)
66+
}
67+
if id, err := handlers.ApplyCloudStackResource(j); err != nil {
68+
log.Fatalf("Local apply failed for %s: %v", kk, err)
69+
} else {
70+
if id != "" {
71+
log.Printf("Applied %s id=%s", kk, id)
72+
}
5973
}
6074
}
6175
return
6276
}
6377

64-
// controller mode: apply by POSTing to controller HTTP API
65-
body, err := ControllerRequest("POST", "/apply", jsonData)
78+
// controller mode: POST the raw file bytes (controller will decode multiple docs)
79+
body, err := ControllerRequest("POST", "/apply", data)
6680
if err != nil {
6781
log.Fatalf("Failed to POST to controller: %v", err)
6882
}
69-
log.Printf("Controller accepted %s: %s", kind, string(body))
83+
// If controller returned a JSON array of per-resource results, print
84+
// a concise one-line summary per resource. Otherwise pretty-print.
85+
var arr []map[string]interface{}
86+
if err := json.Unmarshal(body, &arr); err == nil {
87+
// Remove redundant kind/name from returned JSON
88+
for _, item := range arr {
89+
k, _ := item["kind"].(string)
90+
name, _ := item["name"].(string)
91+
delete(item, "kind")
92+
delete(item, "name")
93+
b, _ := json.Marshal(item)
94+
log.Printf("Controller response for %s/%s: %s", k, name, string(b))
95+
}
96+
} else {
97+
var resp interface{}
98+
if err := json.Unmarshal(body, &resp); err != nil {
99+
log.Printf("Controller accepted %s: %s", kind, string(body))
100+
} else {
101+
pretty, _ := json.MarshalIndent(resp, "", " ")
102+
log.Printf("Controller accepted %s:\n%s", kind, string(pretty))
103+
}
104+
}
70105
},
71106
}
72107

cmd/cli/delete.go

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package cli
22

33
import (
4+
"bytes"
45
"cloudstackctl/pkg/handlers"
56
"encoding/json"
7+
"io"
68
"log"
79
"os"
810

911
"github.com/spf13/cobra"
10-
"sigs.k8s.io/yaml"
12+
"gopkg.in/yaml.v3"
1113
)
1214

1315
// deleteCmd represents the delete command
@@ -19,7 +21,6 @@ var deleteCmd = &cobra.Command{
1921
// file flag support (delete -f <yaml>)
2022
filePath, _ := cmd.Flags().GetString("file")
2123
var name string
22-
var jsonData []byte
2324
var resourceType string
2425
if filePath != "" {
2526
var data []byte
@@ -28,49 +29,74 @@ var deleteCmd = &cobra.Command{
2829
if err != nil {
2930
log.Fatalf("Failed to read file: %v", err)
3031
}
31-
jsonData, err = yaml.YAMLToJSON(data)
32-
if err != nil {
33-
log.Fatalf("Failed to convert YAML to JSON: %v", err)
34-
}
35-
var meta map[string]interface{}
36-
if err := json.Unmarshal(jsonData, &meta); err != nil {
37-
log.Fatalf("Invalid resource JSON: %v", err)
38-
}
39-
kind, _ := meta["kind"].(string)
40-
// metadata may be nested
41-
name = ""
42-
if m, ok := meta["metadata"].(map[string]interface{}); ok {
43-
if n, ok := m["name"].(string); ok {
44-
name = n
32+
// Support multi-document YAML: decode each document and delete in reverse order
33+
dec := yaml.NewDecoder(bytes.NewReader(data))
34+
var docs []map[string]interface{}
35+
for {
36+
var doc map[string]interface{}
37+
if err := dec.Decode(&doc); err != nil {
38+
if err == io.EOF {
39+
break
40+
}
41+
log.Fatalf("Failed to decode YAML: %v", err)
42+
}
43+
if doc == nil {
44+
continue
4545
}
46+
docs = append(docs, doc)
4647
}
47-
if kind == "" || name == "" {
48-
log.Fatal("YAML must contain kind and metadata.name")
48+
if len(docs) == 0 {
49+
log.Fatalf("no resources found in YAML")
4950
}
50-
resourceType = kind
51-
payload := map[string]string{"kind": resourceType, "name": name}
52-
rawPayload, _ := json.Marshal(payload)
5351

54-
if standalone {
55-
// Only unmanaged kinds supported in standalone
56-
if resourceType == "Application" || resourceType == "Component" || resourceType == "VirtualMachineSpec" {
57-
log.Fatalf("'%s' is not supported in standalone mode", resourceType)
52+
// Iterate in reverse order for deletion
53+
for i := len(docs) - 1; i >= 0; i-- {
54+
meta := docs[i]
55+
kind, _ := meta["kind"].(string)
56+
name = ""
57+
if m, ok := meta["metadata"].(map[string]interface{}); ok {
58+
if n, ok := m["name"].(string); ok {
59+
name = n
60+
}
5861
}
59-
if id, err := handlers.DeleteCloudStackResource(rawPayload); err != nil {
60-
log.Fatalf("Local delete failed: %v", err)
61-
} else {
62-
if id != "" {
63-
log.Printf("Deleted %s id=%s", resourceType, id)
62+
if kind == "" || name == "" {
63+
log.Fatalf("each YAML doc must contain kind and metadata.name")
64+
}
65+
resourceType = kind
66+
payload := map[string]string{"kind": resourceType, "name": name}
67+
rawPayload, _ := json.Marshal(payload)
68+
69+
if standalone {
70+
// Only unmanaged kinds supported in standalone
71+
if resourceType == "Application" || resourceType == "Component" || resourceType == "VirtualMachineSpec" {
72+
log.Fatalf("'%s' is not supported in standalone mode", resourceType)
6473
}
74+
if id, err := handlers.DeleteCloudStackResource(rawPayload); err != nil {
75+
log.Fatalf("Local delete failed: %v", err)
76+
} else {
77+
if id != "" {
78+
log.Printf("Deleted %s id=%s", resourceType, id)
79+
}
80+
}
81+
continue
6582
}
66-
return
67-
}
6883

69-
// Cluster mode: send delete request to controller
70-
if body, err := ControllerRequest("POST", "/delete", rawPayload); err != nil {
71-
log.Fatalf("Controller delete failed: %v", err)
72-
} else {
73-
log.Printf("Controller response for %s: %s", resourceType, string(body))
84+
// Cluster mode: send delete request to controller for each resource
85+
if body, err := ControllerRequest("POST", "/delete", rawPayload); err != nil {
86+
log.Fatalf("Controller delete failed for %s/%s: %v", resourceType, name, err)
87+
} else {
88+
// Remove redundant kind/name from returned JSON
89+
var obj map[string]interface{}
90+
if err := json.Unmarshal(body, &obj); err == nil {
91+
delete(obj, "kind")
92+
delete(obj, "name")
93+
if b2, err := json.Marshal(obj); err == nil {
94+
log.Printf("Controller response for %s/%s: %s", resourceType, name, string(b2))
95+
continue
96+
}
97+
}
98+
log.Printf("Controller response for %s/%s: %s", resourceType, name, string(body))
99+
}
74100
}
75101
return
76102
}
@@ -103,7 +129,17 @@ var deleteCmd = &cobra.Command{
103129
if body, err := ControllerRequest("POST", "/delete", rawPayload); err != nil {
104130
log.Fatalf("Controller delete failed: %v", err)
105131
} else {
106-
log.Printf("Controller response for %s: %s", resourceType, string(body))
132+
// Remove redundant kind/name from returned JSON
133+
var obj map[string]interface{}
134+
if err := json.Unmarshal(body, &obj); err == nil {
135+
delete(obj, "kind")
136+
delete(obj, "name")
137+
if b2, err := json.Marshal(obj); err == nil {
138+
log.Printf("Controller response for %s/%s: %s", resourceType, name, string(b2))
139+
return
140+
}
141+
}
142+
log.Printf("Controller response for %s/%s: %s", resourceType, name, string(body))
107143
}
108144
},
109145
}

cmd/cli/get.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ var getCmd = &cobra.Command{
6868
if resourceType == "VirtualMachine" && !getAll {
6969
var vms []v1.VirtualMachine
7070
if err := json.Unmarshal(body, &vms); err == nil {
71-
handlers.PrintVMsFromDB(vms)
71+
handlers.PrintVMsFromController(vms)
7272
return
7373
}
7474
}

0 commit comments

Comments
 (0)