Skip to content

Commit 51e104f

Browse files
committed
pkg: fix test errors in standalone mode
1 parent 96f370f commit 51e104f

16 files changed

Lines changed: 237 additions & 60 deletions

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ kind: VirtualMachineSpec
118118
metadata:
119119
name: basic-vm-spec
120120
spec:
121+
zone: zone-1
121122
template: ubuntu-22.04
122123
serviceOffering: medium
123124
networkIds:
@@ -138,10 +139,12 @@ kind: Application
138139
metadata:
139140
name: app-with-reused-vmspec
140141
spec:
141-
project: 987e6543-e21b-12d3-a456-426655440000
142+
project: project-2
142143
components:
143144
- name: frontend
144145
virtualMachineSpec: basic-vm-spec
146+
overrides:
147+
- sshKeys: web
145148
replicas: 2
146149
healthChecks:
147150
- type: ping
@@ -157,7 +160,7 @@ kind: Application
157160
metadata:
158161
name: simple-app
159162
spec:
160-
project: 987e6543-e21b-12d3-a456-426655440000
163+
project: project-2
161164
components:
162165
- name: frontend
163166
virtualMachineSpec: basic-vm-spec
@@ -175,7 +178,8 @@ kind: VirtualMachine
175178
metadata:
176179
name: standalone-vm
177180
spec:
178-
project: 987e6543-e21b-12d3-a456-426655440000
181+
zone: zone-1
182+
project: project-2
179183
template: ubuntu-22.04
180184
serviceOffering: medium
181185
networkIds:

