diff --git a/CHANGELOG.md b/CHANGELOG.md index 90716a5..bf53df6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,13 @@ All notable changes to this project will be documented in this file. -## 1.0.0 +## 1.1.0 - Unreleased + +### Added + +- Added repository label resource. + +## 1.0.0 - 2025-05-27 ### Added diff --git a/docs/resources/repository_label.md b/docs/resources/repository_label.md new file mode 100644 index 0000000..31fcf3c --- /dev/null +++ b/docs/resources/repository_label.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "forgejo_repository_label Resource - terraform-provider-forgejo" +subcategory: "" +description: |- + Use this resource to create and manage a repository label. +--- + +# forgejo_repository_label (Resource) + +Use this resource to create and manage a repository label. + +## Example Usage + +```terraform +resource "forgejo_repository_label" "main" { + color = "0000ff" + description = "blue label" + name = "test" + owner = "adyxax" + repository = "example" +} +``` + + +## Schema + +### Required + +- `color` (String) The label's color in lowercase rgb format, without a leading `#`. For example `207de5`. +- `description` (String) A description string. +- `name` (String) The label's name. +- `owner` (String) The variable's owner. +- `repository` (String) The label's repository. + +### Optional + +- `exclusive` (Boolean) Whether the label is exclusive or not. Defaults to `false`. Name the label `scope/item` to make it mutually exclusive with other `scope/` labels. +- `is_archived` (Boolean) Whether the repository label is archived or not. Defaults to `false` + +### Read-Only + +- `id` (Number) The identifier of the repository label. +- `url` (String) The repository label's URL. diff --git a/examples/resources/forgejo_repository_label/resource.tf b/examples/resources/forgejo_repository_label/resource.tf new file mode 100644 index 0000000..5f86272 --- /dev/null +++ b/examples/resources/forgejo_repository_label/resource.tf @@ -0,0 +1,7 @@ +resource "forgejo_repository_label" "main" { + color = "0000ff" + description = "blue label" + name = "test" + owner = "adyxax" + repository = "example" +} diff --git a/internal/client/repository_labels.go b/internal/client/repository_labels.go new file mode 100644 index 0000000..84bfb2f --- /dev/null +++ b/internal/client/repository_labels.go @@ -0,0 +1,64 @@ +package client + +import ( + "context" + "fmt" + "net/url" + "path" + "strconv" +) + +type RepositoryLabel struct { + Color string `json:"color"` + Description string `json:"description"` + Exclusive bool `json:"exclusive"` + Id int64 `json:"id"` + IsArchived bool `json:"is_archived"` + Name string `json:"name"` + Url string `json:"url"` +} + +type RepositoryLabelCreateRequest struct { + Color string `json:"color"` + Description string `json:"description"` + Exclusive bool `json:"exclusive"` + IsArchived bool `json:"is_archived"` + Name string `json:"name"` +} + +type RepositoryLabelUpdateRequest = RepositoryLabelCreateRequest + +func (c *Client) RepositoryLabelCreate(ctx context.Context, owner string, repo string, payload *RepositoryLabelCreateRequest) (*RepositoryLabel, error) { + uriRef := url.URL{Path: path.Join("api/v1/repos", owner, repo, "labels")} + response := RepositoryLabel{} + if _, err := c.send(ctx, "POST", &uriRef, payload, &response); err != nil { + return nil, fmt.Errorf("failed to create repository label: %w", err) + } + return &response, nil +} + +func (c *Client) RepositoryLabelDelete(ctx context.Context, owner string, repo string, id int64) error { + uriRef := url.URL{Path: path.Join("api/v1/repos", owner, repo, "labels", strconv.Itoa(int(id)))} + if _, err := c.send(ctx, "DELETE", &uriRef, nil, nil); err != nil { + return fmt.Errorf("failed to delete repository label: %w", err) + } + return nil +} + +func (c *Client) RepositoryLabelGet(ctx context.Context, owner string, repo string, id int64) (*RepositoryLabel, error) { + uriRef := url.URL{Path: path.Join("api/v1/repos", owner, repo, "labels", strconv.Itoa(int(id)))} + response := RepositoryLabel{} + if _, err := c.send(ctx, "GET", &uriRef, nil, &response); err != nil { + return nil, fmt.Errorf("failed to get repository label: %w", err) + } + return &response, nil +} + +func (c *Client) RepositoryLabelUpdate(ctx context.Context, owner string, repo string, id int64, payload *RepositoryLabelUpdateRequest) (*RepositoryLabel, error) { + uriRef := url.URL{Path: path.Join("api/v1/repos", owner, repo, "labels", strconv.Itoa(int(id)))} + response := RepositoryLabel{} + if _, err := c.send(ctx, "PATCH", &uriRef, payload, &response); err != nil { + return nil, fmt.Errorf("failed to update repository label: %w", err) + } + return &response, nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c27ed6e..0089c90 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -89,6 +89,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewRepositoryActionsSecretResource, NewRepositoryActionsVariableResource, + NewRepositoryLabelResource, NewRepositoryPushMirrorResource, } } diff --git a/internal/provider/repository_label_resource.go b/internal/provider/repository_label_resource.go new file mode 100644 index 0000000..bc787a2 --- /dev/null +++ b/internal/provider/repository_label_resource.go @@ -0,0 +1,195 @@ +package provider + +import ( + "context" + "fmt" + + "git.adyxax.org/adyxax/terraform-provider-forgejo/internal/client" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type RepositoryLabelResource struct { + client *client.Client +} + +var _ resource.Resource = &RepositoryLabelResource{} // Ensure provider defined types fully satisfy framework interfaces +func NewRepositoryLabelResource() resource.Resource { + return &RepositoryLabelResource{} +} + +type RepositoryLabelResourceModel struct { + Color types.String `tfsdk:"color"` + Description types.String `tfsdk:"description"` + Exclusive types.Bool `tfsdk:"exclusive"` + Id types.Int64 `tfsdk:"id"` + IsArchived types.Bool `tfsdk:"is_archived"` + Name types.String `tfsdk:"name"` + Owner types.String `tfsdk:"owner"` + Repository types.String `tfsdk:"repository"` + Url types.String `tfsdk:"url"` +} + +func (d *RepositoryLabelResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_repository_label" +} + +func (d *RepositoryLabelResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "color": schema.StringAttribute{ + MarkdownDescription: "The label's color in lowercase rgb format, without a leading `#`. For example `207de5`.", + Required: true, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "A description string.", + Required: true, + }, + "exclusive": schema.BoolAttribute{ + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Whether the label is exclusive or not. Defaults to `false`. Name the label `scope/item` to make it mutually exclusive with other `scope/` labels.", + Optional: true, + }, + "id": schema.Int64Attribute{ + Computed: true, + MarkdownDescription: "The identifier of the repository label.", + }, + "is_archived": schema.BoolAttribute{ + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Whether the repository label is archived or not. Defaults to `false`", + Optional: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The label's name.", + Required: true, + }, + "owner": schema.StringAttribute{ + MarkdownDescription: "The variable's owner.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Required: true, + }, + "repository": schema.StringAttribute{ + MarkdownDescription: "The label's repository.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Required: true, + }, + "url": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The repository label's URL.", + }, + }, + MarkdownDescription: "Use this resource to create and manage a repository label.", + } +} + +func (d *RepositoryLabelResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + d.client, _ = req.ProviderData.(*client.Client) +} + +func (d *RepositoryLabelResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RepositoryLabelResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + request := client.RepositoryLabelCreateRequest{ + Color: data.Color.ValueString(), + Description: data.Description.ValueString(), + Exclusive: data.Exclusive.ValueBool(), + IsArchived: data.IsArchived.ValueBool(), + Name: data.Name.ValueString(), + } + label, err := d.client.RepositoryLabelCreate( + ctx, + data.Owner.ValueString(), + data.Repository.ValueString(), + &request) + if err != nil { + resp.Diagnostics.AddError("CreateRepositoryLabel", fmt.Sprintf("failed to create repository label: %s", err)) + return + } + data.Id = types.Int64Value(label.Id) + data.Url = types.StringValue(label.Url) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *RepositoryLabelResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data RepositoryLabelResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + err := d.client.RepositoryLabelDelete( + ctx, + data.Owner.ValueString(), + data.Repository.ValueString(), + data.Id.ValueInt64()) + if err != nil { + resp.Diagnostics.AddError("DeleteRepositoryLabel", fmt.Sprintf("failed to delete repository label: %s", err)) + return + } +} + +func (d *RepositoryLabelResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data RepositoryLabelResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + label, err := d.client.RepositoryLabelGet( + ctx, + data.Owner.ValueString(), + data.Repository.ValueString(), + data.Id.ValueInt64()) + if err != nil { + resp.Diagnostics.AddError("ReadRepositoryLabel", fmt.Sprintf("failed to get repository label: %s", err)) + return + } + data.Color = types.StringValue(label.Color) + data.Description = types.StringValue(label.Description) + data.Exclusive = types.BoolValue(label.Exclusive) + data.IsArchived = types.BoolValue(label.IsArchived) + data.Name = types.StringValue(label.Name) + data.Url = types.StringValue(label.Url) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *RepositoryLabelResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plannedData RepositoryLabelResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plannedData)...) + var stateData RepositoryLabelResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...) + if resp.Diagnostics.HasError() { + return + } + request := client.RepositoryLabelUpdateRequest{ + Color: plannedData.Color.ValueString(), + Description: plannedData.Description.ValueString(), + Exclusive: plannedData.Exclusive.ValueBool(), + IsArchived: plannedData.IsArchived.ValueBool(), + Name: plannedData.Name.ValueString(), + } + label, err := d.client.RepositoryLabelUpdate( + ctx, + plannedData.Owner.ValueString(), + plannedData.Repository.ValueString(), + stateData.Id.ValueInt64(), + &request) + if err != nil { + resp.Diagnostics.AddError("UpdateRepositoryLabel", fmt.Sprintf("failed to update repository label: %s", err)) + return + } + plannedData.Id = types.Int64Value(label.Id) + plannedData.Url = types.StringValue(label.Url) + resp.Diagnostics.Append(resp.State.Set(ctx, &plannedData)...) +}