internal/provider/resource_gitlab_group_label.go (253 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/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "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" ) var ( _ resource.Resource = &gitlabGroupLabelResource{} _ resource.ResourceWithConfigure = &gitlabGroupLabelResource{} _ resource.ResourceWithImportState = &gitlabGroupLabelResource{} _ resource.ResourceWithUpgradeState = &gitlabGroupLabelResource{} ) func init() { registerResource(NewGitLabGroupLabelResource) } func NewGitLabGroupLabelResource() resource.Resource { return &gitlabGroupLabelResource{} } type gitlabGroupLabelResourceModel struct { ID types.String `tfsdk:"id"` LabelID types.Int64 `tfsdk:"label_id"` Group types.String `tfsdk:"group"` Name types.String `tfsdk:"name"` Color types.String `tfsdk:"color"` Description types.String `tfsdk:"description"` } type gitlabGroupLabelResource struct { client *gitlab.Client } func (r *gitlabGroupLabelResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_group_label" } func (r *gitlabGroupLabelResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = r.getV0Schema() } func (r *gitlabGroupLabelResource) 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 } func (r *gitlabGroupLabelResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } func (r *gitlabGroupLabelResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data *gitlabGroupLabelResourceModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } group := data.Group.ValueString() options := &gitlab.CreateGroupLabelOptions{ Name: gitlab.Ptr(data.Name.ValueString()), Color: gitlab.Ptr(data.Color.ValueString()), } if !data.Description.IsNull() && !data.Description.IsUnknown() { options.Description = gitlab.Ptr(data.Description.ValueString()) } label, _, err := r.client.GroupLabels.CreateGroupLabel(group, options, gitlab.WithContext(ctx)) if err != nil { resp.Diagnostics.AddError("GitLab API error occured", fmt.Sprintf("Unable to create group label: %s", err.Error())) return } labelID := strconv.Itoa(label.ID) data.ID = types.StringValue(utils.BuildTwoPartID(&group, &labelID)) data.modelToStateModel(label, group) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *gitlabGroupLabelResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data *gitlabGroupLabelResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } group, labelID, err := data.ResourceGitlabGroupLabelParseID(data.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to read resource ID", fmt.Sprintf("Unable to parse resource ID: %s, %s", data.ID.ValueString(), err.Error())) return } label, _, err := r.client.GroupLabels.GetGroupLabel(group, labelID, gitlab.WithContext(ctx)) if err != nil { if api.Is404(err) { resp.Diagnostics.AddWarning("GitLab API error occured", fmt.Sprintf("Group label doesn't exist anymore, removing from state: %s", err.Error())) resp.State.RemoveResource(ctx) return } resp.Diagnostics.AddError("GitLab API error occured", fmt.Sprintf("Unable to get group label: %s", err.Error())) return } data.modelToStateModel(label, group) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *gitlabGroupLabelResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var data *gitlabGroupLabelResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } group, labelID, err := data.ResourceGitlabGroupLabelParseID(data.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to read resource ID", fmt.Sprintf("Unable to parse resource ID: %s, %s", data.ID.ValueString(), err.Error())) return } options := &gitlab.UpdateGroupLabelOptions{ NewName: gitlab.Ptr(data.Name.ValueString()), Color: gitlab.Ptr(data.Color.ValueString()), } if !data.Description.IsNull() && !data.Description.IsUnknown() { options.Description = gitlab.Ptr(data.Description.ValueString()) } label, _, err := r.client.GroupLabels.UpdateGroupLabel(group, labelID, options, gitlab.WithContext(ctx)) if err != nil { resp.Diagnostics.AddError("GitLab API error occured", fmt.Sprintf("Unable to update group label: %s", err.Error())) return } data.modelToStateModel(label, group) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *gitlabGroupLabelResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data *gitlabGroupLabelResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } group, labelID, err := data.ResourceGitlabGroupLabelParseID(data.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to read resource ID", fmt.Sprintf("Unable to parse resource ID: %s, %s", data.ID.ValueString(), err.Error())) return } options := &gitlab.DeleteGroupLabelOptions{ Name: gitlab.Ptr(data.Name.ValueString()), } _, err = r.client.GroupLabels.DeleteGroupLabel(group, labelID, options, gitlab.WithContext(ctx)) if err != nil { resp.Diagnostics.AddError("GitLab API error occured", fmt.Sprintf("Unable to delete group label: %s", err.Error())) return } resp.State.RemoveResource(ctx) } // All UpgradeState upgraders in the provider should be registered here, and must upgrade to the current state. // That means currently all upgraders much upgrade fully to V2. func (r *gitlabGroupLabelResource) UpgradeState(context.Context) map[int64]resource.StateUpgrader { schema := r.getV0Schema() // Both v0 and v1 were only ID changes, so they use the same upgrader function. Create that function here. upgraderFunction := func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { var data gitlabGroupLabelResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } err := r.upgradeIdToV2Id(ctx, &data) if err != nil { tflog.Error(ctx, "Failed to upgrade resource ID", map[string]any{ "oldId": data.ID.ValueString(), "group": data.Group.ValueString(), }) resp.Diagnostics.AddError("Failed to upgrade resource ID", fmt.Sprintf("Unable to upgrade resource ID: %s, %s", data.ID.ValueString(), err.Error())) return } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } return map[int64]resource.StateUpgrader{ 0: { PriorSchema: &schema, // The only difference between V0 and V1 is the "id" attribute, which sets the ID to "groupId:labelId" StateUpgrader: upgraderFunction, }, 1: { PriorSchema: &schema, // The only difference between V0 and V2 is the "id" attribute, which sets the ID to "groupId:labelId" StateUpgrader: upgraderFunction, }, } } // This function accepts an input state and will upgrde the "id" attribute to the new V2 ID that is <group_id>:<label_id> func (r *gitlabGroupLabelResource) upgradeIdToV2Id(ctx context.Context, input *gitlabGroupLabelResourceModel) error { // Check if LabelId is in state, and retrieve the value from the API if it isn't. if input.LabelID.IsNull() || input.LabelID.IsUnknown() { tflog.Debug(ctx, "Retrieving label ID from API") label, _, err := r.client.GroupLabels.GetGroupLabel(input.Group.ValueString(), input.ID.ValueString(), gitlab.WithContext(ctx)) if err != nil { return err } // set the value into state input.LabelID = types.Int64Value(int64(label.ID)) } // Saving the string to a variable first for readability reasons. stringLabelId := strconv.Itoa(int(input.LabelID.ValueInt64())) newId := utils.BuildTwoPartID(input.Group.ValueStringPointer(), gitlab.Ptr(stringLabelId)) tflog.Debug(ctx, "Upgrading state to the V2 ID", map[string]any{ "oldId": input.ID.ValueString(), "group": input.Group.ValueString(), "newId": newId, }) input.ID = types.StringValue(newId) return nil } // The V0 (and also V1) schema. This is used in both the Schema function and also the // UpgradeState function. func (r *gitlabGroupLabelResource) getV0Schema() schema.Schema { return schema.Schema{ MarkdownDescription: `The ` + "`gitlab_group_label`" + ` resource allows to manage the lifecycle of labels within a group. **Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/api/group_labels/)`, Version: 2, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "The ID of this Terraform resource. In the format of `<group-id>:<label-id>`.", Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "label_id": schema.Int64Attribute{ MarkdownDescription: "The id of the group label.", Computed: true, }, "group": schema.StringAttribute{ MarkdownDescription: "The name or id of the group to add the label to.", Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "name": schema.StringAttribute{ MarkdownDescription: "The name of the label.", Required: true, }, "color": schema.StringAttribute{ MarkdownDescription: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords).", Required: true, }, "description": schema.StringAttribute{ MarkdownDescription: "The description of the label.", Optional: true, Computed: true, }, }, } } func (r *gitlabGroupLabelResourceModel) modelToStateModel(l *gitlab.GroupLabel, group string) { r.LabelID = types.Int64Value(int64(l.ID)) r.Group = types.StringValue(group) r.Name = types.StringValue(l.Name) r.Color = types.StringValue(l.Color) r.Description = types.StringValue(l.Description) } func (d *gitlabGroupLabelResourceModel) ResourceGitlabGroupLabelParseID(id string) (string, int, error) { group, rawLabelId, err := utils.ParseTwoPartID(id) if err != nil { return "", 0, err } labelId, err := strconv.Atoi(rawLabelId) if err != nil { return "", 0, err } return group, labelId, nil }