From 7d80bf1f63ca94bb64401e4d270b985108dd9431 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Sun, 25 May 2025 17:27:59 +0200 Subject: [PATCH] feat(provider): add repository push mirror resource Closes #12 --- CHANGELOG.md | 1 + docs/resources/repository_push_mirror.md | 45 +++++ .../resource.tf | 7 + internal/client/repository_push_mirrors.go | 73 +++++++ internal/provider/provider.go | 1 + .../repository_push_mirrors_resource.go | 191 ++++++++++++++++++ 6 files changed, 318 insertions(+) create mode 100644 docs/resources/repository_push_mirror.md create mode 100644 examples/resources/forgejo_repository_push_mirror/resource.tf create mode 100644 internal/client/repository_push_mirrors.go create mode 100644 internal/provider/repository_push_mirrors_resource.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b07dbe..ed9b082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,5 +11,6 @@ All notable changes to this project will be documented in this file. - Added repositories data-source. - Added repository actions secret resource. - Added repository actions variable resource. +- Added repository push mirror resource. - Added teams data-source. - Added users data-source. diff --git a/docs/resources/repository_push_mirror.md b/docs/resources/repository_push_mirror.md new file mode 100644 index 0000000..9175a4d --- /dev/null +++ b/docs/resources/repository_push_mirror.md @@ -0,0 +1,45 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "forgejo_repository_push_mirror Resource - terraform-provider-forgejo" +subcategory: "" +description: |- + Use this resource to create and manage a repository push mirror. +--- + +# forgejo_repository_push_mirror (Resource) + +Use this resource to create and manage a repository push mirror. + +## Example Usage + +```terraform +resource "forgejo_repository_push_mirror" "main" { + owner = "adyxax" + remote_address = "https://github.com/adyxax/tfstated" + remote_password = "secret" + remote_username = "adyxax" + repository = "example" +} +``` + + +## Schema + +### Required + +- `owner` (String) The owner of the repository on which to configure a push mirror. +- `remote_address` (String) The push mirror's remote address. +- `repository` (String) The repository on which to configure a push mirror. + +### Optional + +- `interval` (String) The push mirror's sync interval as a string. Defaults to `8h0m0s`. +- `remote_password` (String, Sensitive) The push mirror's remote password. +- `remote_username` (String) The push mirror's remote username. +- `sync_on_commit` (Boolean) Whether the push mirror is synced on each commit pushed to the repository, defaults to `true`. +- `use_ssh` (Boolean) Whether the push mirror is synced over SSH or not (not meaning HTTP), defaults to `false`. + +### Read-Only + +- `created` (String) The push mirror's creation date and time. +- `name` (String) The name of the push mirror. diff --git a/examples/resources/forgejo_repository_push_mirror/resource.tf b/examples/resources/forgejo_repository_push_mirror/resource.tf new file mode 100644 index 0000000..163150c --- /dev/null +++ b/examples/resources/forgejo_repository_push_mirror/resource.tf @@ -0,0 +1,7 @@ +resource "forgejo_repository_push_mirror" "main" { + owner = "adyxax" + remote_address = "https://github.com/adyxax/tfstated" + remote_password = "secret" + remote_username = "adyxax" + repository = "example" +} diff --git a/internal/client/repository_push_mirrors.go b/internal/client/repository_push_mirrors.go new file mode 100644 index 0000000..f7442af --- /dev/null +++ b/internal/client/repository_push_mirrors.go @@ -0,0 +1,73 @@ +package client + +import ( + "context" + "fmt" + "net/url" + "path" + "time" +) + +type RepositoryPushMirror struct { + Created time.Time `json:"created"` + Interval string `json:"interval"` + LastError string `json:"last_error"` + LastUpdate time.Time `json:"last_update"` + PublicKey string `json:"public_key"` + RemoteAddress string `json:"remote_address"` + RemoteName string `json:"remote_name"` + RepoName string `json:"repo_name"` + SyncOnCommit bool `json:"sync_on_commit"` +} + +func (c *Client) RepositoryPushMirrorCreate(ctx context.Context, + owner string, + repo string, + interval string, + remoteAddress string, + remotePassword string, + remoteUsername string, + syncOnCommit bool, + useSsh bool, +) (*RepositoryPushMirror, error) { + uriRef := url.URL{Path: path.Join("api/v1/repos", owner, repo, "push_mirrors")} + type Payload struct { + Interval string `json:"interval"` + RemoteAddress string `json:"remote_address"` + RemotePassword string `json:"remote_password"` + RemoteUsername string `json:"remote_username"` + SyncOnCommit bool `json:"sync_on_commit"` + UseSsh bool `json:"use_ssh"` + } + payload := Payload{ + Interval: interval, + RemoteAddress: remoteAddress, + RemotePassword: remotePassword, + RemoteUsername: remoteUsername, + SyncOnCommit: syncOnCommit, + UseSsh: useSsh, + } + response := RepositoryPushMirror{} + if _, err := c.send(ctx, "POST", &uriRef, &payload, &response); err != nil { + return nil, fmt.Errorf("failed to create repository push mirror: %w", err) + } + return &response, nil +} + +func (c *Client) RepositoryPushMirrorDelete(ctx context.Context, owner string, repo string, name string) (*RepositoryPushMirror, error) { + uriRef := url.URL{Path: path.Join("api/v1/repos", owner, repo, "push_mirrors", name)} + response := RepositoryPushMirror{} + if _, err := c.send(ctx, "DELETE", &uriRef, nil, &response); err != nil { + return nil, fmt.Errorf("failed to delete repository push mirror: %w", err) + } + return &response, nil +} + +func (c *Client) RepositoryPushMirrorGet(ctx context.Context, owner string, repo string, name string) (*RepositoryPushMirror, error) { + uriRef := url.URL{Path: path.Join("api/v1/repos", owner, repo, "push_mirrors", name)} + response := RepositoryPushMirror{} + if _, err := c.send(ctx, "GET", &uriRef, nil, &response); err != nil { + return nil, fmt.Errorf("failed to get repository push mirrors: %w", err) + } + return &response, nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 88fcb1b..f47cf50 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -78,6 +78,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewRepositoryActionsSecretResource, NewRepositoryActionsVariableResource, + NewRepositoryPushMirrorResource, } } diff --git a/internal/provider/repository_push_mirrors_resource.go b/internal/provider/repository_push_mirrors_resource.go new file mode 100644 index 0000000..1aa666f --- /dev/null +++ b/internal/provider/repository_push_mirrors_resource.go @@ -0,0 +1,191 @@ +package provider + +import ( + "context" + "fmt" + + "git.adyxax.org/adyxax/terraform-provider-forgejo/internal/client" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type RepositoryPushMirrorResource struct { + client *client.Client +} + +var _ resource.Resource = &RepositoryPushMirrorResource{} // Ensure provider defined types fully satisfy framework interfaces +func NewRepositoryPushMirrorResource() resource.Resource { + return &RepositoryPushMirrorResource{} +} + +type RepositoryPushMirrorResourceModel struct { + Created timetypes.RFC3339 `tfsdk:"created"` + Interval types.String `tfsdk:"interval"` + Name types.String `tfsdk:"name"` + Owner types.String `tfsdk:"owner"` + RemoteAddress types.String `tfsdk:"remote_address"` + RemotePassword types.String `tfsdk:"remote_password"` + RemoteUsername types.String `tfsdk:"remote_username"` + Repository types.String `tfsdk:"repository"` + SyncOnCommit types.Bool `tfsdk:"sync_on_commit"` + UseSsh types.Bool `tfsdk:"use_ssh"` +} + +func (d *RepositoryPushMirrorResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_repository_push_mirror" +} + +func (d *RepositoryPushMirrorResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "created": schema.StringAttribute{ + Computed: true, + CustomType: timetypes.RFC3339Type{}, + MarkdownDescription: "The push mirror's creation date and time.", + }, + "interval": schema.StringAttribute{ + Computed: true, + Default: stringdefault.StaticString("8h0m0s"), + MarkdownDescription: "The push mirror's sync interval as a string. Defaults to `8h0m0s`.", + Optional: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the push mirror.", + }, + "owner": schema.StringAttribute{ + MarkdownDescription: "The owner of the repository on which to configure a push mirror.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Required: true, + }, + "remote_address": schema.StringAttribute{ + MarkdownDescription: "The push mirror's remote address.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Required: true, + }, + "remote_password": schema.StringAttribute{ + MarkdownDescription: "The push mirror's remote password.", + Optional: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Sensitive: true, + }, + "remote_username": schema.StringAttribute{ + MarkdownDescription: "The push mirror's remote username.", + Optional: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "repository": schema.StringAttribute{ + MarkdownDescription: "The repository on which to configure a push mirror.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Required: true, + }, + "sync_on_commit": schema.BoolAttribute{ + Computed: true, + Default: booldefault.StaticBool(true), + MarkdownDescription: "Whether the push mirror is synced on each commit pushed to the repository, defaults to `true`.", + Optional: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()}, + }, + "use_ssh": schema.BoolAttribute{ + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Whether the push mirror is synced over SSH or not (not meaning HTTP), defaults to `false`.", + Optional: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()}, + }, + }, + MarkdownDescription: "Use this resource to create and manage a repository push mirror.", + } +} + +func (d *RepositoryPushMirrorResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + d.client, _ = req.ProviderData.(*client.Client) +} + +func (d *RepositoryPushMirrorResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RepositoryPushMirrorResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + pushMirror, err := d.client.RepositoryPushMirrorCreate( + ctx, + data.Owner.ValueString(), + data.Repository.ValueString(), + data.Interval.ValueString(), + data.RemoteAddress.ValueString(), + data.RemotePassword.ValueString(), + data.RemoteUsername.ValueString(), + data.SyncOnCommit.ValueBool(), + data.UseSsh.ValueBool()) + if err != nil { + resp.Diagnostics.AddError("CreateRepositoryPushMirror", fmt.Sprintf("failed to create repository push mirror: %s", err)) + return + } + data.Created = timetypes.NewRFC3339TimeValue(pushMirror.Created) + data.Name = types.StringValue(pushMirror.RemoteName) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *RepositoryPushMirrorResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data RepositoryPushMirrorResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + _, err := d.client.RepositoryPushMirrorDelete( + ctx, + data.Owner.ValueString(), + data.Repository.ValueString(), + data.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError("DeleteRepositoryPushMirror", fmt.Sprintf("failed to delete repository push mirror: %s", err)) + return + } +} + +func (d *RepositoryPushMirrorResource) getRepositoryPushMirror( + ctx context.Context, + owner types.String, + repository types.String, + name types.String, +) (*client.RepositoryPushMirror, error) { + pushMirror, err := d.client.RepositoryPushMirrorGet( + ctx, + owner.ValueString(), + repository.ValueString(), + name.ValueString(), + ) + if err != nil { + return nil, fmt.Errorf("failed to get repository push mirror: %w", err) + } + return pushMirror, nil +} + +func (d *RepositoryPushMirrorResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data RepositoryPushMirrorResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + pushMirror, err := d.getRepositoryPushMirror(ctx, data.Owner, data.Repository, data.Name) + if err != nil { + resp.Diagnostics.AddError("ReadRepositoryPushMirror", fmt.Sprintf("failed to get repository push mirror: %s", err)) + return + } + data.Created = timetypes.NewRFC3339TimeValue(pushMirror.Created) + data.Name = types.StringValue(pushMirror.RemoteName) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *RepositoryPushMirrorResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("UpdateRepositoryPushMirror", fmt.Sprintf("unreachable code")) +}