diff --git a/internal/client/partners.go b/internal/client/partners.go new file mode 100644 index 0000000..a7ad814 --- /dev/null +++ b/internal/client/partners.go @@ -0,0 +1,96 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +type Partner struct { + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + InTimeline bool `json:"inTimeline"` + AvatarColor string `json:"avatarColor"` + ProfileImagePath string `json:"profileImagePath"` +} + +type UpdatePartnerRequest struct { + InTimeline bool `json:"inTimeline"` +} + +func (c *Client) GetPartners() ([]Partner, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/partners", c.HostURL), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var partners []Partner + err = json.Unmarshal(body, &partners) + if err != nil { + return nil, err + } + + return partners, nil +} + +func (c *Client) CreatePartner(id string) (*Partner, error) { + req, err := http.NewRequest("POST", fmt.Sprintf("%s/partners/%s", c.HostURL, id), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var partner Partner + err = json.Unmarshal(body, &partner) + if err != nil { + return nil, err + } + + return &partner, nil +} + +func (c *Client) UpdatePartner(id string, update UpdatePartnerRequest) (*Partner, error) { + rb, err := json.Marshal(update) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", fmt.Sprintf("%s/partners/%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 partner Partner + err = json.Unmarshal(body, &partner) + if err != nil { + return nil, err + } + + return &partner, nil +} + +func (c *Client) DeletePartner(id string) error { + req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/partners/%s", c.HostURL, id), nil) + if err != nil { + return err + } + + _, err = c.doRequest(req) + return err +} diff --git a/internal/provider/partner_resource.go b/internal/provider/partner_resource.go new file mode 100644 index 0000000..f3c04a5 --- /dev/null +++ b/internal/provider/partner_resource.go @@ -0,0 +1,196 @@ +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/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" + "github.com/immich-app/terraform-provider-immich/internal/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &partnerResource{} +var _ resource.ResourceWithImportState = &partnerResource{} + +func NewPartnerResource() resource.Resource { + return &partnerResource{} +} + +// partnerResource defines the resource implementation. +type partnerResource struct { + client *client.Client +} + +// partnerResourceModel describes the resource data model. +type partnerResourceModel struct { + PartnerId types.String `tfsdk:"partner_id"` + InTimeline types.Bool `tfsdk:"in_timeline"` + Email types.String `tfsdk:"email"` + Name types.String `tfsdk:"name"` +} + +func (r *partnerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_partner" +} + +func (r *partnerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manages an Immich partner connection.", + + Attributes: map[string]schema.Attribute{ + "partner_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The ID of the user to partner with.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "in_timeline": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + MarkdownDescription: "Whether the partner's assets should appear in your timeline.", + }, + "email": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Email of the partner user.", + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Name of the partner user.", + }, + }, + } +} + +func (r *partnerResource) 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 *partnerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data partnerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + partner, err := r.client.CreatePartner(data.PartnerId.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create partner, got error: %s", err)) + return + } + + // Update timeline visibility if different from default + if !data.InTimeline.IsNull() && data.InTimeline.ValueBool() == false { + _, err = r.client.UpdatePartner(data.PartnerId.ValueString(), client.UpdatePartnerRequest{ + InTimeline: false, + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update partner timeline visibility, got error: %s", err)) + return + } + } + + data.Email = types.StringValue(partner.Email) + data.Name = types.StringValue(partner.Name) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *partnerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data partnerResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + partners, err := r.client.GetPartners() + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read partners, got error: %s", err)) + return + } + + var found *client.Partner + for _, p := range partners { + if p.ID == data.PartnerId.ValueString() { + found = &p + break + } + } + + if found == nil { + resp.State.RemoveResource(ctx) + return + } + + data.Email = types.StringValue(found.Email) + data.Name = types.StringValue(found.Name) + data.InTimeline = types.BoolValue(found.InTimeline) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *partnerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data partnerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.UpdatePartner(data.PartnerId.ValueString(), client.UpdatePartnerRequest{ + InTimeline: data.InTimeline.ValueBool(), + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update partner, got error: %s", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *partnerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data partnerResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeletePartner(data.PartnerId.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete partner, got error: %s", err)) + return + } +} + +func (r *partnerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("partner_id"), req, resp) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b3d2289..f6fd56b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -94,6 +94,7 @@ func (p *immichProvider) Resources(ctx context.Context) []func() resource.Resour NewLibraryResource, NewActivityResource, NewPersonResource, + NewPartnerResource, } }