Skip to content

Commit

Permalink
Setup org member association
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanhristovski committed Aug 22, 2024
1 parent 247eec9 commit a604183
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 0 deletions.
70 changes: 70 additions & 0 deletions internal/pkg/hubclient/client_organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ type OrgTeamMemberRequest struct {
Member string `json:"member"`
}

type OrgMemberRequest struct {
Org string `json:"org"`
Team string `json:"team"`
Invitees []string `json:"invitees"`
Role string `json:"role"`
DryRun bool `json:"dry_run"`
}

type OrgInviteResponse struct {
OrgInvitees []OrgInvitee `json:"invitees"`
}

type OrgInvitee struct {
Invitee string `json:"invitee"`
Status string `json:"status"`
Invite OrgInvite `json:"invite"`
}

type OrgInvite struct {
ID string `json:"id"`
InviterUsername string `json:"inviter_username"`
Invitee string `json:"invitee"`
Team string `json:"team"`
Org string `json:"org"`
Role string `json:"role"`
CreatedAt string `json:"created_at"`
}

type OrgSettingImageAccessManagement struct {
RestrictedImages ImageAccessManagementRestrictedImages `json:"restricted_images"`
}
Expand Down Expand Up @@ -199,3 +227,45 @@ func (c *Client) SetOrgSettingRegistryAccessManagement(ctx context.Context, orgN
}
return c.GetOrgSettingRegistryAccessManagement(ctx, orgName)
}

func (c *Client) InviteOrgMember(ctx context.Context, orgName, teamName, role string, invitees []string, dryRun bool) (OrgInviteResponse, error) {
inviteRequest := OrgMemberRequest{
Org: orgName,
Team: teamName,
Invitees: invitees,
Role: role,
DryRun: dryRun,
}
reqBody, err := json.Marshal(inviteRequest)
if err != nil {
return OrgInviteResponse{}, err
}

var inviteResponse OrgInviteResponse
err = c.sendRequest(ctx, "POST", "/invites/bulk", reqBody, &inviteResponse)
return inviteResponse, err
}

func (c *Client) DeleteOrgInvite(ctx context.Context, inviteID string) error {
url := fmt.Sprintf("/invites/%s", inviteID)
return c.sendRequest(ctx, "DELETE", url, nil, nil)
}

func (c *Client) DeleteOrgMember(ctx context.Context, orgName string, userName string) error {
url := fmt.Sprintf("/orgs/%s/members/%s/", orgName, userName)
return c.sendRequest(ctx, "DELETE", url, nil, nil)
}

// func (c *Client) GetOrgInvitedMember(ctx context.Context, inviteID string) (OrgMembersResponse, error) {
// url := fmt.Sprintf("/invites", inviteID)
// var membersResponse OrgMembersResponse
// err := c.sendRequest(ctx, "GET", url, nil, &membersResponse)
// return membersResponse, err
// }

// func (c *Client) GetOrgMembers(ctx context.Context, orgName string) (OrgMembersResponse, error) {
// url := fmt.Sprintf("/orgs/%s/members", orgName)
// var membersResponse OrgMembersResponse
// err := c.sendRequest(ctx, "GET", url, nil, &membersResponse)
// return membersResponse, err
// }
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ func (p *DockerProvider) Resources(ctx context.Context) []func() resource.Resour
NewOrgTeamMemberAssociationResource,
NewRepositoryResource,
NewRepositoryTeamPermissionResource,
NewOrgMemberAssociationResource,
}
}

Expand Down
177 changes: 177 additions & 0 deletions internal/provider/resource_org_member_association.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package provider

