Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support project UUID identifier in resource manager project #161

Merged
merged 5 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
vicentepinto98 marked this conversation as resolved.
Show resolved Hide resolved

# 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",
vicentepinto98 marked this conversation as resolved.
Show resolved Hide resolved
"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