Skip to content

Commit

Permalink
refactor(organizational_unit): replaced client-v2 with avngen (#1940)
Browse files Browse the repository at this point in the history
  • Loading branch information
vmyroslav authored Dec 13, 2024
1 parent c2e2e40 commit 524ea8f
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 99 deletions.
47 changes: 17 additions & 30 deletions internal/schemautil/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/aiven/aiven-go-client/v2"
"github.com/aiven/go-client-codegen/handler/organization"
"github.com/aiven/go-client-codegen/handler/service"
"github.com/docker/go-units"
"github.com/hashicorp/go-cty/cty"
Expand Down Expand Up @@ -137,16 +138,16 @@ func HumanReadableByteSize(s int) string {
return units.CustomSize("%.12g%s", float64(s), 1024.0, suffixes)
}

// isStringAnOrganizationID is a helper function that returns true if the string is an organization ID.
func isStringAnOrganizationID(s string) bool {
// IsOrganizationID is a helper function that returns true if the string is an organization ID.
func IsOrganizationID(s string) bool {
return strings.HasPrefix(s, "org")
}

// NormalizeOrganizationID is a helper function that returns the ID to use for the API call.
// If the ID is an organization ID, it will be converted to an account ID via the API.
// If the ID is an account ID, it will be returned as is, without performing any API calls.
func NormalizeOrganizationID(ctx context.Context, client *aiven.Client, id string) (string, error) {
if isStringAnOrganizationID(id) {
if IsOrganizationID(id) {
r, err := client.Organization.Get(ctx, id)
if err != nil {
return "", err
Expand All @@ -158,37 +159,23 @@ func NormalizeOrganizationID(ctx context.Context, client *aiven.Client, id strin
return id, nil
}

// DetermineMixedOrganizationConstraintIDToStore is a helper function that returns the ID to store in the state.
// We have several fields that can be either an organization ID or an account ID.
// We want to store the one that was already in the state, if it was already there.
// If it was not, we want to prioritize the organization ID, but if it is not available, we want to store the account
// ID.
// If the ID is an account ID, it will be returned as is, without performing any API calls.
// If the ID is an organization ID, it will be refreshed via the provided account ID and returned.
func DetermineMixedOrganizationConstraintIDToStore(
ctx context.Context,
client *aiven.Client,
stateID string,
accountID string,
) (string, error) {
if len(accountID) == 0 {
return "", nil
}

if !isStringAnOrganizationID(stateID) {
return accountID, nil
}
// organizationGetter helper type to shrinks the avngen.Client interface size.
type organizationGetter interface {
OrganizationGet(ctx context.Context, id string) (*organization.OrganizationGetOut, error)
}

r, err := client.Accounts.Get(ctx, accountID)
if err != nil {
return "", err
}
// ConvertOrganizationToAccountID transforms provided ID to an account ID via API call if it is an organization ID.
func ConvertOrganizationToAccountID(ctx context.Context, id string, client organizationGetter) (string, error) {
if IsOrganizationID(id) {
resp, err := client.OrganizationGet(ctx, id)
if err != nil {
return "", err
}

if len(r.Account.OrganizationId) == 0 {
return accountID, nil
return resp.AccountId, nil
}

return r.Account.OrganizationId, nil
return id, nil
}

// StringToDiagWarning is a function that converts a string to a diag warning.
Expand Down
85 changes: 85 additions & 0 deletions internal/schemautil/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package schemautil

import (
"context"
"testing"

"github.com/aiven/go-client-codegen/handler/organization"
"github.com/stretchr/testify/assert"
)

type mockAvngenClient struct {
get func(ctx context.Context, id string) (*organization.OrganizationGetOut, error)
}

func (c *mockAvngenClient) OrganizationGet(ctx context.Context, id string) (*organization.OrganizationGetOut, error) {
return c.get(ctx, id)
}

func TestDetermineMixedOrganizationConstraintIDToStore(t *testing.T) {
t.Parallel()

var (
ctx = context.Background()
)

type testCase struct {
name string
input string
want string
wantErr bool
}

tests := []testCase{
{
name: "provided Organization ID",
input: "org-123",
want: "acc-123",
wantErr: false,
},
{
name: "provided Account ID",
input: "acc-123",
want: "acc-123",
wantErr: false,
},
{
name: "provided an empty ID",
input: "",
want: "",
wantErr: false,
},
{
name: "error when fetching organization",
input: "org-123",
want: "",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

client := new(mockAvngenClient)
client.get = func(_ context.Context, _ string) (*organization.OrganizationGetOut, error) {
if tt.wantErr {
return nil, assert.AnError
}

return &organization.OrganizationGetOut{AccountId: tt.want}, nil
}

got, err := ConvertOrganizationToAccountID(ctx, tt.input, client)
if tt.wantErr {
assert.Error(t, err)

return
}

assert.NoError(t, err)
assert.Equalf(t, tt.want, got, "expected %s, got %s", tt.want, got)
})
}

}
131 changes: 78 additions & 53 deletions internal/sdkprovider/service/organization/organizational_unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package organization

import (
"context"
"fmt"

"github.com/aiven/aiven-go-client/v2"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
avngen "github.com/aiven/go-client-codegen"
"github.com/aiven/go-client-codegen/handler/account"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/aiven/terraform-provider-aiven/internal/common"
"github.com/aiven/terraform-provider-aiven/internal/schemautil"
)

Expand Down Expand Up @@ -41,10 +43,10 @@ var aivenOrganizationalUnitSchema = map[string]*schema.Schema{
func ResourceOrganizationalUnit() *schema.Resource {
return &schema.Resource{
Description: "Creates and manages an [organizational unit](https://aiven.io/docs/platform/concepts/orgs-units-projects) in an Aiven organization.",
CreateContext: resourceOrganizationalUnitCreate,
ReadContext: resourceOrganizationalUnitRead,
UpdateContext: resourceOrganizationalUnitUpdate,
DeleteContext: resourceOrganizationalUnitDelete,
CreateContext: common.WithGenClient(resourceOrganizationalUnitCreate),
ReadContext: common.WithGenClient(resourceOrganizationalUnitRead),
UpdateContext: common.WithGenClient(resourceOrganizationalUnitUpdate),
DeleteContext: common.WithGenClient(resourceOrganizationalUnitDelete),
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Expand All @@ -54,91 +56,114 @@ func ResourceOrganizationalUnit() *schema.Resource {
}
}

func resourceOrganizationalUnitCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*aiven.Client)
name := d.Get("name").(string)
func resourceOrganizationalUnitCreate(ctx context.Context, d *schema.ResourceData, client avngen.Client) error {
var (
name = d.Get("name").(string)
parentID = d.Get("parent_id").(string)
)

parentID, err := schemautil.NormalizeOrganizationID(ctx, client, d.Get("parent_id").(string))
accID, err := schemautil.ConvertOrganizationToAccountID(ctx, parentID, client)
if err != nil {
return diag.FromErr(err)
return err
}

r, err := client.Accounts.Create(
ctx,
aiven.Account{
Name: name,
ParentAccountId: parentID,
},
)
resp, err := client.AccountCreate(ctx, &account.AccountCreateIn{
AccountName: name,
ParentAccountId: &accID,
PrimaryBillingGroupId: nil,
})
if err != nil {
return diag.FromErr(err)
return err
}

d.SetId(r.Account.Id)
d.SetId(resp.AccountId)

return resourceOrganizationalUnitRead(ctx, d, m)
return resourceOrganizationalUnitRead(ctx, d, client)
}

func resourceOrganizationalUnitRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*aiven.Client)

r, err := client.Accounts.Get(ctx, d.Id())
func resourceOrganizationalUnitRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) error {
resp, err := client.AccountGet(ctx, d.Id())
if err != nil {
return diag.FromErr(schemautil.ResourceReadHandleNotFound(err, d))
return schemautil.ResourceReadHandleNotFound(err, d)
}

if stateID, _ := d.GetOk("parent_id"); true {
idToSet, err := schemautil.DetermineMixedOrganizationConstraintIDToStore(
// the ParentAccountId is required for the resource to be valid and this case should never happen,
// but we still need to check for it due to the definition in the avngen response schema
if resp.ParentAccountId == nil {
return fmt.Errorf("parent_id is not set for organizational unit: %q", d.Id())
}

if stateID, ok := d.GetOk("parent_id"); ok {
idToSet, err := determineMixedOrganizationConstraintIDToStore(
ctx,
client,
stateID.(string),
r.Account.ParentAccountId,
*resp.ParentAccountId,
)
if err != nil {
return diag.FromErr(err)
return err
}

if err := d.Set("parent_id", idToSet); err != nil {
return diag.FromErr(err)
if err = d.Set("parent_id", idToSet); err != nil {
return err
}
}

if err := d.Set("name", r.Account.Name); err != nil {
return diag.FromErr(err)
if err = schemautil.ResourceDataSet(
aivenOrganizationalUnitSchema,
d,
resp,
); err != nil {
return err
}

return nil
}

func determineMixedOrganizationConstraintIDToStore(
ctx context.Context,
client avngen.Client,
stateID string,
accountID string,
) (string, error) {
if len(accountID) == 0 {
return "", nil
}
if err := d.Set("tenant_id", r.Account.TenantId); err != nil {
return diag.FromErr(err)

if !schemautil.IsOrganizationID(stateID) {
return accountID, nil
}
if err := d.Set("create_time", r.Account.CreateTime.String()); err != nil {
return diag.FromErr(err)

r, err := client.AccountGet(ctx, accountID)
if err != nil {
return "", err
}
if err := d.Set("update_time", r.Account.UpdateTime.String()); err != nil {
return diag.FromErr(err)

if len(r.OrganizationId) == 0 {
return accountID, nil
}

return nil
return r.OrganizationId, nil
}

func resourceOrganizationalUnitUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*aiven.Client)
func resourceOrganizationalUnitUpdate(ctx context.Context, d *schema.ResourceData, client avngen.Client) error {
var name = d.Get("name").(string)

r, err := client.Accounts.Update(ctx, d.Id(), aiven.Account{
Name: d.Get("name").(string),
resp, err := client.AccountUpdate(ctx, d.Id(), &account.AccountUpdateIn{
AccountName: &name,
})
if err != nil {
return diag.FromErr(err)
return err
}

d.SetId(r.Account.Id)
d.SetId(resp.AccountId)

return resourceOrganizationalUnitRead(ctx, d, m)
return resourceOrganizationalUnitRead(ctx, d, client)
}

func resourceOrganizationalUnitDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*aiven.Client)

if err := client.Accounts.Delete(ctx, d.Id()); err != nil && !aiven.IsNotFound(err) {
return diag.FromErr(err)
func resourceOrganizationalUnitDelete(ctx context.Context, d *schema.ResourceData, client avngen.Client) error {
if err := client.AccountDelete(ctx, d.Id()); common.IsCritical(err) {
return err
}

return nil
Expand Down
Loading

0 comments on commit 524ea8f

Please sign in to comment.