diff --git a/CHANGELOG.md b/CHANGELOG.md index 9223cb95b..7cc871e5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ nav_order: 1 component, and is a combination of the `host` and `port` fields - Add `external_postgresql` and `external_google_cloud_bigquery` service integration endpoints - Do not return error on `aiven_account_team_member` deletion if the member does not exist +- Deprecating `aiven_organization_user` resource and update data source logic that will be used instead of the corresponding resource +- Add support for the `aiven_organization_user_group_member` resource, allowing the association of groups with the users. Please note that this resource is in the beta stage, and to use it, you would need to set the environment variable PROVIDER_AIVEN_ENABLE_BETA to a non-zero value. ## [4.9.4] - 2023-12-13 diff --git a/Makefile b/Makefile index 3e4f1d7e1..10a1247aa 100644 --- a/Makefile +++ b/Makefile @@ -118,7 +118,7 @@ lint-test: $(TERRAFMT) lint-docs: $(TFPLUGINDOCS) - $(TFPLUGINDOCS) validate + PROVIDER_AIVEN_ENABLE_BETA=true $(TFPLUGINDOCS) validate ################################################# # Format @@ -172,7 +172,7 @@ gen-go: docs: $(TFPLUGINDOCS) - $(TFPLUGINDOCS) generate + PROVIDER_AIVEN_ENABLE_BETA=true $(TFPLUGINDOCS) generate ################################################# # CI diff --git a/docs/data-sources/organization_user_group.md b/docs/data-sources/organization_user_group.md index 1cb12c61b..311f0ad21 100644 --- a/docs/data-sources/organization_user_group.md +++ b/docs/data-sources/organization_user_group.md @@ -24,5 +24,6 @@ The Organization User Groupe data source provides information about the existing - `create_time` (String) Time of creation - `description` (String) The organization user group description. This property cannot be changed, doing so forces recreation of the resource. +- `group_id` (String) The unique organization user group ID - `id` (String) The ID of this resource. - `update_time` (String) Time of last update diff --git a/docs/resources/organization_user_group.md b/docs/resources/organization_user_group.md index 05e5defdc..e00d23eff 100644 --- a/docs/resources/organization_user_group.md +++ b/docs/resources/organization_user_group.md @@ -28,6 +28,7 @@ The Organization User Group resource allows the creation and management of an Ai ### Read-Only - `create_time` (String) Time of creation +- `group_id` (String) The unique organization user group ID - `id` (String) The ID of this resource. - `update_time` (String) Time of last update diff --git a/docs/resources/organization_user_group_member.md b/docs/resources/organization_user_group_member.md new file mode 100644 index 000000000..90f88e89a --- /dev/null +++ b/docs/resources/organization_user_group_member.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +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. +--- + +# 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. + + + + +## Schema + +### Required + +- `group_id` (String) Identifier of the organization user group. +- `organization_id` (String) Identifier of the organization. +- `user_id` (String) Identifier of the organization user group member. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `last_activity_time` (String) Last activity time of the user group member. + + +### 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/internal/plugin/provider.go b/internal/plugin/provider.go index 07a71fd0e..3714079a3 100644 --- a/internal/plugin/provider.go +++ b/internal/plugin/provider.go @@ -108,9 +108,19 @@ func (p *AivenProvider) Configure( // Resources returns the resources supported by this provider. func (p *AivenProvider) Resources(context.Context) []func() resource.Resource { - return []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, } + + // Add to a list of resources that are currently in beta. + if isBeta { + resources = append(resources, organization.NewOrganizationUserGroupMembersResource) + } + + return resources } // DataSources returns the data sources supported by this provider. diff --git a/internal/plugin/service/organization/organization_user_group_member.go b/internal/plugin/service/organization/organization_user_group_member.go new file mode 100644 index 000000000..fc76640a4 --- /dev/null +++ b/internal/plugin/service/organization/organization_user_group_member.go @@ -0,0 +1,268 @@ +package organization + +import ( + "context" + "fmt" + + "github.com/aiven/aiven-go-client/v2" + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "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" +) + +var ( + _ resource.Resource = &organizationUserGroupMembersResource{} + _ resource.ResourceWithConfigure = &organizationUserGroupMembersResource{} + _ resource.ResourceWithImportState = &organizationUserGroupMembersResource{} +) + +// NewOrganizationUserGroupMembersResource is a constructor for the organization resource. +func NewOrganizationUserGroupMembersResource() resource.Resource { + return &organizationUserGroupMembersResource{} +} + +// organizationUserGroupMembersResource is the organization resource implementation. +type organizationUserGroupMembersResource struct { + // client is the instance of the Aiven client to use. + client *aiven.Client + + // typeName is the name of the resource type. + typeName string +} + +// organizationUserGroupMembersResourceModel is the model for the organization 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. +func (r *organizationUserGroupMembersResource) Metadata( + _ context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_organization_user_group_member" + + r.typeName = resp.TypeName +} + +func (r *organizationUserGroupMembersResource) TypeName() string { + return r.typeName +} + +// Schema returns the schema for the organization 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.", + Attributes: map[string]schema.Attribute{ + "organization_id": schema.StringAttribute{ + Description: "Identifier of the organization.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "user_id": schema.StringAttribute{ + Description: "Identifier of the organization user group member.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "group_id": schema.StringAttribute{ + Description: "Identifier of the organization user group.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "last_activity_time": schema.StringAttribute{ + Description: "Last activity time of the user group member.", + Computed: true, + }, + }, + }) +} + +// Configure configures the organization resource. +func (r *organizationUserGroupMembersResource) 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 +} + +// TimeoutSchema returns the schema for resource-specific timeouts. +func (r *organizationUserGroupMembersResource) fillModel( + ctx context.Context, + model *organizationUserGroupMembersResourceModel, +) error { + list, err := r.client.OrganizationUserGroupMembers.List( + ctx, + model.OrganizationID.ValueString(), + model.OrganizationGroupID.ValueString()) + if err != nil { + return err + } + + if len(list.Members) == 0 { + return nil + } + + member := list.Members[0] + model.LastActivityTime = types.StringValue(member.LastActivityTime.String()) + + return nil +} + +// Create creates an organization resource. +func (r *organizationUserGroupMembersResource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + var plan organizationUserGroupMembersResourceModel + + if !util.PlanStateToModel(ctx, &req.Plan, &plan, &resp.Diagnostics) { + return + } + + err := r.client.OrganizationUserGroupMembers.Modify( + ctx, + plan.OrganizationID.ValueString(), + plan.OrganizationGroupID.ValueString(), + aiven.OrganizationUserGroupMemberRequest{ + Operation: "add_members", + MemberIDs: []string{ + plan.OrganizationUserID.ValueString(), + }}) + if err != nil { + resp.Diagnostics = util.DiagErrorCreatingResource(resp.Diagnostics, r, err) + + return + } + + 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 + } +} + +// Delete deletes an organization resource. +func (r *organizationUserGroupMembersResource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + var state organizationUserGroupMembersResourceModel + + 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 + } +} + +// Delete deletes an organization resource. +func (r *organizationUserGroupMembersResource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + var plan organizationUserGroupMembersResourceModel + + if !util.PlanStateToModel(ctx, &req.State, &plan, &resp.Diagnostics) { + return + } + + err := r.client.OrganizationUserGroupMembers.Modify( + ctx, + plan.OrganizationID.ValueString(), + plan.OrganizationGroupID.ValueString(), + aiven.OrganizationUserGroupMemberRequest{ + Operation: "remove_members", + MemberIDs: []string{ + plan.OrganizationGroupID.ValueString(), + }}) + if 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 +func (r *organizationUserGroupMembersResource) ImportState( + _ context.Context, + _ resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + util.DiagErrorUpdatingResource( + resp.Diagnostics, + r, + fmt.Errorf("cannot import %s resource", r.TypeName()), + ) +} diff --git a/internal/plugin/service/organization/organization_user_group_member_test.go b/internal/plugin/service/organization/organization_user_group_member_test.go new file mode 100644 index 000000000..a6ea01bd5 --- /dev/null +++ b/internal/plugin/service/organization/organization_user_group_member_test.go @@ -0,0 +1,53 @@ +package organization_test + +import ( + "fmt" + "os" + "testing" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccOrganizationUserGroupMemeber(t *testing.T) { + orgID := os.Getenv("AIVEN_ORG_ID") + userID := os.Getenv("AIVEN_ORG_USER_ID") + + if orgID == "" || userID == "" { + t.Skip("Skipping test due to missing AIVEN_ORG_ID or AIVEN_ORG_USER_ID environment variable") + } + + if os.Getenv("PROVIDER_AIVEN_ENABLE_BETA") == "" { + t.Skip("Skipping test due to missing PROVIDERX_ENABLE_BETA environment variable") + } + + resourceName := "aiven_organization_user_group_member.foo" + 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"), + ), + }, + }, + }) +} + +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" +} + +resource "aiven_organization_user_group_member" "foo" { + organization_id = "%[1]s" + group_id = aiven_organization_user_group.foo.group_id + user_id = "%[2]s" +} + `, orgID, userID) +} diff --git a/internal/sdkprovider/service/organization/organization_user_group.go b/internal/sdkprovider/service/organization/organization_user_group.go index f0a95ca22..536fd471d 100644 --- a/internal/sdkprovider/service/organization/organization_user_group.go +++ b/internal/sdkprovider/service/organization/organization_user_group.go @@ -39,6 +39,11 @@ var aivenOrganizationUserGroupSchema = map[string]*schema.Schema{ Computed: true, Description: "Time of last update", }, + "group_id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique organization user group ID", + }, } func ResourceOrganizationUserGroup() *schema.Resource { @@ -103,6 +108,9 @@ func resourceOrganizationUserGroupRead(ctx context.Context, d *schema.ResourceDa if err := d.Set("update_time", r.UpdateTime.String()); err != nil { return diag.FromErr(err) } + if err := d.Set("group_id", r.UserGroupID); err != nil { + return diag.FromErr(err) + } return nil }