From a41c992260b7e9528769a38986906eb35013886b Mon Sep 17 00:00:00 2001 From: Russ Long Date: Mon, 1 Jun 2026 11:32:05 -0400 Subject: [PATCH] feat: implement tags api --- internal/client/tags.go | 124 +++++++++++++++++++++ internal/provider/provider.go | 1 + internal/provider/tag_resource.go | 172 ++++++++++++++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 internal/client/tags.go create mode 100644 internal/provider/tag_resource.go diff --git a/internal/client/tags.go b/internal/client/tags.go new file mode 100644 index 0000000..9821e96 --- /dev/null +++ b/internal/client/tags.go @@ -0,0 +1,124 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +type Tag struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` // OBJECT or USER + // Color string `json:"color,omitempty"` +} + +type CreateTagRequest struct { + Name string `json:"name"` + Type string `json:"type"` // OBJECT or USER +} + +type UpdateTagRequest struct { + Name string `json:"name"` +} + +func (c *Client) GetTags() ([]Tag, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/tag", c.HostURL), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var tags []Tag + err = json.Unmarshal(body, &tags) + if err != nil { + return nil, err + } + + return tags, nil +} + +func (c *Client) GetTag(id string) (*Tag, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/tag/%s", c.HostURL, id), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var tag Tag + err = json.Unmarshal(body, &tag) + if err != nil { + return nil, err + } + + return &tag, nil +} + +func (c *Client) CreateTag(tag CreateTagRequest) (*Tag, error) { + rb, err := json.Marshal(tag) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/tag", c.HostURL), bytes.NewBuffer(rb)) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var newTag Tag + err = json.Unmarshal(body, &newTag) + if err != nil { + return nil, err + } + + return &newTag, nil +} + +func (c *Client) UpdateTag(id string, tag UpdateTagRequest) (*Tag, error) { + rb, err := json.Marshal(tag) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", fmt.Sprintf("%s/tag/%s", c.HostURL, id), bytes.NewBuffer(rb)) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var updatedTag Tag + err = json.Unmarshal(body, &updatedTag) + if err != nil { + return nil, err + } + + return &updatedTag, nil +} + +func (c *Client) DeleteTag(id string) error { + req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/tag/%s", c.HostURL, id), nil) + if err != nil { + return err + } + + _, err = c.doRequest(req) + return err +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 0c66ff9..608bf44 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -97,6 +97,7 @@ func (p *immichProvider) Resources(ctx context.Context) []func() resource.Resour NewPartnerResource, NewMemoryResource, NewStackResource, + NewTagResource, } } diff --git a/internal/provider/tag_resource.go b/internal/provider/tag_resource.go new file mode 100644 index 0000000..f1e8e3c --- /dev/null +++ b/internal/provider/tag_resource.go @@ -0,0 +1,172 @@ +package provider + +import ( + "context" + "fmt" + + "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/immich-app/terraform-provider-immich/internal/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &tagResource{} +var _ resource.ResourceWithImportState = &tagResource{} + +func NewTagResource() resource.Resource { + return &tagResource{} +} + +// tagResource defines the resource implementation. +type tagResource struct { + client *client.Client +} + +// tagResourceModel describes the resource data model. +type tagResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` +} + +func (r *tagResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tag" +} + +func (r *tagResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manages an Immich tag.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique identifier for the tag.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Name of the tag.", + }, + "type": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Type of the tag (OBJECT or USER).", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *tagResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *tagResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data tagResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + createReq := client.CreateTagRequest{ + Name: data.Name.ValueString(), + Type: data.Type.ValueString(), + } + + tag, err := r.client.CreateTag(createReq) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create tag, got error: %s", err)) + return + } + + data.ID = types.StringValue(tag.ID) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *tagResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data tagResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + tag, err := r.client.GetTag(data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read tag, got error: %s", err)) + return + } + + data.Name = types.StringValue(tag.Name) + data.Type = types.StringValue(tag.Type) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *tagResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data tagResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + updateReq := client.UpdateTagRequest{ + Name: data.Name.ValueString(), + } + + _, err := r.client.UpdateTag(data.ID.ValueString(), updateReq) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update tag, got error: %s", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *tagResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data tagResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteTag(data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete tag, got error: %s", err)) + return + } +} + +func (r *tagResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +}