Skip to content

Commit faf5e62

Browse files
committed
feat: add cloudstack_project data source and corresponding tests
1 parent 5a0cf9e commit faf5e62

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
//
2+
// Licensed to the Apache Software Foundation (ASF) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The ASF licenses this file
6+
// to you under the Apache License, Version 2.0 (the
7+
// "License"); you may not use this file except in compliance
8+
// with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
//
19+
20+
package cloudstack
21+
22+
import (
23+
"encoding/json"
24+
"fmt"
25+
"log"
26+
"regexp"
27+
"strings"
28+
"time"
29+
30+
"github.com/apache/cloudstack-go/v2/cloudstack"
31+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
32+
)
33+
34+
func dataSourceCloudstackProject() *schema.Resource {
35+
return &schema.Resource{
36+
Read: datasourceCloudStackProjectRead,
37+
Schema: map[string]*schema.Schema{
38+
"filter": dataSourceFiltersSchema(),
39+
40+
// Computed values
41+
"name": {
42+
Type: schema.TypeString,
43+
Computed: true,
44+
},
45+
46+
"display_text": {
47+
Type: schema.TypeString,
48+
Computed: true,
49+
},
50+
51+
"domain": {
52+
Type: schema.TypeString,
53+
Computed: true,
54+
},
55+
56+
"account": {
57+
Type: schema.TypeString,
58+
Computed: true,
59+
},
60+
61+
"state": {
62+
Type: schema.TypeString,
63+
Computed: true,
64+
},
65+
66+
"tags": tagsSchema(),
67+
},
68+
}
69+
}
70+
71+
func datasourceCloudStackProjectRead(d *schema.ResourceData, meta interface{}) error {
72+
cs := meta.(*cloudstack.CloudStackClient)
73+
p := cs.Project.NewListProjectsParams()
74+
csProjects, err := cs.Project.ListProjects(p)
75+
76+
if err != nil {
77+
return fmt.Errorf("failed to list projects: %s", err)
78+
}
79+
80+
filters := d.Get("filter")
81+
var projects []*cloudstack.Project
82+
83+
for _, v := range csProjects.Projects {
84+
match, err := applyProjectFilters(v, filters.(*schema.Set))
85+
if err != nil {
86+
return err
87+
}
88+
if match {
89+
projects = append(projects, v)
90+
}
91+
}
92+
93+
if len(projects) == 0 {
94+
return fmt.Errorf("no project matches the specified filters")
95+
}
96+
97+
// Return the latest project from the list of filtered projects according
98+
// to its creation date
99+
project, err := latestProject(projects)
100+
if err != nil {
101+
return err
102+
}
103+
log.Printf("[DEBUG] Selected project: %s\n", project.Name)
104+
105+
return projectDescriptionAttributes(d, project)
106+
}
107+
108+
func projectDescriptionAttributes(d *schema.ResourceData, project *cloudstack.Project) error {
109+
d.SetId(project.Id)
110+
d.Set("name", project.Name)
111+
d.Set("display_text", project.Displaytext)
112+
d.Set("domain", project.Domain)
113+
d.Set("state", project.State)
114+
115+
// Handle account information safely
116+
if len(project.Owner) > 0 {
117+
for _, owner := range project.Owner {
118+
if account, ok := owner["account"]; ok {
119+
d.Set("account", account)
120+
break
121+
}
122+
}
123+
}
124+
125+
d.Set("tags", tagsToMap(project.Tags))
126+
127+
return nil
128+
}
129+
130+
func latestProject(projects []*cloudstack.Project) (*cloudstack.Project, error) {
131+
var latest time.Time
132+
var project *cloudstack.Project
133+
134+
for _, v := range projects {
135+
created, err := time.Parse("2006-01-02T15:04:05-0700", v.Created)
136+
if err != nil {
137+
return nil, fmt.Errorf("failed to parse creation date of a project: %s", err)
138+
}
139+
140+
if created.After(latest) {
141+
latest = created
142+
project = v
143+
}
144+
}
145+
146+
return project, nil
147+
}
148+
149+
func applyProjectFilters(project *cloudstack.Project, filters *schema.Set) (bool, error) {
150+
var projectJSON map[string]interface{}
151+
k, _ := json.Marshal(project)
152+
err := json.Unmarshal(k, &projectJSON)
153+
if err != nil {
154+
return false, err
155+
}
156+
157+
for _, f := range filters.List() {
158+
m := f.(map[string]interface{})
159+
r, err := regexp.Compile(m["value"].(string))
160+
if err != nil {
161+
return false, fmt.Errorf("invalid regex: %s", err)
162+
}
163+
164+
// Handle special case for owner/account
165+
if m["name"].(string) == "account" {
166+
if len(project.Owner) == 0 {
167+
return false, nil
168+
}
169+
170+
found := false
171+
for _, owner := range project.Owner {
172+
if account, ok := owner["account"]; ok {
173+
if r.MatchString(fmt.Sprintf("%v", account)) {
174+
found = true
175+
break
176+
}
177+
}
178+
}
179+
180+
if !found {
181+
return false, nil
182+
}
183+
continue
184+
}
185+
186+
updatedName := strings.ReplaceAll(m["name"].(string), "_", "")
187+
188+
// Handle fields that might not exist in the JSON
189+
fieldValue, exists := projectJSON[updatedName]
190+
if !exists {
191+
return false, nil
192+
}
193+
194+
// Handle different types of fields
195+
switch v := fieldValue.(type) {
196+
case string:
197+
if !r.MatchString(v) {
198+
return false, nil
199+
}
200+
case float64:
201+
if !r.MatchString(fmt.Sprintf("%v", v)) {
202+
return false, nil
203+
}
204+
case bool:
205+
if !r.MatchString(fmt.Sprintf("%v", v)) {
206+
return false, nil
207+
}
208+
default:
209+
// Skip fields that aren't simple types
210+
continue
211+
}
212+
}
213+
214+
return true, nil
215+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// Licensed to the Apache Software Foundation (ASF) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The ASF licenses this file
6+
// to you under the Apache License, Version 2.0 (the
7+
// "License"); you may not use this file except in compliance
8+
// with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
//
19+
20+
package cloudstack
21+
22+
import (
23+
"testing"
24+
25+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
26+
)
27+
28+
func TestAccProjectDataSource_basic(t *testing.T) {
29+
resourceName := "cloudstack_project.project-resource"
30+
datasourceName := "data.cloudstack_project.project-data-source"
31+
32+
resource.Test(t, resource.TestCase{
33+
PreCheck: func() { testAccPreCheck(t) },
34+
Providers: testAccProviders,
35+
Steps: []resource.TestStep{
36+
{
37+
Config: testProjectDataSourceConfig_basic,
38+
Check: resource.ComposeTestCheckFunc(
39+
resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"),
40+
resource.TestCheckResourceAttrPair(datasourceName, "display_text", resourceName, "display_text"),
41+
resource.TestCheckResourceAttrPair(datasourceName, "domain", resourceName, "domain"),
42+
),
43+
},
44+
},
45+
})
46+
}
47+
48+
func TestAccProjectDataSource_withAccount(t *testing.T) {
49+
resourceName := "cloudstack_project.project-account-resource"
50+
datasourceName := "data.cloudstack_project.project-account-data-source"
51+
52+
resource.Test(t, resource.TestCase{
53+
PreCheck: func() { testAccPreCheck(t) },
54+
Providers: testAccProviders,
55+
Steps: []resource.TestStep{
56+
{
57+
Config: testProjectDataSourceConfig_withAccount,
58+
Check: resource.ComposeTestCheckFunc(
59+
resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"),
60+
resource.TestCheckResourceAttrPair(datasourceName, "display_text", resourceName, "display_text"),
61+
resource.TestCheckResourceAttrPair(datasourceName, "domain", resourceName, "domain"),
62+
resource.TestCheckResourceAttrPair(datasourceName, "account", resourceName, "account"),
63+
),
64+
},
65+
},
66+
})
67+
}
68+
69+
const testProjectDataSourceConfig_basic = `
70+
resource "cloudstack_project" "project-resource" {
71+
name = "test-project-datasource"
72+
display_text = "Test Project for Data Source"
73+
}
74+
75+
data "cloudstack_project" "project-data-source" {
76+
filter {
77+
name = "name"
78+
value = "test-project-datasource"
79+
}
80+
depends_on = [
81+
cloudstack_project.project-resource
82+
]
83+
}
84+
85+
output "project-output" {
86+
value = data.cloudstack_project.project-data-source
87+
}
88+
`
89+
90+
const testProjectDataSourceConfig_withAccount = `
91+
resource "cloudstack_project" "project-account-resource" {
92+
name = "test-project-account-datasource"
93+
display_text = "Test Project with Account for Data Source"
94+
account = "admin"
95+
domain = "ROOT"
96+
}
97+
98+
data "cloudstack_project" "project-account-data-source" {
99+
filter {
100+
name = "name"
101+
value = "test-project-account-datasource"
102+
}
103+
filter {
104+
name = "account"
105+
value = "admin"
106+
}
107+
depends_on = [
108+
cloudstack_project.project-account-resource
109+
]
110+
}
111+
112+
output "project-account-output" {
113+
value = data.cloudstack_project.project-account-data-source
114+
}
115+
`

cloudstack/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func Provider() *schema.Provider {
9090
"cloudstack_user": dataSourceCloudstackUser(),
9191
"cloudstack_vpn_connection": dataSourceCloudstackVPNConnection(),
9292
"cloudstack_pod": dataSourceCloudstackPod(),
93+
"cloudstack_project": dataSourceCloudstackProject(),
9394
},
9495

9596
ResourcesMap: map[string]*schema.Resource{

0 commit comments

Comments
 (0)