Skip to content

Commit

Permalink
Support project UUID identifier in resource manager project (#161)
Browse files Browse the repository at this point in the history
* Add projectId to resource manager project, fix value conversion error

* Support both uuid and container id, update acceptance tests

* Update docs

* Fix unit tests

* Adapt acc test names
  • Loading branch information
vicentepinto98 authored Dec 13, 2023
1 parent dc3c348 commit 62b6a1b
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 64 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ generate-docs:
@echo "Generating documentation with tfplugindocs"
@$(SCRIPTS_BASE)/tfplugindocs.sh

build:
@go build -o bin/terraform-provider-stackit

# TEST
test:
@echo "Running tests for the terraform provider"
Expand Down
11 changes: 6 additions & 5 deletions docs/data-sources/resourcemanager_project.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
page_title: "stackit_resourcemanager_project Data Source - stackit"
subcategory: ""
description: |-
Resource Manager project data source schema.
Resource Manager project data source schema. To identify the project, you need to provider either projectid or containerid. If you provide both, project_id will be used.
---

# stackit_resourcemanager_project (Data Source)

Resource Manager project data source schema.
Resource Manager project data source schema. To identify the project, you need to provider either project_id or container_id. If you provide both, project_id will be used.

## Example Usage

Expand All @@ -22,13 +22,14 @@ data "stackit_resourcemanager_project" "example" {
<!-- schema generated by tfplugindocs -->
## Schema

### Required
### Optional

- `container_id` (String) Project container ID.
- `container_id` (String) Project container ID. Globally unique, user-friendly identifier.
- `project_id` (String) Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.

### Read-Only

- `id` (String) Terraform's internal data source. ID. It is structured as "`container_id`".
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}
- `name` (String) Project name.
- `parent_container_id` (String) Parent container ID
- `parent_container_id` (String) Parent resource identifier. Both container ID (user-friendly) and UUID are supported
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Using this flow is less secure since the token is long-lived. You can provide th
- `region` (String) Region will be used as the default location for regional services. Not all services require a region, some are global
- `resourcemanager_custom_endpoint` (String) Custom endpoint for the Resource Manager service
- `secretsmanager_custom_endpoint` (String) Custom endpoint for the Secrets Manager service
- `service_account_email` (String) Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL
- `service_account_email` (String) Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource.
- `service_account_key` (String) Service account key used for authentication. If set alongside private key, the key flow will be used to authenticate all operations.
- `service_account_key_path` (String) Path for the service account key used for authentication. If set alongside the private key, the key flow will be used to authenticate all operations.
- `service_account_token` (String) Token used for authentication. If set, the token flow will be used to authenticate all operations.
Expand Down
7 changes: 4 additions & 3 deletions docs/resources/resourcemanager_project.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
page_title: "stackit_resourcemanager_project Resource - stackit"
subcategory: ""
description: |-
Resource Manager project resource schema.
Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration.
---

# stackit_resourcemanager_project (Resource)

Resource Manager project resource schema.
Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration.

## Example Usage

Expand All @@ -31,7 +31,7 @@ resource "stackit_resourcemanager_project" "example" {

- `name` (String) Project name.
- `owner_email` (String) Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.
- `parent_container_id` (String) Parent container ID
- `parent_container_id` (String) Parent resource identifier. Both container ID (user-friendly) and UUID are supported

### Optional

Expand All @@ -41,3 +41,4 @@ resource "stackit_resourcemanager_project" "example" {

- `container_id` (String) Project container ID. Globally unique, user-friendly identifier.
- `id` (String) Terraform's internal resource ID. It is structured as "`container_id`".
- `project_id` (String) Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.
5 changes: 4 additions & 1 deletion docs/resources/ske_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,12 @@ Optional:

Required:

- `allowed_cidrs` (List of String) Specify a list of CIDRs to whitelist.
- `enabled` (Boolean) Is ACL enabled?

Optional:

- `allowed_cidrs` (List of String) Specify a list of CIDRs to whitelist.


<a id="nestedatt--extensions--argus"></a>
### Nested Schema for `extensions.argus`
Expand Down
46 changes: 40 additions & 6 deletions stackit/internal/services/resourcemanager/project/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var (

type ModelData struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ContainerId types.String `tfsdk:"container_id"`
ContainerParentId types.String `tfsdk:"parent_container_id"`
Name types.String `tfsdk:"name"`
Expand Down Expand Up @@ -88,10 +89,11 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config
// Schema defines the schema for the data source.
func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
descriptions := map[string]string{
"main": "Resource Manager project data source schema.",
"main": "Resource Manager project data source schema. To identify the project, you need to provider either project_id or container_id. If you provide both, project_id will be used.",
"id": "Terraform's internal data source. ID. It is structured as \"`container_id`\".",
"container_id": "Project container ID.",
"parent_container_id": "Parent container ID",
"project_id": "Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.",
"container_id": "Project container ID. Globally unique, user-friendly identifier.",
"parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported",
"name": "Project name.",
"labels": `Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`,
}
Expand All @@ -103,9 +105,16 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
Description: descriptions["id"],
Computed: true,
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Optional: true,
Validators: []validator.String{
validate.UUID(),
},
},
"container_id": schema.StringAttribute{
Description: descriptions["container_id"],
Required: true,
Optional: true,
Validators: []validator.String{
validate.NoSeparator(),
},
Expand Down Expand Up @@ -154,10 +163,25 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest
if resp.Diagnostics.HasError() {
return
}

projectId := state.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)

containerId := state.ContainerId.ValueString()
ctx = tflog.SetField(ctx, "project_id", containerId)
ctx = tflog.SetField(ctx, "container_id", containerId)

projectResp, err := d.client.GetProject(ctx, containerId).Execute()
if containerId == "" && projectId == "" {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", "Either container_id or project_id must be set")
return
}

// set project identifier. If projectId is provided, it takes precedence over containerId
var identifier = containerId
if projectId != "" {
identifier = projectId
}

projectResp, err := d.client.GetProject(ctx, identifier).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Calling API: %v", err))
return
Expand All @@ -184,6 +208,15 @@ func mapDataFields(ctx context.Context, projectResp *resourcemanager.ProjectResp
return fmt.Errorf("model input is nil")
}

var projectId string
if model.ProjectId.ValueString() != "" {
projectId = model.ProjectId.ValueString()
} else if projectResp.ProjectId != nil {
projectId = *projectResp.ProjectId
} else {
return fmt.Errorf("project id not present")
}

var containerId string
if model.ContainerId.ValueString() != "" {
containerId = model.ContainerId.ValueString()
Expand All @@ -204,6 +237,7 @@ func mapDataFields(ctx context.Context, projectResp *resourcemanager.ProjectResp
}

model.Id = types.StringValue(containerId)
model.ProjectId = types.StringValue(projectId)
model.ContainerId = types.StringValue(containerId)
model.ContainerParentId = types.StringPointerValue(projectResp.Parent.ContainerId)
model.Name = types.StringPointerValue(projectResp.Name)
Expand Down
35 changes: 32 additions & 3 deletions stackit/internal/services/resourcemanager/project/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"regexp"
"strings"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
Expand Down Expand Up @@ -39,6 +40,7 @@ const (

type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ContainerId types.String `tfsdk:"container_id"`
ContainerParentId types.String `tfsdk:"parent_container_id"`
Name types.String `tfsdk:"name"`
Expand Down Expand Up @@ -102,10 +104,11 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR
// Schema defines the schema for the resource.
func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
descriptions := map[string]string{
"main": "Resource Manager project resource schema.",
"main": "Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration.",
"id": "Terraform's internal resource ID. It is structured as \"`container_id`\".",
"project_id": "Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.",
"container_id": "Project container ID. Globally unique, user-friendly identifier.",
"parent_container_id": "Parent container ID",
"parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported",
"name": "Project name.",
"labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}",
"owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.",
Expand All @@ -121,6 +124,16 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re
stringplanmodifier.UseStateForUnknown(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
},
},
"container_id": schema.StringAttribute{
Description: descriptions["container_id"],
Computed: true,
Expand Down Expand Up @@ -357,6 +370,15 @@ func mapFields(ctx context.Context, projectResp *resourcemanager.ProjectResponse
return fmt.Errorf("model input is nil")
}

var projectId string
if model.ProjectId.ValueString() != "" {
projectId = model.ProjectId.ValueString()
} else if projectResp.ProjectId != nil {
projectId = *projectResp.ProjectId
} else {
return fmt.Errorf("project id not present")
}

var containerId string
if model.ContainerId.ValueString() != "" {
containerId = model.ContainerId.ValueString()
Expand All @@ -377,9 +399,16 @@ func mapFields(ctx context.Context, projectResp *resourcemanager.ProjectResponse
}

model.Id = types.StringValue(containerId)
model.ProjectId = types.StringValue(projectId)
model.ContainerId = types.StringValue(containerId)
if projectResp.Parent != nil {
model.ContainerParentId = types.StringPointerValue(projectResp.Parent.ContainerId)
if _, err := uuid.Parse(model.ContainerParentId.ValueString()); err == nil {
// the provided containerParentId is the UUID identifier
model.ContainerParentId = types.StringPointerValue(projectResp.Parent.Id)
} else {
// the provided containerParentId is the user-friendly container id
model.ContainerParentId = types.StringPointerValue(projectResp.Parent.ContainerId)
}
} else {
model.ContainerParentId = types.StringNull()
}
Expand Down
64 changes: 55 additions & 9 deletions stackit/internal/services/resourcemanager/project/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,90 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
)

func TestMapFields(t *testing.T) {
testUUID := uuid.New().String()
tests := []struct {
description string
input *resourcemanager.ProjectResponseWithParents
expected Model
expectedLabels *map[string]string
isValid bool
description string
uuidContainerParentId bool
input *resourcemanager.ProjectResponseWithParents
expected Model
expectedLabels *map[string]string
isValid bool
}{
{
"default_ok",
false,
&resourcemanager.ProjectResponseWithParents{
ContainerId: utils.Ptr("cid"),
ProjectId: utils.Ptr("pid"),
},
Model{
Id: types.StringValue("cid"),
ContainerId: types.StringValue("cid"),
ProjectId: types.StringValue("pid"),
ContainerParentId: types.StringNull(),
Name: types.StringNull(),
},
nil,
true,
},
{
"values_ok",
"container_parent_id_ok",
false,
&resourcemanager.ProjectResponseWithParents{
ContainerId: utils.Ptr("cid"),
ProjectId: utils.Ptr("pid"),
Labels: &map[string]string{
"label1": "ref1",
"label2": "ref2",
},
Parent: &resourcemanager.Parent{
ContainerId: utils.Ptr("pid"),
ContainerId: utils.Ptr("parent_cid"),
Id: utils.Ptr("parent_pid"),
},
Name: utils.Ptr("name"),
},
Model{
Id: types.StringValue("cid"),
ContainerId: types.StringValue("cid"),
ContainerParentId: types.StringValue("pid"),
ProjectId: types.StringValue("pid"),
ContainerParentId: types.StringValue("parent_cid"),
Name: types.StringValue("name"),
},
&map[string]string{
"label1": "ref1",
"label2": "ref2",
},
true,
},
{
"uuid_parent_id_ok",
true,
&resourcemanager.ProjectResponseWithParents{
ContainerId: utils.Ptr("cid"),
ProjectId: utils.Ptr("pid"),
Labels: &map[string]string{
"label1": "ref1",
"label2": "ref2",
},
Parent: &resourcemanager.Parent{
ContainerId: utils.Ptr("parent_cid"),
Id: utils.Ptr(testUUID),
},
Name: utils.Ptr("name"),
},
Model{
Id: types.StringValue("cid"),
ContainerId: types.StringValue("cid"),
ProjectId: types.StringValue("pid"),
ContainerParentId: types.StringValue(testUUID),
Name: types.StringValue("name"),
},
&map[string]string{
Expand All @@ -60,13 +99,15 @@ func TestMapFields(t *testing.T) {
},
{
"response_nil_fail",
false,
nil,
Model{},
nil,
false,
},
{
"no_resource_id",
false,
&resourcemanager.ProjectResponseWithParents{},
Model{},
nil,
Expand All @@ -84,8 +125,13 @@ func TestMapFields(t *testing.T) {
}
tt.expected.Labels = convertedLabels
}
var containerParentId = types.StringNull()
if tt.uuidContainerParentId {
containerParentId = types.StringValue(testUUID)
}
state := &Model{
ContainerId: tt.expected.ContainerId,
ContainerId: tt.expected.ContainerId,
ContainerParentId: containerParentId,
}

err := mapFields(context.Background(), tt.input, state)
Expand Down
Loading

0 comments on commit 62b6a1b

Please sign in to comment.