internal/provider/resource_gitlab_group_membership.go (262 lines of code) (raw):

package provider import ( "context" "fmt" "strconv" "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "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" "github.com/hashicorp/terraform-plugin-log/tflog" gitlab "gitlab.com/gitlab-org/api/client-go" "gitlab.com/gitlab-org/terraform-provider-gitlab/internal/provider/api" "gitlab.com/gitlab-org/terraform-provider-gitlab/internal/provider/utils" ) // Ensure provider defined types fully satisfy framework interfaces var ( _ resource.Resource = &gitlabGroupMembershipResource{} _ resource.ResourceWithConfigure = &gitlabGroupMembershipResource{} _ resource.ResourceWithImportState = &gitlabGroupMembershipResource{} ) func init() { registerResource(NewGitlabGroupMembershipResource) } func NewGitlabGroupMembershipResource() resource.Resource { return &gitlabGroupMembershipResource{} } type gitlabGroupMembershipResource struct { client *gitlab.Client } type gitlabGroupMembershipResourceModel struct { ID types.String `tfsdk:"id"` GroupID types.Int64 `tfsdk:"group_id"` UserID types.Int64 `tfsdk:"user_id"` AccessLevel types.String `tfsdk:"access_level"` MemberRoleID types.Int64 `tfsdk:"member_role_id"` ExpiresAt types.String `tfsdk:"expires_at"` SkipSubresourcesOnDestroy types.Bool `tfsdk:"skip_subresources_on_destroy"` UnassignIssuablesOnDestroy types.Bool `tfsdk:"unassign_issuables_on_destroy"` } func (r *gitlabGroupMembershipResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_group_membership" } func (r *gitlabGroupMembershipResource) Schema(ctc context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: `The ` + "`gitlab_group_membership`" + ` resource allows to manage the lifecycle of a users group membership. -> If a group should grant membership to another group use the ` + "`gitlab_group_share_group`" + ` resource instead. **Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/api/members/)`, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, MarkdownDescription: "The ID of the group membership. In the format of `<group-id:user-id>`.", PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "group_id": schema.Int64Attribute{ MarkdownDescription: "The ID of the group.", Required: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}, }, "user_id": schema.Int64Attribute{ MarkdownDescription: "The ID of the user.", Required: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}, }, "access_level": schema.StringAttribute{ MarkdownDescription: fmt.Sprintf("Access level for the member. Valid values are: %s.", utils.RenderValueListForDocs(api.ValidGroupAccessLevelNames)), Required: true, Validators: []validator.String{stringvalidator.OneOf(api.ValidGroupAccessLevelNames...)}, }, "member_role_id": schema.Int64Attribute{ MarkdownDescription: "The ID of a custom member role. Only available for Ultimate instances.", PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, Optional: true, Computed: true, }, "expires_at": schema.StringAttribute{ MarkdownDescription: "Expiration date for the group membership. Format: `YYYY-MM-DD`", PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, Optional: true, Computed: true, }, "skip_subresources_on_destroy": schema.BoolAttribute{ MarkdownDescription: "Whether the deletion of direct memberships of the removed member in subgroups and projects should be skipped. Only used during a destroy.", Optional: true, Computed: true, Default: booldefault.StaticBool(false), PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, }, "unassign_issuables_on_destroy": schema.BoolAttribute{ MarkdownDescription: "Whether the removed member should be unassigned from any issues or merge requests inside a given group or project. Only used during a destroy.", Optional: true, Computed: true, Default: booldefault.StaticBool(false), PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, }, }, } } func (r *gitlabGroupMembershipResource) Configure(ctx context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { if req.ProviderData == nil { return } resourceData := req.ProviderData.(*GitLabResourceData) r.client = resourceData.Client } func (d *gitlabGroupMembershipResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } func (d *gitlabGroupMembershipResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data *gitlabGroupMembershipResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } userId := int(data.UserID.ValueInt64()) groupId := int(data.GroupID.ValueInt64()) expiresAt := data.ExpiresAt.ValueString() accessLevelId := api.AccessLevelNameToValue[strings.ToLower(data.AccessLevel.ValueString())] options := &gitlab.AddGroupMemberOptions{ UserID: &userId, AccessLevel: &accessLevelId, ExpiresAt: &expiresAt, } if !data.MemberRoleID.IsNull() && !data.MemberRoleID.IsUnknown() { options.MemberRoleID = gitlab.Ptr(int(data.MemberRoleID.ValueInt64())) } tflog.Debug(ctx, fmt.Sprintf("[DEBUG] create gitlab group groupMember for %d in %d", options.UserID, groupId)) groupMember, _, err := d.client.GroupMembers.AddGroupMember(groupId, options, gitlab.WithContext(ctx)) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error creating new GitLab group membership", fmt.Sprintf("couldn't create new GitLab group membership: %v", err))) return } resp.Diagnostics.Append(data.groupMembershipToStateModel(groupId, groupMember)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (d *gitlabGroupMembershipResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data gitlabGroupMembershipResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } id := data.ID.ValueString() tflog.Debug(ctx, fmt.Sprintf("[DEBUG] read gitlab group groupMember %s", id)) groupIdString, userIdString, err := utils.ParseTwoPartID(id) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error getting group and user ID from resource ID", fmt.Sprintf("Error getting group and user ID from resource ID: %v", err))) return } groupId, err := strconv.Atoi(groupIdString) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error converting group ID to int", fmt.Sprintf("Error converting group ID to int: %v", err))) return } userId, err := strconv.Atoi(userIdString) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error converting user ID to int", fmt.Sprintf("Error converting user ID to int: %v", err))) return } groupMember, _, err := d.client.GroupMembers.GetGroupMember(groupId, userId, gitlab.WithContext(ctx)) if err != nil { if api.Is404(err) { tflog.Debug(ctx, fmt.Sprintf("[DEBUG] gitlab group membership for %s not found so removing from state", id)) resp.State.RemoveResource(ctx) return } resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error reading GitLab group membership", fmt.Sprintf("Error reading GitLab group membership: %v", err))) return } resp.Diagnostics.Append(data.groupMembershipToStateModel(groupId, groupMember)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (d *gitlabGroupMembershipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var data gitlabGroupMembershipResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } userId := int(data.UserID.ValueInt64()) groupId := int(data.GroupID.ValueInt64()) expiresAt := data.ExpiresAt.ValueString() accessLevelId := api.AccessLevelNameToValue[strings.ToLower(data.AccessLevel.ValueString())] options := gitlab.EditGroupMemberOptions{ AccessLevel: &accessLevelId, ExpiresAt: &expiresAt, } if !data.MemberRoleID.IsNull() && !data.MemberRoleID.IsUnknown() { options.MemberRoleID = gitlab.Ptr(int(data.MemberRoleID.ValueInt64())) } tflog.Debug(ctx, fmt.Sprintf("[DEBUG] update gitlab group membership %v for %v", userId, groupId)) groupMember, _, err := d.client.GroupMembers.EditGroupMember(groupId, userId, &options, gitlab.WithContext(ctx)) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error updating GitLab group membership", fmt.Sprintf("Error updating GitLab group membership: %v", err))) return } resp.Diagnostics.Append(data.groupMembershipToStateModel(groupId, groupMember)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (d *gitlabGroupMembershipResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data gitlabGroupMembershipResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } id := data.ID.ValueString() groupIdString, userIdString, err := utils.ParseTwoPartID(id) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error getting group and user ID from resource ID", fmt.Sprintf("Error getting group and user ID from resource ID: %v", err))) return } groupId, err := strconv.Atoi(groupIdString) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error converting group ID to int", fmt.Sprintf("Error converting group ID to int: %v", err))) return } userId, err := strconv.Atoi(userIdString) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error converting user ID to int", fmt.Sprintf("Error converting user ID to int: %v", err))) return } options := gitlab.RemoveGroupMemberOptions{ SkipSubresources: gitlab.Ptr(data.SkipSubresourcesOnDestroy.ValueBool()), UnassignIssuables: gitlab.Ptr(data.UnassignIssuablesOnDestroy.ValueBool()), } tflog.Debug(ctx, fmt.Sprintf("[DEBUG] Delete gitlab group membership %v for %v with options: %+v", userId, groupId, options)) _, err = d.client.GroupMembers.RemoveGroupMember(groupId, userId, &options, gitlab.WithContext(ctx)) if err != nil { resp.Diagnostics.Append(diag.NewErrorDiagnostic("Error deleting GitLab group membership", fmt.Sprintf("Error deleting GitLab group membership: %v", err))) return } } func (data *gitlabGroupMembershipResourceModel) groupMembershipToStateModel(groupId int, groupMember *gitlab.GroupMember) diag.Diagnostics { groupIDString := strconv.Itoa(groupId) userIDString := strconv.Itoa(groupMember.ID) data.ID = types.StringValue(utils.BuildTwoPartID(&groupIDString, &userIDString)) data.GroupID = types.Int64Value(int64(groupId)) data.UserID = types.Int64Value(int64(groupMember.ID)) data.AccessLevel = types.StringValue(api.AccessLevelValueToName[groupMember.AccessLevel]) if groupMember.MemberRole != nil { data.MemberRoleID = types.Int64Value(int64(groupMember.MemberRole.ID)) } else { data.MemberRoleID = types.Int64Null() } if groupMember.ExpiresAt != nil { data.ExpiresAt = types.StringValue(groupMember.ExpiresAt.String()) } else { data.ExpiresAt = types.StringNull() } if data.SkipSubresourcesOnDestroy.IsNull() { data.SkipSubresourcesOnDestroy = types.BoolValue(false) } if data.UnassignIssuablesOnDestroy.IsNull() { data.UnassignIssuablesOnDestroy = types.BoolValue(false) } return nil }