Handle project members (#531)
* deprecate members field and make it valid only in creation * remove owner and members from datasource * Revert "remove owner and members from datasource" This reverts commit 31d2302166cc85abd84c2c72a0ac2ce6e70ec103. * update acc test * add creation limitation in members description --------- Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud>
This commit is contained in:
parent
4de8552303
commit
4347c6ea2d
5 changed files with 39 additions and 489 deletions
|
|
@ -24,18 +24,17 @@ resource "stackit_resourcemanager_project" "example" {
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- schema generated by tfplugindocs -->
|
<!-- schema generated by tfplugindocs -->
|
||||||
|
|
||||||
## Schema
|
## Schema
|
||||||
|
|
||||||
### Required
|
### Required
|
||||||
|
|
||||||
- `name` (String) Project name.
|
- `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 resource identifier. Both container ID (user-friendly) and UUID are supported
|
- `parent_container_id` (String) Parent resource identifier. Both container ID (user-friendly) and UUID are supported
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|
||||||
- `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}
|
- `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}
|
||||||
- `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.
|
|
||||||
|
|
||||||
### Read-Only
|
### Read-Only
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,17 +102,18 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config
|
||||||
// Schema defines the schema for the data source.
|
// Schema defines the schema for the data source.
|
||||||
func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||||
descriptions := map[string]string{
|
descriptions := map[string]string{
|
||||||
"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.",
|
"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`\".",
|
"id": "Terraform's internal data source. 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.",
|
"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.",
|
"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",
|
"parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported",
|
||||||
"name": "Project name.",
|
"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}`,
|
"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.",
|
"owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.",
|
||||||
"members": "The members assigned to the project. At least one subject needs to be a user, and not a client or service account.",
|
"members": "The members assigned to the project. At least one subject needs to be a user, and not a client or service account. This value is only considered during creation. Changing it afterwards will have no effect.",
|
||||||
"members.role": fmt.Sprintf("The role of the member in the project. Legacy roles (%s) are not supported.", strings.Join(utils.QuoteValues(utils.LegacyProjectRoles), ", ")),
|
"members.role": fmt.Sprintf("The role of the member in the project. Legacy roles (%s) are not supported.", strings.Join(utils.QuoteValues(utils.LegacyProjectRoles), ", ")),
|
||||||
"members.subject": "Unique identifier of the user, service account or client. This is usually the email address for users or service accounts, and the name in case of clients.",
|
"members.subject": "Unique identifier of the user, service account or client. This is usually the email address for users or service accounts, and the name in case of clients.",
|
||||||
|
"members_deprecation_message": "The \"members\" field has been deprecated in favor of the \"owner_email\" field. Please use the \"owner_email\" field to assign the owner role to a user.",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
|
@ -173,8 +174,10 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
"members": schema.ListNestedAttribute{
|
"members": schema.ListNestedAttribute{
|
||||||
Description: descriptions["members"],
|
Description: descriptions["members"],
|
||||||
Computed: true,
|
DeprecationMessage: descriptions["members_deprecation_message"],
|
||||||
|
MarkdownDescription: fmt.Sprintf("%s\n\n!> %s", descriptions["members"], descriptions["members_deprecation_message"]),
|
||||||
|
Computed: true,
|
||||||
NestedObject: schema.NestedAttributeObject{
|
NestedObject: schema.NestedAttributeObject{
|
||||||
Attributes: map[string]schema.Attribute{
|
Attributes: map[string]schema.Attribute{
|
||||||
"role": schema.StringAttribute{
|
"role": schema.StringAttribute{
|
||||||
|
|
@ -237,17 +240,6 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
membersResp, err := d.membershipClient.ListMembersExecute(ctx, projectResourceType, *projectResp.ProjectId)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Reading members: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mapMembersFields(ctx, membersResp.Members, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API response: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
diags = resp.State.Set(ctx, &model)
|
diags = resp.State.Set(ctx, &model)
|
||||||
resp.Diagnostics.Append(diags...)
|
resp.Diagnostics.Append(diags...)
|
||||||
if resp.Diagnostics.HasError() {
|
if resp.Diagnostics.HasError() {
|
||||||
|
|
|
||||||
|
|
@ -147,17 +147,18 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR
|
||||||
// Schema defines the schema for the resource.
|
// Schema defines the schema for the resource.
|
||||||
func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
descriptions := map[string]string{
|
descriptions := map[string]string{
|
||||||
"main": "Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration.",
|
"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`\".",
|
"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.",
|
"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.",
|
"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",
|
"parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported",
|
||||||
"name": "Project name.",
|
"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}",
|
"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.",
|
"owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.",
|
||||||
"members": "The members assigned to the project. At least one subject needs to be a user, and not a client or service account.",
|
"members": "The members assigned to the project. At least one subject needs to be a user, and not a client or service account. This value is only considered during creation. Changing it afterwards will have no effect.",
|
||||||
"members.role": fmt.Sprintf("The role of the member in the project. Possible values include, but are not limited to: `owner`, `editor`, `reader`. Legacy roles (%s) are not supported.", strings.Join(utils.QuoteValues(utils.LegacyProjectRoles), ", ")),
|
"members.role": fmt.Sprintf("The role of the member in the project. Possible values include, but are not limited to: `owner`, `editor`, `reader`. Legacy roles (%s) are not supported.", strings.Join(utils.QuoteValues(utils.LegacyProjectRoles), ", ")),
|
||||||
"members.subject": "Unique identifier of the user, service account or client. This is usually the email address for users or service accounts, and the name in case of clients.",
|
"members.subject": "Unique identifier of the user, service account or client. This is usually the email address for users or service accounts, and the name in case of clients.",
|
||||||
|
"members_deprecation_message": "The \"members\" field has been deprecated in favor of the \"owner_email\" field. Please use the \"owner_email\" field to assign the owner role to a user.",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
|
@ -224,12 +225,13 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re
|
||||||
},
|
},
|
||||||
"owner_email": schema.StringAttribute{
|
"owner_email": schema.StringAttribute{
|
||||||
Description: descriptions["owner_email"],
|
Description: descriptions["owner_email"],
|
||||||
// When removing the owner_email field, we should mark the members field as required and add a listvalidator.SizeAtLeast(1) validator to it
|
Required: true,
|
||||||
Optional: true,
|
|
||||||
},
|
},
|
||||||
"members": schema.ListNestedAttribute{
|
"members": schema.ListNestedAttribute{
|
||||||
Description: descriptions["members"],
|
Description: descriptions["members"],
|
||||||
Optional: true,
|
DeprecationMessage: descriptions["members_deprecation_message"],
|
||||||
|
MarkdownDescription: fmt.Sprintf("%s\n\n!> %s", descriptions["members"], descriptions["members_deprecation_message"]),
|
||||||
|
Optional: true,
|
||||||
NestedObject: schema.NestedAttributeObject{
|
NestedObject: schema.NestedAttributeObject{
|
||||||
Attributes: map[string]schema.Attribute{
|
Attributes: map[string]schema.Attribute{
|
||||||
"role": schema.StringAttribute{
|
"role": schema.StringAttribute{
|
||||||
|
|
@ -393,17 +395,6 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
membersResp, err := r.authorizationClient.ListMembersExecute(ctx, projectResourceType, *projectResp.ProjectId)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Reading members: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mapMembersFields(ctx, membersResp.Members, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API response: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set refreshed model
|
// Set refreshed model
|
||||||
diags = resp.State.Set(ctx, model)
|
diags = resp.State.Set(ctx, model)
|
||||||
resp.Diagnostics.Append(diags...)
|
resp.Diagnostics.Append(diags...)
|
||||||
|
|
@ -451,23 +442,6 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
members, err := toMembersPayload(ctx, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Processing members: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = updateMembers(ctx, *projectResp.ProjectId, members, r.authorizationClient)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Updating members: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mapMembersFields(ctx, members, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Processing API response: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
diags = resp.State.Set(ctx, model)
|
diags = resp.State.Set(ctx, model)
|
||||||
resp.Diagnostics.Append(diags...)
|
resp.Diagnostics.Append(diags...)
|
||||||
if resp.Diagnostics.HasError() {
|
if resp.Diagnostics.HasError() {
|
||||||
|
|
@ -690,7 +664,7 @@ func toMembersPayload(ctx context.Context, model *Model) (*[]authorization.Membe
|
||||||
return nil, core.DiagsToError(diags)
|
return nil, core.DiagsToError(diags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the new "members" fields is set, it has precedence over the deprecated "owner_email" field
|
// If the new "members" fields is set, it has precedence over the "owner_email" field
|
||||||
members := []authorization.Member{}
|
members := []authorization.Member{}
|
||||||
for _, m := range membersModel {
|
for _, m := range membersModel {
|
||||||
members = append(members, authorization.Member{
|
members = append(members, authorization.Member{
|
||||||
|
|
@ -756,102 +730,6 @@ func toUpdatePayload(model *Model) (*resourcemanager.PartialUpdateProjectPayload
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateMembers adds and removes members to match the model
|
|
||||||
func updateMembers(ctx context.Context, projectId string, modelMembers *[]authorization.Member, client *authorization.APIClient) error {
|
|
||||||
if modelMembers == nil || len(*modelMembers) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current members
|
|
||||||
currentMembersResp, err := client.ListMembersExecute(ctx, projectResourceType, projectId)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get members: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type memberState struct {
|
|
||||||
isInModel bool
|
|
||||||
isCreated bool
|
|
||||||
subject string
|
|
||||||
role string
|
|
||||||
}
|
|
||||||
|
|
||||||
membersState := make(map[string]*memberState) // Key in the form of "subject,role"
|
|
||||||
for _, m := range *modelMembers {
|
|
||||||
mId := memberId(m)
|
|
||||||
membersState[mId] = &memberState{
|
|
||||||
isInModel: true,
|
|
||||||
subject: *m.Subject,
|
|
||||||
role: *m.Role,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range *currentMembersResp.Members {
|
|
||||||
if utils.IsLegacyProjectRole(*m.Role) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mId := memberId(m)
|
|
||||||
_, ok := membersState[mId]
|
|
||||||
if !ok {
|
|
||||||
membersState[mId] = &memberState{}
|
|
||||||
}
|
|
||||||
membersState[mId].isCreated = true
|
|
||||||
membersState[mId].subject = *m.Subject
|
|
||||||
membersState[mId].role = *m.Role
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add/remove members
|
|
||||||
membersToAdd := make([]authorization.Member, 0)
|
|
||||||
membersToRemove := make([]authorization.Member, 0)
|
|
||||||
for _, state := range membersState {
|
|
||||||
if state.isInModel && !state.isCreated {
|
|
||||||
m := authorization.Member{
|
|
||||||
Subject: &state.subject,
|
|
||||||
Role: &state.role,
|
|
||||||
}
|
|
||||||
membersToAdd = append(membersToAdd, m)
|
|
||||||
|
|
||||||
infoMsg := fmt.Sprintf("### Will add member to project: { role: %s, subject: %s }", state.role, state.subject)
|
|
||||||
tflog.Warn(ctx, infoMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !state.isInModel && state.isCreated {
|
|
||||||
m := authorization.Member{
|
|
||||||
Subject: &state.subject,
|
|
||||||
Role: &state.role,
|
|
||||||
}
|
|
||||||
membersToRemove = append(membersToRemove, m)
|
|
||||||
|
|
||||||
infoMsg := fmt.Sprintf("### Will remove member from project: { role: %s, subject: %s }", state.role, state.subject)
|
|
||||||
tflog.Warn(ctx, infoMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(membersToAdd) > 0 {
|
|
||||||
payload := authorization.AddMembersPayload{
|
|
||||||
Members: &membersToAdd,
|
|
||||||
ResourceType: sdkUtils.Ptr(projectResourceType),
|
|
||||||
}
|
|
||||||
_, err := client.AddMembers(ctx, projectId).AddMembersPayload(payload).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("add members: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(membersToRemove) > 0 {
|
|
||||||
payload := authorization.RemoveMembersPayload{
|
|
||||||
Members: &membersToRemove,
|
|
||||||
ResourceType: sdkUtils.Ptr(projectResourceType),
|
|
||||||
}
|
|
||||||
_, err := client.RemoveMembers(ctx, projectId).RemoveMembersPayload(payload).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("remove members: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal representation of a member, which is uniquely identified by the subject and role
|
// Internal representation of a member, which is uniquely identified by the subject and role
|
||||||
func memberId(member authorization.Member) string {
|
func memberId(member authorization.Member) string {
|
||||||
return fmt.Sprintf("%s,%s", *member.Subject, *member.Role)
|
return fmt.Sprintf("%s,%s", *member.Subject, *member.Role)
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,13 @@ package project
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/authorization"
|
"github.com/stackitcloud/stackit-sdk-go/services/authorization"
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
|
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
|
||||||
|
|
@ -607,309 +602,3 @@ func TestToUpdatePayload(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fixtureMembers = []authorization.Member{
|
|
||||||
{
|
|
||||||
Subject: utils.Ptr("email_owner"),
|
|
||||||
Role: utils.Ptr("owner"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Subject: utils.Ptr("email_owner_2"),
|
|
||||||
Role: utils.Ptr("owner"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Subject: utils.Ptr("email_reader"),
|
|
||||||
Role: utils.Ptr("reader"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateMembers(t *testing.T) {
|
|
||||||
// This is the response used when getting all members currently, across all tests
|
|
||||||
getAllMembersResp := authorization.MembersResponse{
|
|
||||||
Members: &fixtureMembers,
|
|
||||||
}
|
|
||||||
getAllMembersRespBytes, err := json.Marshal(getAllMembersResp)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal get all Members response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the response used whenever an API returns a failure response
|
|
||||||
failureRespBytes := []byte("{\"message\": \"Something bad happened\"")
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
description string
|
|
||||||
modelMembers []authorization.Member
|
|
||||||
getAllMembersFails bool
|
|
||||||
addMembersFails bool
|
|
||||||
removeMembersFails bool
|
|
||||||
isValid bool
|
|
||||||
expectedMembersStates map[string]bool // Keys are member; value is true if member should exist at the end, false otherwise
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
description: "no changes",
|
|
||||||
modelMembers: fixtureMembers,
|
|
||||||
expectedMembersStates: map[string]bool{
|
|
||||||
memberId(fixtureMembers[0]): true,
|
|
||||||
memberId(fixtureMembers[1]): true,
|
|
||||||
memberId(fixtureMembers[2]): true,
|
|
||||||
},
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "add one member",
|
|
||||||
modelMembers: append(
|
|
||||||
fixtureMembers,
|
|
||||||
authorization.Member{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")},
|
|
||||||
),
|
|
||||||
expectedMembersStates: map[string]bool{
|
|
||||||
memberId(fixtureMembers[0]): true,
|
|
||||||
memberId(fixtureMembers[1]): true,
|
|
||||||
memberId(fixtureMembers[2]): true,
|
|
||||||
"email_reader_2,reader": true,
|
|
||||||
},
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "add multiple members",
|
|
||||||
modelMembers: append(
|
|
||||||
fixtureMembers,
|
|
||||||
authorization.Member{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")},
|
|
||||||
authorization.Member{Subject: utils.Ptr("email_reader_3"), Role: utils.Ptr("reader")},
|
|
||||||
),
|
|
||||||
expectedMembersStates: map[string]bool{
|
|
||||||
memberId(fixtureMembers[0]): true,
|
|
||||||
memberId(fixtureMembers[1]): true,
|
|
||||||
memberId(fixtureMembers[2]): true,
|
|
||||||
"email_reader_2,reader": true,
|
|
||||||
"email_reader_3,reader": true,
|
|
||||||
},
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "removing member",
|
|
||||||
modelMembers: fixtureMembers[:2],
|
|
||||||
expectedMembersStates: map[string]bool{
|
|
||||||
memberId(fixtureMembers[0]): true,
|
|
||||||
memberId(fixtureMembers[1]): true,
|
|
||||||
memberId(fixtureMembers[2]): false,
|
|
||||||
},
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "removing multiple members",
|
|
||||||
modelMembers: fixtureMembers[:1],
|
|
||||||
expectedMembersStates: map[string]bool{
|
|
||||||
memberId(fixtureMembers[0]): true,
|
|
||||||
memberId(fixtureMembers[1]): false,
|
|
||||||
memberId(fixtureMembers[2]): false,
|
|
||||||
},
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "multiple changes (add and remove)",
|
|
||||||
modelMembers: append(
|
|
||||||
fixtureMembers[:2],
|
|
||||||
authorization.Member{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")},
|
|
||||||
authorization.Member{Subject: utils.Ptr("email_reader_3"), Role: utils.Ptr("reader")},
|
|
||||||
),
|
|
||||||
expectedMembersStates: map[string]bool{
|
|
||||||
memberId(fixtureMembers[0]): true,
|
|
||||||
memberId(fixtureMembers[1]): true,
|
|
||||||
memberId(fixtureMembers[2]): false,
|
|
||||||
"email_reader_2,reader": true,
|
|
||||||
"email_reader_3,reader": true,
|
|
||||||
},
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "multiple changes 2 (add and remove)",
|
|
||||||
modelMembers: []authorization.Member{
|
|
||||||
{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")},
|
|
||||||
{Subject: utils.Ptr("email_reader_3"), Role: utils.Ptr("reader")},
|
|
||||||
},
|
|
||||||
expectedMembersStates: map[string]bool{
|
|
||||||
memberId(fixtureMembers[0]): false,
|
|
||||||
memberId(fixtureMembers[1]): false,
|
|
||||||
memberId(fixtureMembers[2]): false,
|
|
||||||
"email_reader_2,reader": true,
|
|
||||||
"email_reader_3,reader": true,
|
|
||||||
},
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "get fails",
|
|
||||||
modelMembers: fixtureMembers,
|
|
||||||
getAllMembersFails: true,
|
|
||||||
isValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "add fails",
|
|
||||||
modelMembers: append(
|
|
||||||
fixtureMembers,
|
|
||||||
authorization.Member{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")},
|
|
||||||
),
|
|
||||||
addMembersFails: true,
|
|
||||||
isValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "remove fails",
|
|
||||||
modelMembers: fixtureMembers[:1],
|
|
||||||
removeMembersFails: true,
|
|
||||||
isValid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.description, func(t *testing.T) {
|
|
||||||
// Will be compared to tt.expectedMembersStates at the end
|
|
||||||
membersStates := map[string]bool{
|
|
||||||
memberId(fixtureMembers[0]): true,
|
|
||||||
memberId(fixtureMembers[1]): true,
|
|
||||||
memberId(fixtureMembers[2]): true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler for getting all Members
|
|
||||||
getAllMembersHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if tt.getAllMembersFails {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
_, err := w.Write(failureRespBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get all Members handler: failed to write bad response: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := w.Write(getAllMembersRespBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get all Members handler: failed to write response: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handler for adding members
|
|
||||||
addMembersHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var payload authorization.AddMembersPayload
|
|
||||||
err := decoder.Decode(&payload)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Add members handler: failed to parse payload")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if payload.Members == nil {
|
|
||||||
t.Errorf("Add members handler: nil members")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
members := *payload.Members
|
|
||||||
for _, m := range members {
|
|
||||||
if memberExists, memberWasAdded := membersStates[memberId(m)]; memberWasAdded && memberExists {
|
|
||||||
t.Errorf("Add members handler: attempted to add member '%v' that already exists", memberId(m))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if tt.addMembersFails {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
_, err := w.Write(failureRespBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Add members handler: failed to write bad response: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range members {
|
|
||||||
membersStates[memberId(m)] = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handler for removing members
|
|
||||||
removeMembersHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var payload authorization.RemoveMembersPayload
|
|
||||||
err := decoder.Decode(&payload)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Remove members handler: failed to parse payload")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if payload.Members == nil {
|
|
||||||
t.Errorf("Remove members handler: nil members")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
members := *payload.Members
|
|
||||||
for _, m := range members {
|
|
||||||
memberExists, memberWasCreated := membersStates[memberId(m)]
|
|
||||||
if !memberWasCreated {
|
|
||||||
t.Errorf("Remove members handler: attempted to remove member '%v' that wasn't created", memberId(m))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if memberWasCreated && !memberExists {
|
|
||||||
t.Errorf("Remove members handler: attempted to remove member '%v' that was already removed", memberId(m))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if tt.removeMembersFails {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
_, err := w.Write(failureRespBytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Remove members handler: failed to write bad response: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range members {
|
|
||||||
membersStates[memberId(m)] = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup server and client
|
|
||||||
router := mux.NewRouter()
|
|
||||||
router.HandleFunc("/v2/{resourceType}/{resourceId}/members", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
getAllMembersHandler(w, r)
|
|
||||||
} else {
|
|
||||||
t.Fatalf("Unexpected method: %v", r.Method)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
router.HandleFunc("/v2/{resourceId}/members", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodPatch {
|
|
||||||
addMembersHandler(w, r)
|
|
||||||
} else {
|
|
||||||
t.Fatalf("Unexpected method: %v", r.Method)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
router.HandleFunc("/v2/{resourceId}/members/remove", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
removeMembersHandler(w, r)
|
|
||||||
} else {
|
|
||||||
t.Fatalf("Unexpected method: %v", r.Method)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
mockedServer := httptest.NewServer(router)
|
|
||||||
defer mockedServer.Close()
|
|
||||||
client, err := authorization.NewAPIClient(
|
|
||||||
config.WithEndpoint(mockedServer.URL),
|
|
||||||
config.WithoutAuthentication(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to initialize client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run test
|
|
||||||
err = updateMembers(context.Background(), "pid", &tt.modelMembers, client)
|
|
||||||
if !tt.isValid && err == nil {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tt.isValid && err != nil {
|
|
||||||
t.Fatalf("Should not have failed: %v", err)
|
|
||||||
}
|
|
||||||
if tt.isValid {
|
|
||||||
diff := cmp.Diff(membersStates, tt.expectedMembersStates)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Member states do not match: %s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ func resourceConfig(name string, label *string, members string) string {
|
||||||
members = [
|
members = [
|
||||||
%[7]s
|
%[7]s
|
||||||
]
|
]
|
||||||
|
owner_email = "%[8]s"
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "stackit_resourcemanager_project" "parent_by_uuid" {
|
resource "stackit_resourcemanager_project" "parent_by_uuid" {
|
||||||
|
|
@ -64,6 +65,7 @@ func resourceConfig(name string, label *string, members string) string {
|
||||||
members = [
|
members = [
|
||||||
%[7]s
|
%[7]s
|
||||||
]
|
]
|
||||||
|
owner_email = "%[8]s"
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
testutil.ResourceManagerProviderConfig(),
|
testutil.ResourceManagerProviderConfig(),
|
||||||
|
|
@ -73,6 +75,7 @@ func resourceConfig(name string, label *string, members string) string {
|
||||||
labelConfig,
|
labelConfig,
|
||||||
projectResource["parent_uuid"],
|
projectResource["parent_uuid"],
|
||||||
members,
|
members,
|
||||||
|
testutil.TestProjectServiceAccountEmail,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,17 +87,6 @@ func TestAccResourceManagerResource(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
updatedMembersConfig := membersConfig([]authorization.Member{
|
|
||||||
{
|
|
||||||
Subject: &testutil.TestProjectUserEmail,
|
|
||||||
Role: utils.Ptr("owner"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Subject: &testutil.TestProjectUserEmail,
|
|
||||||
Role: utils.Ptr("reader"),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
resource.Test(t, resource.TestCase{
|
resource.Test(t, resource.TestCase{
|
||||||
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
|
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
|
||||||
CheckDestroy: testAccCheckResourceManagerDestroy,
|
CheckDestroy: testAccCheckResourceManagerDestroy,
|
||||||
|
|
@ -190,7 +182,7 @@ func TestAccResourceManagerResource(t *testing.T) {
|
||||||
},
|
},
|
||||||
// Update
|
// Update
|
||||||
{
|
{
|
||||||
Config: resourceConfig(fmt.Sprintf("%s-new", projectResource["name"]), utils.Ptr("a-label"), updatedMembersConfig),
|
Config: resourceConfig(fmt.Sprintf("%s-new", projectResource["name"]), utils.Ptr("a-label"), initialMembersConfig),
|
||||||
Check: resource.ComposeAggregateTestCheckFunc(
|
Check: resource.ComposeAggregateTestCheckFunc(
|
||||||
// Project data
|
// Project data
|
||||||
resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_container", "container_id"),
|
resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_container", "container_id"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue