Skip to content

Commit 5a0cf9e

Browse files
committed
fix: include domain ID when looking up projects by ID
Fix issue where getProjectByID() would always return "id not found" while getProjectByName() could find the same project. CloudStack projects are only unique within a domain context, so we now include domain ID in lookups. - Modified getProjectByID() to accept optional domain parameter - Updated all calls to include domain when available - Updated test functions accordingly - Updated documentation to clarify domain requirement for project imports
1 parent 62623c3 commit 5a0cf9e

File tree

2 files changed

+402
-49
lines changed

2 files changed

+402
-49
lines changed

cloudstack/resource_cloudstack_project.go

Lines changed: 200 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
package cloudstack
2020

2121
import (
22+
"context"
2223
"fmt"
2324
"log"
2425
"strings"
2526
"time"
2627

2728
"github.com/apache/cloudstack-go/v2/cloudstack"
29+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
2830
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
29-
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
3031
)
3132

3233
func resourceCloudStackProject() *schema.Resource {
@@ -75,22 +76,48 @@ func resourceCloudStackProject() *schema.Resource {
7576
}
7677
}
7778

78-
func resourceCloudStackProjectCreate(d *schema.ResourceData, meta interface{}) error {
79+
func resourceCloudStackProjectCreate(d *schema.ResourceData, meta any) error {
7980
cs := meta.(*cloudstack.CloudStackClient)
8081

8182
// Get the name and display_text
8283
name := d.Get("name").(string)
8384
displaytext := d.Get("display_text").(string)
8485

86+
// Get domain if provided
87+
var domain string
88+
if domainParam, ok := d.GetOk("domain"); ok {
89+
domain = domainParam.(string)
90+
}
91+
92+
// Check if a project with this name already exists
93+
existingProject, err := getProjectByName(cs, name, domain)
94+
if err == nil {
95+
// Project with this name already exists
96+
log.Printf("[DEBUG] Project with name %s already exists, using existing project with ID: %s", name, existingProject.Id)
97+
d.SetId(existingProject.Id)
98+
99+
// Set the basic attributes to match the existing project
100+
d.Set("name", existingProject.Name)
101+
d.Set("display_text", existingProject.Displaytext)
102+
d.Set("domain", existingProject.Domain)
103+
104+
return resourceCloudStackProjectRead(d, meta)
105+
} else if !strings.Contains(err.Error(), "not found") {
106+
// If we got an error other than "not found", return it
107+
return fmt.Errorf("error checking for existing project: %s", err)
108+
}
109+
110+
// Project doesn't exist, create a new one
111+
85112
// The CloudStack API parameter order differs between versions:
86113
// - In API 4.18 and lower: displaytext is the first parameter and name is the second
87114
// - In API 4.19 and higher: name is the first parameter and displaytext is optional
88115
// The CloudStack Go SDK uses the API 4.18 parameter order
89116
p := cs.Project.NewCreateProjectParams(displaytext, name)
90117

91118
// Set the domain if provided
92-
if domain, ok := d.GetOk("domain"); ok {
93-
domainid, e := retrieveID(cs, "domain", domain.(string))
119+
if domain != "" {
120+
domainid, e := retrieveID(cs, "domain", domain)
94121
if e != nil {
95122
return e.Error()
96123
}
@@ -115,21 +142,24 @@ func resourceCloudStackProjectCreate(d *schema.ResourceData, meta interface{}) e
115142
log.Printf("[DEBUG] Creating project %s", name)
116143
r, err := cs.Project.CreateProject(p)
117144
if err != nil {
118-
return fmt.Errorf("Error creating project %s: %s", name, err)
145+
return fmt.Errorf("error creating project %s: %s", name, err)
119146
}
120147

121148
d.SetId(r.Id)
149+
log.Printf("[DEBUG] Project created with ID: %s", r.Id)
122150

123-
// Wait for the project to be available, but with a shorter timeout
124-
// to prevent getting stuck indefinitely
125-
err = resource.Retry(30*time.Second, func() *resource.RetryError {
126-
project, err := getProjectByID(cs, d.Id())
151+
// Wait for the project to be available
152+
// Use a longer timeout to ensure project creation completes
153+
ctx := context.Background()
154+
155+
err = retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
156+
project, err := getProjectByID(cs, d.Id(), domain)
127157
if err != nil {
128158
if strings.Contains(err.Error(), "not found") {
129159
log.Printf("[DEBUG] Project %s not found yet, retrying...", d.Id())
130-
return resource.RetryableError(fmt.Errorf("Project not yet created: %s", err))
160+
return retry.RetryableError(fmt.Errorf("project not yet created: %s", err))
131161
}
132-
return resource.NonRetryableError(fmt.Errorf("Error retrieving project: %s", err))
162+
return retry.NonRetryableError(fmt.Errorf("Error retrieving project: %s", err))
133163
}
134164

135165
log.Printf("[DEBUG] Project %s found with name %s", d.Id(), project.Name)
@@ -147,40 +177,122 @@ func resourceCloudStackProjectCreate(d *schema.ResourceData, meta interface{}) e
147177
}
148178

149179
// Helper function to get a project by ID
150-
func getProjectByID(cs *cloudstack.CloudStackClient, id string) (*cloudstack.Project, error) {
180+
func getProjectByID(cs *cloudstack.CloudStackClient, id string, domain ...string) (*cloudstack.Project, error) {
151181
p := cs.Project.NewListProjectsParams()
152182
p.SetId(id)
153183

184+
// If domain is provided, use it to narrow the search
185+
if len(domain) > 0 && domain[0] != "" {
186+
log.Printf("[DEBUG] Looking up project with ID: %s in domain: %s", id, domain[0])
187+
domainID, err := retrieveID(cs, "domain", domain[0])
188+
if err != nil {
189+
log.Printf("[WARN] Error retrieving domain ID for domain %s: %v", domain[0], err)
190+
// Continue without domain ID, but log the warning
191+
} else {
192+
p.SetDomainid(domainID)
193+
}
194+
} else {
195+
log.Printf("[DEBUG] Looking up project with ID: %s (no domain specified)", id)
196+
}
197+
154198
l, err := cs.Project.ListProjects(p)
155199
if err != nil {
200+
log.Printf("[ERROR] Error calling ListProjects with ID %s: %v", id, err)
156201
return nil, err
157202
}
158203

204+
log.Printf("[DEBUG] ListProjects returned Count: %d for ID: %s", l.Count, id)
205+
159206
if l.Count == 0 {
160207
return nil, fmt.Errorf("project with id %s not found", id)
161208
}
162209

210+
// Add validation to ensure the returned project ID matches the requested ID
211+
if l.Projects[0].Id != id {
212+
log.Printf("[WARN] Project ID mismatch - requested: %s, got: %s", id, l.Projects[0].Id)
213+
// Continue anyway to see if this is the issue
214+
}
215+
216+
log.Printf("[DEBUG] Found project with ID: %s, Name: %s", l.Projects[0].Id, l.Projects[0].Name)
163217
return l.Projects[0], nil
164218
}
165219

166-
func resourceCloudStackProjectRead(d *schema.ResourceData, meta interface{}) error {
220+
// Helper function to get a project by name
221+
func getProjectByName(cs *cloudstack.CloudStackClient, name string, domain string) (*cloudstack.Project, error) {
222+
p := cs.Project.NewListProjectsParams()
223+
p.SetName(name)
224+
225+
// If domain is provided, use it to narrow the search
226+
if domain != "" {
227+
domainID, err := retrieveID(cs, "domain", domain)
228+
if err != nil {
229+
return nil, fmt.Errorf("error retrieving domain ID: %v", err)
230+
}
231+
p.SetDomainid(domainID)
232+
}
233+
234+
log.Printf("[DEBUG] Looking up project with name: %s", name)
235+
l, err := cs.Project.ListProjects(p)
236+
if err != nil {
237+
return nil, err
238+
}
239+
240+
if l.Count == 0 {
241+
return nil, fmt.Errorf("project with name %s not found", name)
242+
}
243+
244+
// If multiple projects with the same name exist, log a warning and return the first one
245+
if l.Count > 1 {
246+
log.Printf("[WARN] Multiple projects found with name %s, using the first one", name)
247+
}
248+
249+
log.Printf("[DEBUG] Found project %s with ID: %s", name, l.Projects[0].Id)
250+
return l.Projects[0], nil
251+
}
252+
253+
func resourceCloudStackProjectRead(d *schema.ResourceData, meta any) error {
167254
cs := meta.(*cloudstack.CloudStackClient)
168255

169256
log.Printf("[DEBUG] Retrieving project %s", d.Id())
170257

171-
// Get the project details
172-
project, err := getProjectByID(cs, d.Id())
173-
if err != nil {
174-
if strings.Contains(err.Error(), "not found") ||
175-
strings.Contains(err.Error(), fmt.Sprintf(
176-
"Invalid parameter id value=%s due to incorrect long value format, "+
177-
"or entity does not exist", d.Id())) {
178-
log.Printf("[DEBUG] Project %s does no longer exist", d.Id())
179-
d.SetId("")
180-
return nil
258+
// Get project name and domain for potential fallback lookup
259+
name := d.Get("name").(string)
260+
var domain string
261+
if domainParam, ok := d.GetOk("domain"); ok {
262+
domain = domainParam.(string)
263+
}
264+
265+
// Get the project details by ID
266+
project, err := getProjectByID(cs, d.Id(), domain)
267+
268+
// If project not found by ID and we have a name, try to find it by name
269+
if err != nil && name != "" && (strings.Contains(err.Error(), "not found") ||
270+
strings.Contains(err.Error(), "does not exist") ||
271+
strings.Contains(err.Error(), "could not be found") ||
272+
strings.Contains(err.Error(), fmt.Sprintf(
273+
"Invalid parameter id value=%s due to incorrect long value format, "+
274+
"or entity does not exist", d.Id()))) {
275+
276+
log.Printf("[DEBUG] Project %s not found by ID, trying to find by name: %s", d.Id(), name)
277+
project, err = getProjectByName(cs, name, domain)
278+
279+
// If project not found by name either, resource doesn't exist
280+
if err != nil {
281+
if strings.Contains(err.Error(), "not found") {
282+
log.Printf("[DEBUG] Project with name %s not found either, marking as gone", name)
283+
d.SetId("")
284+
return nil
285+
}
286+
// For other errors during name lookup, return them
287+
return fmt.Errorf("error looking up project by name: %s", err)
181288
}
182289

183-
return err
290+
// Found by name, update the ID
291+
log.Printf("[DEBUG] Found project by name %s with ID: %s", name, project.Id)
292+
d.SetId(project.Id)
293+
} else if err != nil {
294+
// For other errors during ID lookup, return them
295+
return fmt.Errorf("error retrieving project %s: %s", d.Id(), err)
184296
}
185297

186298
log.Printf("[DEBUG] Found project %s: %s", d.Id(), project.Name)
@@ -253,7 +365,7 @@ func resourceCloudStackProjectRead(d *schema.ResourceData, meta interface{}) err
253365
return nil
254366
}
255367

256-
func resourceCloudStackProjectUpdate(d *schema.ResourceData, meta interface{}) error {
368+
func resourceCloudStackProjectUpdate(d *schema.ResourceData, meta any) error {
257369
cs := meta.(*cloudstack.CloudStackClient)
258370

259371
// Check if the name or display text is changed
@@ -314,26 +426,34 @@ func resourceCloudStackProjectUpdate(d *schema.ResourceData, meta interface{}) e
314426
}
315427
}
316428

317-
// Wait for the project to be updated, but with a shorter timeout
318-
err := resource.Retry(30*time.Second, func() *resource.RetryError {
319-
project, err := getProjectByID(cs, d.Id())
429+
// Wait for the project to be updated
430+
ctx := context.Background()
431+
432+
// Get domain if provided
433+
var domain string
434+
if domainParam, ok := d.GetOk("domain"); ok {
435+
domain = domainParam.(string)
436+
}
437+
438+
err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
439+
project, err := getProjectByID(cs, d.Id(), domain)
320440
if err != nil {
321441
if strings.Contains(err.Error(), "not found") {
322442
log.Printf("[DEBUG] Project %s not found after update, retrying...", d.Id())
323-
return resource.RetryableError(fmt.Errorf("Project not found after update: %s", err))
443+
return retry.RetryableError(fmt.Errorf("project not found after update: %s", err))
324444
}
325-
return resource.NonRetryableError(fmt.Errorf("Error retrieving project after update: %s", err))
445+
return retry.NonRetryableError(fmt.Errorf("Error retrieving project after update: %s", err))
326446
}
327447

328448
// Check if the project has the expected values
329449
if d.HasChange("name") && project.Name != d.Get("name").(string) {
330450
log.Printf("[DEBUG] Project %s name not updated yet, retrying...", d.Id())
331-
return resource.RetryableError(fmt.Errorf("Project name not updated yet"))
451+
return retry.RetryableError(fmt.Errorf("project name not updated yet"))
332452
}
333453

334454
if d.HasChange("display_text") && project.Displaytext != d.Get("display_text").(string) {
335455
log.Printf("[DEBUG] Project %s display_text not updated yet, retrying...", d.Id())
336-
return resource.RetryableError(fmt.Errorf("Project display_text not updated yet"))
456+
return retry.RetryableError(fmt.Errorf("project display_text not updated yet"))
337457
}
338458

339459
log.Printf("[DEBUG] Project %s updated successfully", d.Id())
@@ -350,24 +470,64 @@ func resourceCloudStackProjectUpdate(d *schema.ResourceData, meta interface{}) e
350470
return resourceCloudStackProjectRead(d, meta)
351471
}
352472

353-
func resourceCloudStackProjectDelete(d *schema.ResourceData, meta interface{}) error {
473+
func resourceCloudStackProjectDelete(d *schema.ResourceData, meta any) error {
354474
cs := meta.(*cloudstack.CloudStackClient)
355475

476+
// Get project name and domain for potential fallback lookup
477+
name := d.Get("name").(string)
478+
var domain string
479+
if domainParam, ok := d.GetOk("domain"); ok {
480+
domain = domainParam.(string)
481+
}
482+
483+
// First check if the project still exists by ID
484+
log.Printf("[DEBUG] Checking if project %s exists before deleting", d.Id())
485+
project, err := getProjectByID(cs, d.Id(), domain)
486+
487+
// If project not found by ID, try to find it by name
488+
if err != nil && strings.Contains(err.Error(), "not found") {
489+
log.Printf("[DEBUG] Project %s not found by ID, trying to find by name: %s", d.Id(), name)
490+
project, err = getProjectByName(cs, name, domain)
491+
492+
// If project not found by name either, we're done
493+
if err != nil {
494+
if strings.Contains(err.Error(), "not found") {
495+
log.Printf("[DEBUG] Project with name %s not found either, nothing to delete", name)
496+
return nil
497+
}
498+
// For other errors during name lookup, return them
499+
return fmt.Errorf("error looking up project by name: %s", err)
500+
}
501+
502+
// Found by name, update the ID
503+
log.Printf("[DEBUG] Found project by name %s with ID: %s", name, project.Id)
504+
d.SetId(project.Id)
505+
} else if err != nil {
506+
// For other errors during ID lookup, return them
507+
return fmt.Errorf("error checking project existence before delete: %s", err)
508+
}
509+
510+
log.Printf("[DEBUG] Found project %s (%s), proceeding with delete", d.Id(), project.Name)
511+
356512
// Create a new parameter struct
357513
p := cs.Project.NewDeleteProjectParams(d.Id())
358514

359-
log.Printf("[INFO] Deleting project: %s", d.Id())
360-
_, err := cs.Project.DeleteProject(p)
515+
log.Printf("[INFO] Deleting project: %s (%s)", d.Id(), project.Name)
516+
_, err = cs.Project.DeleteProject(p)
361517
if err != nil {
362-
// This is a very poor way to be told the ID does no longer exist :(
363-
if strings.Contains(err.Error(), fmt.Sprintf(
364-
"Invalid parameter id value=%s due to incorrect long value format, "+
365-
"or entity does not exist", d.Id())) {
518+
// Check for various "not found" or "does not exist" error patterns
519+
if strings.Contains(err.Error(), "not found") ||
520+
strings.Contains(err.Error(), "does not exist") ||
521+
strings.Contains(err.Error(), fmt.Sprintf(
522+
"Invalid parameter id value=%s due to incorrect long value format, "+
523+
"or entity does not exist", d.Id())) {
524+
log.Printf("[DEBUG] Project %s no longer exists after delete attempt", d.Id())
366525
return nil
367526
}
368527

369-
return fmt.Errorf("Error deleting project %s: %s", d.Id(), err)
528+
return fmt.Errorf("error deleting project %s: %s", d.Id(), err)
370529
}
371530

531+
log.Printf("[DEBUG] Successfully deleted project: %s (%s)", d.Id(), project.Name)
372532
return nil
373533
}

0 commit comments

Comments
 (0)