From e124fbcef83af0a8be9c4cbb80a9e6db1cdfe1cd Mon Sep 17 00:00:00 2001 From: Aleksander Zaruczewski Date: Sat, 6 Jan 2024 13:49:51 +0200 Subject: [PATCH] feat(organization): application users support (#1508) Co-authored-by: Ivan Savciuc --- CHANGELOG.md | 1 + .../organization_application_user.md | 26 ++ .../organization_application_user.md | 41 +++ docs/resources/organization_group_project.md | 8 +- .../organization_user_group_member.md | 4 +- go.mod | 2 +- go.sum | 10 +- internal/acctest/acctest.go | 72 +++++ internal/plugin/errmsg/errmsg.go | 19 ++ internal/plugin/provider.go | 29 +- .../organization_application_user.go | 296 ++++++++++++++++++ ...ganization_application_user_data_source.go | 160 ++++++++++ .../organization_application_user_test.go | 113 +++++++ .../organization_group_project.go | 177 ++++++----- .../organization_group_project_test.go | 70 +++-- .../organization/organization_resource.go | 8 +- .../organization_user_group_member.go | 106 ++++--- .../organization_user_group_member_test.go | 72 +++-- internal/plugin/util/diag.go | 9 + internal/plugin/util/helpers.go | 30 ++ internal/plugin/util/importstate.go | 24 ++ .../service/connectionpool/sweep.go | 60 ++++ .../organization_user_data_source.go | 1 + internal/sweep/sweep_test.go | 1 + 24 files changed, 1145 insertions(+), 194 deletions(-) create mode 100644 docs/data-sources/organization_application_user.md create mode 100644 docs/resources/organization_application_user.md create mode 100644 internal/plugin/service/organization/organization_application_user.go create mode 100644 internal/plugin/service/organization/organization_application_user_data_source.go create mode 100644 internal/plugin/service/organization/organization_application_user_test.go create mode 100644 internal/plugin/util/helpers.go create mode 100644 internal/plugin/util/importstate.go create mode 100644 internal/sdkprovider/service/connectionpool/sweep.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 122d12ab8..0ce6cd587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ nav_order: 1 ## [MAJOR.MINOR.PATCH] - YYYY-MM-DD +- Add organization application users support ## [4.12.1] - 2024-01-05 diff --git a/docs/data-sources/organization_application_user.md b/docs/data-sources/organization_application_user.md new file mode 100644 index 000000000..017705b49 --- /dev/null +++ b/docs/data-sources/organization_application_user.md @@ -0,0 +1,26 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_organization_application_user Data Source - terraform-provider-aiven" +subcategory: "" +description: |- + Retrieves information about an organization application user from Aiven. +--- + +# aiven_organization_application_user (Data Source) + +Retrieves information about an organization application user from Aiven. + + + + +## Schema + +### Required + +- `organization_id` (String) Identifier of the organization the application user belongs to. +- `user_id` (String) Identifier of the organization application user. + +### Read-Only + +- `email` (String) Email of the organization application user. +- `name` (String) Name of the organization application user. diff --git a/docs/resources/organization_application_user.md b/docs/resources/organization_application_user.md new file mode 100644 index 000000000..3f9bc6d69 --- /dev/null +++ b/docs/resources/organization_application_user.md @@ -0,0 +1,41 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_organization_application_user Resource - terraform-provider-aiven" +subcategory: "" +description: |- + Creates and manages an organization application user in Aiven. Please note that this resource is in beta and may change without notice. To use this resource, please set the PROVIDERAIVENENABLE_BETA environment variable. +--- + +# aiven_organization_application_user (Resource) + +Creates and manages an organization application user in Aiven. Please note that this resource is in beta and may change without notice. To use this resource, please set the PROVIDER_AIVEN_ENABLE_BETA environment variable. + + + + +## Schema + +### Required + +- `name` (String) Name of the organization application user. +- `organization_id` (String) Identifier of the organization the application user belongs to. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `email` (String) Email of the organization application user. +- `id` (String) Compound identifier of the organization application user. +- `user_id` (String) Identifier of the organization application user. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). diff --git a/docs/resources/organization_group_project.md b/docs/resources/organization_group_project.md index ca2a4c847..6af93ce71 100644 --- a/docs/resources/organization_group_project.md +++ b/docs/resources/organization_group_project.md @@ -3,12 +3,12 @@ page_title: "aiven_organization_group_project Resource - terraform-provider-aiven" subcategory: "" description: |- - Creates and manages an organization group project relations in Aiven. + Creates and manages an organization group project relations in Aiven. Please note that this resource is in beta and may change without notice. To use this resource, please set the PROVIDERAIVENENABLE_BETA environment variable. --- # aiven_organization_group_project (Resource) -Creates and manages an organization group project relations in Aiven. +Creates and manages an organization group project relations in Aiven. Please note that this resource is in beta and may change without notice. To use this resource, please set the PROVIDER_AIVEN_ENABLE_BETA environment variable. @@ -25,6 +25,10 @@ Creates and manages an organization group project relations in Aiven. - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +### Read-Only + +- `id` (String) Compound identifier of the organization group project relation. + ### Nested Schema for `timeouts` diff --git a/docs/resources/organization_user_group_member.md b/docs/resources/organization_user_group_member.md index 90f88e89a..0b5a686cb 100644 --- a/docs/resources/organization_user_group_member.md +++ b/docs/resources/organization_user_group_member.md @@ -3,12 +3,12 @@ page_title: "aiven_organization_user_group_member Resource - terraform-provider-aiven" subcategory: "" description: |- - Creates and manages an organization user group members in Aiven. Please no that this resource is in beta and may change without notice. To use it please use the beta environment variable PROVIDERAIVENENABLE_BETA. + Creates and manages an organization user group members in Aiven. Please note that this resource is in beta and may change without notice. To use this resource, please set the PROVIDERAIVENENABLE_BETA environment variable. --- # aiven_organization_user_group_member (Resource) -Creates and manages an organization user group members in Aiven. Please no that this resource is in beta and may change without notice. To use it please use the beta environment variable PROVIDER_AIVEN_ENABLE_BETA. +Creates and manages an organization user group members in Aiven. Please note that this resource is in beta and may change without notice. To use this resource, please set the PROVIDER_AIVEN_ENABLE_BETA environment variable. diff --git a/go.mod b/go.mod index 1817827d1..0838e0b86 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/aiven/terraform-provider-aiven go 1.21 require ( - github.com/aiven/aiven-go-client/v2 v2.8.0 + github.com/aiven/aiven-go-client/v2 v2.9.0 github.com/avast/retry-go v3.0.0+incompatible github.com/dave/jennifer v1.7.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index da7ea587f..c5b8e461a 100644 --- a/go.sum +++ b/go.sum @@ -197,14 +197,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjA github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/aiven/aiven-go-client/v2 v2.6.0 h1:mSWs0rqkvt3BZ6ljX8H0paSyMkiFU7hptTYBmUmB6E0= -github.com/aiven/aiven-go-client/v2 v2.6.0/go.mod h1:x0xhzxWEKAwKv0xY5FvECiI6tesWshcPHvjwl0B/1SU= -github.com/aiven/aiven-go-client/v2 v2.6.1-0.20240102144320-a7653ed76349 h1:8yrp+pihD3pLLqttT75m1aa0JRIfSk3xbdpDsRj4Jjw= -github.com/aiven/aiven-go-client/v2 v2.6.1-0.20240102144320-a7653ed76349/go.mod h1:x0xhzxWEKAwKv0xY5FvECiI6tesWshcPHvjwl0B/1SU= -github.com/aiven/aiven-go-client/v2 v2.7.0 h1:nvDfzxsELIt+df7zLqePbrI6ZtazmNvhy/8LyKOZ810= -github.com/aiven/aiven-go-client/v2 v2.7.0/go.mod h1:x0xhzxWEKAwKv0xY5FvECiI6tesWshcPHvjwl0B/1SU= -github.com/aiven/aiven-go-client/v2 v2.8.0 h1:1JZVmpp2emjrIWrDiec9+7CqTRyH/qTnhzJxkYUgVlU= -github.com/aiven/aiven-go-client/v2 v2.8.0/go.mod h1:x0xhzxWEKAwKv0xY5FvECiI6tesWshcPHvjwl0B/1SU= +github.com/aiven/aiven-go-client/v2 v2.9.0 h1:LpfilLbahoCzDglh77jbwD5L8sJI3p7BvuZ/NXZoA8w= +github.com/aiven/aiven-go-client/v2 v2.9.0/go.mod h1:x0xhzxWEKAwKv0xY5FvECiI6tesWshcPHvjwl0B/1SU= github.com/aiven/go-api-schemas v1.51.0 h1:6e9oxSTIhKFixOJV3fbnrpKAULhVxls4U4smZjRE7cU= github.com/aiven/go-api-schemas v1.51.0/go.mod h1:/bPxBUHza/2Aeer6hIIdB++GxKiw9K1KCBtRa2rtZ5I= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index 828b5e690..56d71b57b 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/plugin/errmsg" "github.com/aiven/terraform-provider-aiven/internal/schemautil" "github.com/aiven/terraform-provider-aiven/internal/server" ) @@ -29,6 +30,7 @@ var ( } ) +// GetTestAivenClient returns a new Aiven client that can be used for acceptance tests. func GetTestAivenClient() *aiven.Client { testAivenClientOnce.Do(func() { client, err := common.NewAivenClient() @@ -40,6 +42,65 @@ func GetTestAivenClient() *aiven.Client { return testAivenClient } +// commonTestDependencies is a struct that contains common dependencies that are used by acceptance tests. +type commonTestDependencies struct { + // isBeta is a flag that indicates whether the provider is in beta mode. + isBeta bool + // organizationName is the name of the organization that is used for acceptance tests. + organizationName string + // organizationUserID is the ID of the organization user that is used for acceptance tests. + organizationUserID *string +} + +// IsBeta returns a flag that indicates whether the provider is in beta mode. +func (d *commonTestDependencies) IsBeta() bool { + return d.isBeta +} + +// OrganizationName returns the name of the organization that is used for acceptance tests. +func (d *commonTestDependencies) OrganizationName() string { + return d.organizationName +} + +// OrganizationUserID returns the ID of the organization user that is used for acceptance tests. +func (d *commonTestDependencies) OrganizationUserID() *string { + return d.organizationUserID +} + +// CommonTestDependencies returns a new commonTestDependencies struct that contains common dependencies that are +// used by acceptance tests. +// nolint:revive // Ignore unexported type error because this type is not meant to be used outside of this package. +func CommonTestDependencies(t *testing.T) *commonTestDependencies { + // We mimic the real error message that is returned by Terraform when the acceptance tests are skipped. + // + // This is done because the tests that use this function are running it before the real Terraform check takes + // place, and we want to avoid false positively running this function when the acceptance tests are not actually + // ran, e.g. if unit tests are ran instead. + // + // See https://github.com/hashicorp/terraform-plugin-testing/blob/v1.6.0/helper/resource/testing.go#L849-L857 for + // more details on the real check. + if _, ok := os.LookupEnv("TF_ACC"); !ok { + t.Skip("Acceptance tests skipped unless env 'TF_ACC' set") + } + + deps := &commonTestDependencies{ + isBeta: os.Getenv("PROVIDER_AIVEN_ENABLE_BETA") != "", + } + + organizationName, ok := os.LookupEnv("AIVEN_ORGANIZATION_NAME") + if !ok { + t.Fatal("AIVEN_ORGANIZATION_NAME environment variable must be set for acceptance tests.") + } + deps.organizationName = organizationName + + organizationUserID, ok := os.LookupEnv("AIVEN_ORGANIZATION_USER_ID") + if ok { + deps.organizationUserID = &organizationUserID + } + + return deps +} + const ( // DefaultResourceNamePrefix is the default prefix used for resource names in acceptance tests. DefaultResourceNamePrefix = "test-acc" @@ -60,6 +121,7 @@ func TestAccPreCheck(t *testing.T) { } } +// TestAccCheckAivenServiceResourceDestroy verifies that the given service is destroyed. func TestAccCheckAivenServiceResourceDestroy(s *terraform.State) error { c := GetTestAivenClient() @@ -112,3 +174,13 @@ func TestAccCheckAivenServiceResourceDestroy(s *terraform.State) error { return nil } + +// ResourceFromState returns a resource state from the given Terraform state. +func ResourceFromState(state *terraform.State, name string) (*terraform.ResourceState, error) { + rs, ok := state.RootModule().Resources[name] + if !ok { + return nil, fmt.Errorf(errmsg.ResourceNotFound, name) + } + + return rs, nil +} diff --git a/internal/plugin/errmsg/errmsg.go b/internal/plugin/errmsg/errmsg.go index 561d4c70d..3f77cbc33 100644 --- a/internal/plugin/errmsg/errmsg.go +++ b/internal/plugin/errmsg/errmsg.go @@ -80,6 +80,9 @@ var ( // DetailErrorUpdatingResource is the detailed error message for when a resource cannot be updated. DetailErrorUpdatingResource = "An unexpected error occurred while updating the resource (%s): %s." + // DetailErrorUpdatingResourceNotSupported is the detailed error message for when a resource cannot be updated. + DetailErrorUpdatingResourceNotSupported = "Updating the resource (%s) is not supported." + // DetailErrorDeletingResource is the detailed error message for when a resource cannot be deleted. DetailErrorDeletingResource = "An unexpected error occurred while deleting the resource (%s): %s." @@ -90,3 +93,19 @@ var ( // DetailErrorReadingDataSource is the detailed error message for when a data source cannot be read. DetailErrorReadingDataSource = "An unexpected error occurred while reading the data source (%s): %s." ) + +// Below is the list of classic Go-style error messages that are used in the provider. +// The classic Go-style error messages are used to provide more information about the error. +// The classic Go-style error messages should start with a lowercase letter and SHOULD NOT end with a period. +// The classic Go-style error messages may contain placeholders for values that are not known at the time of writing. +// +// See Go error handling for more information: +// https://blog.golang.org/error-handling-and-go. +var ( + // ResourceNotFound is the error message for when a resource cannot be found. + // This error is intended to be used in acceptance tests. + ResourceNotFound = "resource not found: %s" + + // AivenResourceNotFound is the error message for when an Aiven resource cannot be found. + AivenResourceNotFound = "aiven resource %s with compound ID %s not found" +) diff --git a/internal/plugin/provider.go b/internal/plugin/provider.go index 1969d4fd5..ec150b6f7 100644 --- a/internal/plugin/provider.go +++ b/internal/plugin/provider.go @@ -108,17 +108,22 @@ func (p *AivenProvider) Configure( // Resources returns the resources supported by this provider. func (p *AivenProvider) Resources(context.Context) []func() resource.Resource { - isBeta := os.Getenv("PROVIDER_AIVEN_ENABLE_BETA") != "" - // List of resources that are currently available in the provider. resources := []func() resource.Resource{ organization.NewOrganizationResource, } + isBeta := os.Getenv("PROVIDER_AIVEN_ENABLE_BETA") != "" + // Add to a list of resources that are currently in beta. if isBeta { - resources = append(resources, organization.NewOrganizationUserGroupMembersResource) - resources = append(resources, organization.NewOrganizationGroupProjectResource) + betaResources := []func() resource.Resource{ + organization.NewOrganizationUserGroupMembersResource, + organization.NewOrganizationGroupProjectResource, + organization.NewOrganizationApplicationUser, + } + + resources = append(resources, betaResources...) } return resources @@ -126,9 +131,23 @@ func (p *AivenProvider) Resources(context.Context) []func() resource.Resource { // DataSources returns the data sources supported by this provider. func (p *AivenProvider) DataSources(context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ + // List of data sources that are currently available in the provider. + dataSources := []func() datasource.DataSource{ organization.NewOrganizationDataSource, } + + isBeta := os.Getenv("PROVIDER_AIVEN_ENABLE_BETA") != "" + + // Add to a list of data sources that are currently in beta. + if isBeta { + betaDataSources := []func() datasource.DataSource{ + organization.NewOrganizationApplicationUserDataSource, + } + + dataSources = append(dataSources, betaDataSources...) + } + + return dataSources } // New returns a new provider factory for the Aiven provider. diff --git a/internal/plugin/service/organization/organization_application_user.go b/internal/plugin/service/organization/organization_application_user.go new file mode 100644 index 000000000..b6c1d0ec7 --- /dev/null +++ b/internal/plugin/service/organization/organization_application_user.go @@ -0,0 +1,296 @@ +package organization + +import ( + "context" + "fmt" + + "github.com/aiven/aiven-go-client/v2" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/aiven/terraform-provider-aiven/internal/plugin/errmsg" + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" +) + +var ( + _ resource.Resource = &organizationApplicationUser{} + _ resource.ResourceWithConfigure = &organizationApplicationUser{} + _ resource.ResourceWithImportState = &organizationApplicationUser{} + + _ util.TypeNameable = &organizationApplicationUser{} +) + +// NewOrganizationApplicationUser is a constructor for the organization application user resource. +func NewOrganizationApplicationUser() resource.Resource { + return &organizationApplicationUser{} +} + +// organizationApplicationUser is the organization application user resource implementation. +type organizationApplicationUser struct { + // client is the instance of the Aiven client to use. + client *aiven.Client + + // typeName is the name of the resource type. + typeName string +} + +// organizationApplicationUserModel is the model for the organization application user resource. +type organizationApplicationUserModel struct { + // ID is the identifier of the organization application user. + ID types.String `tfsdk:"id"` + // UserID is the identifier of the organization application user. + UserID types.String `tfsdk:"user_id"` + // OrganizationID is the identifier of the organization the application user belongs to. + OrganizationID types.String `tfsdk:"organization_id"` + // Name is the name of the organization application user. + Name types.String `tfsdk:"name"` + // Email is the email of the organization application user. + Email types.String `tfsdk:"email"` + // Timeouts is the configuration for resource-specific timeouts. + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// Metadata returns the metadata for the organization application user resource. +func (r *organizationApplicationUser) Metadata( + _ context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_organization_application_user" + + r.typeName = resp.TypeName +} + +// TypeName returns the resource type name for the organization application user resource. +func (r *organizationApplicationUser) TypeName() string { + return r.typeName +} + +// Schema defines the schema for the organization application user resource. +func (r *organizationApplicationUser) Schema( + ctx context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = util.GeneralizeSchema(ctx, schema.Schema{ + Description: util.BetaDescription("Creates and manages an organization application user in Aiven."), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Compound identifier of the organization application user.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "user_id": schema.StringAttribute{ + Description: "Identifier of the organization application user.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "organization_id": schema.StringAttribute{ + Description: "Identifier of the organization the application user belongs to.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "Name of the organization application user.", + Required: true, + }, + "email": schema.StringAttribute{ + Description: "Email of the organization application user.", + Computed: true, + }, + }, + }) +} + +// Configure sets up the organization application user resource. +func (r *organizationApplicationUser) Configure( + _ context.Context, + req resource.ConfigureRequest, + resp *resource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*aiven.Client) + if !ok { + resp.Diagnostics = util.DiagErrorUnexpectedProviderDataType(resp.Diagnostics, req.ProviderData) + + return + } + + r.client = client +} + +// fillModel fills the organization application user resource model from the Aiven API. +func (r *organizationApplicationUser) fillModel( + ctx context.Context, + model *organizationApplicationUserModel, +) (err error) { + appUsers, err := r.client.OrganizationApplicationUserHandler.List(ctx, model.OrganizationID.ValueString()) + if err != nil { + return err + } + + var appUser *aiven.ApplicationUserInfo + + for _, u := range appUsers.Users { + if u.UserID == model.UserID.ValueString() { + appUser = &u + break + } + } + + if appUser == nil { + return fmt.Errorf(errmsg.AivenResourceNotFound, r.TypeName(), model.ID.ValueString()) + } + + model.Name = types.StringValue(appUser.Name) + + model.Email = types.StringValue(appUser.UserEmail) + + return err +} + +// Create creates an organization application user resource. +func (r *organizationApplicationUser) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + var plan organizationApplicationUserModel + + if !util.PlanStateToModel(ctx, &req.Plan, &plan, &resp.Diagnostics) { + return + } + + appUser, err := r.client.OrganizationApplicationUserHandler.Create( + ctx, + plan.OrganizationID.ValueString(), + aiven.ApplicationUserCreateRequest{ + Name: plan.Name.ValueString(), + }, + ) + if err != nil { + resp.Diagnostics = util.DiagErrorCreatingResource(resp.Diagnostics, r, err) + + return + } + + plan.ID = types.StringValue(util.ComposeID(plan.OrganizationID.ValueString(), appUser.UserID)) + plan.UserID = types.StringValue(appUser.UserID) + + err = r.fillModel(ctx, &plan) + if err != nil { + resp.Diagnostics = util.DiagErrorCreatingResource(resp.Diagnostics, r, err) + + return + } + + if !util.ModelToPlanState(ctx, plan, &resp.State, &resp.Diagnostics) { + return + } +} + +// Read reads an organization application user resource. +func (r *organizationApplicationUser) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + var state organizationApplicationUserModel + + if !util.PlanStateToModel(ctx, &req.State, &state, &resp.Diagnostics) { + return + } + + err := r.fillModel(ctx, &state) + if err != nil { + resp.Diagnostics = util.DiagErrorReadingResource(resp.Diagnostics, r, err) + + return + } + + if !util.ModelToPlanState(ctx, state, &resp.State, &resp.Diagnostics) { + return + } +} + +// Update updates an organization resource. +func (r *organizationApplicationUser) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + var plan organizationApplicationUserModel + + if !util.PlanStateToModel(ctx, &req.Plan, &plan, &resp.Diagnostics) { + return + } + + if _, err := r.client.OrganizationUser.Update( + ctx, + plan.OrganizationID.ValueString(), + plan.UserID.ValueString(), + aiven.OrganizationUserUpdateRequest{ + RealName: util.Ref(plan.Name.ValueString()), + }, + ); err != nil { + resp.Diagnostics = util.DiagErrorUpdatingResource(resp.Diagnostics, r, err) + + return + } + + err := r.fillModel(ctx, &plan) + if err != nil { + resp.Diagnostics = util.DiagErrorUpdatingResource(resp.Diagnostics, r, err) + + return + } + + if !util.ModelToPlanState(ctx, plan, &resp.State, &resp.Diagnostics) { + return + } +} + +// Delete deletes an organization application user resource. +func (r *organizationApplicationUser) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + var state organizationApplicationUserModel + + if !util.PlanStateToModel(ctx, &req.State, &state, &resp.Diagnostics) { + return + } + + if err := r.client.OrganizationApplicationUserHandler.Delete( + ctx, + state.OrganizationID.ValueString(), + state.UserID.ValueString(), + ); err != nil { + resp.Diagnostics = util.DiagErrorDeletingResource(resp.Diagnostics, r, err) + + return + } +} + +// ImportState handles resource's state import requests. +func (r *organizationApplicationUser) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + + util.UnpackCompoundID(ctx, req, resp, "organization_id", "user_id") +} diff --git a/internal/plugin/service/organization/organization_application_user_data_source.go b/internal/plugin/service/organization/organization_application_user_data_source.go new file mode 100644 index 000000000..387649ddc --- /dev/null +++ b/internal/plugin/service/organization/organization_application_user_data_source.go @@ -0,0 +1,160 @@ +package organization + +import ( + "context" + + "github.com/aiven/aiven-go-client/v2" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" +) + +var ( + _ datasource.DataSource = &organizationApplicationUserDataSource{} + _ datasource.DataSourceWithConfigure = &organizationApplicationUserDataSource{} + + _ util.TypeNameable = &organizationApplicationUserDataSource{} +) + +// NewOrganizationApplicationUserDataSource is a constructor for the organization application user data source. +func NewOrganizationApplicationUserDataSource() datasource.DataSource { + return &organizationApplicationUserDataSource{} +} + +// organizationApplicationUserDataSource is the organization application user data source implementation. +type organizationApplicationUserDataSource struct { + // client is the instance of the Aiven client to use. + client *aiven.Client + + // typeName is the name of the data source type. + typeName string +} + +// organizationApplicationUserDataSourceModel is the model for the organization application user data source. +type organizationApplicationUserDataSourceModel struct { + // UserID is the identifier of the organization application user. + UserID types.String `tfsdk:"user_id"` + // OrganizationID is the identifier of the organization the application user belongs to. + OrganizationID types.String `tfsdk:"organization_id"` + // Name is the name of the organization application user. + Name types.String `tfsdk:"name"` + // Email is the email of the organization application user. + Email types.String `tfsdk:"email"` +} + +// Metadata returns the metadata for the organization application user data source. +func (r *organizationApplicationUserDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_organization_application_user" + + r.typeName = resp.TypeName +} + +// TypeName returns the data source type name for the organization application user data source. +func (r *organizationApplicationUserDataSource) TypeName() string { + return r.typeName +} + +// Schema defines the schema for the organization application user data source. +func (r *organizationApplicationUserDataSource) Schema( + _ context.Context, + _ datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "Retrieves information about an organization application user from Aiven.", + Attributes: map[string]schema.Attribute{ + "user_id": schema.StringAttribute{ + Description: "Identifier of the organization application user.", + Required: true, + }, + "organization_id": schema.StringAttribute{ + Description: "Identifier of the organization the application user belongs to.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "Name of the organization application user.", + Computed: true, + }, + "email": schema.StringAttribute{ + Description: "Email of the organization application user.", + Computed: true, + }, + }, + } +} + +// Configure sets up the organization application user data source. +func (r *organizationApplicationUserDataSource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*aiven.Client) + if !ok { + resp.Diagnostics = util.DiagErrorUnexpectedProviderDataType(resp.Diagnostics, req.ProviderData) + + return + } + + r.client = client +} + +// fillModel fills the organization application user data source model from the Aiven API. +func (r *organizationApplicationUserDataSource) fillModel( + ctx context.Context, + model *organizationApplicationUserDataSourceModel, +) (err error) { + appUsers, err := r.client.OrganizationApplicationUserHandler.List(ctx, model.OrganizationID.ValueString()) + if err != nil { + return + } + + var appUser *aiven.ApplicationUserInfo + + for _, u := range appUsers.Users { + if u.UserID == model.UserID.ValueString() { + appUser = &u + break + } + } + + model.Name = types.StringValue(appUser.Name) + + model.Email = types.StringValue(appUser.UserEmail) + + return +} + +// Read reads an organization application user data source. +func (r *organizationApplicationUserDataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + var state organizationApplicationUserDataSourceModel + + if !util.ConfigToModel(ctx, &req.Config, &state, &resp.Diagnostics) { + return + } + + err := r.fillModel(ctx, &state) + if err != nil { + resp.Diagnostics = util.DiagErrorReadingDataSource(resp.Diagnostics, r, err) + + return + } + + if !util.ModelToPlanState(ctx, state, &resp.State, &resp.Diagnostics) { + return + } +} diff --git a/internal/plugin/service/organization/organization_application_user_test.go b/internal/plugin/service/organization/organization_application_user_test.go new file mode 100644 index 000000000..ebe300d64 --- /dev/null +++ b/internal/plugin/service/organization/organization_application_user_test.go @@ -0,0 +1,113 @@ +package organization_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" +) + +// TestAccOrganizationApplicationUserResourceDataSource tests the organization application user resource and data +// source. +func TestAccOrganizationApplicationUserResourceDataSource(t *testing.T) { + deps := acc.CommonTestDependencies(t) + + if !deps.IsBeta() { + t.Skip("PROVIDER_AIVEN_ENABLE_BETA must be set for this test to run.") + } + + name := "aiven_organization_application_user.foo" + dname := "data.aiven_organization_application_user.foo" + + suffix := acctest.RandStringFromCharSet(acc.DefaultRandomSuffixLength, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +data "aiven_organization" "foo" { + name = "%[3]s" +} + +resource "aiven_organization_application_user" "foo" { + organization_id = data.aiven_organization.foo.id + name = "%[1]s-org-appuser-%[2]s" +} +`, acc.DefaultResourceNamePrefix, suffix, deps.OrganizationName()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + name, + "name", + fmt.Sprintf("%s-org-appuser-%s", acc.DefaultResourceNamePrefix, suffix), + ), + resource.TestCheckResourceAttrSet(name, "id"), + ), + }, + { + ResourceName: name, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(state *terraform.State) (string, error) { + rs, err := acc.ResourceFromState(state, name) + if err != nil { + return "", err + } + + return util.ComposeID( + rs.Primary.Attributes["organization_id"], rs.Primary.Attributes["user_id"], + ), nil + }, + }, + { + Config: fmt.Sprintf(` +data "aiven_organization" "foo" { + name = "%[3]s" +} + +resource "aiven_organization_application_user" "foo" { + organization_id = data.aiven_organization.foo.id + name = "%[1]s-org-appuser-%[2]s-1" +} +`, acc.DefaultResourceNamePrefix, suffix, deps.OrganizationName()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + name, + "name", + fmt.Sprintf("%s-org-appuser-%s-1", acc.DefaultResourceNamePrefix, suffix), + ), + ), + }, + { + Config: fmt.Sprintf(` +data "aiven_organization" "foo" { + name = "%[3]s" +} + +resource "aiven_organization_application_user" "foo" { + organization_id = data.aiven_organization.foo.id + name = "%[1]s-org-appuser-%[2]s-1" +} + +data "aiven_organization_application_user" "foo" { + user_id = aiven_organization_application_user.foo.user_id + organization_id = data.aiven_organization.foo.id +} +`, acc.DefaultResourceNamePrefix, suffix, deps.OrganizationName()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + dname, + "name", + fmt.Sprintf("%s-org-appuser-%s-1", acc.DefaultResourceNamePrefix, suffix), + ), + ), + }, + }, + }) +} diff --git a/internal/plugin/service/organization/organization_group_project.go b/internal/plugin/service/organization/organization_group_project.go index aa732cc47..f14312dcf 100644 --- a/internal/plugin/service/organization/organization_group_project.go +++ b/internal/plugin/service/organization/organization_group_project.go @@ -6,10 +6,16 @@ import ( "github.com/aiven/aiven-go-client/v2" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/aiven/terraform-provider-aiven/internal/plugin/errmsg" "github.com/aiven/terraform-provider-aiven/internal/plugin/util" ) @@ -21,12 +27,12 @@ var ( _ util.TypeNameable = &organizationGroupProjectResource{} ) -// NewOrganizationGroupProjectResource is a constructor for the organization resource. +// NewOrganizationGroupProjectResource is a constructor for the organization group project relation resource. func NewOrganizationGroupProjectResource() resource.Resource { return &organizationGroupProjectResource{} } -// organizationGroupUserResource is the organization resource implementation. +// organizationGroupUserResource is the organization group project relation resource implementation. type organizationGroupProjectResource struct { // client is the instance of the Aiven client to use. client *aiven.Client @@ -35,19 +41,21 @@ type organizationGroupProjectResource struct { typeName string } -// organizationGroupProjectResourceModel is the model for the organization resource. +// organizationGroupProjectResourceModel is the model for the organization group project relation resource. type organizationGroupProjectResourceModel struct { - // Name is the name of the organization. + // ID is the compound identifier of the organization group project relation. + ID types.String `tfsdk:"id"` + // Project is the name of the project. Project types.String `tfsdk:"project"` - // OrganizationID is the identifier of the organization group. - OrganizationGroupID types.String `tfsdk:"group_id"` + // GroupID is the identifier of the organization group. + GroupID types.String `tfsdk:"group_id"` // Role is the role of the organization group project relation. Role types.String `tfsdk:"role"` // Timeouts is the configuration for resource-specific timeouts. Timeouts timeouts.Value `tfsdk:"timeouts"` } -// Metadata returns the metadata for the organization resource. +// Metadata returns the metadata for the organization group project relation resource. func (r *organizationGroupProjectResource) Metadata( _ context.Context, req resource.MetadataRequest, @@ -69,19 +77,40 @@ func (r *organizationGroupProjectResource) Schema( _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = util.GeneralizeSchema(ctx, schema.Schema{ - Description: "Creates and manages an organization group project relations in Aiven.", + Description: util.BetaDescription( + "Creates and manages an organization group project relations in Aiven.", + ), Attributes: map[string]schema.Attribute{ - "group_id": schema.StringAttribute{ - Description: "Organization group identifier of the organization group project relation.", - Required: true, + "id": schema.StringAttribute{ + Description: "Compound identifier of the organization group project relation.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "project": schema.StringAttribute{ Description: "Tenant identifier of the organization.", Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "group_id": schema.StringAttribute{ + Description: "Organization group identifier of the organization group project relation.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "role": schema.StringAttribute{ Description: "Role of the organization group project relation.", Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("admin", "developer", "operator", "read_only"), + }, }, }, }) @@ -107,60 +136,68 @@ func (r *organizationGroupProjectResource) Configure( r.client = client } -// CustomizeDiff helps to customize the diff for the resource. +// fillModel fills the organization group project relation model from the Aiven API. func (r *organizationGroupProjectResource) fillModel( ctx context.Context, - m *organizationGroupProjectResourceModel, + model *organizationGroupProjectResourceModel, ) error { - list, err := r.client.ProjectOrganization.List( - ctx, - m.Project.ValueString()) + list, err := r.client.ProjectOrganization.List(ctx, model.Project.ValueString()) if err != nil { return err } - var isFound bool - for _, project := range list { - if project.OrganizationGroupID == m.OrganizationGroupID.ValueString() { - isFound = true - m.OrganizationGroupID = types.StringValue(project.OrganizationGroupID) - m.Role = types.StringValue(project.Role) + var group *aiven.ProjectUserGroup + + for _, g := range list { + if g.OrganizationGroupID == model.GroupID.ValueString() { + group = g + break } } - if !isFound { - return fmt.Errorf("organization group project relation not found, organization group id: %s, project: %s", - m.OrganizationGroupID.ValueString(), m.Project.ValueString()) + if group == nil { + return fmt.Errorf( + errmsg.AivenResourceNotFound, + r.TypeName(), + util.ComposeID(model.Project.ValueString(), model.GroupID.ValueString()), + ) } - // There is not API endpoint to get the permission of the organization group project relation. + model.GroupID = types.StringValue(group.OrganizationGroupID) + + model.Role = types.StringValue(group.Role) + + // There is no API endpoint to get the permissions of the organization group project relation. return nil } -// Diff helps to differentiate desired from the existing state of the resource. +// Create creates an organization group project relation resource. func (r *organizationGroupProjectResource) Create( ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, ) { var plan organizationGroupProjectResourceModel + if !util.PlanStateToModel(ctx, &req.Plan, &plan, &resp.Diagnostics) { return } - err := r.client.ProjectOrganization.Add( + if err := r.client.ProjectOrganization.Add( ctx, plan.Project.ValueString(), - plan.OrganizationGroupID.ValueString(), - plan.Role.ValueString()) - if err != nil { + plan.GroupID.ValueString(), + plan.Role.ValueString(), + ); err != nil { resp.Diagnostics = util.DiagErrorCreatingResource(resp.Diagnostics, r, err) return } - err = r.fillModel(ctx, &plan) + plan.ID = types.StringValue(util.ComposeID(plan.Project.ValueString(), plan.GroupID.ValueString())) + + err := r.fillModel(ctx, &plan) if err != nil { resp.Diagnostics = util.DiagErrorCreatingResource(resp.Diagnostics, r, err) @@ -172,29 +209,6 @@ func (r *organizationGroupProjectResource) Create( } } -// Delete deletes an organization resource. -func (r *organizationGroupProjectResource) Delete( - ctx context.Context, - req resource.DeleteRequest, - resp *resource.DeleteResponse, -) { - var plan organizationGroupProjectResourceModel - - if !util.PlanStateToModel(ctx, &req.State, &plan, &resp.Diagnostics) { - return - } - - err := r.client.ProjectOrganization.Delete( - ctx, - plan.Project.ValueString(), - plan.OrganizationGroupID.ValueString()) - if err != nil { - resp.Diagnostics = util.DiagErrorDeletingResource(resp.Diagnostics, r, err) - - return - } -} - // Read reads the existing state of the resource. func (r *organizationGroupProjectResource) Read( ctx context.Context, @@ -219,28 +233,45 @@ func (r *organizationGroupProjectResource) Read( } } -// ImportState imports an existing resource into Terraform. -func (r *organizationGroupProjectResource) ImportState( - _ context.Context, - _ resource.ImportStateRequest, - resp *resource.ImportStateResponse, -) { - util.DiagErrorUpdatingResource( - resp.Diagnostics, - r, - fmt.Errorf("cannot import %s resource", r.TypeName()), - ) -} - // Update updates an organization group project resource. func (r *organizationGroupProjectResource) Update( _ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse, ) { - util.DiagErrorUpdatingResource( - resp.Diagnostics, - r, - fmt.Errorf("cannot update %s resource", r.TypeName()), - ) + resp.Diagnostics = util.DiagErrorUpdatingResourceNotSupported(resp.Diagnostics, r) +} + +// Delete deletes an organization group project relation resource. +func (r *organizationGroupProjectResource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + var plan organizationGroupProjectResourceModel + + if !util.PlanStateToModel(ctx, &req.State, &plan, &resp.Diagnostics) { + return + } + + if err := r.client.ProjectOrganization.Delete( + ctx, + plan.Project.ValueString(), + plan.GroupID.ValueString(), + ); err != nil { + resp.Diagnostics = util.DiagErrorDeletingResource(resp.Diagnostics, r, err) + + return + } +} + +// ImportState imports an existing resource into Terraform. +func (r *organizationGroupProjectResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + + util.UnpackCompoundID(ctx, req, resp, "project", "group_id") } diff --git a/internal/plugin/service/organization/organization_group_project_test.go b/internal/plugin/service/organization/organization_group_project_test.go index 4ccce80c9..b24f4126e 100644 --- a/internal/plugin/service/organization/organization_group_project_test.go +++ b/internal/plugin/service/organization/organization_group_project_test.go @@ -2,62 +2,78 @@ package organization_test import ( "fmt" - "os" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" ) +// TestAccOrganizationGroupProject tests the organization group project relation resource. func TestAccOrganizationGroupProject(t *testing.T) { - orgID, found := os.LookupEnv("AIVEN_ORG_ID") - if !found { - t.Skip("Skipping test due to missing AIVEN_ORG_ID environment variable") - } + deps := acc.CommonTestDependencies(t) - if _, ok := os.LookupEnv("PROVIDER_AIVEN_ENABLE_BETA"); !ok { - t.Skip("Skipping test due to missing PROVIDER_AIVEN_ENABLE_BETA environment variable") + if !deps.IsBeta() { + t.Skip("PROVIDER_AIVEN_ENABLE_BETA must be set for this test to run.") } + name := "aiven_organization_group_project.foo" + suffix := acctest.RandStringFromCharSet(acc.DefaultRandomSuffixLength, acctest.CharSetAlphaNum) - resourceName := "aiven_organization_group_project.foo" resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, Steps: []resource.TestStep{ { - Config: testAccOrganizationUserGroupProjectResource(suffix, orgID), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "project", "pr-"+suffix), - resource.TestCheckResourceAttrSet(resourceName, "group_id"), - resource.TestCheckResourceAttr(resourceName, "role", "admin"), - ), - }, - }, - }) + Config: fmt.Sprintf(` +data "aiven_organization" "foo" { + name = "%[3]s" } -func testAccOrganizationUserGroupProjectResource(rand, orgID string) string { - return fmt.Sprintf(` resource "aiven_organization_user_group" "foo" { - organization_id = "%[1]s" - name = "test-group" - description = "test-group-description" + organization_id = data.aiven_organization.foo.id + name = "%[1]s-usr-group-%[2]s" + description = "Terraform acceptance tests" } resource "aiven_project" "foo" { - project = "pr-%[2]s" - parent_id = "%[1]s" + project = "%[1]s-pr-%[2]s" + parent_id = data.aiven_organization.foo.id } + resource "aiven_organization_group_project" "foo" { project = aiven_project.foo.project group_id = aiven_organization_user_group.foo.group_id role = "admin" - - depends_on = [aiven_organization_user_group.foo, aiven_project.foo] } -`, orgID, rand) +`, acc.DefaultResourceNamePrefix, suffix, deps.OrganizationName()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + name, + "project", + fmt.Sprintf("%s-pr-%s", acc.DefaultResourceNamePrefix, suffix), + ), + resource.TestCheckResourceAttrSet(name, "group_id"), + resource.TestCheckResourceAttr(name, "role", "admin"), + ), + }, + { + ResourceName: name, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(state *terraform.State) (string, error) { + rs, err := acc.ResourceFromState(state, name) + if err != nil { + return "", err + } + + return util.ComposeID(rs.Primary.Attributes["project"], rs.Primary.Attributes["group_id"]), nil + }, + }, + }, + }) } diff --git a/internal/plugin/service/organization/organization_resource.go b/internal/plugin/service/organization/organization_resource.go index d0ac680b4..ea7fa5a18 100644 --- a/internal/plugin/service/organization/organization_resource.go +++ b/internal/plugin/service/organization/organization_resource.go @@ -211,10 +211,9 @@ func (r *organizationResource) Update(ctx context.Context, req resource.UpdateRe return } - _, err = r.client.Accounts.Update(ctx, normalizedID, aiven.Account{ + if _, err = r.client.Accounts.Update(ctx, normalizedID, aiven.Account{ Name: plan.Name.ValueString(), - }) - if err != nil { + }); err != nil { resp.Diagnostics = util.DiagErrorUpdatingResource(resp.Diagnostics, r, err) return @@ -247,8 +246,7 @@ func (r *organizationResource) Delete(ctx context.Context, req resource.DeleteRe return } - err = r.client.Accounts.Delete(ctx, normalizedID) - if err != nil { + if err = r.client.Accounts.Delete(ctx, normalizedID); err != nil { resp.Diagnostics = util.DiagErrorDeletingResource(resp.Diagnostics, r, err) return diff --git a/internal/plugin/service/organization/organization_user_group_member.go b/internal/plugin/service/organization/organization_user_group_member.go index a4e173ba1..2c73e0d04 100644 --- a/internal/plugin/service/organization/organization_user_group_member.go +++ b/internal/plugin/service/organization/organization_user_group_member.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/aiven/terraform-provider-aiven/internal/plugin/errmsg" "github.com/aiven/terraform-provider-aiven/internal/plugin/util" ) @@ -21,12 +22,12 @@ var ( _ resource.ResourceWithImportState = &organizationUserGroupMembersResource{} ) -// NewOrganizationUserGroupMembersResource is a constructor for the organization resource. +// NewOrganizationUserGroupMembersResource is a constructor for the organization user group member resource. func NewOrganizationUserGroupMembersResource() resource.Resource { return &organizationUserGroupMembersResource{} } -// organizationUserGroupMembersResource is the organization resource implementation. +// organizationUserGroupMembersResource is the organization user group member resource implementation. type organizationUserGroupMembersResource struct { // client is the instance of the Aiven client to use. client *aiven.Client @@ -35,25 +36,21 @@ type organizationUserGroupMembersResource struct { typeName string } -// organizationUserGroupMembersResourceModel is the model for the organization resource. +// organizationUserGroupMembersResourceModel is the model for the organization user group member resource. type organizationUserGroupMembersResourceModel struct { // ID is the identifier of the organization. OrganizationID types.String `tfsdk:"organization_id"` - // ID is the identifier of the organization user group. OrganizationGroupID types.String `tfsdk:"group_id"` - // ID is the identifier of the organization user group member. OrganizationUserID types.String `tfsdk:"user_id"` - // Last activity time of the user group member. LastActivityTime types.String `tfsdk:"last_activity_time"` - // Timeouts is the configuration for resource-specific timeouts. Timeouts timeouts.Value `tfsdk:"timeouts"` } -// Metadata returns the metadata for the organization resource. +// Metadata returns the metadata for the organization user group member resource. func (r *organizationUserGroupMembersResource) Metadata( _ context.Context, req resource.MetadataRequest, @@ -64,20 +61,19 @@ func (r *organizationUserGroupMembersResource) Metadata( r.typeName = resp.TypeName } +// TypeName returns the resource type name. func (r *organizationUserGroupMembersResource) TypeName() string { return r.typeName } -// Schema returns the schema for the organization resource. +// Schema returns the schema for the organization user group member resource. func (r *organizationUserGroupMembersResource) Schema( ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse, ) { resp.Schema = util.GeneralizeSchema(ctx, schema.Schema{ - Description: "Creates and manages an organization user group members in Aiven. " + - "Please no that this resource is " + " in beta and may change without notice. " + - "To use it please use the beta environment variable PROVIDER_AIVEN_ENABLE_BETA.", + Description: util.BetaDescription("Creates and manages an organization user group members in Aiven."), Attributes: map[string]schema.Attribute{ "organization_id": schema.StringAttribute{ Description: "Identifier of the organization.", @@ -86,15 +82,15 @@ func (r *organizationUserGroupMembersResource) Schema( stringplanmodifier.RequiresReplace(), }, }, - "user_id": schema.StringAttribute{ - Description: "Identifier of the organization user group member.", + "group_id": schema.StringAttribute{ + Description: "Identifier of the organization user group.", Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, - "group_id": schema.StringAttribute{ - Description: "Identifier of the organization user group.", + "user_id": schema.StringAttribute{ + Description: "Identifier of the organization user group member.", Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), @@ -108,7 +104,7 @@ func (r *organizationUserGroupMembersResource) Schema( }) } -// Configure configures the organization resource. +// Configure configures the organization user group member resource. func (r *organizationUserGroupMembersResource) Configure( _ context.Context, req resource.ConfigureRequest, @@ -136,7 +132,8 @@ func (r *organizationUserGroupMembersResource) fillModel( list, err := r.client.OrganizationUserGroupMembers.List( ctx, model.OrganizationID.ValueString(), - model.OrganizationGroupID.ValueString()) + model.OrganizationGroupID.ValueString(), + ) if err != nil { return err } @@ -145,13 +142,33 @@ func (r *organizationUserGroupMembersResource) fillModel( return nil } - member := list.Members[0] + var member *aiven.OrganizationUserGroupMember + + for _, m := range list.Members { + if m.UserID == model.OrganizationUserID.ValueString() { + member = &m + break + } + } + + if member == nil { + return fmt.Errorf( + errmsg.AivenResourceNotFound, + r.TypeName(), + util.ComposeID( + model.OrganizationID.ValueString(), + model.OrganizationGroupID.ValueString(), + model.OrganizationUserID.ValueString(), + ), + ) + } + model.LastActivityTime = types.StringValue(member.LastActivityTime.String()) return nil } -// Create creates an organization resource. +// Create creates an organization user group member resource. func (r *organizationUserGroupMembersResource) Create( ctx context.Context, req resource.CreateRequest, @@ -171,7 +188,9 @@ func (r *organizationUserGroupMembersResource) Create( Operation: "add_members", MemberIDs: []string{ plan.OrganizationUserID.ValueString(), - }}) + }, + }, + ) if err != nil { resp.Diagnostics = util.DiagErrorCreatingResource(resp.Diagnostics, r, err) @@ -190,7 +209,7 @@ func (r *organizationUserGroupMembersResource) Create( } } -// Delete deletes an organization resource. +// Delete deletes an organization user group member resource. func (r *organizationUserGroupMembersResource) Read( ctx context.Context, req resource.ReadRequest, @@ -214,7 +233,16 @@ func (r *organizationUserGroupMembersResource) Read( } } -// Delete deletes an organization resource. +// Update updates an organization user group member resource. +func (r *organizationUserGroupMembersResource) Update( + _ context.Context, + _ resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + resp.Diagnostics = util.DiagErrorUpdatingResourceNotSupported(resp.Diagnostics, r) +} + +// Delete deletes an organization user group member resource. func (r *organizationUserGroupMembersResource) Delete( ctx context.Context, req resource.DeleteRequest, @@ -226,7 +254,7 @@ func (r *organizationUserGroupMembersResource) Delete( return } - err := r.client.OrganizationUserGroupMembers.Modify( + if err := r.client.OrganizationUserGroupMembers.Modify( ctx, plan.OrganizationID.ValueString(), plan.OrganizationGroupID.ValueString(), @@ -234,36 +262,20 @@ func (r *organizationUserGroupMembersResource) Delete( Operation: "remove_members", MemberIDs: []string{ plan.OrganizationGroupID.ValueString(), - }}) - if err != nil { + }, + }, + ); err != nil { resp.Diagnostics = util.DiagErrorDeletingResource(resp.Diagnostics, r, err) return } } -// Update updates an organization resource. -func (r *organizationUserGroupMembersResource) Update( - _ context.Context, - _ resource.UpdateRequest, - resp *resource.UpdateResponse, -) { - util.DiagErrorUpdatingResource( - resp.Diagnostics, - r, - fmt.Errorf("cannot update %s resource", r.TypeName()), - ) -} - -// ImportStatePassthroughID is a helper function to set the import +// ImportState handles resource's state import requests. func (r *organizationUserGroupMembersResource) ImportState( - _ context.Context, - _ resource.ImportStateRequest, + ctx context.Context, + req resource.ImportStateRequest, resp *resource.ImportStateResponse, ) { - util.DiagErrorUpdatingResource( - resp.Diagnostics, - r, - fmt.Errorf("cannot import %s resource", r.TypeName()), - ) + util.UnpackCompoundID(ctx, req, resp, "organization_id", "group_id", "user_id") } diff --git a/internal/plugin/service/organization/organization_user_group_member_test.go b/internal/plugin/service/organization/organization_user_group_member_test.go index 0cc0fd15e..174c30ed6 100644 --- a/internal/plugin/service/organization/organization_user_group_member_test.go +++ b/internal/plugin/service/organization/organization_user_group_member_test.go @@ -2,53 +2,77 @@ package organization_test import ( "fmt" - "os" "testing" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" ) -func TestAccOrganizationUserGroupMemeber(t *testing.T) { - orgID := os.Getenv("AIVEN_ORG_ID") - userID := os.Getenv("AIVEN_ORG_USER_ID") +// TestAccOrganizationUserGroupMember tests the organization user group member resource. +func TestAccOrganizationUserGroupMember(t *testing.T) { + deps := acc.CommonTestDependencies(t) - if orgID == "" || userID == "" { - t.Skip("Skipping test due to missing AIVEN_ORG_ID or AIVEN_ORG_USER_ID environment variable") + if !deps.IsBeta() { + t.Skip("PROVIDER_AIVEN_ENABLE_BETA must be set for this test to run.") } - if os.Getenv("PROVIDER_AIVEN_ENABLE_BETA") == "" { - t.Skip("Skipping test due to missing PROVIDERX_ENABLE_BETA environment variable") + userID := deps.OrganizationUserID() + if userID == nil { + t.Skip("AIVEN_ORGANIZATION_USER_ID must be set for this test to run.") } - resourceName := "aiven_organization_user_group_member.foo" + name := "aiven_organization_user_group_member.foo" + + suffix := acctest.RandStringFromCharSet(acc.DefaultRandomSuffixLength, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, Steps: []resource.TestStep{ { - Config: testAccOrganizationUserGroupMemberResource(orgID, userID), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "last_activity_time"), - ), - }, - }, - }) + Config: fmt.Sprintf(` +data "aiven_organization" "foo" { + name = "%[3]s" } -func testAccOrganizationUserGroupMemberResource(orgID, userID string) string { - return fmt.Sprintf(` resource "aiven_organization_user_group" "foo" { - organization_id = "%[1]s" - name = "testacc-dummy-user-group" - description = "testacc-dummy-user-group-description" + organization_id = data.aiven_organization.foo.id + name = "%[1]s-usr-group-%[2]s" + description = "Terraform acceptance tests" } resource "aiven_organization_user_group_member" "foo" { - organization_id = "%[1]s" + organization_id = data.aiven_organization.foo.id group_id = aiven_organization_user_group.foo.group_id - user_id = "%[2]s" + user_id = "%[4]s" } - `, orgID, userID) + `, acc.DefaultResourceNamePrefix, suffix, deps.OrganizationName(), util.Deref(userID)), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(name, "last_activity_time"), + ), + }, + { + ResourceName: name, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "user_id", + ImportStateIdFunc: func(state *terraform.State) (string, error) { + rs, err := acc.ResourceFromState(state, name) + if err != nil { + return "", err + } + + return util.ComposeID( + rs.Primary.Attributes["organization_id"], + rs.Primary.Attributes["group_id"], + rs.Primary.Attributes["user_id"], + ), nil + }, + }, + }, + }) } diff --git a/internal/plugin/util/diag.go b/internal/plugin/util/diag.go index fe7aa003e..7815ce476 100644 --- a/internal/plugin/util/diag.go +++ b/internal/plugin/util/diag.go @@ -53,6 +53,15 @@ func DiagErrorUpdatingResource(diagnostics diag.Diagnostics, typenameable TypeNa return diagnostics } +func DiagErrorUpdatingResourceNotSupported(diagnostics diag.Diagnostics, typenameable TypeNameable) diag.Diagnostics { + diagnostics.AddError( + errmsg.SummaryErrorUpdatingResource, + fmt.Sprintf(errmsg.DetailErrorUpdatingResourceNotSupported, typenameable.TypeName()), + ) + + return diagnostics +} + // DiagErrorDeletingResource is a function that adds a resource deleting error to the diagnostics and returns it. // It is used in the Delete method of the resource structs. func DiagErrorDeletingResource(diagnostics diag.Diagnostics, typenameable TypeNameable, err error) diag.Diagnostics { diff --git a/internal/plugin/util/helpers.go b/internal/plugin/util/helpers.go new file mode 100644 index 000000000..f3f497e6b --- /dev/null +++ b/internal/plugin/util/helpers.go @@ -0,0 +1,30 @@ +package util + +import "strings" + +// Ref is a helper function that returns a pointer to the value passed in. +func Ref[T any](v T) *T { + return &v +} + +// Deref is a helper function that dereferences any pointer type and returns the value. +func Deref[T any](p *T) T { + var result T + + if p != nil { + result = *p + } + + return result +} + +// ComposeID is a helper function that composes an ID from the parts passed in. +func ComposeID(parts ...string) string { + return strings.Join(parts, "/") +} + +// BetaDescription is a helper function that returns a description for beta resources. +func BetaDescription(description string) string { + return description + " Please note that this resource is in beta and may change without notice. " + + "To use this resource, please set the PROVIDER_AIVEN_ENABLE_BETA environment variable." +} diff --git a/internal/plugin/util/importstate.go b/internal/plugin/util/importstate.go new file mode 100644 index 000000000..4ec68b7ab --- /dev/null +++ b/internal/plugin/util/importstate.go @@ -0,0 +1,24 @@ +package util + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// UnpackCompoundID is a helper function that splits the ID by the separator and sets the attributes in the response. +// It is used in the ImportState method of the resource structs. +func UnpackCompoundID( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, + paths ...string, +) { + splitResourceID := strings.Split(req.ID, "/") + + for idx, v := range splitResourceID { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(paths[idx]), v)...) + } +} diff --git a/internal/sdkprovider/service/connectionpool/sweep.go b/internal/sdkprovider/service/connectionpool/sweep.go new file mode 100644 index 000000000..ab35fadd4 --- /dev/null +++ b/internal/sdkprovider/service/connectionpool/sweep.go @@ -0,0 +1,60 @@ +//go:build sweep + +package connectionpool + +import ( + "context" + "fmt" + "os" + + "github.com/aiven/aiven-go-client/v2" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/aiven/terraform-provider-aiven/internal/schemautil" + "github.com/aiven/terraform-provider-aiven/internal/sweep" +) + +func init() { + ctx := context.Background() + + client, err := sweep.SharedClient() + if err != nil { + panic(fmt.Sprintf("error getting client: %s", err)) + } + + resource.AddTestSweepers("aiven_connection_poll", &resource.Sweeper{ + Name: "aiven_connection_poll", + F: sweepConnectionPoll(ctx, client), + }) + +} + +func sweepConnectionPoll(ctx context.Context, client *aiven.Client) func(string) error { + return func(id string) error { + projectName := os.Getenv("AIVEN_PROJECT_NAME") + + services, err := client.Services.List(ctx, projectName) + if err != nil && !aiven.IsNotFound(err) { + return fmt.Errorf("error retrieving a list of services for a project `%s`: %w", projectName, err) + } + + for _, s := range services { + if s.Type != schemautil.ServiceTypePG { + continue + } + + l, err := client.ConnectionPools.List(ctx, projectName, s.Name) + if err != nil && !aiven.IsNotFound(err) { + return err + } + + for _, pool := range l { + err = client.ConnectionPools.Delete(ctx, projectName, s.Name, pool.PoolName) + if err != nil && !aiven.IsNotFound(err) { + return err + } + } + } + return nil + } +} diff --git a/internal/sdkprovider/service/organization/organization_user_data_source.go b/internal/sdkprovider/service/organization/organization_user_data_source.go index 9a0c5a239..1563625ba 100644 --- a/internal/sdkprovider/service/organization/organization_user_data_source.go +++ b/internal/sdkprovider/service/organization/organization_user_data_source.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/aiven/aiven-go-client/v2" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" ) diff --git a/internal/sweep/sweep_test.go b/internal/sweep/sweep_test.go index a2610e124..ac9463b2e 100644 --- a/internal/sweep/sweep_test.go +++ b/internal/sweep/sweep_test.go @@ -10,6 +10,7 @@ import ( _ "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/account" _ "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/cassandra" _ "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/clickhouse" + _ "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/connectionpool" _ "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/flink" _ "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/grafana" _ "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/influxdb"