diff --git a/README.md b/README.md index 32dbae1c..1b31d7ca 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ When Docker started the container you can go to http://localhost:8080/client and Once the login page is shown and you can login, you need to provision a simulated data-center: ```sh -docker exec -it cloudstack-simulator python /root/tools/marvin/marvin/deployDataCenter.py -i /root/setup/dev/advanced.cfg +docker exec -it simulator python /root/tools/marvin/marvin/deployDataCenter.py -i /root/setup/dev/advanced.cfg ``` If you refresh the client or login again, you will now get passed the initial welcome screen and be able to go to your account details and retrieve the API key and secret. Export those together with the URL: @@ -200,7 +200,7 @@ Check and ensure TF provider passes builds, GA and run this for local checks: goreleaser release --snapshot --clean ``` -Next, create a personalised Github token:
 https://github.com/settings/tokens/new?scopes=repo,write:packages +Next, create a personalised Github token: https://github.com/settings/tokens/new?scopes=repo,write:packages ``` export GITHUB_TOKEN="YOUR_GH_TOKEN" diff --git a/cloudstack/data_source_cloudstack_project.go b/cloudstack/data_source_cloudstack_project.go new file mode 100644 index 00000000..e1dae66d --- /dev/null +++ b/cloudstack/data_source_cloudstack_project.go @@ -0,0 +1,215 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "encoding/json" + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackProject() *schema.Resource { + return &schema.Resource{ + Read: datasourceCloudStackProjectRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + + // Computed values + "name": { + Type: schema.TypeString, + Computed: true, + }, + + "display_text": { + Type: schema.TypeString, + Computed: true, + }, + + "domain": { + Type: schema.TypeString, + Computed: true, + }, + + "account": { + Type: schema.TypeString, + Computed: true, + }, + + "state": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tagsSchema(), + }, + } +} + +func datasourceCloudStackProjectRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + p := cs.Project.NewListProjectsParams() + csProjects, err := cs.Project.ListProjects(p) + + if err != nil { + return fmt.Errorf("failed to list projects: %s", err) + } + + filters := d.Get("filter") + var projects []*cloudstack.Project + + for _, v := range csProjects.Projects { + match, err := applyProjectFilters(v, filters.(*schema.Set)) + if err != nil { + return err + } + if match { + projects = append(projects, v) + } + } + + if len(projects) == 0 { + return fmt.Errorf("no project matches the specified filters") + } + + // Return the latest project from the list of filtered projects according + // to its creation date + project, err := latestProject(projects) + if err != nil { + return err + } + log.Printf("[DEBUG] Selected project: %s\n", project.Name) + + return projectDescriptionAttributes(d, project) +} + +func projectDescriptionAttributes(d *schema.ResourceData, project *cloudstack.Project) error { + d.SetId(project.Id) + d.Set("name", project.Name) + d.Set("display_text", project.Displaytext) + d.Set("domain", project.Domain) + d.Set("state", project.State) + + // Handle account information safely + if len(project.Owner) > 0 { + for _, owner := range project.Owner { + if account, ok := owner["account"]; ok { + d.Set("account", account) + break + } + } + } + + d.Set("tags", tagsToMap(project.Tags)) + + return nil +} + +func latestProject(projects []*cloudstack.Project) (*cloudstack.Project, error) { + var latest time.Time + var project *cloudstack.Project + + for _, v := range projects { + created, err := time.Parse("2006-01-02T15:04:05-0700", v.Created) + if err != nil { + return nil, fmt.Errorf("failed to parse creation date of a project: %s", err) + } + + if created.After(latest) { + latest = created + project = v + } + } + + return project, nil +} + +func applyProjectFilters(project *cloudstack.Project, filters *schema.Set) (bool, error) { + var projectJSON map[string]interface{} + k, _ := json.Marshal(project) + err := json.Unmarshal(k, &projectJSON) + if err != nil { + return false, err + } + + for _, f := range filters.List() { + m := f.(map[string]interface{}) + r, err := regexp.Compile(m["value"].(string)) + if err != nil { + return false, fmt.Errorf("invalid regex: %s", err) + } + + // Handle special case for owner/account + if m["name"].(string) == "account" { + if len(project.Owner) == 0 { + return false, nil + } + + found := false + for _, owner := range project.Owner { + if account, ok := owner["account"]; ok { + if r.MatchString(fmt.Sprintf("%v", account)) { + found = true + break + } + } + } + + if !found { + return false, nil + } + continue + } + + updatedName := strings.ReplaceAll(m["name"].(string), "_", "") + + // Handle fields that might not exist in the JSON + fieldValue, exists := projectJSON[updatedName] + if !exists { + return false, nil + } + + // Handle different types of fields + switch v := fieldValue.(type) { + case string: + if !r.MatchString(v) { + return false, nil + } + case float64: + if !r.MatchString(fmt.Sprintf("%v", v)) { + return false, nil + } + case bool: + if !r.MatchString(fmt.Sprintf("%v", v)) { + return false, nil + } + default: + // Skip fields that aren't simple types + continue + } + } + + return true, nil +} diff --git a/cloudstack/data_source_cloudstack_project_test.go b/cloudstack/data_source_cloudstack_project_test.go new file mode 100644 index 00000000..cbbaecd5 --- /dev/null +++ b/cloudstack/data_source_cloudstack_project_test.go @@ -0,0 +1,115 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccProjectDataSource_basic(t *testing.T) { + resourceName := "cloudstack_project.project-resource" + datasourceName := "data.cloudstack_project.project-data-source" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testProjectDataSourceConfig_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "display_text", resourceName, "display_text"), + resource.TestCheckResourceAttrPair(datasourceName, "domain", resourceName, "domain"), + ), + }, + }, + }) +} + +func TestAccProjectDataSource_withAccount(t *testing.T) { + resourceName := "cloudstack_project.project-account-resource" + datasourceName := "data.cloudstack_project.project-account-data-source" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testProjectDataSourceConfig_withAccount, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "display_text", resourceName, "display_text"), + resource.TestCheckResourceAttrPair(datasourceName, "domain", resourceName, "domain"), + resource.TestCheckResourceAttrPair(datasourceName, "account", resourceName, "account"), + ), + }, + }, + }) +} + +const testProjectDataSourceConfig_basic = ` +resource "cloudstack_project" "project-resource" { + name = "test-project-datasource" + display_text = "Test Project for Data Source" +} + +data "cloudstack_project" "project-data-source" { + filter { + name = "name" + value = "test-project-datasource" + } + depends_on = [ + cloudstack_project.project-resource + ] +} + +output "project-output" { + value = data.cloudstack_project.project-data-source +} +` + +const testProjectDataSourceConfig_withAccount = ` +resource "cloudstack_project" "project-account-resource" { + name = "test-project-account-datasource" + display_text = "Test Project with Account for Data Source" + account = "admin" + domain = "ROOT" +} + +data "cloudstack_project" "project-account-data-source" { + filter { + name = "name" + value = "test-project-account-datasource" + } + filter { + name = "account" + value = "admin" + } + depends_on = [ + cloudstack_project.project-account-resource + ] +} + +output "project-account-output" { + value = data.cloudstack_project.project-account-data-source +} +` diff --git a/cloudstack/provider.go b/cloudstack/provider.go index a71df0e5..7696ba28 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -90,6 +90,7 @@ func Provider() *schema.Provider { "cloudstack_user": dataSourceCloudstackUser(), "cloudstack_vpn_connection": dataSourceCloudstackVPNConnection(), "cloudstack_pod": dataSourceCloudstackPod(), + "cloudstack_project": dataSourceCloudstackProject(), }, ResourcesMap: map[string]*schema.Resource{ @@ -112,6 +113,7 @@ func Provider() *schema.Provider { "cloudstack_nic": resourceCloudStackNIC(), "cloudstack_port_forward": resourceCloudStackPortForward(), "cloudstack_private_gateway": resourceCloudStackPrivateGateway(), + "cloudstack_project": resourceCloudStackProject(), "cloudstack_secondary_ipaddress": resourceCloudStackSecondaryIPAddress(), "cloudstack_security_group": resourceCloudStackSecurityGroup(), "cloudstack_security_group_rule": resourceCloudStackSecurityGroupRule(), diff --git a/cloudstack/resource_cloudstack_egress_firewall.go b/cloudstack/resource_cloudstack_egress_firewall.go index 9b7f3946..e2a83e4c 100644 --- a/cloudstack/resource_cloudstack_egress_firewall.go +++ b/cloudstack/resource_cloudstack_egress_firewall.go @@ -45,6 +45,13 @@ func resourceCloudStackEgressFirewall() *schema.Resource { ForceNew: true, }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "managed": { Type: schema.TypeBool, Optional: true, @@ -265,6 +272,11 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface p.SetNetworkid(d.Id()) p.SetListall(true) + // If there is a project supplied, we retrieve and set the project id + if err := setProjectid(p, cs, d); err != nil { + return err + } + l, err := cs.Firewall.ListEgressFirewallRules(p) if err != nil { return err diff --git a/cloudstack/resource_cloudstack_firewall.go b/cloudstack/resource_cloudstack_firewall.go index b7ba7e0b..a39628c7 100644 --- a/cloudstack/resource_cloudstack_firewall.go +++ b/cloudstack/resource_cloudstack_firewall.go @@ -45,6 +45,13 @@ func resourceCloudStackFirewall() *schema.Resource { ForceNew: true, }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "managed": { Type: schema.TypeBool, Optional: true, @@ -256,6 +263,11 @@ func resourceCloudStackFirewallRead(d *schema.ResourceData, meta interface{}) er p.SetIpaddressid(d.Id()) p.SetListall(true) + // If there is a project supplied, we retrieve and set the project id + if err := setProjectid(p, cs, d); err != nil { + return err + } + l, err := cs.Firewall.ListFirewallRules(p) if err != nil { return err diff --git a/cloudstack/resource_cloudstack_project.go b/cloudstack/resource_cloudstack_project.go new file mode 100644 index 00000000..49d05583 --- /dev/null +++ b/cloudstack/resource_cloudstack_project.go @@ -0,0 +1,537 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudStackProject() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackProjectCreate, + Read: resourceCloudStackProjectRead, + Update: resourceCloudStackProjectUpdate, + Delete: resourceCloudStackProjectDelete, + Importer: &schema.ResourceImporter{ + State: importStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "display_text": { + Type: schema.TypeString, + Required: true, // Required for API version 4.18 and lower. TODO: Make this optional when support for API versions older than 4.18 is dropped. + }, + + "domain": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "account": { + Type: schema.TypeString, + Optional: true, + }, + + "accountid": { + Type: schema.TypeString, + Optional: true, + }, + + "userid": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceCloudStackProjectCreate(d *schema.ResourceData, meta any) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the name and display_text + name := d.Get("name").(string) + displaytext := d.Get("display_text").(string) + + // Get domain if provided + var domain string + domainSet := false + if domainParam, ok := d.GetOk("domain"); ok { + domain = domainParam.(string) + domainSet = true + } + + // Only check for an existing project if domain is set + if domainSet { + existingProject, err := getProjectByName(cs, name, domain) + if err == nil { + // Project with this name and domain already exists + log.Printf("[DEBUG] Project with name %s and domain %s already exists, using existing project with ID: %s", name, domain, existingProject.Id) + d.SetId(existingProject.Id) + + // Set the basic attributes to match the existing project + d.Set("name", existingProject.Name) + d.Set("display_text", existingProject.Displaytext) + d.Set("domain", existingProject.Domain) + + return resourceCloudStackProjectRead(d, meta) + } else if !strings.Contains(err.Error(), "not found") { + // If we got an error other than "not found", return it + return fmt.Errorf("error checking for existing project: %s", err) + } + } + + // Project doesn't exist, create a new one + + // The CloudStack API parameter order differs between versions: + // - In API 4.18 and lower: displaytext is the first parameter and name is the second + // - In API 4.19 and higher: name is the first parameter and displaytext is optional + // The CloudStack Go SDK uses the API 4.18 parameter order + p := cs.Project.NewCreateProjectParams(displaytext, name) + + // Set the domain if provided + if domain != "" { + domainid, e := retrieveID(cs, "domain", domain) + if e != nil { + return fmt.Errorf("error retrieving domain ID: %v", e) + } + p.SetDomainid(domainid) + } + + // Set the account if provided + if account, ok := d.GetOk("account"); ok { + p.SetAccount(account.(string)) + } + + // Set the accountid if provided + if accountid, ok := d.GetOk("accountid"); ok { + p.SetAccountid(accountid.(string)) + } + + // Set the userid if provided + if userid, ok := d.GetOk("userid"); ok { + p.SetUserid(userid.(string)) + } + + log.Printf("[DEBUG] Creating project %s", name) + r, err := cs.Project.CreateProject(p) + if err != nil { + return fmt.Errorf("error creating project %s: %s", name, err) + } + + d.SetId(r.Id) + log.Printf("[DEBUG] Project created with ID: %s", r.Id) + + // Wait for the project to be available + // Use a longer timeout to ensure project creation completes + ctx := context.Background() + + err = retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + project, err := getProjectByID(cs, d.Id(), domain) + if err != nil { + if strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] Project %s not found yet, retrying...", d.Id()) + return retry.RetryableError(fmt.Errorf("project not yet created: %s", err)) + } + return retry.NonRetryableError(fmt.Errorf("Error retrieving project: %s", err)) + } + + log.Printf("[DEBUG] Project %s found with name %s", d.Id(), project.Name) + return nil + }) + + // Even if the retry times out, we should still try to read the resource + // since it might have been created successfully + if err != nil { + log.Printf("[WARN] Timeout waiting for project %s to be available: %s", d.Id(), err) + } + + // Read the resource state + return resourceCloudStackProjectRead(d, meta) +} + +// Helper function to get a project by ID +func getProjectByID(cs *cloudstack.CloudStackClient, id string, domain ...string) (*cloudstack.Project, error) { + p := cs.Project.NewListProjectsParams() + p.SetId(id) + + // If domain is provided, use it to narrow the search + if len(domain) > 0 && domain[0] != "" { + log.Printf("[DEBUG] Looking up project with ID: %s in domain: %s", id, domain[0]) + domainID, err := retrieveID(cs, "domain", domain[0]) + if err != nil { + log.Printf("[WARN] Error retrieving domain ID for domain %s: %v", domain[0], err) + // Continue without domain ID, but log the warning + } else { + p.SetDomainid(domainID) + } + } else { + log.Printf("[DEBUG] Looking up project with ID: %s (no domain specified)", id) + } + + l, err := cs.Project.ListProjects(p) + if err != nil { + log.Printf("[ERROR] Error calling ListProjects with ID %s: %v", id, err) + return nil, err + } + + log.Printf("[DEBUG] ListProjects returned Count: %d for ID: %s", l.Count, id) + + if l.Count == 0 { + return nil, fmt.Errorf("project with id %s not found", id) + } + + // Add validation to ensure the returned project ID matches the requested ID + if l.Projects[0].Id != id { + log.Printf("[WARN] Project ID mismatch - requested: %s, got: %s", id, l.Projects[0].Id) + // Continue anyway to see if this is the issue + } + + log.Printf("[DEBUG] Found project with ID: %s, Name: %s", l.Projects[0].Id, l.Projects[0].Name) + return l.Projects[0], nil +} + +// Helper function to get a project by name +func getProjectByName(cs *cloudstack.CloudStackClient, name string, domain string) (*cloudstack.Project, error) { + p := cs.Project.NewListProjectsParams() + p.SetName(name) + + // If domain is provided, use it to narrow the search + if domain != "" { + domainID, err := retrieveID(cs, "domain", domain) + if err != nil { + return nil, fmt.Errorf("error retrieving domain ID: %v", err) + } + p.SetDomainid(domainID) + } + + log.Printf("[DEBUG] Looking up project with name: %s", name) + l, err := cs.Project.ListProjects(p) + if err != nil { + return nil, err + } + + if l.Count == 0 { + return nil, fmt.Errorf("project with name %s not found", name) + } + + // If multiple projects with the same name exist, log a warning and return the first one + if l.Count > 1 { + log.Printf("[WARN] Multiple projects found with name %s, using the first one", name) + } + + log.Printf("[DEBUG] Found project %s with ID: %s", name, l.Projects[0].Id) + return l.Projects[0], nil +} + +func resourceCloudStackProjectRead(d *schema.ResourceData, meta any) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Retrieving project %s", d.Id()) + + // Get project name and domain for potential fallback lookup + name := d.Get("name").(string) + var domain string + if domainParam, ok := d.GetOk("domain"); ok { + domain = domainParam.(string) + } + + // Get the project details by ID + project, err := getProjectByID(cs, d.Id(), domain) + + // If project not found by ID and we have a name, try to find it by name + if err != nil && name != "" && (strings.Contains(err.Error(), "not found") || + strings.Contains(err.Error(), "does not exist") || + strings.Contains(err.Error(), "could not be found") || + strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id()))) { + + log.Printf("[DEBUG] Project %s not found by ID, trying to find by name: %s", d.Id(), name) + project, err = getProjectByName(cs, name, domain) + + // If project not found by name either, resource doesn't exist + if err != nil { + if strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] Project with name %s not found either, marking as gone", name) + d.SetId("") + return nil + } + // For other errors during name lookup, return them + return fmt.Errorf("error looking up project by name: %s", err) + } + + // Found by name, update the ID + log.Printf("[DEBUG] Found project by name %s with ID: %s", name, project.Id) + d.SetId(project.Id) + } else if err != nil { + // For other errors during ID lookup, return them + return fmt.Errorf("error retrieving project %s: %s", d.Id(), err) + } + + log.Printf("[DEBUG] Found project %s: %s", d.Id(), project.Name) + + // Set the basic attributes + d.Set("name", project.Name) + d.Set("display_text", project.Displaytext) + d.Set("domain", project.Domain) + + // Handle owner information more safely + // Only set the account, accountid, and userid if they were explicitly set in the configuration + // and if the owner information is available + if _, ok := d.GetOk("account"); ok { + // Safely handle the case where project.Owner might be nil or empty + if len(project.Owner) > 0 { + foundAccount := false + for _, owner := range project.Owner { + if account, ok := owner["account"]; ok { + d.Set("account", account) + foundAccount = true + break + } + } + if !foundAccount { + log.Printf("[DEBUG] Project %s owner information doesn't contain account, keeping original value", d.Id()) + } + } else { + // Keep the original account value from the configuration + // This prevents Terraform from thinking the resource has disappeared + log.Printf("[DEBUG] Project %s owner information not available yet, keeping original account value", d.Id()) + } + } + + if _, ok := d.GetOk("accountid"); ok { + if len(project.Owner) > 0 { + foundAccountID := false + for _, owner := range project.Owner { + if accountid, ok := owner["accountid"]; ok { + d.Set("accountid", accountid) + foundAccountID = true + break + } + } + if !foundAccountID { + log.Printf("[DEBUG] Project %s owner information doesn't contain accountid, keeping original value", d.Id()) + } + } else { + log.Printf("[DEBUG] Project %s owner information not available yet, keeping original accountid value", d.Id()) + } + } + + if _, ok := d.GetOk("userid"); ok { + if len(project.Owner) > 0 { + foundUserID := false + for _, owner := range project.Owner { + if userid, ok := owner["userid"]; ok { + d.Set("userid", userid) + foundUserID = true + break + } + } + if !foundUserID { + log.Printf("[DEBUG] Project %s owner information doesn't contain userid, keeping original value", d.Id()) + } + } else { + log.Printf("[DEBUG] Project %s owner information not available yet, keeping original userid value", d.Id()) + } + } + + return nil +} + +func resourceCloudStackProjectUpdate(d *schema.ResourceData, meta any) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Check if the name or display text is changed + if d.HasChange("name") || d.HasChange("display_text") { + // Create a new parameter struct + p := cs.Project.NewUpdateProjectParams(d.Id()) + + // Set the name and display_text if they have changed + // Note: The 'name' parameter is only available in API 4.19 and higher + // If you're using API 4.18 or lower, the SetName method might not work + // In that case, you might need to update the display_text only + if d.HasChange("name") { + p.SetName(d.Get("name").(string)) + } + + if d.HasChange("display_text") { + p.SetDisplaytext(d.Get("display_text").(string)) + } + + log.Printf("[DEBUG] Updating project %s", d.Id()) + _, err := cs.Project.UpdateProject(p) + if err != nil { + return fmt.Errorf("Error updating project %s: %s", d.Id(), err) + } + } + + // Check if the account, accountid, or userid is changed + if d.HasChange("account") || d.HasChange("accountid") || d.HasChange("userid") { + // Create a new parameter struct + p := cs.Project.NewUpdateProjectParams(d.Id()) + + // Set swapowner to true to swap ownership with the account/user provided + p.SetSwapowner(true) + + // Set the account if it has changed + if d.HasChange("account") { + p.SetAccount(d.Get("account").(string)) + } + + // Set the userid if it has changed + if d.HasChange("userid") { + p.SetUserid(d.Get("userid").(string)) + } + + // Note: accountid is not directly supported by the UpdateProject API, + // but we can use the account parameter instead if accountid has changed + if d.HasChange("accountid") && !d.HasChange("account") { + // If accountid has changed but account hasn't, we need to look up the account name + // This is a placeholder - in a real implementation, you would need to look up + // the account name from the accountid + log.Printf("[WARN] Updating accountid is not directly supported by the API. Please use account instead.") + } + + log.Printf("[DEBUG] Updating project owner %s", d.Id()) + _, err := cs.Project.UpdateProject(p) + if err != nil { + return fmt.Errorf("Error updating project owner %s: %s", d.Id(), err) + } + } + + // Wait for the project to be updated + ctx := context.Background() + + // Get domain if provided + var domain string + if domainParam, ok := d.GetOk("domain"); ok { + domain = domainParam.(string) + } + + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + project, err := getProjectByID(cs, d.Id(), domain) + if err != nil { + if strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] Project %s not found after update, retrying...", d.Id()) + return retry.RetryableError(fmt.Errorf("project not found after update: %s", err)) + } + return retry.NonRetryableError(fmt.Errorf("Error retrieving project after update: %s", err)) + } + + // Check if the project has the expected values + if d.HasChange("name") && project.Name != d.Get("name").(string) { + log.Printf("[DEBUG] Project %s name not updated yet, retrying...", d.Id()) + return retry.RetryableError(fmt.Errorf("project name not updated yet")) + } + + if d.HasChange("display_text") && project.Displaytext != d.Get("display_text").(string) { + log.Printf("[DEBUG] Project %s display_text not updated yet, retrying...", d.Id()) + return retry.RetryableError(fmt.Errorf("project display_text not updated yet")) + } + + log.Printf("[DEBUG] Project %s updated successfully", d.Id()) + return nil + }) + + // Even if the retry times out, we should still try to read the resource + // since it might have been updated successfully + if err != nil { + log.Printf("[WARN] Timeout waiting for project %s to be updated: %s", d.Id(), err) + } + + // Read the resource state + return resourceCloudStackProjectRead(d, meta) +} + +func resourceCloudStackProjectDelete(d *schema.ResourceData, meta any) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get project name and domain for potential fallback lookup + name := d.Get("name").(string) + var domain string + if domainParam, ok := d.GetOk("domain"); ok { + domain = domainParam.(string) + } + + // First check if the project still exists by ID + log.Printf("[DEBUG] Checking if project %s exists before deleting", d.Id()) + project, err := getProjectByID(cs, d.Id(), domain) + + // If project not found by ID, try to find it by name + if err != nil && strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] Project %s not found by ID, trying to find by name: %s", d.Id(), name) + project, err = getProjectByName(cs, name, domain) + + // If project not found by name either, we're done + if err != nil { + if strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] Project with name %s not found either, nothing to delete", name) + return nil + } + // For other errors during name lookup, return them + return fmt.Errorf("error looking up project by name: %s", err) + } + + // Found by name, update the ID + log.Printf("[DEBUG] Found project by name %s with ID: %s", name, project.Id) + d.SetId(project.Id) + } else if err != nil { + // For other errors during ID lookup, return them + return fmt.Errorf("error checking project existence before delete: %s", err) + } + + log.Printf("[DEBUG] Found project %s (%s), proceeding with delete", d.Id(), project.Name) + + // Create a new parameter struct + p := cs.Project.NewDeleteProjectParams(d.Id()) + + log.Printf("[INFO] Deleting project: %s (%s)", d.Id(), project.Name) + _, err = cs.Project.DeleteProject(p) + if err != nil { + // Check for various "not found" or "does not exist" error patterns + if strings.Contains(err.Error(), "not found") || + strings.Contains(err.Error(), "does not exist") || + strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + log.Printf("[DEBUG] Project %s no longer exists after delete attempt", d.Id()) + return nil + } + + return fmt.Errorf("error deleting project %s: %s", d.Id(), err) + } + + log.Printf("[DEBUG] Successfully deleted project: %s (%s)", d.Id(), project.Name) + return nil +} diff --git a/cloudstack/resource_cloudstack_project_test.go b/cloudstack/resource_cloudstack_project_test.go new file mode 100644 index 00000000..362ffeab --- /dev/null +++ b/cloudstack/resource_cloudstack_project_test.go @@ -0,0 +1,523 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "strings" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackProject_basic(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.foo", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "name", "terraform-test-project"), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "display_text", "Terraform Test Project"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_update(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.foo", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "name", "terraform-test-project"), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "display_text", "Terraform Test Project"), + ), + }, + { + Config: testAccCloudStackProject_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.foo", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "name", "terraform-test-project-updated"), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "display_text", "Terraform Test Project Updated"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_import(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_basic, + }, + { + ResourceName: "cloudstack_project.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccCloudStackProject_account(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_account, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.bar", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "name", "terraform-test-project-account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "display_text", "Terraform Test Project with Account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "account", "admin"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_updateAccount(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_account, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.bar", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "name", "terraform-test-project-account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "display_text", "Terraform Test Project with Account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "account", "admin"), + ), + }, + { + Config: testAccCloudStackProject_updateAccount, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.bar", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "name", "terraform-test-project-account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "display_text", "Terraform Test Project with Account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "account", "admin"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_emptyDisplayText(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_emptyDisplayText, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.empty", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.empty", "name", "terraform-test-project-empty-display"), + resource.TestCheckResourceAttr( + "cloudstack_project.empty", "display_text", "terraform-test-project-empty-display"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_updateUserid(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_userid, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.baz", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.baz", "name", "terraform-test-project-userid"), + resource.TestCheckResourceAttr( + "cloudstack_project.baz", "display_text", "Terraform Test Project with Userid"), + ), + }, + { + Config: testAccCloudStackProject_updateUserid, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.baz", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.baz", "name", "terraform-test-project-userid-updated"), + resource.TestCheckResourceAttr( + "cloudstack_project.baz", "display_text", "Terraform Test Project with Userid Updated"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackProjectExists( + n string, project *cloudstack.Project) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No project ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + // Get domain if available + var domain string + if domainAttr, ok := rs.Primary.Attributes["domain"]; ok && domainAttr != "" { + domain = domainAttr + } + + // First try to find the project by ID with domain if available + p := cs.Project.NewListProjectsParams() + p.SetId(rs.Primary.ID) + + // Add domain if available + if domain != "" { + domainID, err := retrieveID(cs, "domain", domain) + if err == nil { + p.SetDomainid(domainID) + } + } + + list, err := cs.Project.ListProjects(p) + if err == nil && list.Count > 0 && list.Projects[0].Id == rs.Primary.ID { + // Found by ID, set the project and return + *project = *list.Projects[0] + return nil + } + + // If not found by ID or there was an error, try by name + if err != nil || list.Count == 0 || list.Projects[0].Id != rs.Primary.ID { + name := rs.Primary.Attributes["name"] + if name == "" { + return fmt.Errorf("Project not found by ID and name attribute is empty") + } + + // Try to find by name + p := cs.Project.NewListProjectsParams() + p.SetName(name) + + // Add domain if available + if domain, ok := rs.Primary.Attributes["domain"]; ok && domain != "" { + domainID, err := retrieveID(cs, "domain", domain) + if err != nil { + return fmt.Errorf("Error retrieving domain ID: %v", err) + } + p.SetDomainid(domainID) + } + + list, err := cs.Project.ListProjects(p) + if err != nil { + return fmt.Errorf("Error retrieving project by name: %s", err) + } + + if list.Count == 0 { + return fmt.Errorf("Project with name %s not found", name) + } + + // Find the project with the matching ID if possible + found := false + for _, proj := range list.Projects { + if proj.Id == rs.Primary.ID { + *project = *proj + found = true + break + } + } + + // If we didn't find a project with matching ID, use the first one + if !found { + *project = *list.Projects[0] + // Update the resource ID to match the found project + rs.Primary.ID = list.Projects[0].Id + } + + return nil + } + + return fmt.Errorf("Project not found by ID or name") + } +} + +func testAccCheckCloudStackProjectDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_project" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No project ID is set") + } + + // Get domain if available + var domain string + if domainAttr, ok := rs.Primary.Attributes["domain"]; ok && domainAttr != "" { + domain = domainAttr + } + + // Try to find the project by ID + p := cs.Project.NewListProjectsParams() + p.SetId(rs.Primary.ID) + + // Add domain if available + if domain != "" { + domainID, err := retrieveID(cs, "domain", domain) + if err == nil { + p.SetDomainid(domainID) + } + } + + list, err := cs.Project.ListProjects(p) + + // If we get an error, check if it's a "not found" error + if err != nil { + if strings.Contains(err.Error(), "not found") || + strings.Contains(err.Error(), "does not exist") || + strings.Contains(err.Error(), "could not be found") || + strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", rs.Primary.ID)) { + // Project doesn't exist, which is what we want + continue + } + // For other errors, return them + return fmt.Errorf("error checking if project %s exists: %s", rs.Primary.ID, err) + } + + // If we found the project, it still exists + if list.Count != 0 { + return fmt.Errorf("project %s still exists (found by ID)", rs.Primary.ID) + } + + // Also check by name to be thorough + name := rs.Primary.Attributes["name"] + if name != "" { + // Try to find the project by name + p := cs.Project.NewListProjectsParams() + p.SetName(name) + + // Add domain if available + if domain, ok := rs.Primary.Attributes["domain"]; ok && domain != "" { + domainID, err := retrieveID(cs, "domain", domain) + if err == nil { + p.SetDomainid(domainID) + } + } + + list, err := cs.Project.ListProjects(p) + if err != nil { + // Ignore errors for name lookup + continue + } + + // Check if any of the returned projects match our ID + for _, proj := range list.Projects { + if proj.Id == rs.Primary.ID { + return fmt.Errorf("project %s still exists (found by name %s)", rs.Primary.ID, name) + } + } + } + } + + return nil +} + +const testAccCloudStackProject_basic = ` +resource "cloudstack_project" "foo" { + name = "terraform-test-project" + display_text = "Terraform Test Project" +}` + +const testAccCloudStackProject_update = ` +resource "cloudstack_project" "foo" { + name = "terraform-test-project-updated" + display_text = "Terraform Test Project Updated" +}` + +const testAccCloudStackProject_account = ` +resource "cloudstack_project" "bar" { + name = "terraform-test-project-account" + display_text = "Terraform Test Project with Account" + account = "admin" + domain = "ROOT" +}` + +const testAccCloudStackProject_updateAccount = ` +resource "cloudstack_project" "bar" { + name = "terraform-test-project-account" + display_text = "Terraform Test Project with Account" + account = "admin" + domain = "ROOT" +}` + +const testAccCloudStackProject_userid = ` +resource "cloudstack_project" "baz" { + name = "terraform-test-project-userid" + display_text = "Terraform Test Project with Userid" + domain = "ROOT" +}` + +const testAccCloudStackProject_updateUserid = ` +resource "cloudstack_project" "baz" { + name = "terraform-test-project-userid-updated" + display_text = "Terraform Test Project with Userid Updated" + domain = "ROOT" +}` + +const testAccCloudStackProject_emptyDisplayText = ` +resource "cloudstack_project" "empty" { + name = "terraform-test-project-empty-display" + display_text = "terraform-test-project-empty-display" +}` + +func TestAccCloudStackProject_list(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_list, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectsExist("cloudstack_project.project1", "cloudstack_project.project2"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackProjectsExist(projectNames ...string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Get CloudStack client + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + // Create a map to track which projects we've found + foundProjects := make(map[string]bool) + for _, name := range projectNames { + // Get the project resource from state + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No project ID is set for %s", name) + } + + // Add the project ID to our tracking map + foundProjects[rs.Primary.ID] = false + } + + // List all projects + p := cs.Project.NewListProjectsParams() + list, err := cs.Project.ListProjects(p) + if err != nil { + return err + } + + // Check if all our projects are in the list + for _, project := range list.Projects { + if _, exists := foundProjects[project.Id]; exists { + foundProjects[project.Id] = true + } + } + + // Verify all projects were found + for id, found := range foundProjects { + if !found { + return fmt.Errorf("Project with ID %s was not found in the list", id) + } + } + + return nil + } +} + +const testAccCloudStackProject_list = ` +resource "cloudstack_project" "project1" { + name = "terraform-test-project-list-1" + display_text = "Terraform Test Project List 1" +} + +resource "cloudstack_project" "project2" { + name = "terraform-test-project-list-2" + display_text = "Terraform Test Project List 2" +}` diff --git a/cloudstack/resources.go b/cloudstack/resources.go index 22b2adcc..5a75b77d 100644 --- a/cloudstack/resources.go +++ b/cloudstack/resources.go @@ -72,6 +72,8 @@ func retrieveID(cs *cloudstack.CloudStackClient, name string, value string, opts switch name { case "disk_offering": id, _, err = cs.DiskOffering.GetDiskOfferingID(value) + case "domain": + id, _, err = cs.Domain.GetDomainID(value) case "kubernetes_version": id, _, err = cs.Kubernetes.GetKubernetesSupportedVersionID(value) case "network_offering": diff --git a/go.mod b/go.mod index 5be79a83..d030f290 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/terraform-providers/terraform-provider-cloudstack require ( - github.com/apache/cloudstack-go/v2 v2.16.1 + github.com/apache/cloudstack-go/v2 v2.17.1 github.com/go-ini/ini v1.67.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/terraform-plugin-framework v1.7.0 @@ -16,9 +16,8 @@ require ( github.com/ProtonMail/go-crypto v1.1.0-alpha.0 // indirect github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/fatih/color v1.16.0 // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect @@ -48,18 +47,18 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.14.3 // indirect + go.uber.org/mock v0.5.0 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/mod v0.17.0 // indirect + golang.org/x/mod v0.18.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/grpc v1.62.1 // indirect diff --git a/go.sum b/go.sum index 6e3d58bb..e280f902 100644 --- a/go.sum +++ b/go.sum @@ -6,15 +6,15 @@ github.com/ProtonMail/go-crypto v1.1.0-alpha.0 h1:nHGfwXmFvJrSR9xu8qL7BkO4DqTHXE github.com/ProtonMail/go-crypto v1.1.0-alpha.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/apache/cloudstack-go/v2 v2.16.1 h1:2wOE4RKEjWPRZNO7ZNnZYmR3JJ+JJPQwhoc7W1fkiK4= -github.com/apache/cloudstack-go/v2 v2.16.1/go.mod h1:cZsgFe+VmrgLBm7QjeHTJBXYe8E5+yGYkdfwGb+Pu9c= +github.com/apache/cloudstack-go/v2 v2.17.1 h1:XD0bGDOv+MCavXJfc/qxILgJh+cHJbudpqQ1FzA2sDI= +github.com/apache/cloudstack-go/v2 v2.17.1/go.mod h1:p/YBUwIEkQN6CQxFhw8Ff0wzf1MY0qRRRuGYNbcb1F8= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -38,8 +38,6 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -150,8 +148,8 @@ github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2 github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -161,39 +159,33 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -215,14 +207,11 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= diff --git a/website/cloudstack.erb b/website/cloudstack.erb index f900196a..fff2c8e2 100644 --- a/website/cloudstack.erb +++ b/website/cloudstack.erb @@ -78,6 +78,10 @@ cloudstack_private_gateway + > + cloudstack_project + + > cloudstack_secondary_ipaddress diff --git a/website/docs/d/project.html.markdown b/website/docs/d/project.html.markdown new file mode 100644 index 00000000..a1d4656b --- /dev/null +++ b/website/docs/d/project.html.markdown @@ -0,0 +1,59 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_project" +sidebar_current: "docs-cloudstack-cloudstack_project" +description: |- + Gets information about CloudStack projects. +--- + +# cloudstack_project + +Use this datasource to get information about a CloudStack project for use in other resources. + +## Example Usage + +### Basic Usage + +```hcl +data "cloudstack_project" "my_project" { + filter { + name = "name" + value = "my-project" + } +} +``` + +### With Multiple Filters + +```hcl +data "cloudstack_project" "admin_project" { + filter { + name = "name" + value = "admin-project" + } + filter { + name = "domain" + value = "ROOT" + } + filter { + name = "account" + value = "admin" + } +} +``` + +## Argument Reference + +* `filter` - (Required) One or more name/value pairs to filter off of. You can apply filters on any exported attributes. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the project. +* `name` - The name of the project. +* `display_text` - The display text of the project. +* `domain` - The domain where the project belongs. +* `account` - The account who is the admin for the project. +* `state` - The current state of the project. +* `tags` - A map of tags assigned to the project. \ No newline at end of file diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown new file mode 100644 index 00000000..fbf9669d --- /dev/null +++ b/website/docs/r/project.html.markdown @@ -0,0 +1,61 @@ +--- +subcategory: "CloudStack" +layout: "cloudstack" +page_title: "CloudStack: cloudstack_project" +description: |- + Creates a project. +--- + +# cloudstack_project + +Creates a project. + +## Example Usage + +```hcl +resource "cloudstack_project" "myproject" { + name = "terraform-project" + display_text = "Terraform Managed Project" + domain = "root" +} +``` + +### With Account and User ID + +```hcl +resource "cloudstack_project" "myproject" { + name = "terraform-project" + display_text = "Terraform Managed Project" + domain = "root" + account = "admin" + userid = "b0afc3ca-a99c-4fb4-98ad-8564acab10a4" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the project. +* `display_text` - (Required) The display text of the project. Required for API version 4.18 and lower compatibility. This requirement will be removed when support for API versions older than 4.18 is dropped. +* `domain` - (Optional) The domain where the project will be created. This cannot be changed after the project is created. +* `account` - (Optional) The account who will be Admin for the project. Requires `domain` to be set. This can be updated after the project is created. +* `accountid` - (Optional) The ID of the account owning the project. This can be updated after the project is created. +* `userid` - (Optional) The user ID of the account to be assigned as owner of the project (Project Admin). This can be updated after the project is created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the project. +* `name` - The name of the project. +* `display_text` - The display text of the project. +* `domain` - The domain where the project was created. + +## Import + +Projects can be imported using the project ID, e.g. + +```sh +terraform import cloudstack_project.myproject 5cf69677-7e4b-4bf4-b868-f0b02bb72ee0 +```