diff --git a/internal/client/stacks.go b/internal/client/stacks.go new file mode 100644 index 0000000..6808b95 --- /dev/null +++ b/internal/client/stacks.go @@ -0,0 +1,122 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +type Stack struct { + ID string `json:"id"` + PrimaryAssetId string `json:"primaryAssetId"` + // Assets []AssetResponseDto `json:"assets"` +} + +type CreateStackRequest struct { + AssetIds []string `json:"assetIds"` +} + +type UpdateStackRequest struct { + PrimaryAssetId string `json:"primaryAssetId"` +} + +func (c *Client) GetStacks() ([]Stack, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/stacks", c.HostURL), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var stacks []Stack + err = json.Unmarshal(body, &stacks) + if err != nil { + return nil, err + } + + return stacks, nil +} + +func (c *Client) GetStack(id string) (*Stack, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/stacks/%s", c.HostURL, id), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var stack Stack + err = json.Unmarshal(body, &stack) + if err != nil { + return nil, err + } + + return &stack, nil +} + +func (c *Client) CreateStack(stack CreateStackRequest) (*Stack, error) { + rb, err := json.Marshal(stack) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/stacks", c.HostURL), bytes.NewBuffer(rb)) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var newStack Stack + err = json.Unmarshal(body, &newStack) + if err != nil { + return nil, err + } + + return &newStack, nil +} + +func (c *Client) UpdateStack(id string, stack UpdateStackRequest) (*Stack, error) { + rb, err := json.Marshal(stack) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", fmt.Sprintf("%s/stacks/%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 updatedStack Stack + err = json.Unmarshal(body, &updatedStack) + if err != nil { + return nil, err + } + + return &updatedStack, nil +} + +func (c *Client) DeleteStack(id string) error { + req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/stacks/%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 80e7a93..0c66ff9 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -96,6 +96,7 @@ func (p *immichProvider) Resources(ctx context.Context) []func() resource.Resour NewPersonResource, NewPartnerResource, NewMemoryResource, + NewStackResource, } } diff --git a/internal/provider/stack_resource.go b/internal/provider/stack_resource.go new file mode 100644 index 0000000..84f022e --- /dev/null +++ b/internal/provider/stack_resource.go @@ -0,0 +1,191 @@ +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 = &stackResource{} +var _ resource.ResourceWithImportState = &stackResource{} + +func NewStackResource() resource.Resource { + return &stackResource{} +} + +// stackResource defines the resource implementation. +type stackResource struct { + client *client.Client +} + +// stackResourceModel describes the resource data model. +type stackResourceModel struct { + ID types.String `tfsdk:"id"` + PrimaryAssetId types.String `tfsdk:"primary_asset_id"` + AssetIds []types.String `tfsdk:"asset_ids"` +} + +func (r *stackResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_stack" +} + +func (r *stackResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manages an Immich asset stack.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique identifier for the stack.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "primary_asset_id": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "The ID of the primary asset in the stack.", + }, + "asset_ids": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + MarkdownDescription: "List of asset IDs to include in the stack. The first ID will be the primary asset by default.", + PlanModifiers: []planmodifier.List{ + // Creating a stack requires at least 2 assets. + // Updating assets in a stack might require different endpoints (Add/Remove). + // For simplicity, we'll use RequiresReplace if it's too complex to diff. + }, + }, + }, + } +} + +func (r *stackResource) 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 *stackResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data stackResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + assetIds := make([]string, len(data.AssetIds)) + for i, id := range data.AssetIds { + assetIds[i] = id.ValueString() + } + + createReq := client.CreateStackRequest{ + AssetIds: assetIds, + } + + stack, err := r.client.CreateStack(createReq) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create stack, got error: %s", err)) + return + } + + data.ID = types.StringValue(stack.ID) + data.PrimaryAssetId = types.StringValue(stack.PrimaryAssetId) + + // If primary_asset_id was explicitly set in plan and it's different from the first in asset_ids + if !data.PrimaryAssetId.IsNull() && data.PrimaryAssetId.ValueString() != stack.PrimaryAssetId { + _, err = r.client.UpdateStack(stack.ID, client.UpdateStackRequest{ + PrimaryAssetId: data.PrimaryAssetId.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to set primary asset, got error: %s", err)) + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *stackResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data stackResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + stack, err := r.client.GetStack(data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read stack, got error: %s", err)) + return + } + + data.PrimaryAssetId = types.StringValue(stack.PrimaryAssetId) + // assets are returned as objects, we'd need to map them back to IDs if we wanted to refresh asset_ids. + // For now, we'll keep what's in state for asset_ids. + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *stackResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data stackResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.UpdateStack(data.ID.ValueString(), client.UpdateStackRequest{ + PrimaryAssetId: data.PrimaryAssetId.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update stack, got error: %s", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *stackResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data stackResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteStack(data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete stack, got error: %s", err)) + return + } +} + +func (r *stackResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +}