Skip to content

Commit

Permalink
feat(organization): add support for group projects (#1496)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-savciuc authored Dec 30, 2023
1 parent e649c48 commit 51b3340
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions docs/resources/organization_group_project.md
Original file line number Diff line number Diff line change
@@ -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 generated by tfplugindocs -->
## 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))

<a id="nestedblock--timeouts"></a>
### 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).
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions internal/plugin/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
246 changes: 246 additions & 0 deletions internal/plugin/service/organization/organization_group_project.go
Original file line number Diff line number Diff line change
@@ -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()),
)
}
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 51b3340

Please sign in to comment.