This is an automated email from the ASF dual-hosted git repository.

kiranchavala pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git


The following commit(s) were added to refs/heads/main by this push:
     new afa7a18  Add `cloudstack_project` resource (#167)
afa7a18 is described below

commit afa7a18188519a08fee753432a82f1c50844d2c4
Author: Ian <ian.cro...@telus.com>
AuthorDate: Tue Sep 16 06:41:16 2025 -0700

    Add `cloudstack_project` resource (#167)
    
    * Add CloudStack project resource
    
    * Add test for empty display_text defaulting to name value
    
    * Uncomment and implement tests for accountid and userid in project resource
    
    * Minor README Fix
    
    * Update display_text to required for API compatibility and adjust 
documentation
    
    * Clean up tests for 4.20.1.0
    
    * 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
    
    * feat: add cloudstack_project data source and corresponding tests
    
    * remove rogue testing script
    
    * Update cloudstack/resource_cloudstack_project.go
    
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
    
    * adding domain validation to ensure projects are only reused within the 
intended scope
    
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
    
    * Updated cloudstack go sdk to v2.17.1 (#193)
    
    * Fix creation of firewall & Egress firewall rules when created in a project
    
    * chore(deps): bump github.com/cloudflare/circl from 1.3.7 to 1.6.1
    
    Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) 
from 1.3.7 to 1.6.1.
    - [Release notes](https://github.com/cloudflare/circl/releases)
    - [Commits](https://github.com/cloudflare/circl/compare/v1.3.7...v1.6.1)
    
    ---
    updated-dependencies:
    - dependency-name: github.com/cloudflare/circl
      dependency-version: 1.6.1
      dependency-type: indirect
    ...
    
    Signed-off-by: dependabot[bot] <supp...@github.com>
    
    * resolve retrieveError issue
    
    * Update cloudstack/resource_cloudstack_project.go
    
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
    
    * Update cloudstack/resource_cloudstack_project.go
    
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
    
    * Change display_text field from required to optional in 
resourceCloudStackProject
    
    * Pin github actions version for opentofu
    
    * rat + excludes and add licenses to other files (#200)
    
    * readme: add specific test instruction in readme (#211)
    
    Add instructions for specific test execution
    
    * data: get vpc in project by project name (#209)
    
    * Support additional parameters for cloudstack_nic resource (#210)
    
    * serviceoffering: add params for custom offering, storage tags, 
encryptroot (#212)
    
    * Support desc and ruleId in create_network_acl_rule
    
    * fix review comment
    
    * change rule_id -> rule_number and add doc
    
    * add params in unit tests
    
    * verify description and rule_number in unit test
    
    * use fields defined in schema
    
    * fix test verification sequence
    
    * handle review comments
    
    * Add support for additional optional parameters for creating network 
offerings (#205)
    
    * Add disk_offering & override_disk_offering to instance resource
    
    * Update website/docs/r/instance.html.markdown
    
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
    
    * Allow specifying private end port & public end port for port forward rules
    
    * Update tests
    
    * Add `cloudstack_physicalnetwork` and some underlying additional resources 
(#201)
    
    * feat: add cidrlist parameter to loadbalancer rule (#147)
    
    * feat: add cloudstack_project resource to provider
    
    * fix: update display_text to displaytext in project resource and tests.
    fix: update lookup to use getAccountNameByID helper function
    
    * fix: rename display_text to displaytext in project resource and tests
    
    ---------
    
    Signed-off-by: dependabot[bot] <supp...@github.com>
    Co-authored-by: Copilot <175728472+copi...@users.noreply.github.com>
    Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anapa...@gmail.com>
    Co-authored-by: Pearl Dsilva <pearl1...@gmail.com>
    Co-authored-by: dependabot[bot] 
<49699333+dependabot[bot]@users.noreply.github.com>
    Co-authored-by: vishesh92 <vishes...@gmail.com>
    Co-authored-by: dahn <d...@onecht.net>
    Co-authored-by: Manoj Kumar <manojkr.it...@gmail.com>
    Co-authored-by: Wei Zhou <weiz...@apache.org>
    Co-authored-by: Abhishek Kumar <abhishek.mr...@gmail.com>
    Co-authored-by: ABW <49398549+chrxmv...@users.noreply.github.com>
---
 README.md                                         |   4 +-
 cloudstack/data_source_cloudstack_project.go      | 215 ++++++++
 cloudstack/data_source_cloudstack_project_test.go | 115 +++++
 cloudstack/provider.go                            |   2 +
 cloudstack/resource_cloudstack_project.go         | 572 +++++++++++++++++++++
 cloudstack/resource_cloudstack_project_test.go    | 577 ++++++++++++++++++++++
 cloudstack/resources.go                           |   2 +
 website/cloudstack.erb                            |   4 +
 website/docs/d/project.html.markdown              |  59 +++
 website/docs/r/project.html.markdown              |  61 +++
 10 files changed, 1609 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 8a51798..4f74d01 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:
@@ -206,7 +206,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 0000000..27e57f0
--- /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,
+                       },
+
+                       "displaytext": {
+                               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("displaytext", 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 0000000..2aebc1d
--- /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, "displaytext", resourceName, 
"displaytext"),
+                                       
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, "displaytext", resourceName, 
"displaytext"),
+                                       
resource.TestCheckResourceAttrPair(datasourceName, "domain", resourceName, 
"domain"),
+                                       
resource.TestCheckResourceAttrPair(datasourceName, "account", resourceName, 
"account"),
+                               ),
+                       },
+               },
+       })
+}
+
+const testProjectDataSourceConfig_basic = `
+resource "cloudstack_project" "project-resource" {
+  name = "test-project-datasource"
+  displaytext = "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"
+  displaytext = "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 f4eedc1..58c7fa5 100644
--- a/cloudstack/provider.go
+++ b/cloudstack/provider.go
@@ -91,6 +91,7 @@ func Provider() *schema.Provider {
                        "cloudstack_vpn_connection":   
dataSourceCloudstackVPNConnection(),
                        "cloudstack_pod":              
dataSourceCloudstackPod(),
                        "cloudstack_domain":           
dataSourceCloudstackDomain(),
+                       "cloudstack_project":          
dataSourceCloudstackProject(),
                        "cloudstack_physical_network": 
dataSourceCloudStackPhysicalNetwork(),
                        "cloudstack_role":             
dataSourceCloudstackRole(),
                        "cloudstack_cluster":          
dataSourceCloudstackCluster(),
@@ -141,6 +142,7 @@ func Provider() *schema.Provider {
                        "cloudstack_zone":                           
resourceCloudStackZone(),
                        "cloudstack_service_offering":               
resourceCloudStackServiceOffering(),
                        "cloudstack_account":                        
resourceCloudStackAccount(),
+                       "cloudstack_project":                        
resourceCloudStackProject(),
                        "cloudstack_user":                           
resourceCloudStackUser(),
                        "cloudstack_domain":                         
resourceCloudStackDomain(),
                        "cloudstack_network_service_provider":       
resourceCloudStackNetworkServiceProvider(),
diff --git a/cloudstack/resource_cloudstack_project.go 
b/cloudstack/resource_cloudstack_project.go
new file mode 100644
index 0000000..ce113e4
--- /dev/null
+++ b/cloudstack/resource_cloudstack_project.go
@@ -0,0 +1,572 @@
+// 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,
+                       },
+
+                       "displaytext": {
+                               Type:     schema.TypeString,
+                               Optional: true,
+                       },
+
+                       "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 displaytext
+       name := d.Get("name").(string)
+       displaytext := d.Get("displaytext").(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("displaytext", 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 Go SDK expects parameters in the API 4.18 order: 
displaytext, name.
+       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 an account name by account ID
+func getAccountNameByID(cs *cloudstack.CloudStackClient, accountID string) 
(string, error) {
+       // Create parameters for listing accounts
+       p := cs.Account.NewListAccountsParams()
+       p.SetId(accountID)
+
+       // Call the API to list accounts with the specified ID
+       accounts, err := cs.Account.ListAccounts(p)
+       if err != nil {
+               return "", fmt.Errorf("error retrieving account with ID %s: 
%s", accountID, err)
+       }
+
+       // Check if we found the account
+       if accounts.Count == 0 {
+               return "", fmt.Errorf("account with ID %s not found", accountID)
+       }
+
+       // Return the account name
+       account := accounts.Accounts[0]
+       if account.Name == "" {
+               return "", fmt.Errorf("account with ID %s has no name", 
accountID)
+       }
+
+       return account.Name, 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("displaytext", 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 displaytext is changed
+       if d.HasChange("name") || d.HasChange("displaytext") {
+               // Create a new parameter struct
+               p := cs.Project.NewUpdateProjectParams(d.Id())
+
+               // Set the name and displaytext if they have changed
+               // Note: The 'name' parameter is only available in CloudStack 
API 4.19+ and in cloudstack-go SDK v2.11.0+.
+               // If you're using API 4.18 or lower, or an older SDK, the 
SetName method might not work.
+               // In that case, you might need to update the displaytext only.
+               if d.HasChange("name") {
+                       p.SetName(d.Get("name").(string))
+               }
+
+               if d.HasChange("displaytext") {
+                       p.SetDisplaytext(d.Get("displaytext").(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: The UpdateProject API does not accept 'accountid' 
directly.
+               // If 'accountid' has changed but 'account' has not, we perform 
a lookup to get the account name
+               // corresponding to the new 'accountid' and set it using the 
'account' parameter instead.
+               // This is necessary because the API only allows updating the 
owner via the account name, not the account ID.
+               // Only perform the lookup if "account" itself hasn't changed, 
to avoid conflicting updates.
+               if d.HasChange("accountid") && !d.HasChange("account") {
+                       // If accountid has changed but account hasn't, we need 
to look up the account name
+                       // from the accountid and use it in the account 
parameter
+                       accountid := d.Get("accountid").(string)
+                       if accountid != "" {
+                               accountName, err := getAccountNameByID(cs, 
accountid)
+                               if err != nil {
+                                       log.Printf("[WARN] Failed to look up 
account name for accountid %s: %s. Skipping account update as account name 
could not be determined.", accountid, err)
+                               } else {
+                                       log.Printf("[DEBUG] Found account name 
'%s' for accountid %s, using account parameter", accountName, accountid)
+                                       p.SetAccount(accountName)
+                               }
+                       }
+               }
+
+               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("displaytext") && project.Displaytext != 
d.Get("displaytext").(string) {
+                       log.Printf("[DEBUG] Project %s displaytext not updated 
yet, retrying...", d.Id())
+                       return retry.RetryableError(fmt.Errorf("project 
displaytext 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())
+       result, 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)
+       }
+       if result == nil {
+               log.Printf("[WARN] DeleteProject returned nil result for 
project: %s (%s)", d.Id(), project.Name)
+       }
+
+       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 0000000..b4d60c8
--- /dev/null
+++ b/cloudstack/resource_cloudstack_project_test.go
@@ -0,0 +1,577 @@
+// 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", 
"displaytext", "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", 
"displaytext", "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", 
"displaytext", "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", 
"displaytext", "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", 
"displaytext", "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", 
"displaytext", "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", 
"displaytext", "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", 
"displaytext", "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", 
"displaytext", "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"
+  displaytext = "Terraform Test Project"
+}`
+
+const testAccCloudStackProject_update = `
+resource "cloudstack_project" "foo" {
+  name = "terraform-test-project-updated"
+  displaytext = "Terraform Test Project Updated"
+}`
+
+const testAccCloudStackProject_account = `
+resource "cloudstack_project" "bar" {
+  name = "terraform-test-project-account"
+  displaytext = "Terraform Test Project with Account"
+  account = "admin"
+  domain = "ROOT"
+}`
+
+const testAccCloudStackProject_updateAccount = `
+resource "cloudstack_project" "bar" {
+  name = "terraform-test-project-account"
+  displaytext = "Terraform Test Project with Account"
+  account = "admin"
+  domain = "ROOT"
+}`
+
+const testAccCloudStackProject_userid = `
+resource "cloudstack_project" "baz" {
+  name = "terraform-test-project-userid"
+  displaytext = "Terraform Test Project with Userid"
+  domain = "ROOT"
+}`
+
+const testAccCloudStackProject_updateUserid = `
+resource "cloudstack_project" "baz" {
+  name = "terraform-test-project-userid-updated"
+  displaytext = "Terraform Test Project with Userid Updated"
+  domain = "ROOT"
+}`
+
+const testAccCloudStackProject_emptyDisplayText = `
+resource "cloudstack_project" "empty" {
+  name = "terraform-test-project-empty-display"
+  displaytext = "terraform-test-project-empty-display"
+}`
+
+func TestAccCloudStackProject_updateAccountid(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_accountid,
+                               Check: resource.ComposeTestCheckFunc(
+                                       testAccCheckCloudStackProjectExists(
+                                               
"cloudstack_project.accountid_test", &project),
+                                       resource.TestCheckResourceAttr(
+                                               
"cloudstack_project.accountid_test", "name", 
"terraform-test-project-accountid"),
+                                       resource.TestCheckResourceAttr(
+                                               
"cloudstack_project.accountid_test", "displaytext", "Terraform Test Project 
with Accountid"),
+                                       resource.TestCheckResourceAttrSet(
+                                               
"cloudstack_project.accountid_test", "accountid"),
+                               ),
+                       },
+                       {
+                               Config: 
testAccCloudStackProject_updateAccountid,
+                               Check: resource.ComposeTestCheckFunc(
+                                       testAccCheckCloudStackProjectExists(
+                                               
"cloudstack_project.accountid_test", &project),
+                                       resource.TestCheckResourceAttr(
+                                               
"cloudstack_project.accountid_test", "name", 
"terraform-test-project-accountid"),
+                                       resource.TestCheckResourceAttr(
+                                               
"cloudstack_project.accountid_test", "displaytext", "Terraform Test Project 
with Accountid"),
+                                       resource.TestCheckResourceAttrSet(
+                                               
"cloudstack_project.accountid_test", "accountid"),
+                               ),
+                       },
+               },
+       })
+}
+
+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_accountid = `
+resource "cloudstack_project" "accountid_test" {
+  name = "terraform-test-project-accountid"
+  displaytext = "Terraform Test Project with Accountid"
+  accountid = "1"
+  domain = "ROOT"
+}`
+
+const testAccCloudStackProject_updateAccountid = `
+resource "cloudstack_project" "accountid_test" {
+  name = "terraform-test-project-accountid"
+  displaytext = "Terraform Test Project with Accountid"
+  accountid = "2"
+  domain = "ROOT"
+}`
+
+const testAccCloudStackProject_list = `
+resource "cloudstack_project" "project1" {
+  name = "terraform-test-project-list-1"
+  displaytext = "Terraform Test Project List 1"
+}
+
+resource "cloudstack_project" "project2" {
+  name = "terraform-test-project-list-2"
+  displaytext = "Terraform Test Project List 2"
+}`
diff --git a/cloudstack/resources.go b/cloudstack/resources.go
index 22b2adc..5a75b77 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/website/cloudstack.erb b/website/cloudstack.erb
index fbc70b4..1ea7f7e 100644
--- a/website/cloudstack.erb
+++ b/website/cloudstack.erb
@@ -81,6 +81,10 @@
                             <a 
href="/docs/providers/cloudstack/r/private_gateway.html">cloudstack_private_gateway</a>
                         </li>
 
+                        <li<%= 
sidebar_current("docs-cloudstack-resource-project") %>>
+                            <a 
href="/docs/providers/cloudstack/r/project.html">cloudstack_project</a>
+                        </li>
+
                         <li<%= 
sidebar_current("docs-cloudstack-resource-secondary-ipaddress") %>>
                             <a 
href="/docs/providers/cloudstack/r/secondary_ipaddress.html">cloudstack_secondary_ipaddress</a>
                         </li>
diff --git a/website/docs/d/project.html.markdown 
b/website/docs/d/project.html.markdown
new file mode 100644
index 0000000..a1d4656
--- /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 0000000..fbf9669
--- /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
+```

Reply via email to