1919package cloudstack
2020
2121import (
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
3233func 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