From 51b3340becaf4ccfdec4d288c93b8f4620aa6b73 Mon Sep 17 00:00:00 2001 From: Ivan Savciuc Date: Sat, 30 Dec 2023 07:17:55 +0200 Subject: [PATCH] feat(organization): add support for group projects (#1496) --- CHANGELOG.md | 2 + docs/resources/organization_group_project.md | 36 +++ go.mod | 2 +- go.sum | 4 +- internal/plugin/provider.go | 1 + .../organization_group_project.go | 246 ++++++++++++++++++ .../organization_group_project_test.go | 63 +++++ 7 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 docs/resources/organization_group_project.md create mode 100644 internal/plugin/service/organization/organization_group_project.go create mode 100644 internal/plugin/service/organization/organization_group_project_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 013db2790..a57085b87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ nav_order: 1 ## [MAJOR.MINOR.PATCH] - YYYY-MM-DD + +- Add support for `aiven_organization_group_project`. 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. - Deprecating `aiven_organization_user` resource and update data source logic that will be used instead of the corresponding resource ## [4.10.0] - 2023-12-27 diff --git a/docs/resources/organization_group_project.md b/docs/resources/organization_group_project.md new file mode 100644 index 000000000..ca2a4c847 --- /dev/null +++ b/docs/resources/organization_group_project.md @@ -0,0 +1,36 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_organization_group_project Resource - terraform-provider-aiven" +subcategory: "" +description: |- + Creates and manages an organization group project relations in Aiven. +--- + +# aiven_organization_group_project (Resource) + +Creates and manages an organization group project relations in Aiven. + + + + +## Schema + +### Required + +- `group_id` (String) Organization group identifier of the organization group project relation. +- `project` (String) Tenant identifier of the organization. +- `role` (String) Role of the organization group project relation. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + + +### 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/go.mod b/go.mod index 924ab4f89..ab03f4eb1 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.5.0 + github.com/aiven/aiven-go-client/v2 v2.6.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 f354757d9..0d2e90f23 100644 --- a/go.sum +++ b/go.sum @@ -197,8 +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.5.0 h1:HlXQwvmHD1FEKeUiBsk7xx8udoaT7UGC3JQWVZt7XxY= -github.com/aiven/aiven-go-client/v2 v2.5.0/go.mod h1:x0xhzxWEKAwKv0xY5FvECiI6tesWshcPHvjwl0B/1SU= +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/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/plugin/provider.go b/internal/plugin/provider.go index 3714079a3..1969d4fd5 100644 --- a/internal/plugin/provider.go +++ b/internal/plugin/provider.go @@ -118,6 +118,7 @@ func (p *AivenProvider) Resources(context.Context) []func() resource.Resource { // Add to a list of resources that are currently in beta. if isBeta { resources = append(resources, organization.NewOrganizationUserGroupMembersResource) + resources = append(resources, organization.NewOrganizationGroupProjectResource) } return resources diff --git a/internal/plugin/service/organization/organization_group_project.go b/internal/plugin/service/organization/organization_group_project.go new file mode 100644 index 000000000..aa732cc47 --- /dev/null +++ b/internal/plugin/service/organization/organization_group_project.go @@ -0,0 +1,246 @@ +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/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" +) + +var ( + _ resource.Resource = &organizationGroupProjectResource{} + _ resource.ResourceWithConfigure = &organizationGroupProjectResource{} + _ resource.ResourceWithImportState = &organizationGroupProjectResource{} + + _ util.TypeNameable = &organizationGroupProjectResource{} +) + +// NewOrganizationGroupProjectResource is a constructor for the organization resource. +func NewOrganizationGroupProjectResource() resource.Resource { + return &organizationGroupProjectResource{} +} + +// organizationGroupUserResource is the organization resource implementation. +type organizationGroupProjectResource struct { + // client is the instance of the Aiven client to use. + client *aiven.Client + + // typeName is the name of the resource type. + typeName string +} + +// organizationGroupProjectResourceModel is the model for the organization resource. +type organizationGroupProjectResourceModel struct { + // Name is the name of the organization. + Project types.String `tfsdk:"project"` + // OrganizationID is the identifier of the organization group. + OrganizationGroupID 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. +func (r *organizationGroupProjectResource) Metadata( + _ context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_organization_group_project" + + r.typeName = resp.TypeName +} + +// TypeName returns the resource type name. +func (r *organizationGroupProjectResource) TypeName() string { + return r.typeName +} + +// Schema returns the schema for the resource. +func (r *organizationGroupProjectResource) Schema( + ctx context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse) { + resp.Schema = util.GeneralizeSchema(ctx, schema.Schema{ + Description: "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, + }, + "project": schema.StringAttribute{ + Description: "Tenant identifier of the organization.", + Required: true, + }, + "role": schema.StringAttribute{ + Description: "Role of the organization group project relation.", + Required: true, + }, + }, + }) +} + +// Configure is called to configure the resource. +func (r *organizationGroupProjectResource) 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 +} + +// CustomizeDiff helps to customize the diff for the resource. +func (r *organizationGroupProjectResource) fillModel( + ctx context.Context, + m *organizationGroupProjectResourceModel, +) error { + list, err := r.client.ProjectOrganization.List( + ctx, + m.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) + } + } + + if !isFound { + return fmt.Errorf("organization group project relation not found, organization group id: %s, project: %s", + m.OrganizationGroupID.ValueString(), m.Project.ValueString()) + } + + // There is not API endpoint to get the permission of the organization group project relation. + + return nil +} + +// Diff helps to differentiate desired from the existing state of the 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( + ctx, + plan.Project.ValueString(), + plan.OrganizationGroupID.ValueString(), + plan.Role.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 *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, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + var state organizationGroupProjectResourceModel + + 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 + } +} + +// 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()), + ) +} diff --git a/internal/plugin/service/organization/organization_group_project_test.go b/internal/plugin/service/organization/organization_group_project_test.go new file mode 100644 index 000000000..4ccce80c9 --- /dev/null +++ b/internal/plugin/service/organization/organization_group_project_test.go @@ -0,0 +1,63 @@ +package organization_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" +) + +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") + } + + if _, ok := os.LookupEnv("PROVIDER_AIVEN_ENABLE_BETA"); !ok { + t.Skip("Skipping test due to missing PROVIDER_AIVEN_ENABLE_BETA environment variable") + } + + 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"), + ), + }, + }, + }) +} + +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" +} + +resource "aiven_project" "foo" { + project = "pr-%[2]s" + parent_id = "%[1]s" +} +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) +}