diff --git a/equinix/provider.go b/equinix/provider.go index 45dc2f0b9..e1af82f75 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -133,7 +133,6 @@ func Provider() *schema.Provider { "equinix_metal_project_api_key": resourceMetalProjectAPIKey(), "equinix_metal_device": resourceMetalDevice(), "equinix_metal_device_network_type": resourceMetalDeviceNetworkType(), - "equinix_metal_organization_member": resourceMetalOrganizationMember(), "equinix_metal_port": resourceMetalPort(), "equinix_metal_project": metal_project.Resource(), "equinix_metal_reserved_ip_block": resourceMetalReservedIPBlock(), diff --git a/equinix/resource_metal_organization_member.go b/equinix/resource_metal_organization_member.go deleted file mode 100644 index 585a6ec44..000000000 --- a/equinix/resource_metal_organization_member.go +++ /dev/null @@ -1,285 +0,0 @@ -package equinix - -import ( - "fmt" - "log" - "path" - "strings" - - "github.com/equinix/terraform-provider-equinix/internal/converters" - - equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" - equinix_schema "github.com/equinix/terraform-provider-equinix/internal/schema" - - "github.com/equinix/terraform-provider-equinix/internal/config" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/packethost/packngo" -) - -type member struct { - *packngo.Member - *packngo.Invitation -} - -func (m *member) isMember() bool { - return m.Member != nil -} - -func (m *member) isInvitation() bool { - return m.Invitation != nil -} - -func resourceMetalOrganizationMember() *schema.Resource { - return &schema.Resource{ - Create: resourceMetalOrganizationMemberCreate, - Read: resourceMetalOrganizationMemberRead, - Delete: resourceMetalOrganizationMemberDelete, - Importer: &schema.ResourceImporter{ - State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - parts := strings.Split(d.Id(), ":") - invitee := parts[0] - orgID := parts[1] - d.SetId(d.Id()) - d.Set("invitee", invitee) - d.Set("organization_id", orgID) - if err := resourceMetalOrganizationMemberRead(d, meta); err != nil { - return nil, err - } - if d.Id() == "" { - return nil, fmt.Errorf("Member %s does not exist in organization %s.", invitee, orgID) - } - return []*schema.ResourceData{d}, nil - }, - }, - - Schema: map[string]*schema.Schema{ - "invitee": { - Type: schema.TypeString, - Description: "The email address of the user to invite", - Required: true, - ForceNew: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - "invited_by": { - Type: schema.TypeString, - Description: "The user id of the user that sent the invitation (only known in the invitation stage)", - Computed: true, - }, - "organization_id": { - Type: schema.TypeString, - Description: "The organization to invite the user to", - Required: true, - ForceNew: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - "projects_ids": { - Type: schema.TypeSet, - Description: "Project IDs the member has access to within the organization. If the member is an 'owner', the projects list should be empty.", - Required: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validation.StringIsNotEmpty, - }, - // TODO: Update should be supported. packngo.InvitationService does not offer an Update method. - ForceNew: true, - }, - "nonce": { - Type: schema.TypeString, - Description: "The nonce for the invitation (only known in the invitation stage)", - Computed: true, - }, - "message": { - Type: schema.TypeString, - Description: "A message to the invitee (only used during the invitation stage)", - Optional: true, - ForceNew: true, - }, - "created": { - Type: schema.TypeString, - Description: "When the invitation was created (only known in the invitation stage)", - Computed: true, - }, - "updated": { - Type: schema.TypeString, - Description: "When the invitation was updated (only known in the invitation stage)", - Computed: true, - }, - "roles": { - Type: schema.TypeSet, - Description: "Organization roles (owner, collaborator, limited_collaborator, billing)", - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - // TODO: Update should be supported. packngo.InvitationService does not offer an Update method. - ForceNew: true, - }, - "state": { - Type: schema.TypeString, - Description: "The state of the membership ('invited' when an invitation is open, 'active' when the user is an organization member)", - Computed: true, - }, - }, - } -} - -func resourceMetalOrganizationMemberCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*config.Config).Metal - - email := d.Get("invitee").(string) - createRequest := &packngo.InvitationCreateRequest{ - Invitee: email, - Roles: converters.IfArrToStringArr(d.Get("roles").(*schema.Set).List()), - ProjectsIDs: converters.IfArrToStringArr(d.Get("projects_ids").(*schema.Set).List()), - Message: strings.TrimSpace(d.Get("message").(string)), - } - - orgID := d.Get("organization_id").(string) - _, _, err := client.Invitations.Create(orgID, createRequest, nil) - if err != nil { - return equinix_errors.FriendlyError(err) - } - - d.SetId(fmt.Sprintf("%s:%s", email, orgID)) - - return resourceMetalOrganizationMemberRead(d, meta) -} - -func findMember(invitee string, members []packngo.Member, invitations []packngo.Invitation) (*member, error) { - for _, mbr := range members { - if mbr.User.Email == invitee { - return &member{Member: &mbr}, nil - } - } - - for _, inv := range invitations { - if inv.Invitee == invitee { - return &member{Invitation: &inv}, nil - } - } - return nil, fmt.Errorf("member not found") -} - -func resourceMetalOrganizationMemberRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*config.Config).Metal - parts := strings.Split(d.Id(), ":") - invitee := parts[0] - orgID := parts[1] - - listOpts := &packngo.ListOptions{Includes: []string{"user"}} - invitations, _, err := client.Invitations.List(orgID, listOpts) - if err != nil { - err = equinix_errors.FriendlyError(err) - // If the org was destroyed, mark as gone. - if equinix_errors.IsNotFound(err) { - d.SetId("") - return nil - } - return err - } - - members, _, err := client.Members.List(orgID, &packngo.GetOptions{Includes: []string{"user"}}) - if err != nil { - err = equinix_errors.FriendlyError(err) - // If the org was destroyed, mark as gone. - if equinix_errors.IsNotFound(err) { - d.SetId("") - return nil - } - return err - } - member, err := findMember(invitee, members, invitations) - if !d.IsNewResource() && err != nil { - log.Printf("[WARN] Could not find member %s in organization, removing from state", d.Id()) - d.SetId("") - return nil - } - - if member.isMember() { - projectIDs := []string{} - for _, project := range member.Member.Projects { - projectIDs = append(projectIDs, path.Base(project.URL)) - } - return equinix_schema.SetMap(d, map[string]interface{}{ - "state": "active", - "roles": converters.StringArrToIfArr(member.Member.Roles), - "projects_ids": converters.StringArrToIfArr(projectIDs), - "organization_id": path.Base(member.Member.Organization.URL), - }) - } else if member.isInvitation() { - projectIDs := []string{} - for _, project := range member.Invitation.Projects { - projectIDs = append(projectIDs, path.Base(project.Href)) - } - return equinix_schema.SetMap(d, map[string]interface{}{ - "state": "invited", - "organization_id": path.Base(member.Invitation.Organization.Href), - "roles": member.Invitation.Roles, - "projects_ids": projectIDs, - "created": member.Invitation.CreatedAt.String(), - "updated": member.Invitation.UpdatedAt.String(), - "nonce": member.Invitation.Nonce, - "invited_by": path.Base(member.Invitation.InvitedBy.Href), - }) - } - return fmt.Errorf("got an invalid member object") -} - -func resourceMetalOrganizationMemberDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*config.Config).Metal - - listOpts := &packngo.ListOptions{Includes: []string{"user"}} - invitations, _, err := client.Invitations.List(d.Get("organization_id").(string), listOpts) - if err != nil { - err = equinix_errors.FriendlyError(err) - // If the org was destroyed, mark as gone. - if equinix_errors.IsNotFound(err) { - d.SetId("") - return nil - } - return err - } - - orgID := d.Get("organization_id").(string) - org, _, err := client.Organizations.Get(orgID, &packngo.GetOptions{Includes: []string{"members", "members.user"}}) - if err != nil { - err = equinix_errors.FriendlyError(err) - // If the org was destroyed, mark as gone. - if equinix_errors.IsNotFound(err) { - d.SetId("") - return nil - } - return err - } - - member, err := findMember(d.Get("invitee").(string), org.Members, invitations) - if err != nil { - d.SetId("") - return nil - } - - if member.isMember() { - _, err = client.Members.Delete(orgID, member.Member.ID) - if err != nil { - err = equinix_errors.FriendlyError(err) - // If the member was deleted, mark as gone. - if equinix_errors.IsNotFound(err) { - d.SetId("") - return nil - } - return err - } - } else if member.isInvitation() { - _, err = client.Invitations.Delete(member.Invitation.ID) - if err != nil { - err = equinix_errors.FriendlyError(err) - // If the invitation was deleted, mark as gone. - if equinix_errors.IsNotFound(err) { - d.SetId("") - return nil - } - return err - } - } - return nil -} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e682f63c3..f5677c607 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -8,6 +8,7 @@ import ( metalconnection "github.com/equinix/terraform-provider-equinix/internal/resources/metal/connection" metalgateway "github.com/equinix/terraform-provider-equinix/internal/resources/metal/gateway" metalorganization "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization" + metalorganizationmember "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization_member" metalprojectsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project_ssh_key" metalsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/ssh_key" equinix_validation "github.com/equinix/terraform-provider-equinix/internal/validation" @@ -116,6 +117,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res metalsshkey.NewResource, metalconnection.NewResource, metalorganization.NewResource, + metalorganizationmember.NewResource, } } diff --git a/internal/resources/metal/organization_member/models.go b/internal/resources/metal/organization_member/models.go new file mode 100644 index 000000000..9a9b16ce8 --- /dev/null +++ b/internal/resources/metal/organization_member/models.go @@ -0,0 +1,77 @@ +package organizationmember + +import ( + "context" + "path" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type ResourceModel struct { + ID types.String `tfsdk:"id"` + Invitee types.String `tfsdk:"invitee"` + InvitedBy types.String `tfsdk:"invited_by"` + OrganizationID types.String `tfsdk:"organization_id"` + ProjectsIDs types.Set `tfsdk:"projects_ids"` + Nonce types.String `tfsdk:"nonce"` + Message types.String `tfsdk:"message"` + Created types.String `tfsdk:"created"` + Updated types.String `tfsdk:"updated"` + Roles types.Set `tfsdk:"roles"` + State types.String `tfsdk:"state"` +} + +// func (m *ResourceModel) parse(ctx context.Context, invitee string, members []packngo.Member, invitations []packngo.Invitation) diag.Diagnostics { +func (m *ResourceModel) parse(ctx context.Context, member *member) diag.Diagnostics { + var diags diag.Diagnostics + + if member.isMember() { + projectIDs := []string{} + for _, project := range member.Member.Projects { + projectIDs = append(projectIDs, path.Base(project.URL)) + } + + projectsList, diag := types.SetValueFrom(ctx, types.StringType, projectIDs) + if diag.HasError() { + return diag + } + m.ProjectsIDs = projectsList + m.State = types.StringValue("active") + + rolesList, diag := types.SetValueFrom(ctx, types.StringType, member.Member.Roles) + if diag.HasError() { + return diag + } + m.Roles = rolesList + m.OrganizationID = types.StringValue(member.Member.Organization.URL) + + } else if member.isInvitation() { + projectIDs := []string{} + for _, project := range member.Invitation.Projects { + projectIDs = append(projectIDs, path.Base(project.Href)) + } + projectsList, diag := types.SetValueFrom(ctx, types.StringType, projectIDs) + if diag.HasError() { + return diag + } + m.ProjectsIDs = projectsList + + m.State = types.StringValue("invited") + + rolesList, diag := types.SetValueFrom(ctx, types.StringType, member.Invitation.Roles) + if diag.HasError() { + return diag + } + m.Roles = rolesList + + m.OrganizationID = types.StringValue(path.Base(member.Invitation.Organization.Href)) + m.Created = types.StringValue(member.Invitation.CreatedAt.String()) + m.Updated = types.StringValue(member.Invitation.UpdatedAt.String()) + m.Nonce = types.StringValue(member.Invitation.Nonce) + + m.InvitedBy = types.StringValue(path.Base(member.Invitation.InvitedBy.Href)) + m.ID = types.StringValue(member.Invitation.ID) + } + return diags +} diff --git a/internal/resources/metal/organization_member/resource.go b/internal/resources/metal/organization_member/resource.go new file mode 100644 index 000000000..7b7c7ec88 --- /dev/null +++ b/internal/resources/metal/organization_member/resource.go @@ -0,0 +1,339 @@ +package organizationmember + +import ( + "context" + "fmt" + "log" + "strings" + + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + "github.com/equinix/terraform-provider-equinix/internal/framework" + tfpath "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/packethost/packngo" +) + +type member struct { + *packngo.Member + *packngo.Invitation +} + +func (m *member) isMember() bool { + return m.Member != nil +} + +func (m *member) isInvitation() bool { + return m.Invitation != nil +} + +func NewResource() resource.Resource { + return &Resource{ + BaseResource: framework.NewBaseResource( + framework.BaseResourceConfig{ + Name: "equinix_metal_organization_member", + Schema: GetResourceSchema(), + }, + ), + } +} + +type Resource struct { + framework.BaseResource +} + +func (r *Resource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + tflog.Debug(ctx, "importer Organization") + + parts := strings.Split(req.ID, ":") + if len(parts) != 2 { + return + + } + invitee := parts[0] + orgID := parts[1] + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, tfpath.Root("invitee"), invitee)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, tfpath.Root("organization_id"), orgID)...) +} + +func (r *Resource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta) + client := r.Meta.Metal + + var plan ResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + invite := plan.Invitee.ValueString() + + roles := make([]string, 0) + for _, elem := range plan.Roles.Elements() { + if strValue, ok := elem.(types.String); ok { + + if !strValue.IsNull() { + roles = append(roles, strValue.ValueString()) + } + } + } + + projects := make([]string, 0) + for _, elem := range plan.ProjectsIDs.Elements() { + if strValue, ok := elem.(types.String); ok { + projects = append(projects, strValue.ValueString()) + } + } + + createRequest := &packngo.InvitationCreateRequest{ + Invitee: invite, + Roles: roles, + ProjectsIDs: projects, + Message: strings.TrimSpace(plan.Message.ValueString()), + } + + orgID := plan.OrganizationID.ValueString() + _, _, err := client.Invitations.Create(orgID, createRequest, nil) + if err != nil { + resp.Diagnostics.AddError( + "Failed to create invitation", + err.Error(), + ) + return + } + + listOpts := &packngo.ListOptions{Includes: []string{"user"}} + invitations, _, err := client.Invitations.List(orgID, listOpts) + if err != nil { + err = equinix_errors.FriendlyError(err) + // If the org was destroyed, mark as gone. + if equinix_errors.IsNotFound(err) { + plan.OrganizationID = basetypes.NewStringNull() + return + } + resp.Diagnostics.AddError( + "Failed to list invitations", + err.Error(), + ) + return + } + + members, _, err := client.Members.List(orgID, listOpts) + if err != nil { + err = equinix_errors.FriendlyError(err) + // If the org was destroyed, mark as gone. + if equinix_errors.IsNotFound(err) { + return + } + resp.Diagnostics.AddError( + "Failed to List members", + err.Error(), + ) + return + } + + member, err := findMember(invite, members, invitations) + if err != nil { + log.Printf("[WARN] Could not find member %s in organization, removing from state", plan.OrganizationID) + plan.OrganizationID = basetypes.NewStringNull() + resp.Diagnostics.AddError( + "Failed to find members", + err.Error(), + ) + return + } + + // Parse API response into the Terraform state + if member != nil { + diags := plan.parse(ctx, member) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + } + + // Set state to fully populated data + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *Resource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + tflog.Debug(ctx, "Read Organization") + r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta) + client := r.Meta.Metal + + // Retrieve values from plan + var data ResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + invitee := data.Invitee.ValueString() + orgID := data.OrganizationID.ValueString() + + listOpts := &packngo.ListOptions{Includes: []string{"user"}} + invitations, _, err := client.Invitations.List(orgID, listOpts) + if err != nil { + err = equinix_errors.FriendlyError(err) + // If the org was destroyed, mark as gone. + if equinix_errors.IsNotFound(err) { + data.OrganizationID = basetypes.NewStringNull() + return + } + resp.Diagnostics.AddError( + "Failed to list invitations", + err.Error(), + ) + return + } + + members, _, err := client.Members.List(orgID, &packngo.GetOptions{Includes: []string{"user"}}) + if err != nil { + err = equinix_errors.FriendlyError(err) + // If the org was destroyed, mark as gone. + if equinix_errors.IsNotFound(err) { + data.OrganizationID = basetypes.NewStringNull() + return + } + resp.Diagnostics.AddError( + "Failed to list members", + err.Error(), + ) + return + } + member, err := findMember(invitee, members, invitations) + if err != nil { + log.Printf("[WARN] Could not find member %s in organization, removing from state", data.OrganizationID) + data.OrganizationID = basetypes.NewStringNull() + return + } + + diags := data.parse(ctx, member) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + // Set state to fully populated data + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *Resource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + tflog.Debug(ctx, "Delete Organization") + r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta) + client := r.Meta.Metal + + var data ResourceModel + + listOpts := &packngo.ListOptions{Includes: []string{"user"}} + invitations, _, err := client.Invitations.List(data.OrganizationID.ValueString(), listOpts) + if err != nil { + err = equinix_errors.FriendlyError(err) + // If the org was destroyed, mark as gone. + if equinix_errors.IsNotFound(err) { + data.OrganizationID = types.StringNull() + return + } + resp.Diagnostics.AddError( + "Failed to list invitations", + err.Error(), + ) + return + } + + org, _, err := client.Organizations.Get(data.OrganizationID.ValueString(), &packngo.GetOptions{Includes: []string{"members", "members.user"}}) + if err != nil { + err = equinix_errors.FriendlyError(err) + // If the org was destroyed, mark as gone. + if equinix_errors.IsNotFound(err) { + data.OrganizationID = types.StringNull() + return + } + + resp.Diagnostics.AddError( + "Failed to get Organizations", + err.Error(), + ) + return + } + + member, err := findMember(data.Invitee.ValueString(), org.Members, invitations) + if err != nil { + data.OrganizationID = types.StringNull() + return + } + + if member.isMember() { + _, err = client.Members.Delete(data.OrganizationID.ValueString(), member.Member.ID) + if err != nil { + err = equinix_errors.FriendlyError(err) + // If the member was deleted, mark as gone. + if equinix_errors.IsNotFound(err) { + data.OrganizationID = types.StringNull() + return + } + resp.Diagnostics.AddError( + "Failed to delete member", + err.Error(), + ) + return + } + } else if member.isInvitation() { + _, err = client.Invitations.Delete(member.Invitation.ID) + if err != nil { + err = equinix_errors.FriendlyError(err) + // If the invitation was deleted, mark as gone. + if equinix_errors.IsNotFound(err) { + data.OrganizationID = types.StringNull() + return + } + + resp.Diagnostics.AddError( + "Failed to delete invitation", + err.Error(), + ) + return + } + } +} + +func (r *Resource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + // This resource does not support updates +} + +func findMember(invitee string, members []packngo.Member, invitations []packngo.Invitation) (*member, error) { + for _, mbr := range members { + if mbr.User.Email == invitee { + return &member{Member: &mbr}, nil + } + } + + for _, inv := range invitations { + if inv.Invitee == invitee { + return &member{Invitation: &inv}, nil + } + } + return nil, fmt.Errorf("member not found") +} diff --git a/internal/resources/metal/organization_member/resource_schema.go b/internal/resources/metal/organization_member/resource_schema.go new file mode 100644 index 000000000..7808020bd --- /dev/null +++ b/internal/resources/metal/organization_member/resource_schema.go @@ -0,0 +1,67 @@ +package organizationmember + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func GetResourceSchema() *schema.Schema { + return &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier for the organization member.", + Computed: true, + }, + "invitee": schema.StringAttribute{ + Description: "The email address of the user to invite", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "invited_by": schema.StringAttribute{ + Description: "The user id of the user that sent the invitation (only known in the invitation stage)", + Computed: true, + }, + "organization_id": schema.StringAttribute{ + Description: "The organization to invite the user to", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "projects_ids": schema.SetAttribute{ + Description: "Project IDs the member has access to within the organization. If the member is an 'owner', the projects list should be empty.", + Required: true, + ElementType: types.StringType, + }, + "nonce": schema.StringAttribute{ + Description: "The nonce for the invitation (only known in the invitation stage)", + Computed: true, + }, + "message": schema.StringAttribute{ + Description: "A message to the invitee (only used during the invitation stage)", + Optional: true, + }, + "created": schema.StringAttribute{ + Description: "When the invitation was created (only known in the invitation stage)", + Computed: true, + }, + "updated": schema.StringAttribute{ + Description: "When the invitation was updated (only known in the invitation stage)", + Computed: true, + }, + "roles": schema.SetAttribute{ + ElementType: types.StringType, + Description: "Organization roles (owner, collaborator, limited_collaborator, billing)", + Required: true, + }, + "state": schema.StringAttribute{ + Description: "The state of the membership ('invited' when an invitation is open, 'active' when the user is an organization member)", + Computed: true, + }, + }, + } +} diff --git a/equinix/resource_metal_organization_member_acc_test.go b/internal/resources/metal/organization_member/resource_test.go similarity index 77% rename from equinix/resource_metal_organization_member_acc_test.go rename to internal/resources/metal/organization_member/resource_test.go index 9e201437c..41f53942d 100644 --- a/equinix/resource_metal_organization_member_acc_test.go +++ b/internal/resources/metal/organization_member/resource_test.go @@ -1,9 +1,10 @@ -package equinix +package organizationmember_test import ( "fmt" "testing" + "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -15,11 +16,9 @@ func TestAccResourceMetalOrganizationMember_owner(t *testing.T) { rInt := acctest.RandInt() org := &packngo.Organization{} resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, - // TODO: CheckDestroy: testAccMetalOrganizationMemberCheckDestroyed, - CheckDestroy: nil, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t); acceptance.TestAccPreCheckProviderConfigured(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: testAccMetalOrganizationCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccResourceMetalOrganizationMember_basic(rInt), @@ -35,13 +34,6 @@ func TestAccResourceMetalOrganizationMember_owner(t *testing.T) { }), ImportState: true, }, - /* - { - ResourceName: "equinix_metal_organization_member.owner", - Config: testAccResourceMetalOrganizationMember_basic(rInt) + testAccResourceMetalOrganizationMember_owner(), - ExpectError: regexp.MustCompile("User is already a member of the Organization"), - }, - */ { Config: testAccResourceMetalOrganizationMember_basic(rInt), }, @@ -53,11 +45,9 @@ func TestAccResourceMetalOrganizationMember_basic(t *testing.T) { rInt := acctest.RandInt() org := &packngo.Organization{} resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, - // TODO: CheckDestroy: testAccMetalOrganizationMemberCheckDestroyed, - CheckDestroy: nil, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t); acceptance.TestAccPreCheckProviderConfigured(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: testAccMetalOrganizationCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccResourceMetalOrganizationMember_basic(rInt), @@ -104,21 +94,21 @@ resource "equinix_metal_project" "test" { } func testAccResourceMetalOrganizationMember_owner() string { - return fmt.Sprintf(` + return ` resource "equinix_metal_organization_member" "owner" { invitee = "/* TODO: Add org owner email or token owner email here */" roles = ["owner"] projects_ids = [] organization_id = equinix_metal_organization.test.id } - `) + ` } func testAccResourceMetalOrganizationMember_member() string { return ` resource "equinix_metal_organization_member" "member" { invitee = "tfacc.testing.member@equinixmetal.com" - roles = ["limited_collaborator"] + roles = ["limited_collaborator"] projects_ids = [equinix_metal_project.test.id] organization_id = equinix_metal_organization.test.id message = "This invitation was sent by the github.com/equinix/terraform-provider-equinix acceptance tests to test equinix_metal_organization_member resources." @@ -126,6 +116,21 @@ resource "equinix_metal_organization_member" "member" { ` } +func testAccMetalOrganizationCheckDestroyed(s *terraform.State) error { + client := acceptance.TestAccProvider.Meta().(*config.Config).Metal + + for _, rs := range s.RootModule().Resources { + if rs.Type != "equinix_metal_organization" { + continue + } + if _, _, err := client.Organizations.Get(rs.Primary.ID, nil); err == nil { + return fmt.Errorf("Metal Organization still exists") + } + } + + return nil +} + func testAccMetalOrganizationExists(n string, org *packngo.Organization) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -136,7 +141,7 @@ func testAccMetalOrganizationExists(n string, org *packngo.Organization) resourc return fmt.Errorf("No Record ID is set") } - client := testAccProvider.Meta().(*config.Config).Metal + client := acceptance.TestAccProvider.Meta().(*config.Config).Metal foundOrg, _, err := client.Organizations.Get(rs.Primary.ID, &packngo.GetOptions{Includes: []string{"address", "primary_owner"}}) if err != nil {