Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bf19e23
Add CloudStack project resource
ianc769 Apr 3, 2025
7973ad8
Add test for empty display_text defaulting to name value
ianc769 Apr 14, 2025
33e3852
Uncomment and implement tests for accountid and userid in project res…
ianc769 Apr 14, 2025
ce5a4a3
Minor README Fix
ianc769 May 21, 2025
1296923
Update display_text to required for API compatibility and adjust docu…
ianc769 May 21, 2025
c5356b1
Merge branch 'main' into feature/project
ianc769 Jun 12, 2025
62623c3
Clean up tests for 4.20.1.0
ianc769 Jun 16, 2025
5a0cf9e
fix: include domain ID when looking up projects by ID
ianc769 Jul 17, 2025
faf5e62
feat: add cloudstack_project data source and corresponding tests
ianc769 Aug 7, 2025
f8ebeec
remove rogue testing script
ianc769 Aug 7, 2025
c0fac58
Update cloudstack/resource_cloudstack_project.go
ianc769 Aug 7, 2025
b3ff103
adding domain validation to ensure projects are only reused within th…
ianc769 Aug 7, 2025
973fc7e
Updated cloudstack go sdk to v2.17.1 (#193)
sureshanaparti Jul 14, 2025
f9eb777
Fix creation of firewall & Egress firewall rules when created in a pr…
Pearl1594 Jul 15, 2025
41a62c8
chore(deps): bump github.com/cloudflare/circl from 1.3.7 to 1.6.1
dependabot[bot] Jun 12, 2025
52a60b9
resolve retrieveError issue
ianc769 Aug 11, 2025
f84234d
Update cloudstack/resource_cloudstack_project.go
ianc769 Sep 3, 2025
4e8cbe8
Update cloudstack/resource_cloudstack_project.go
ianc769 Sep 3, 2025
1b7d19d
Change display_text field from required to optional in resourceCloudS…
ianc769 Sep 3, 2025
2e8c8e0
Pin github actions version for opentofu
vishesh92 Aug 26, 2025
8c3360b
rat + excludes and add licenses to other files (#200)
DaanHoogland Aug 26, 2025
488f7a1
readme: add specific test instruction in readme (#211)
sudo87 Aug 27, 2025
4d4f6e2
data: get vpc in project by project name (#209)
weizhouapache Aug 27, 2025
ff89972
Support additional parameters for cloudstack_nic resource (#210)
Pearl1594 Aug 28, 2025
43c2e40
serviceoffering: add params for custom offering, storage tags, encryp…
shwstppr Aug 28, 2025
84890c4
Support desc and ruleId in create_network_acl_rule
sudo87 Aug 25, 2025
feb4281
fix review comment
sudo87 Aug 25, 2025
4cdf492
change rule_id -> rule_number and add doc
sudo87 Aug 26, 2025
173fe66
add params in unit tests
sudo87 Aug 26, 2025
ab55fac
verify description and rule_number in unit test
sudo87 Aug 26, 2025
30b9d99
use fields defined in schema
sudo87 Aug 26, 2025
b970ec1
fix test verification sequence
sudo87 Aug 27, 2025
854ee22
handle review comments
sudo87 Aug 27, 2025
e96263f
Add support for additional optional parameters for creating network o…
Pearl1594 Aug 29, 2025
32c237e
Add disk_offering & override_disk_offering to instance resource
vishesh92 Aug 26, 2025
27cdc1b
Update website/docs/r/instance.html.markdown
vishesh92 Aug 26, 2025
eaf7578
Allow specifying private end port & public end port for port forward …
vishesh92 Aug 25, 2025
0ad724c
Update tests
vishesh92 Aug 26, 2025
e2a97a5
Add `cloudstack_physicalnetwork` and some underlying additional resou…
ianc769 Aug 31, 2025
6100cf5
feat: add cidrlist parameter to loadbalancer rule (#147)
chrxmvtik Aug 31, 2025
97a3b24
feat: add cloudstack_project resource to provider
ianc769 Sep 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down
215 changes: 215 additions & 0 deletions cloudstack/data_source_cloudstack_project.go
Original file line number Diff line number Diff line change
@@ -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
}
115 changes: 115 additions & 0 deletions cloudstack/data_source_cloudstack_project_test.go
Original file line number Diff line number Diff line change
@@ -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
}
`
2 changes: 2 additions & 0 deletions cloudstack/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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(),
Expand Down
12 changes: 12 additions & 0 deletions cloudstack/resource_cloudstack_egress_firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading