internal/provider/resource_gitlab_user_identity.go (174 lines of code) (raw):

package provider import ( "context" "fmt" "strconv" "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/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/types" 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 = &gitlabUserIdentityResource{} _ resource.ResourceWithConfigure = &gitlabUserIdentityResource{} _ resource.ResourceWithImportState = &gitlabUserIdentityResource{} ) func init() { registerResource(NewGitlabUserIdentityResource) } // NewGitlabUserIdentityResource is a helper function to simplify the provider implementation. func NewGitlabUserIdentityResource() resource.Resource { return &gitlabUserIdentityResource{} } // gitlabUserIdentityResourceModel describes the resource data model. type gitlabUserIdentityResourceModel struct { ID types.String `tfsdk:"id"` UserID types.Int64 `tfsdk:"user_id"` ExternalProvider types.String `tfsdk:"external_provider"` ExternalUID types.String `tfsdk:"external_uid"` } // gitlabUserIdentityResource defines the resource implementation type gitlabUserIdentityResource struct { client *gitlab.Client } func (r *gitlabUserIdentityResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_user_identity" } func (r *gitlabUserIdentityResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: `The ` + "`gitlab_user_identity`" + ` resource is for managing the lifecycle of a user's external identity. -> the provider needs to be configured with admin-level access for this resource to work. **Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/api/users/)`, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "The ID of this Terraform resource. In the format <user-id:external-provider>", Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "user_id": schema.Int64Attribute{ MarkdownDescription: "The GitLab ID of the user.", Required: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}, }, "external_provider": schema.StringAttribute{ MarkdownDescription: "The external provider name.", Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "external_uid": schema.StringAttribute{ MarkdownDescription: "A specific external authentication provider UID.", Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, }, } } // Configure adds the provider configured client to the resource. func (r *gitlabUserIdentityResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return } resourceData := req.ProviderData.(*GitLabResourceData) r.client = resourceData.Client } // ImportState imports the resource into the Terraform state. func (r *gitlabUserIdentityResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } // Create creates a new upstream resources and adds it into the Terraform state. func (r *gitlabUserIdentityResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data *gitlabUserIdentityResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } options := &gitlab.ModifyUserOptions{ ExternUID: data.ExternalUID.ValueStringPointer(), Provider: data.ExternalProvider.ValueStringPointer(), } user, _, err := r.client.Users.ModifyUser(int(data.UserID.ValueInt64()), options, gitlab.WithContext(ctx)) if err != nil { resp.Diagnostics.AddError("GitLab API error occured", fmt.Sprintf("Unable to create user identity: %s", err.Error())) return } err = data.modelToStateModel(user.ID, data.ExternalProvider.ValueString(), user.Identities) if err != nil { resp.Diagnostics.AddError("User identity not found", err.Error()) return } // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } // Read refreshes the Terraform state with the latest data. func (r *gitlabUserIdentityResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data *gitlabUserIdentityResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } userID, provider, err := resourceGitlabUserIdentityParseID(data.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Invalid ID", fmt.Sprintf("Unable to parse ID: %s", err.Error())) return } user, _, err := r.client.Users.GetUser(int(userID), gitlab.GetUsersOptions{}, gitlab.WithContext(ctx)) if err != nil { if api.Is404(err) { resp.Diagnostics.AddWarning("User not found", fmt.Sprintf("[DEBUG] user %d not found so removing from state", userID)) resp.State.RemoveResource(ctx) return } resp.Diagnostics.AddError("GitLab API error occurred", fmt.Sprintf("Unable to read user %d: %s", userID, err.Error())) return } err = data.modelToStateModel(userID, provider, user.Identities) if err != nil { resp.Diagnostics.AddWarning("User identity not found, removing from state", err.Error()) resp.State.RemoveResource(ctx) return } // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } // All attributes require resource to be deleting and recreated on change, so this should never be called. func (r *gitlabUserIdentityResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { resp.Diagnostics.AddError("Provider Error, report upstream", "Somehow the resource was requested to perform an in-place update which is not possible.") } // Deletes removes the resource. func (r *gitlabUserIdentityResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data *gitlabUserIdentityResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } userID, provider, err := resourceGitlabUserIdentityParseID(data.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Invalid ID", fmt.Sprintf("Unable to parse ID: %s", err.Error())) return } _, err = r.client.Users.DeleteUserIdentity(userID, provider, gitlab.WithContext(ctx)) if err != nil { resp.Diagnostics.AddError("GitLab API error occurred", fmt.Sprintf("Unable to delete user identity provider: %s", err.Error())) return } resp.State.RemoveResource(ctx) } func (data *gitlabUserIdentityResourceModel) modelToStateModel(userID int, provider string, identities []*gitlab.UserIdentity) error { userIDStr := strconv.Itoa(userID) // Find the added identity in the return user identities for _, identity := range identities { if identity.Provider == provider { data.ID = types.StringValue(utils.BuildTwoPartID(&userIDStr, &identity.Provider)) data.UserID = types.Int64Value(int64(userID)) data.ExternalProvider = types.StringValue(identity.Provider) data.ExternalUID = types.StringValue(identity.ExternUID) return nil } } return fmt.Errorf("Unable to find user identity %s", data.ID.ValueString()) } // Parse resource ID into user ID and provider. func resourceGitlabUserIdentityParseID(id string) (int, string, error) { userIDStr, provider, err := utils.ParseTwoPartID(id) if err != nil { return 0, "", err } userID, err := strconv.Atoi(userIDStr) if err != nil { return 0, "", err } return userID, provider, nil }