apis/v1/types.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,15 @@ type Network struct {
139139

140140
// NetworkSpec defines the desired state of a Network
141141
type NetworkSpec struct {
142-
Zone string `yaml:"zone"` // CloudStack zone ID or name
143-
NetworkOffering string `yaml:"networkOffering,omitempty"` // Network offering ID or name for creation
144-
Description string `yaml:"description,omitempty"` // Human-friendly description / displayText
145-
Gateway string `yaml:"gateway,omitempty"` // Gateway IP for shared networks
146-
Netmask string `yaml:"netmask,omitempty"` // Netmask for shared networks
147-
StartIP string `yaml:"startIp,omitempty"` // Start IP for static IP range (shared network)
148-
EndIP string `yaml:"endIp,omitempty"` // End IP for static IP range (shared network)
142+
Zone string `yaml:"zone"` // CloudStack zone ID or name
143+
NetworkOffering string `yaml:"networkOffering,omitempty"` // Network offering ID or name for creation
144+
Vlan interface{} `yaml:"vlan,omitempty"` // Optional VLAN tag for shared networks; may be string or number
145+
BypassVlanOverlapCheck bool `yaml:"bypassVlanOverlapCheck,omitempty"` // When true, do not normalize or validate VLAN value
146+
Description string `yaml:"description,omitempty"` // Human-friendly description / displayText
147+
Gateway string `yaml:"gateway,omitempty"` // Gateway IP for shared networks
148+
Netmask string `yaml:"netmask,omitempty"` // Netmask for shared networks
149+
StartIP string `yaml:"startIp,omitempty"` // Start IP for static IP range (shared network)
150+
EndIP string `yaml:"endIp,omitempty"` // End IP for static IP range (shared network)
149151
}
150152

151153
// Volume represents a disk attached to a VM in CloudStack
@@ -170,10 +172,16 @@ type VolumeSpec struct {
170172
// SSHKey represents an SSH key pair for VM access in CloudStack
171173
type SSHKey struct {
172174
gorm.Model
173-
APIVersion string `json:"apiVersion" yaml:"apiVersion"`
174-
Kind string `json:"kind" yaml:"kind"` // "SSHKey"
175-
Metadata Metadata `json:"metadata" yaml:"metadata" gorm:"embedded"`
176-
Status Status `json:"status,omitempty" gorm:"embedded"`
175+
APIVersion string `json:"apiVersion" yaml:"apiVersion"`
176+
Kind string `json:"kind" yaml:"kind"` // "SSHKey"
177+
Metadata Metadata `json:"metadata" yaml:"metadata" gorm:"embedded"`
178+
Spec SSHKeySpec `json:"spec,omitempty" yaml:"spec,omitempty" gorm:"embedded"`
179+
Status Status `json:"status,omitempty" gorm:"embedded"`
180+
}
181+
182+
// SSHKeySpec holds the public key material for registering an SSH keypair
183+
type SSHKeySpec struct {
184+
PublicKey string `yaml:"publicKey"`
177185
}
178186

179187
// SecurityGroup represents a firewall rule set for VMs in CloudStack

examples/standalone/affinitygroup.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ kind: AffinityGroup
33
metadata:
44
name: vm-spread
55
spec:
6-
type: hostAntiAffinity
6+
type: host anti-affinity
77
name: vm-spread
88
description: Spread VMs across hosts
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: cloudstackctl/v1
2+
kind: Network
3+
metadata:
4+
name: example-network-isolated
5+
spec:
6+
zone: zone-1
7+
networkOffering: isolated-network-offering
8+
cidr: 10.0.0.0/24
9+
description: "Isolated network example used by standalone VMs"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: cloudstackctl/v1
2+
kind: Network
3+
metadata:
4+
name: example-network-shared
5+
spec:
6+
zone: zone-1
7+
networkOffering: shared-network-offering
8+
description: "Shared network example with static IP range"
9+
vlan: 1000
10+
startIp: 10.0.0.10
11+
endIp: 10.0.0.100
12+
gateway: 10.0.0.1
13+
netmask: 255.255.255.0
14+
bypassVlanOverlapCheck: true

examples/standalone/network.yaml

Lines changed: 0 additions & 9 deletions
This file was deleted.

examples/standalone/volume.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ kind: Volume
33
metadata:
44
name: data-disk
55
spec:
6-
name: data-disk
76
diskOffering: standard-hdd
87
size: 50
98
zone: zone-1

pkg/handlers/affinitygroup.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,25 @@ func ResolveAffinityGroup(name string) (string, error) {
105105
}
106106
return existing.Id, nil
107107
}
108+
109+
// DeleteAffinityGroup deletes an affinity group by name.
110+
func DeleteAffinityGroup(name string) error {
111+
client, err := cloudstack.NewClient()
112+
if err != nil {
113+
return fmt.Errorf("failed to create CloudStack client: %w", err)
114+
}
115+
existing, _, err := client.AffinityGroup.GetAffinityGroupByName(name)
116+
if err != nil {
117+
return fmt.Errorf("cloudstack API error: %w", err)
118+
}
119+
if existing == nil {
120+
return fmt.Errorf("affinity group %s not found", name)
121+
}
122+
dp := client.AffinityGroup.NewDeleteAffinityGroupParams()
123+
dp.SetId(existing.Id)
124+
if _, err := client.AffinityGroup.DeleteAffinityGroup(dp); err != nil {
125+
return fmt.Errorf("failed to delete affinity group %s: %w", name, err)
126+
}
127+
log.Printf("AffinityGroup %s deleted from CloudStack (id=%s)", name, existing.Id)
128+
return nil
129+
}

pkg/handlers/network.go

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"log"
77
"os"
8+
"strconv"
9+
"strings"
810
"text/tabwriter"
911

1012
v1 "cloudstackctl/apis/v1"
@@ -49,7 +51,7 @@ func ListNetworks(name string) error {
4951
return fmt.Errorf("cloudstack API error: %w", err)
5052
}
5153
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
52-
fmt.Fprintln(w, "NAME\tID\tZONE\tDISPLAY TEXT\tTYPE\tSTATE")
54+
fmt.Fprintln(w, "NAME\tID\tZONE\tVLAN\tDISPLAY TEXT\tTYPE\tSTATE")
5355
for _, n := range resp.Networks {
5456
display := n.Displaytext
5557
if display == "" {
@@ -66,7 +68,21 @@ func ListNetworks(name string) error {
6668
}
6769
}
6870

69-
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", n.Name, n.Id, zoneName, display, n.Type, n.State)
71+
// Attempt to extract VLAN information from the returned network object
72+
// via JSON to avoid SDK field name differences across versions.
73+
vlan := ""
74+
if b, merr := json.Marshal(n); merr == nil {
75+
var m map[string]interface{}
76+
if uerr := json.Unmarshal(b, &m); uerr == nil {
77+
if v, ok := m["vlan"].(string); ok {
78+
vlan = v
79+
} else if v, ok := m["vlanid"].(string); ok {
80+
vlan = v
81+
}
82+
}
83+
}
84+
85+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", n.Name, n.Id, zoneName, vlan, display, n.Type, n.State)
7086
}
7187
w.Flush()
7288
return nil
@@ -150,10 +166,60 @@ func ApplyNetwork(netRes *v1.Network) error {
150166
if zerr != nil {
151167
return fmt.Errorf("failed to resolve zone %s: %w", netRes.Spec.Zone, zerr)
152168
}
153-
createParams := client.Network.NewCreateNetworkParams(name, netRes.Spec.NetworkOffering, zoneID)
169+
// Resolve network offering name to ID; require resolution.
170+
offeringID, offErr := ResolveNetworkOffering(netRes.Spec.NetworkOffering)
171+
if offErr != nil {
172+
return fmt.Errorf("failed to resolve network offering %s: %w", netRes.Spec.NetworkOffering, offErr)
173+
}
174+
createParams := client.Network.NewCreateNetworkParams(name, offeringID, zoneID)
154175
if netRes.Spec.Description != "" {
155176
createParams.SetDisplaytext(netRes.Spec.Description)
156177
}
178+
179+
// Pass bypassvlanoverlapcheck through to CloudStack (supported by API)
180+
createParams.SetBypassvlanoverlapcheck(netRes.Spec.BypassVlanOverlapCheck)
181+
182+
// If shared network fields are present, set them on the create params.
183+
if netRes.Spec.Gateway != "" {
184+
createParams.SetGateway(netRes.Spec.Gateway)
185+
}
186+
if netRes.Spec.Netmask != "" {
187+
createParams.SetNetmask(netRes.Spec.Netmask)
188+
}
189+
if netRes.Spec.StartIP != "" {
190+
createParams.SetStartip(netRes.Spec.StartIP)
191+
}
192+
if netRes.Spec.EndIP != "" {
193+
createParams.SetEndip(netRes.Spec.EndIP)
194+
}
195+
if netRes.Spec.Vlan != nil {
196+
var vlanVal string
197+
switch v := netRes.Spec.Vlan.(type) {
198+
case string:
199+
vlanVal = v
200+
case int:
201+
vlanVal = strconv.Itoa(v)
202+
case int64:
203+
vlanVal = strconv.FormatInt(v, 10)
204+
case float64:
205+
// YAML numbers may be decoded as float64
206+
vlanVal = strconv.FormatInt(int64(v), 10)
207+
default:
208+
vlanVal = fmt.Sprintf("%v", v)
209+
}
210+
// Accept numeric VLANs like "1000" or the full URI "vlan://1000".
211+
// If the value already contains a scheme ("://"), trust it
212+
// (supports vlan://, vxlan://, etc.). Only numeric values
213+
// without a scheme get prefixed with "vlan://".
214+
if vlanVal != "" {
215+
if !strings.Contains(vlanVal, "://") {
216+
if _, err := strconv.Atoi(vlanVal); err == nil {
217+
vlanVal = "vlan://" + vlanVal
218+
}
219+
}
220+
createParams.SetVlan(vlanVal)
221+
}
222+
}
157223
resp, err := client.Network.CreateNetwork(createParams)
158224
if err != nil {
159225
return fmt.Errorf("cloudstack create network error: %w", err)

pkg/handlers/resolve_helpers.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,29 @@ func ResolveDiskOffering(name string) (string, error) {
7676
return resp.DiskOfferings[0].Id, nil
7777
}
7878

79+
// ResolveNetworkOffering returns the network offering ID for a given name.
80+
func ResolveNetworkOffering(name string) (string, error) {
81+
// If the value looks like a UUID, treat it as an ID and return it.
82+
if IsUUID(name) {
83+
return name, nil
84+
}
85+
86+
client, err := cloudstack.NewClient()
87+
if err != nil {
88+
return "", fmt.Errorf("failed to create CloudStack client: %w", err)
89+
}
90+
params := client.NetworkOffering.NewListNetworkOfferingsParams()
91+
params.SetName(name)
92+
resp, err := client.NetworkOffering.ListNetworkOfferings(params)
93+
if err != nil {
94+
return "", fmt.Errorf("cloudstack API error: %w", err)
95+
}
96+
if resp == nil || len(resp.NetworkOfferings) == 0 {
97+
return "", fmt.Errorf("network offering %s not found", name)
98+
}
99+
return resp.NetworkOfferings[0].Id, nil
100+
}
101+
79102
// ResolveProject returns the CloudStack project ID for a given project name.
80103
func ResolveProject(name string) (string, error) {
81104
// If the value looks like a UUID, treat it as an ID and return it.

0 commit comments

Comments
 (0)