diff --git a/docs/resources/indexer_torrent_rss.md b/docs/resources/indexer_torrent_rss.md new file mode 100644 index 00000000..4b207086 --- /dev/null +++ b/docs/resources/indexer_torrent_rss.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "radarr_indexer_torrent_rss Resource - terraform-provider-radarr" +subcategory: "Indexers" +description: |- + Indexer Torrent RSS resource. + For more information refer to Indexer https://wiki.servarr.com/radarr/settings#indexers and Torrent RSS https://wiki.servarr.com/radarr/supported#torrentrssindexer. +--- + +# radarr_indexer_torrent_rss (Resource) + +Indexer Torrent RSS resource. +For more information refer to [Indexer](https://wiki.servarr.com/radarr/settings#indexers) and [Torrent RSS](https://wiki.servarr.com/radarr/supported#torrentrssindexer). + +## Example Usage + +```terraform +resource "radarr_indexer_torrent_rss" "example" { + enable_automatic_search = true + name = "Example" + base_url = "https://rss.io" + allow_zero_size = true + minimum_seeders = 1 +} +``` + + +## Schema + +### Required + +- `base_url` (String) Base URL. +- `name` (String) IndexerTorrentRss name. + +### Optional + +- `allow_zero_size` (Boolean) Allow zero size files. +- `cookie` (String) Cookie. +- `download_client_id` (Number) Download client ID. +- `enable_rss` (Boolean) Enable RSS flag. +- `minimum_seeders` (Number) Minimum seeders. +- `multi_languages` (Set of Number) Languages list. +- `priority` (Number) Priority. +- `required_flags` (Set of Number) Flag list. +- `seed_ratio` (Number) Seed ratio. +- `seed_time` (Number) Seed time. +- `tags` (Set of Number) List of associated tags. + +### Read-Only + +- `id` (Number) IndexerTorrentRss ID. + +## Import + +Import is supported using the following syntax: + +```shell +# import using the API/UI ID +terraform import radarr_indexer_torrent_rss.example 1 +``` diff --git a/examples/resources/radarr_indexer_torrent_rss/import.sh b/examples/resources/radarr_indexer_torrent_rss/import.sh new file mode 100644 index 00000000..cb4c5c5e --- /dev/null +++ b/examples/resources/radarr_indexer_torrent_rss/import.sh @@ -0,0 +1,2 @@ +# import using the API/UI ID +terraform import radarr_indexer_torrent_rss.example 1 \ No newline at end of file diff --git a/examples/resources/radarr_indexer_torrent_rss/resource.tf b/examples/resources/radarr_indexer_torrent_rss/resource.tf new file mode 100644 index 00000000..79fd5d39 --- /dev/null +++ b/examples/resources/radarr_indexer_torrent_rss/resource.tf @@ -0,0 +1,7 @@ +resource "radarr_indexer_torrent_rss" "example" { + enable_automatic_search = true + name = "Example" + base_url = "https://rss.io" + allow_zero_size = true + minimum_seeders = 1 +} diff --git a/internal/provider/indexer_torrent_rss_resource.go b/internal/provider/indexer_torrent_rss_resource.go new file mode 100644 index 00000000..5529faeb --- /dev/null +++ b/internal/provider/indexer_torrent_rss_resource.go @@ -0,0 +1,342 @@ +package provider + +import ( + "context" + "fmt" + "strconv" + + "github.com/devopsarr/terraform-provider-sonarr/tools" + "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/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "golift.io/starr/radarr" +) + +const ( + indexerTorrentRssResourceName = "indexer_torrent_rss" + IndexerTorrentRssImplementation = "TorrentRssIndexer" + IndexerTorrentRssConfigContrat = "TorrentRssIndexerSettings" + IndexerTorrentRssProtocol = "torrent" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &IndexerTorrentRssResource{} +var _ resource.ResourceWithImportState = &IndexerTorrentRssResource{} + +func NewIndexerTorrentRssResource() resource.Resource { + return &IndexerTorrentRssResource{} +} + +// IndexerTorrentRssResource defines the TorrentRss indexer implementation. +type IndexerTorrentRssResource struct { + client *radarr.Radarr +} + +// IndexerTorrentRss describes the TorrentRss indexer data model. +type IndexerTorrentRss struct { + Tags types.Set `tfsdk:"tags"` + MultiLanguages types.Set `tfsdk:"multi_languages"` + RequiredFlags types.Set `tfsdk:"required_flags"` + Name types.String `tfsdk:"name"` + BaseURL types.String `tfsdk:"base_url"` + Cookie types.String `tfsdk:"cookie"` + Priority types.Int64 `tfsdk:"priority"` + ID types.Int64 `tfsdk:"id"` + DownloadClientID types.Int64 `tfsdk:"download_client_id"` + MinimumSeeders types.Int64 `tfsdk:"minimum_seeders"` + SeedTime types.Int64 `tfsdk:"seed_time"` + SeedRatio types.Float64 `tfsdk:"seed_ratio"` + AllowZeroSize types.Bool `tfsdk:"allow_zero_size"` + EnableRss types.Bool `tfsdk:"enable_rss"` +} + +func (i IndexerTorrentRss) toIndexer() *Indexer { + return &Indexer{ + EnableRss: i.EnableRss, + AllowZeroSize: i.AllowZeroSize, + Priority: i.Priority, + DownloadClientID: i.DownloadClientID, + ID: i.ID, + Name: i.Name, + Cookie: i.Cookie, + MinimumSeeders: i.MinimumSeeders, + SeedTime: i.SeedTime, + SeedRatio: i.SeedRatio, + BaseURL: i.BaseURL, + Tags: i.Tags, + MultiLanguages: i.MultiLanguages, + RequiredFlags: i.RequiredFlags, + } +} + +func (i *IndexerTorrentRss) fromIndexer(indexer *Indexer) { + i.EnableRss = indexer.EnableRss + i.AllowZeroSize = indexer.AllowZeroSize + i.Priority = indexer.Priority + i.DownloadClientID = indexer.DownloadClientID + i.ID = indexer.ID + i.Name = indexer.Name + i.Cookie = indexer.Cookie + i.MinimumSeeders = indexer.MinimumSeeders + i.SeedTime = indexer.SeedTime + i.SeedRatio = indexer.SeedRatio + i.BaseURL = indexer.BaseURL + i.Tags = indexer.Tags + i.MultiLanguages = indexer.MultiLanguages + i.RequiredFlags = indexer.RequiredFlags +} + +func (r *IndexerTorrentRssResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + indexerTorrentRssResourceName +} + +func (r *IndexerTorrentRssResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Indexer Torrent RSS resource.\nFor more information refer to [Indexer](https://wiki.servarr.com/radarr/settings#indexers) and [Torrent RSS](https://wiki.servarr.com/radarr/supported#torrentrssindexer).", + Attributes: map[string]schema.Attribute{ + "enable_rss": schema.BoolAttribute{ + MarkdownDescription: "Enable RSS flag.", + Optional: true, + Computed: true, + }, + "priority": schema.Int64Attribute{ + MarkdownDescription: "Priority.", + Optional: true, + Computed: true, + }, + "download_client_id": schema.Int64Attribute{ + MarkdownDescription: "Download client ID.", + Optional: true, + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "IndexerTorrentRss name.", + Required: true, + }, + "tags": schema.SetAttribute{ + MarkdownDescription: "List of associated tags.", + Optional: true, + Computed: true, + ElementType: types.Int64Type, + }, + "id": schema.Int64Attribute{ + MarkdownDescription: "IndexerTorrentRss ID.", + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + // Field values + "allow_zero_size": schema.BoolAttribute{ + MarkdownDescription: "Allow zero size files.", + Optional: true, + Computed: true, + }, + "minimum_seeders": schema.Int64Attribute{ + MarkdownDescription: "Minimum seeders.", + Optional: true, + Computed: true, + }, + "seed_time": schema.Int64Attribute{ + MarkdownDescription: "Seed time.", + Optional: true, + Computed: true, + }, + "seed_ratio": schema.Float64Attribute{ + MarkdownDescription: "Seed ratio.", + Optional: true, + Computed: true, + }, + "base_url": schema.StringAttribute{ + MarkdownDescription: "Base URL.", + Required: true, + }, + "cookie": schema.StringAttribute{ + MarkdownDescription: "Cookie.", + Optional: true, + Computed: true, + }, + "multi_languages": schema.SetAttribute{ + MarkdownDescription: "Languages list.", + Optional: true, + Computed: true, + ElementType: types.Int64Type, + }, + "required_flags": schema.SetAttribute{ + MarkdownDescription: "Flag list.", + Optional: true, + Computed: true, + ElementType: types.Int64Type, + }, + }, + } +} + +func (r *IndexerTorrentRssResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*radarr.Radarr) + if !ok { + resp.Diagnostics.AddError( + tools.UnexpectedResourceConfigureType, + fmt.Sprintf("Expected *radarr.Radarr, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *IndexerTorrentRssResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var indexer *IndexerTorrentRss + + resp.Diagnostics.Append(req.Plan.Get(ctx, &indexer)...) + + if resp.Diagnostics.HasError() { + return + } + + // Create new IndexerTorrentRss + request := indexer.read(ctx) + + response, err := r.client.AddIndexerContext(ctx, request) + if err != nil { + resp.Diagnostics.AddError(tools.ClientError, fmt.Sprintf("Unable to create %s, got error: %s", indexerTorrentRssResourceName, err)) + + return + } + + tflog.Trace(ctx, "created "+indexerTorrentRssResourceName+": "+strconv.Itoa(int(response.ID))) + // Generate resource state struct + indexer.write(ctx, response) + resp.Diagnostics.Append(resp.State.Set(ctx, &indexer)...) +} + +func (r *IndexerTorrentRssResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var indexer *IndexerTorrentRss + + resp.Diagnostics.Append(req.State.Get(ctx, &indexer)...) + + if resp.Diagnostics.HasError() { + return + } + + // Get IndexerTorrentRss current value + response, err := r.client.GetIndexerContext(ctx, indexer.ID.ValueInt64()) + if err != nil { + resp.Diagnostics.AddError(tools.ClientError, fmt.Sprintf("Unable to read %s, got error: %s", indexerTorrentRssResourceName, err)) + + return + } + + tflog.Trace(ctx, "read "+indexerTorrentRssResourceName+": "+strconv.Itoa(int(response.ID))) + // Map response body to resource schema attribute + indexer.write(ctx, response) + resp.Diagnostics.Append(resp.State.Set(ctx, &indexer)...) +} + +func (r *IndexerTorrentRssResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var indexer *IndexerTorrentRss + + resp.Diagnostics.Append(req.Plan.Get(ctx, &indexer)...) + + if resp.Diagnostics.HasError() { + return + } + + // Update IndexerTorrentRss + request := indexer.read(ctx) + + response, err := r.client.UpdateIndexerContext(ctx, request) + if err != nil { + resp.Diagnostics.AddError(tools.ClientError, fmt.Sprintf("Unable to update "+indexerTorrentRssResourceName+", got error: %s", err)) + + return + } + + tflog.Trace(ctx, "updated "+indexerTorrentRssResourceName+": "+strconv.Itoa(int(response.ID))) + // Generate resource state struct + indexer.write(ctx, response) + resp.Diagnostics.Append(resp.State.Set(ctx, &indexer)...) +} + +func (r *IndexerTorrentRssResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var indexer *IndexerTorrentRss + + resp.Diagnostics.Append(req.State.Get(ctx, &indexer)...) + + if resp.Diagnostics.HasError() { + return + } + + // Delete IndexerTorrentRss current value + err := r.client.DeleteIndexerContext(ctx, indexer.ID.ValueInt64()) + if err != nil { + resp.Diagnostics.AddError(tools.ClientError, fmt.Sprintf("Unable to read %s, got error: %s", indexerTorrentRssResourceName, err)) + + return + } + + tflog.Trace(ctx, "deleted "+indexerTorrentRssResourceName+": "+strconv.Itoa(int(indexer.ID.ValueInt64()))) + resp.State.RemoveResource(ctx) +} + +func (r *IndexerTorrentRssResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + id, err := strconv.Atoi(req.ID) + if err != nil { + resp.Diagnostics.AddError( + tools.UnexpectedImportIdentifier, + fmt.Sprintf("Expected import identifier with format: ID. Got: %q", req.ID), + ) + + return + } + + tflog.Trace(ctx, "imported "+indexerTorrentRssResourceName+": "+strconv.Itoa(id)) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (i *IndexerTorrentRss) write(ctx context.Context, indexer *radarr.IndexerOutput) { + genericIndexer := Indexer{ + EnableRss: types.BoolValue(indexer.EnableRss), + Priority: types.Int64Value(indexer.Priority), + DownloadClientID: types.Int64Value(indexer.DownloadClientID), + ID: types.Int64Value(indexer.ID), + Name: types.StringValue(indexer.Name), + } + genericIndexer.Tags, _ = types.SetValueFrom(ctx, types.Int64Type, indexer.Tags) + genericIndexer.writeFields(ctx, indexer.Fields) + i.fromIndexer(&genericIndexer) +} + +func (i *IndexerTorrentRss) read(ctx context.Context) *radarr.IndexerInput { + var tags []int + + tfsdk.ValueAs(ctx, i.Tags, &tags) + + return &radarr.IndexerInput{ + EnableRss: i.EnableRss.ValueBool(), + Priority: i.Priority.ValueInt64(), + DownloadClientID: i.DownloadClientID.ValueInt64(), + ID: i.ID.ValueInt64(), + ConfigContract: IndexerTorrentRssConfigContrat, + Implementation: IndexerTorrentRssImplementation, + Name: i.Name.ValueString(), + Protocol: IndexerTorrentRssProtocol, + Tags: tags, + Fields: i.toIndexer().readFields(ctx), + } +} diff --git a/internal/provider/indexer_torrent_rss_resource_test.go b/internal/provider/indexer_torrent_rss_resource_test.go new file mode 100644 index 00000000..01e78432 --- /dev/null +++ b/internal/provider/indexer_torrent_rss_resource_test.go @@ -0,0 +1,52 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccIndexerTorrentRssResource(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccIndexerTorrentRssResourceConfig("rssResourceTest", "https://rss.org"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("radarr_indexer_torrent_rss.test", "base_url", "https://rss.org"), + resource.TestCheckResourceAttrSet("radarr_indexer_torrent_rss.test", "id"), + ), + }, + // Update and Read testing + { + Config: testAccIndexerTorrentRssResourceConfig("rssResourceTest", "https://rss.net"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("radarr_indexer_torrent_rss.test", "base_url", "https://rss.net"), + ), + }, + // ImportState testing + { + ResourceName: "radarr_indexer_torrent_rss.test", + ImportState: true, + ImportStateVerify: true, + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccIndexerTorrentRssResourceConfig(name, url string) string { + return fmt.Sprintf(` + resource "radarr_indexer_torrent_rss" "test" { + enable_rss = false + name = "%s" + base_url = "%s" + allow_zero_size = true + minimum_seeders = 1 + }`, name, url) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a9129cc3..8fdd2d7c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -160,6 +160,7 @@ func (p *RadarrProvider) Resources(ctx context.Context) []func() resource.Resour NewIndexerNewznabResource, NewIndexerNyaaResource, NewIndexerOmgwtfnzbsResource, + NewIndexerTorrentRssResource, NewIndexerRarbgResource, NewRestrictionResource,