import (
"context"
"fmt"
"log"

"github.com/docker/terraform-provider-docker/internal/pkg/hubclient"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"

Check failure on line 10 in internal/provider/resource_org_member_association.go

View workflow job for this annotation

GitHub Actions / Build and Test

"github.com/hashicorp/terraform-plugin-framework/path" imported and not used
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var (
_ resource.Resource = &OrgMemberAssociationResource{}
_ resource.ResourceWithConfigure = &OrgMemberAssociationResource{}
_ resource.ResourceWithImportState = &OrgMemberAssociationResource{}
)

func NewOrgMemberAssociationResource() resource.Resource {
return &OrgMemberAssociationResource{}
}

type OrgMemberAssociationResource struct {
client *hubclient.Client
}

type OrgMemberAssociationResourceModel struct {
OrgName types.String `tfsdk:"org_name"`
TeamName types.String `tfsdk:"team_name"`
UserName types.String `tfsdk:"user_name"`
Role types.String `tfsdk:"role"` // New field for role
InviteID types.String `tfsdk:"invite_id"` // This is needed for deletion
}

func (r *OrgMemberAssociationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {

if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*hubclient.Client)
if !ok {
errMsg := fmt.Sprintf("Expected *hubclient.Client, got: %T", req.ProviderData)
resp.Diagnostics.AddError("Unexpected Resource Configure Type", errMsg)
return
}

r.client = client
}

func (r *OrgMemberAssociationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_org_member_association"
}

func (r *OrgMemberAssociationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages the association of a member with a team in an organization.",

Attributes: map[string]schema.Attribute{
"org_name": schema.StringAttribute{
MarkdownDescription: "Organization name",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"team_name": schema.StringAttribute{
MarkdownDescription: "Team name within the organization",
Required: false,
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"user_name": schema.StringAttribute{
MarkdownDescription: "User name (email) of the member being associated with the team",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"role": schema.StringAttribute{
MarkdownDescription: "Role assigned to the user within the organization (e.g., 'member', 'admin').",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.OneOf("member", "editor", "owner"),
},
},
"invite_id": schema.StringAttribute{
MarkdownDescription: "The ID of the invite. Used for managing the association, especially for deletion.",
Computed: true,
},
},
}
}

func (r *OrgMemberAssociationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {

var data OrgMemberAssociationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

inviteResp, err := r.client.InviteOrgMember(ctx, data.OrgName.ValueString(), data.TeamName.ValueString(), data.Role.ValueString(), []string{data.UserName.ValueString()}, false)
if err != nil {
errMsg := fmt.Sprintf("Unable to create org_member_association resource: %v", err)
resp.Diagnostics.AddError("Error Creating Resource", errMsg)
return
}

if len(inviteResp.OrgInvitees) == 0 {
errMsg := "Invite failed: No invitees were returned from the Docker Hub API."
resp.Diagnostics.AddError("Invite Failed", errMsg)
return
}

invite := inviteResp.OrgInvitees[0]
data.InviteID = types.StringValue(invite.Invite.ID)

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

// TODO: finish read
func (r *OrgMemberAssociationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {

var data OrgMemberAssociationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
}

// TODO: setup update
func (r *OrgMemberAssociationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
return
}

func (r *OrgMemberAssociationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data OrgMemberAssociationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// Deleting an established member (accepted inv) vs deleting an invited member has different API calls
// Invited members that have not accepted do not have a recorded username in the org afaik
// Attempt to delete by inviteID first
err := r.client.DeleteOrgInvite(ctx, data.InviteID.ValueString())
if err != nil {
// If deleting by inviteID fails, try deleting by orgName and userName
err = r.client.DeleteOrgMember(ctx, data.OrgName.ValueString(), data.UserName.ValueString())
if err != nil {
errMsg := fmt.Sprintf("Unable to delete org_member_association resource: %v", err)
log.Println(errMsg)
resp.Diagnostics.AddError("Error Deleting Resource", errMsg)
return
}
}

resp.State.RemoveResource(ctx)
log.Println("Successfully deleted OrgMemberAssociationResource.")
}

// TODO: setup import state
func (r *OrgMemberAssociationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
return
}
46 changes: 46 additions & 0 deletions internal/provider/resource_org_member_association_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package provider

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccOrgMemberAssociationResource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testOrgMemberAssociationResourceConfig(),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_org_member_association.test", "invite_id"),
resource.TestCheckResourceAttr("docker_org_member_association.test", "org_name", "dockerhackathon"),
resource.TestCheckResourceAttr("docker_org_member_association.test", "team_name", "test"),
resource.TestCheckResourceAttr("docker_org_member_association.test", "user_name", "[email protected]"),
resource.TestCheckResourceAttr("docker_org_member_association.test", "role", "member"),
),
},
// {
// ResourceName: "docker_org_member_association.test",
// ImportState: false,
// ImportStateVerify: false,
// ImportStateVerifyIgnore: []string{"invite_id"},
// },
},
})
}

func testOrgMemberAssociationResourceConfig() string {
return `
provider "docker" {
}
resource "docker_org_member_association" "test" {
org_name = "dockerhackathon"
team_name = "test"
user_name = "[email protected]"
role = "member"
}
`
}

0 comments on commit a604183

Please sign in to comment.