Skip to content

Commit ccd3f52

Browse files
committed
pkg/controller: consider project in yaml metadata
1 parent 2d9a2c0 commit ccd3f52

8 files changed

Lines changed: 147 additions & 20 deletions

File tree

controller/controller.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,15 @@ func (c *Controller) handleApply(w http.ResponseWriter, r *http.Request) {
279279
http.Error(w, "invalid resource document", http.StatusBadRequest)
280280
return
281281
}
282+
if p, ok := meta["project"].(string); ok && p != "" {
283+
if m, ok := meta["metadata"].(map[string]interface{}); ok {
284+
if _, hasProject := m["project"]; !hasProject {
285+
m["project"] = p
286+
meta["metadata"] = m
287+
raw, _ = json.Marshal(meta)
288+
}
289+
}
290+
}
282291
kind, _ := meta["kind"].(string)
283292

284293
switch kind {
@@ -794,7 +803,6 @@ func (c *Controller) handleDescribe(w http.ResponseWriter, r *http.Request) {
794803
}
795804
}
796805

797-
798806
func (c *Controller) handleDelete(w http.ResponseWriter, r *http.Request) {
799807
if r.Method != http.MethodPost {
800808
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -1173,6 +1181,16 @@ func (c *Controller) applyComponent(comp *v1.Component) error {
11731181

11741182
// applyVMSpec creates/updates a reusable VirtualMachineSpec resource
11751183
func (c *Controller) applyVMSpec(vs *v1.VirtualMachineSpecResource) error {
1184+
// Accept project in either metadata.project or spec.project and keep them in sync.
1185+
vmsProject := strings.TrimSpace(vs.Metadata.Project)
1186+
if vmsProject == "" {
1187+
vmsProject = strings.TrimSpace(vs.Spec.Project)
1188+
}
1189+
if vmsProject != "" {
1190+
vs.Metadata.Project = vmsProject
1191+
vs.Spec.Project = vmsProject
1192+
}
1193+
11761194
// Only allow create (apply) or idempotent re-apply with identical spec.
11771195
var existing v1.VirtualMachineSpecResource
11781196
if err := db.DB.Where("name = ?", vs.Metadata.Name).First(&existing).Error; err == nil {
@@ -1192,6 +1210,16 @@ func (c *Controller) applyVMSpec(vs *v1.VirtualMachineSpecResource) error {
11921210

11931211
// applyVM creates/updates a VirtualMachine resource
11941212
func (c *Controller) applyVM(vm *v1.VirtualMachine) error {
1213+
// Accept project in either metadata.project or spec.project and keep them in sync.
1214+
vmProject := strings.TrimSpace(vm.Metadata.Project)
1215+
if vmProject == "" {
1216+
vmProject = strings.TrimSpace(vm.Spec.Project)
1217+
}
1218+
if vmProject != "" {
1219+
vm.Metadata.Project = vmProject
1220+
vm.Spec.Project = vmProject
1221+
}
1222+
11951223
// Attempt to find the VM in CloudStack by CloudStackID or by name
11961224
// If CloudStackID is empty, try to discover VM in CloudStack
11971225
if vm.CloudStackID == "" {

pkg/handlers/affinitygroup.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,21 @@ func ApplyAffinityGroup(ag *v1.AffinityGroup) (string, error) {
2222
return "", fmt.Errorf("failed to create CloudStack client: %w", err)
2323
}
2424

25-
// Try to find by name
26-
existing, _, err := client.AffinityGroup.GetAffinityGroupByName(name)
27-
if err == nil && existing != nil {
25+
// Try to find by name in the provided project scope.
26+
listParams := client.AffinityGroup.NewListAffinityGroupsParams()
27+
listParams.SetName(name)
28+
if err := setProjectOnParams(listParams, ag.Metadata.Project); err != nil {
29+
return "", err
30+
}
31+
listResp, err := client.AffinityGroup.ListAffinityGroups(listParams)
32+
if err != nil {
33+
return "", fmt.Errorf("failed to list affinity groups: %w", err)
34+
}
35+
if listResp != nil && len(listResp.AffinityGroups) > 0 {
36+
existing := listResp.AffinityGroups[0]
37+
if ag.Metadata.Project != "" {
38+
return "", fmt.Errorf("affinitygroup %s already exists in project %s (id=%s); updates are not supported", name, ag.Metadata.Project, existing.Id)
39+
}
2840
return "", fmt.Errorf("affinitygroup %s already exists in CloudStack (id=%s); updates are not supported", name, existing.Id)
2941
}
3042

pkg/handlers/network.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,5 +195,8 @@ func ApplyNetwork(netRes *v1.Network) (string, error) {
195195
}
196196
// Resource exists — updates are not supported at this time
197197
existing := listResp.Networks[0]
198+
if netRes.Metadata.Project != "" {
199+
return "", fmt.Errorf("network %s already exists in project %s (id=%s); updates are not supported", name, netRes.Metadata.Project, existing.Id)
200+
}
198201
return "", fmt.Errorf("network %s already exists in CloudStack (id=%s); updates are not supported", name, existing.Id)
199202
}

pkg/handlers/securitygroup.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,20 @@ func ApplySecurityGroup(sg *v1.SecurityGroup) (string, error) {
8080
if err != nil {
8181
return "", fmt.Errorf("failed to create CloudStack client: %w", err)
8282
}
83-
existing, _, err := client.SecurityGroup.GetSecurityGroupByName(sg.Metadata.Name)
84-
if existing != nil {
83+
listParams := client.SecurityGroup.NewListSecurityGroupsParams()
84+
listParams.SetSecuritygroupname(sg.Metadata.Name)
85+
if err := setProjectOnParams(listParams, sg.Metadata.Project); err != nil {
86+
return "", err
87+
}
88+
listResp, err := client.SecurityGroup.ListSecurityGroups(listParams)
89+
if err != nil {
90+
return "", fmt.Errorf("failed to list security groups: %w", err)
91+
}
92+
if listResp != nil && len(listResp.SecurityGroups) > 0 {
93+
existing := listResp.SecurityGroups[0]
94+
if sg.Metadata.Project != "" {
95+
return "", fmt.Errorf("securitygroup %s already exists in project %s (id=%s); updates are not supported", sg.Metadata.Name, sg.Metadata.Project, existing.Id)
96+
}
8597
return "", fmt.Errorf("securitygroup %s already exists in CloudStack (id=%s); updates are not supported", sg.Metadata.Name, existing.Id)
8698
}
8799
// Create security group with optional description from metadata.annotations["description"]

pkg/handlers/sshkey.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ func ApplySSHKey(key *v1.SSHKey) (string, error) {
9292
return "", fmt.Errorf("cloudstack API error: %w", err)
9393
}
9494
if resp != nil && len(resp.SSHKeyPairs) > 0 {
95+
if key.Metadata.Project != "" {
96+
return "", fmt.Errorf("sshkey %s already exists in project %s (fingerprint=%s); updates are not supported", key.Metadata.Name, key.Metadata.Project, resp.SSHKeyPairs[0].Fingerprint)
97+
}
9598
return "", fmt.Errorf("sshkey %s already exists in CloudStack (fingerprint=%s); updates are not supported", key.Metadata.Name, resp.SSHKeyPairs[0].Fingerprint)
9699
}
97100

pkg/handlers/userdata.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ func ApplyUserData(ud *v1.UserData) (string, error) {
2323
if err != nil {
2424
return "", fmt.Errorf("failed to create CloudStack client: %w", err)
2525
}
26+
listParams := client.User.NewListUserDataParams()
27+
listParams.SetName(ud.Metadata.Name)
28+
if err := setProjectOnParams(listParams, ud.Metadata.Project); err != nil {
29+
return "", err
30+
}
31+
listResp, err := client.User.ListUserData(listParams)
32+
if err != nil {
33+
return "", fmt.Errorf("failed to list userdata: %w", err)
34+
}
35+
if listResp != nil && len(listResp.UserData) > 0 {
36+
existing := listResp.UserData[0]
37+
if ud.Metadata.Project != "" {
38+
return "", fmt.Errorf("userdata %s already exists in project %s (id=%s); updates are not supported", ud.Metadata.Name, ud.Metadata.Project, existing.Id)
39+
}
40+
return "", fmt.Errorf("userdata %s already exists in CloudStack (id=%s); updates are not supported", ud.Metadata.Name, existing.Id)
41+
}
2642
// Register UserData in CloudStack as a standalone UserData entry.
2743
// Use the SDK register/create method; CloudStack expects the content
2844
// to be provided as a string.

pkg/handlers/virtualmachine.go

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,48 @@ import (
1111
cs "github.com/apache/cloudstack-go/v2/cloudstack"
1212
)
1313

14+
func vmAttachedToAnyNetwork(vm *cs.VirtualMachine, networkIDs []string) bool {
15+
if vm == nil || len(networkIDs) == 0 {
16+
return false
17+
}
18+
requested := make(map[string]struct{}, len(networkIDs))
19+
for _, id := range networkIDs {
20+
if id != "" {
21+
requested[id] = struct{}{}
22+
}
23+
}
24+
for _, nic := range vm.Nic {
25+
if _, ok := requested[nic.Networkid]; ok {
26+
return true
27+
}
28+
}
29+
return false
30+
}
31+
32+
func findExistingVMInScope(client *cs.CloudStackClient, name, project string, networkIDs []string) (*cs.VirtualMachine, error) {
33+
params := client.VirtualMachine.NewListVirtualMachinesParams()
34+
params.SetName(name)
35+
if err := setProjectOnParams(params, project); err != nil {
36+
return nil, err
37+
}
38+
resp, err := client.VirtualMachine.ListVirtualMachines(params)
39+
if err != nil {
40+
return nil, fmt.Errorf("failed to list virtual machines: %w", err)
41+
}
42+
if resp == nil || len(resp.VirtualMachines) == 0 {
43+
return nil, nil
44+
}
45+
if len(networkIDs) == 0 {
46+
return resp.VirtualMachines[0], nil
47+
}
48+
for _, existing := range resp.VirtualMachines {
49+
if vmAttachedToAnyNetwork(existing, networkIDs) {
50+
return existing, nil
51+
}
52+
}
53+
return nil, nil
54+
}
55+
1456
// ListVMs queries CloudStack and returns the SDK response for callers to format.
1557
func ListVMs(name, project string, allProjects bool) (any, error) {
1658
client, err := cloudstack.NewClient()
@@ -103,12 +145,9 @@ func ApplyVirtualMachineManaged(vm *v1.VirtualMachine, managed bool) (string, er
103145
if err != nil {
104146
return "", fmt.Errorf("failed to create CloudStack client: %w", err)
105147
}
106-
// Check for existing VM with the same name - we don't support updates
107-
listParams := client.VirtualMachine.NewListVirtualMachinesParams()
108-
listParams.SetName(vm.Metadata.Name)
109-
listResp, lerr := client.VirtualMachine.ListVirtualMachines(listParams)
110-
if lerr == nil && listResp != nil && len(listResp.VirtualMachines) > 0 {
111-
return "", fmt.Errorf("virtualmachine %s already exists in CloudStack (id=%s); updates are not supported", vm.Metadata.Name, listResp.VirtualMachines[0].Id)
148+
project := vm.Spec.Project
149+
if project == "" {
150+
project = vm.Metadata.Project
112151
}
113152

114153
// Resolve potential name references to IDs for template, service offering, and networks
@@ -132,6 +171,19 @@ func ApplyVirtualMachineManaged(vm *v1.VirtualMachine, managed bool) (string, er
132171
resolvedNets = append(resolvedNets, nid)
133172
}
134173

174+
// For create via apply, only treat the VM as existing when it is found in the
175+
// same project scope and, when requested, attached to one of the requested networks.
176+
existingVM, lerr := findExistingVMInScope(client, vm.Metadata.Name, project, resolvedNets)
177+
if lerr != nil {
178+
return "", lerr
179+
}
180+
if existingVM != nil {
181+
if project != "" {
182+
return "", fmt.Errorf("virtualmachine %s already exists in project %s (id=%s); updates are not supported", vm.Metadata.Name, project, existingVM.Id)
183+
}
184+
return "", fmt.Errorf("virtualmachine %s already exists in CloudStack (id=%s); updates are not supported", vm.Metadata.Name, existingVM.Id)
185+
}
186+
135187
params := client.VirtualMachine.NewDeployVirtualMachineParams(serviceOfferingID, templateID, "")
136188
params.SetName(vm.Metadata.Name)
137189
// If a zone is provided in the spec, try to resolve the zone name to an ID.
@@ -143,13 +195,8 @@ func ApplyVirtualMachineManaged(vm *v1.VirtualMachine, managed bool) (string, er
143195
}
144196
params.SetZoneid(zid)
145197
}
146-
if vm.Spec.Project != "" {
147-
// Accept either a project UUID or a project name; try resolving name first.
148-
if pid, perr := ResolveProject(vm.Spec.Project); perr == nil {
149-
params.SetProjectid(pid)
150-
} else {
151-
params.SetProjectid(vm.Spec.Project)
152-
}
198+
if err := setProjectOnParams(params, project); err != nil {
199+
return "", err
153200
}
154201
if len(vm.Spec.Networks) > 0 {
155202
params.SetNetworkids(resolvedNets)

pkg/handlers/volume.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,13 @@ func ApplyVolume(vol *v1.Volume) (string, error) {
8383
return "", err
8484
}
8585
resp, err := client.Volume.ListVolumes(params)
86-
if err == nil && resp != nil && len(resp.Volumes) > 0 {
86+
if err != nil {
87+
return "", fmt.Errorf("failed to list volumes: %w", err)
88+
}
89+
if resp != nil && len(resp.Volumes) > 0 {
90+
if vol.Metadata.Project != "" {
91+
return "", fmt.Errorf("volume %s already exists in project %s (id=%s); updates are not supported", vol.Metadata.Name, vol.Metadata.Project, resp.Volumes[0].Id)
92+
}
8793
return "", fmt.Errorf("volume %s already exists in CloudStack (id=%s); updates are not supported", vol.Metadata.Name, resp.Volumes[0].Id)
8894
}
8995

0 commit comments

Comments
 (0)