From a6eb81af08f175ab078ae0369e0246425e77de6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Brauer?= Date: Sun, 8 Sep 2024 16:54:16 +0200 Subject: [PATCH] feat(acme): implement resources and data sources for ACME plugins (#1479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(acme): implement CRUD API for proxmox cluster ACME plugins Signed-off-by: Björn Brauer * feat(acme): implement acme_plugins data source Signed-off-by: Björn Brauer * feat(acme): implement acme_plugin data source Signed-off-by: Björn Brauer * feat(acme): implement plugin resource creation Signed-off-by: Björn Brauer * feat(acme): implement plugin resource read Signed-off-by: Björn Brauer * feat(acme): implement plugin resource update Signed-off-by: Björn Brauer * feat(acme): implement plugin resource deletion Signed-off-by: Björn Brauer * feat(acme): implement plugin resource import Signed-off-by: Björn Brauer * docs(acme): generate documentation Signed-off-by: Björn Brauer * fix: apply suggestions from code review Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Signed-off-by: Björn Brauer * refactor: extract common fields into BasePluginData Signed-off-by: Björn Brauer * fix: restrict plugin resource to type=dns only because type=standalone is not configurable and always enabled by default. Signed-off-by: Björn Brauer * fix: remove unused 'nodes' property https://github.com/bpg/terraform-provider-proxmox/pull/1479/files#r1710916265 Signed-off-by: Björn Brauer * fix: remove "delete" property https://github.com/bpg/terraform-provider-proxmox/pull/1479/files#r1710908809 Signed-off-by: Björn Brauer * feat: implement attribute deletion Signed-off-by: Björn Brauer * fix: ignore empty lines in dns plugin data Signed-off-by: Björn Brauer * fix: partial revert of code review suggestions Joining the values with a string literal would produce \\n instead of \n and splitting at \\n doesn't match a newline. Signed-off-by: Björn Brauer * refactor: extract acme plugin models into separate file Signed-off-by: Björn Brauer * fix: format disable parameter as int Signed-off-by: Björn Brauer --------- Signed-off-by: Björn Brauer Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .../virtual_environment_acme_plugin.md | 39 +++ .../virtual_environment_acme_plugins.md | 41 +++ .../virtual_environment_acme_dns_plugin.md | 50 +++ .../data-source.tf | 7 + .../data-source.tf | 5 + .../import.sh | 3 + .../resource.tf | 8 + fwprovider/acme/datasource_acme_plugin.go | 153 +++++++++ fwprovider/acme/datasource_acme_plugins.go | 158 +++++++++ fwprovider/acme/plugin_model.go | 43 +++ fwprovider/acme/resource_acme_dns_plugin.go | 306 ++++++++++++++++++ fwprovider/provider.go | 3 + proxmox/cluster/acme/client.go | 8 +- proxmox/cluster/acme/plugins/acme_plugins.go | 83 +++++ .../acme/plugins/acme_plugins_types.go | 117 +++++++ proxmox/cluster/acme/plugins/client.go | 23 ++ tools/tools.go | 3 + 17 files changed, 1049 insertions(+), 1 deletion(-) create mode 100644 docs/data-sources/virtual_environment_acme_plugin.md create mode 100644 docs/data-sources/virtual_environment_acme_plugins.md create mode 100644 docs/resources/virtual_environment_acme_dns_plugin.md create mode 100644 examples/data-sources/proxmox_virtual_environment_acme_plugin/data-source.tf create mode 100644 examples/data-sources/proxmox_virtual_environment_acme_plugins/data-source.tf create mode 100755 examples/resources/proxmox_virtual_environment_acme_dns_plugin/import.sh create mode 100644 examples/resources/proxmox_virtual_environment_acme_dns_plugin/resource.tf create mode 100644 fwprovider/acme/datasource_acme_plugin.go create mode 100644 fwprovider/acme/datasource_acme_plugins.go create mode 100644 fwprovider/acme/plugin_model.go create mode 100644 fwprovider/acme/resource_acme_dns_plugin.go create mode 100644 proxmox/cluster/acme/plugins/acme_plugins.go create mode 100644 proxmox/cluster/acme/plugins/acme_plugins_types.go create mode 100644 proxmox/cluster/acme/plugins/client.go diff --git a/docs/data-sources/virtual_environment_acme_plugin.md b/docs/data-sources/virtual_environment_acme_plugin.md new file mode 100644 index 00000000..09031282 --- /dev/null +++ b/docs/data-sources/virtual_environment_acme_plugin.md @@ -0,0 +1,39 @@ +--- +layout: page +title: proxmox_virtual_environment_acme_plugin +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves a single ACME plugin by plugin ID name. +--- + +# Data Source: proxmox_virtual_environment_acme_plugin + +Retrieves a single ACME plugin by plugin ID name. + +## Example Usage + +```terraform +data "proxmox_virtual_environment_acme_plugin" "example" { + plugin = "standalone" +} + +output "data_proxmox_virtual_environment_acme_plugin" { + value = data.proxmox_virtual_environment_acme_plugin.example +} +``` + + +## Schema + +### Required + +- `plugin` (String) ACME Plugin ID name. + +### Read-Only + +- `api` (String) API plugin name. +- `data` (Map of String) DNS plugin data. +- `digest` (String) Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications. +- `type` (String) ACME challenge type (dns, standalone). +- `validation_delay` (Number) Extra delay in seconds to wait before requesting validation. Allows to cope with a long TTL of DNS records (0 - 172800). diff --git a/docs/data-sources/virtual_environment_acme_plugins.md b/docs/data-sources/virtual_environment_acme_plugins.md new file mode 100644 index 00000000..f9f6b86c --- /dev/null +++ b/docs/data-sources/virtual_environment_acme_plugins.md @@ -0,0 +1,41 @@ +--- +layout: page +title: proxmox_virtual_environment_acme_plugins +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves the list of ACME plugins. +--- + +# Data Source: proxmox_virtual_environment_acme_plugins + +Retrieves the list of ACME plugins. + +## Example Usage + +```terraform +data "proxmox_virtual_environment_acme_plugins" "example" {} + +output "data_proxmox_virtual_environment_acme_plugins" { + value = data.proxmox_virtual_environment_acme_plugins.example.plugins +} +``` + + +## Schema + +### Read-Only + +- `plugins` (Attributes List) List of ACME plugins (see [below for nested schema](#nestedatt--plugins)) + + +### Nested Schema for `plugins` + +Read-Only: + +- `api` (String) API plugin name. +- `data` (Map of String) DNS plugin data. +- `digest` (String) Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications. +- `plugin` (String) ACME Plugin ID name. +- `type` (String) ACME challenge type (dns, standalone). +- `validation_delay` (Number) Extra delay in seconds to wait before requesting validation. Allows to cope with a long TTL of DNS records (0 - 172800). diff --git a/docs/resources/virtual_environment_acme_dns_plugin.md b/docs/resources/virtual_environment_acme_dns_plugin.md new file mode 100644 index 00000000..35fca1f9 --- /dev/null +++ b/docs/resources/virtual_environment_acme_dns_plugin.md @@ -0,0 +1,50 @@ +--- +layout: page +title: proxmox_virtual_environment_acme_dns_plugin +parent: Resources +subcategory: Virtual Environment +description: |- + Manages an ACME plugin in a Proxmox VE cluster. +--- + +# Resource: proxmox_virtual_environment_acme_dns_plugin + +Manages an ACME plugin in a Proxmox VE cluster. + +## Example Usage + +```terraform +resource "proxmox_virtual_environment_acme_dns_plugin" "example" { + plugin = "test" + api = "aws" + data = { + AWS_ACCESS_KEY_ID = "EXAMPLE" + AWS_SECRET_ACCESS_KEY = "EXAMPLE" + } +} +``` + + +## Schema + +### Required + +- `api` (String) API plugin name. +- `plugin` (String) ACME Plugin ID name. + +### Optional + +- `data` (Map of String) DNS plugin data. +- `digest` (String) SHA1 digest of the current configuration. Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications. +- `disable` (Boolean) Flag to disable the config. +- `validation_delay` (Number) Extra delay in seconds to wait before requesting validation. Allows to cope with a long TTL of DNS records (0 - 172800). + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# ACME accounts can be imported using their name, e.g.: +terraform import proxmox_virtual_environment_acme_dns_plugin.example test +``` diff --git a/examples/data-sources/proxmox_virtual_environment_acme_plugin/data-source.tf b/examples/data-sources/proxmox_virtual_environment_acme_plugin/data-source.tf new file mode 100644 index 00000000..294b2732 --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_acme_plugin/data-source.tf @@ -0,0 +1,7 @@ +data "proxmox_virtual_environment_acme_plugin" "example" { + plugin = "standalone" +} + +output "data_proxmox_virtual_environment_acme_plugin" { + value = data.proxmox_virtual_environment_acme_plugin.example +} diff --git a/examples/data-sources/proxmox_virtual_environment_acme_plugins/data-source.tf b/examples/data-sources/proxmox_virtual_environment_acme_plugins/data-source.tf new file mode 100644 index 00000000..077d330b --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_acme_plugins/data-source.tf @@ -0,0 +1,5 @@ +data "proxmox_virtual_environment_acme_plugins" "example" {} + +output "data_proxmox_virtual_environment_acme_plugins" { + value = data.proxmox_virtual_environment_acme_plugins.example.plugins +} diff --git a/examples/resources/proxmox_virtual_environment_acme_dns_plugin/import.sh b/examples/resources/proxmox_virtual_environment_acme_dns_plugin/import.sh new file mode 100755 index 00000000..2cac6845 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_acme_dns_plugin/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# ACME accounts can be imported using their name, e.g.: +terraform import proxmox_virtual_environment_acme_dns_plugin.example test diff --git a/examples/resources/proxmox_virtual_environment_acme_dns_plugin/resource.tf b/examples/resources/proxmox_virtual_environment_acme_dns_plugin/resource.tf new file mode 100644 index 00000000..696f00bc --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_acme_dns_plugin/resource.tf @@ -0,0 +1,8 @@ +resource "proxmox_virtual_environment_acme_dns_plugin" "example" { + plugin = "test" + api = "aws" + data = { + AWS_ACCESS_KEY_ID = "EXAMPLE" + AWS_SECRET_ACCESS_KEY = "EXAMPLE" + } +} diff --git a/fwprovider/acme/datasource_acme_plugin.go b/fwprovider/acme/datasource_acme_plugin.go new file mode 100644 index 00000000..ad3d850b --- /dev/null +++ b/fwprovider/acme/datasource_acme_plugin.go @@ -0,0 +1,153 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package acme + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/plugins" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &acmePluginDatasource{} + _ datasource.DataSourceWithConfigure = &acmePluginDatasource{} +) + +// NewACMEPluginDataSource is a helper function to simplify the provider implementation. +func NewACMEPluginDataSource() datasource.DataSource { + return &acmePluginDatasource{} +} + +// acmePluginDatasource is the data source implementation for ACME plugin. +type acmePluginDatasource struct { + client *plugins.Client +} + +// Metadata returns the data source type name. +func (d *acmePluginDatasource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_acme_plugin" +} + +// Schema returns the schema for the data source. +func (d *acmePluginDatasource) Schema( + _ context.Context, + _ datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "Retrieves a single ACME plugin by plugin ID name.", + Attributes: map[string]schema.Attribute{ + "api": schema.StringAttribute{ + Description: "API plugin name.", + Computed: true, + }, + "data": schema.MapAttribute{ + Description: "DNS plugin data.", + Computed: true, + ElementType: types.StringType, + }, + "digest": schema.StringAttribute{ + Description: "Prevent changes if current configuration file has a different digest. " + + "This can be used to prevent concurrent modifications.", + Computed: true, + }, + "plugin": schema.StringAttribute{ + Description: "ACME Plugin ID name.", + Required: true, + }, + "type": schema.StringAttribute{ + Description: "ACME challenge type (dns, standalone).", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf("dns", "standalone"), + }, + }, + "validation_delay": schema.Int64Attribute{ + Description: "Extra delay in seconds to wait before requesting validation. " + + "Allows to cope with a long TTL of DNS records (0 - 172800).", + Computed: true, + Validators: []validator.Int64{ + int64validator.Between(0, 172800), + }, + }, + }, + } +} + +// Configure adds the provider-configured client to the data source. +func (d *acmePluginDatasource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(proxmox.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *proxmox.Client, got: %T", + req.ProviderData), + ) + + return + } + + d.client = client.Cluster().ACME().Plugins() +} + +// Read fetches the ACME plugin from the Proxmox cluster. +func (d *acmePluginDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state acmePluginModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + id := state.Plugin.ValueString() + + plugin, err := d.client.Get(ctx, id) + if err != nil { + resp.Diagnostics.AddError( + "Unable to read ACME plugin", + err.Error(), + ) + + return + } + + state.API = types.StringValue(plugin.API) + + mapValue, diags := types.MapValueFrom(ctx, types.StringType, plugin.Data) + resp.Diagnostics.Append(diags...) + + state.Data = mapValue + state.Digest = types.StringValue(plugin.Digest) + state.Plugin = types.StringValue(plugin.Plugin) + state.Type = types.StringValue(plugin.Type) + state.ValidationDelay = types.Int64Value(plugin.ValidationDelay) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} diff --git a/fwprovider/acme/datasource_acme_plugins.go b/fwprovider/acme/datasource_acme_plugins.go new file mode 100644 index 00000000..0e16fcee --- /dev/null +++ b/fwprovider/acme/datasource_acme_plugins.go @@ -0,0 +1,158 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package acme + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/plugins" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &acmePluginsDatasource{} + _ datasource.DataSourceWithConfigure = &acmePluginsDatasource{} +) + +// NewACMEPluginsDataSource is a helper function to simplify the provider implementation. +func NewACMEPluginsDataSource() datasource.DataSource { + return &acmePluginsDatasource{} +} + +// acmePluginsDatasource is the data source implementation for ACME plugins. +type acmePluginsDatasource struct { + client *plugins.Client +} + +// Metadata returns the data source type name. +func (d *acmePluginsDatasource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_acme_plugins" +} + +// Schema returns the schema for the data source. +func (d *acmePluginsDatasource) Schema( + _ context.Context, + _ datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "Retrieves the list of ACME plugins.", + Attributes: map[string]schema.Attribute{ + "plugins": schema.ListNestedAttribute{ + Description: "List of ACME plugins", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "api": schema.StringAttribute{ + Description: "API plugin name.", + Computed: true, + }, + "data": schema.MapAttribute{ + Description: "DNS plugin data.", + Computed: true, + ElementType: types.StringType, + }, + "digest": schema.StringAttribute{ + Description: "Prevent changes if current configuration file has a different digest. " + + "This can be used to prevent concurrent modifications.", + Computed: true, + }, + "plugin": schema.StringAttribute{ + Description: "ACME Plugin ID name.", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "ACME challenge type (dns, standalone).", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf("dns", "standalone"), + }, + }, + "validation_delay": schema.Int64Attribute{ + Description: "Extra delay in seconds to wait before requesting validation. " + + "Allows to cope with a long TTL of DNS records (0 - 172800).", + Computed: true, + Validators: []validator.Int64{ + int64validator.Between(0, 172800), + }, + }, + }, + }, + Computed: true, + }, + }, + } +} + +// Configure adds the provider-configured client to the data source. +func (d *acmePluginsDatasource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(proxmox.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *proxmox.Client, got: %T", + req.ProviderData), + ) + + return + } + + d.client = client.Cluster().ACME().Plugins() +} + +// Read fetches the list of ACME plugins from the Proxmox cluster. +func (d *acmePluginsDatasource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { + var state acmePluginsModel + + list, err := d.client.List(ctx) + if err != nil { + resp.Diagnostics.AddError( + "Unable to read ACME plugins", + err.Error(), + ) + + return + } + + for _, plugin := range list { + mapValue, diags := types.MapValueFrom(ctx, types.StringType, plugin.Data) + resp.Diagnostics.Append(diags...) + + state.Plugins = append(state.Plugins, acmePluginModel{ + baseACMEPluginModel: baseACMEPluginModel{ + API: types.StringValue(plugin.API), + Data: mapValue, + Digest: types.StringValue(plugin.Digest), + Plugin: types.StringValue(plugin.Plugin), + ValidationDelay: types.Int64Value(plugin.ValidationDelay), + }, + Type: types.StringValue(plugin.Type), + }) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} diff --git a/fwprovider/acme/plugin_model.go b/fwprovider/acme/plugin_model.go new file mode 100644 index 00000000..2f2a39f4 --- /dev/null +++ b/fwprovider/acme/plugin_model.go @@ -0,0 +1,43 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package acme + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// acmePluginsModel maps the schema data for the ACME plugins data source. +type acmePluginsModel struct { + Plugins []acmePluginModel `tfsdk:"plugins"` +} + +type baseACMEPluginModel struct { + // API plugin name + API types.String `tfsdk:"api"` + // DNS plugin data + Data types.Map `tfsdk:"data"` + // Prevent changes if current configuration file has a different digest. + // This can be used to prevent concurrent modifications. + Digest types.String `tfsdk:"digest"` + // Plugin ID name + Plugin types.String `tfsdk:"plugin"` + // Extra delay in seconds to wait before requesting validation (0 - 172800) + ValidationDelay types.Int64 `tfsdk:"validation_delay"` +} + +// acmePluginModel maps the schema data for an ACME plugin. +type acmePluginModel struct { + baseACMEPluginModel + Type types.String `tfsdk:"type"` +} + +// acmePluginCreateModel maps the schema data for an ACME plugin. +type acmePluginCreateModel struct { + baseACMEPluginModel + // Flag to disable the config + Disable types.Bool `tfsdk:"disable"` +} diff --git a/fwprovider/acme/resource_acme_dns_plugin.go b/fwprovider/acme/resource_acme_dns_plugin.go new file mode 100644 index 00000000..4dd4cbf8 --- /dev/null +++ b/fwprovider/acme/resource_acme_dns_plugin.go @@ -0,0 +1,306 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package acme + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "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/int64default" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/plugins" +) + +var ( + _ resource.Resource = &acmePluginResource{} + _ resource.ResourceWithConfigure = &acmePluginResource{} + _ resource.ResourceWithImportState = &acmePluginResource{} +) + +// NewACMEPluginResource creates a new resource for managing ACME plugins. +func NewACMEPluginResource() resource.Resource { + return &acmePluginResource{} +} + +// acmePluginResource contains the resource's internal data. +type acmePluginResource struct { + // The ACME account API client + client plugins.Client +} + +// Metadata defines the name of the resource. +func (r *acmePluginResource) Metadata( + _ context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_acme_dns_plugin" +} + +// Schema defines the schema for the resource. +func (r *acmePluginResource) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "Manages an ACME plugin in a Proxmox VE cluster.", + Attributes: map[string]schema.Attribute{ + "api": schema.StringAttribute{ + Description: "API plugin name.", + Required: true, + }, + "data": schema.MapAttribute{ + Description: "DNS plugin data.", + Optional: true, + ElementType: types.StringType, + }, + "digest": schema.StringAttribute{ + Description: "SHA1 digest of the current configuration.", + MarkdownDescription: "SHA1 digest of the current configuration. " + + "Prevent changes if current configuration file has a different digest. " + + "This can be used to prevent concurrent modifications.", + Optional: true, + Computed: true, + }, + "disable": schema.BoolAttribute{ + Description: "Flag to disable the config.", + Optional: true, + }, + "plugin": schema.StringAttribute{ + Description: "ACME Plugin ID name.", + Required: true, + }, + "validation_delay": schema.Int64Attribute{ + Description: "Extra delay in seconds to wait before requesting validation.", + MarkdownDescription: "Extra delay in seconds to wait before requesting validation. " + + "Allows to cope with a long TTL of DNS records (0 - 172800).", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(30), + Validators: []validator.Int64{ + int64validator.Between(0, 172800), + }, + }, + }, + } +} + +// Configure accesses the provider-configured Proxmox API client on behalf of the resource. +func (r *acmePluginResource) Configure( + _ context.Context, + req resource.ConfigureRequest, + resp *resource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(proxmox.Client) + if ok { + r.client = *client.Cluster().ACME().Plugins() + } else { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *proxmox.Client, got: %T", + req.ProviderData), + ) + } +} + +// Create creates a new ACME plugin on the Proxmox cluster. +func (r *acmePluginResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan acmePluginCreateModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + createRequest := &plugins.ACMEPluginsCreateRequestBody{} + createRequest.Plugin = plan.Plugin.ValueString() + createRequest.Type = "dns" + createRequest.API = plan.API.ValueString() + data := make(plugins.DNSPluginData) + + plan.Data.ElementsAs(ctx, &data, false) + + createRequest.Data = &data + createRequest.Disable = plan.Disable.ValueBool() + createRequest.ValidationDelay = plan.ValidationDelay.ValueInt64() + + err := r.client.Create(ctx, createRequest) + if err != nil { + if !strings.Contains(err.Error(), "already exists") { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to create ACME plugin '%s'", createRequest.Plugin), + err.Error(), + ) + + return + } + + resp.Diagnostics.AddError( + fmt.Sprintf("ACME plugin '%s' already exists", createRequest.Plugin), + err.Error(), + ) + } + + plugin, err := r.client.Get(ctx, plan.Plugin.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to read ACME plugin", + err.Error(), + ) + + return + } + + plan.Digest = types.StringValue(plugin.Digest) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +// Read retrieves the current state of the ACME plugin from the Proxmox cluster. +func (r *acmePluginResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state acmePluginCreateModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + id := state.Plugin.ValueString() + + plugin, err := r.client.Get(ctx, id) + if err != nil { + resp.Diagnostics.AddError( + "Unable to read ACME plugin", + err.Error(), + ) + + return + } + + state.API = types.StringValue(plugin.API) + state.Digest = types.StringValue(plugin.Digest) + state.ValidationDelay = types.Int64Value(plugin.ValidationDelay) + + mapValue, diags := types.MapValueFrom(ctx, types.StringType, plugin.Data) + resp.Diagnostics.Append(diags...) + + state.Data = mapValue + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +// Update modifies an existing ACME plugin on the Proxmox cluster. +func (r *acmePluginResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state acmePluginCreateModel + + toDelete := make([]string, 0) + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + updateRequest := &plugins.ACMEPluginsUpdateRequestBody{} + updateRequest.API = plan.API.ValueString() + + data := make(plugins.DNSPluginData) + + plan.Data.ElementsAs(ctx, &data, false) + + if plan.Data.IsNull() && !state.Data.IsNull() { + toDelete = append(toDelete, "data") + } else { + updateRequest.Data = &data + } + + updateRequest.Digest = plan.Digest.ValueString() + + if plan.Disable.IsNull() && !state.Disable.IsNull() || !plan.Disable.ValueBool() { + toDelete = append(toDelete, "disable") + } else { + updateRequest.Disable = plan.Disable.ValueBool() + } + + if plan.ValidationDelay.IsNull() && !state.ValidationDelay.IsNull() { + toDelete = append(toDelete, "validation_delay") + } else { + updateRequest.ValidationDelay = plan.ValidationDelay.ValueInt64() + } + + if len(toDelete) > 0 { + updateRequest.Delete = strings.Join(toDelete, ",") + } + + err := r.client.Update(ctx, plan.Plugin.ValueString(), updateRequest) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to update ACME account '%s'", plan.Plugin.ValueString()), + err.Error(), + ) + + return + } + + plugin, err := r.client.Get(ctx, plan.Plugin.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to read ACME plugin", + err.Error(), + ) + + return + } + + plan.Digest = types.StringValue(plugin.Digest) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +// Delete removes an existing ACME plugin from the Proxmox cluster. +func (r *acmePluginResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state acmePluginCreateModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.Delete(ctx, state.Plugin.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to delete ACME plugin '%s'", state.Plugin.ValueString()), + err.Error(), + ) + } +} + +// ImportState retrieves the current state of an existing ACME plugin from the Proxmox cluster. +func (r *acmePluginResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + resource.ImportStatePassthroughID(ctx, path.Root("plugin"), req, resp) +} diff --git a/fwprovider/provider.go b/fwprovider/provider.go index ee37373a..e8186e5f 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -446,6 +446,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc NewClusterOptionsResource, NewDownloadFileResource, acme.NewACMEAccountResource, + acme.NewACMEPluginResource, apt.NewRepositoryResource, apt.NewStandardRepositoryResource, access.NewACLResource, @@ -465,6 +466,8 @@ func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.Dat NewVersionDataSource, acme.NewACMEAccountsDataSource, acme.NewACMEAccountDataSource, + acme.NewACMEPluginsDataSource, + acme.NewACMEPluginDataSource, apt.NewRepositoryDataSource, apt.NewStandardRepositoryDataSource, ha.NewHAGroupDataSource, diff --git a/proxmox/cluster/acme/client.go b/proxmox/cluster/acme/client.go index e2ac780f..32036528 100644 --- a/proxmox/cluster/acme/client.go +++ b/proxmox/cluster/acme/client.go @@ -11,6 +11,7 @@ import ( "github.com/bpg/terraform-provider-proxmox/proxmox/api" "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/account" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/plugins" ) // Client is an interface for accessing the Proxmox ACME API. @@ -18,7 +19,7 @@ type Client struct { api.Client } -// ExpandPath expands a relative path to a full cluster API path. +// ExpandPath expands a relative path to a full cluster ACME API path. func (c *Client) ExpandPath(path string) string { return fmt.Sprintf("cluster/acme/%s", path) } @@ -27,3 +28,8 @@ func (c *Client) ExpandPath(path string) string { func (c *Client) Account() *account.Client { return &account.Client{Client: c.Client} } + +// Plugins returns a client for managing the cluster's ACME plugins. +func (c *Client) Plugins() *plugins.Client { + return &plugins.Client{Client: c.Client} +} diff --git a/proxmox/cluster/acme/plugins/acme_plugins.go b/proxmox/cluster/acme/plugins/acme_plugins.go new file mode 100644 index 00000000..b8e51655 --- /dev/null +++ b/proxmox/cluster/acme/plugins/acme_plugins.go @@ -0,0 +1,83 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package plugins + +import ( + "context" + "fmt" + "net/http" + "net/url" + "sort" + + "github.com/bpg/terraform-provider-proxmox/proxmox/api" +) + +// List returns a list of ACME plugins. +func (c *Client) List(ctx context.Context) ([]*ACMEPluginsListResponseData, error) { + resBody := &ACMEPluginsListResponseBody{} + + err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(""), nil, resBody) + if err != nil { + return nil, fmt.Errorf("error listing ACME plugins: %w", err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + sort.Slice(resBody.Data, func(i, j int) bool { + return resBody.Data[i].Plugin < resBody.Data[j].Plugin + }) + + return resBody.Data, nil +} + +// Get retrieves a single ACME plugin based on its identifier. +func (c *Client) Get(ctx context.Context, id string) (*ACMEPluginsGetResponseData, error) { + resBody := &ACMEPluginsGetResponseBody{} + + err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(url.PathEscape(id)), nil, resBody) + if err != nil { + return nil, fmt.Errorf("error reading ACME plugin: %w", err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + return resBody.Data, nil +} + +// Create creates a new ACME plugin. +func (c *Client) Create(ctx context.Context, data *ACMEPluginsCreateRequestBody) error { + err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(""), data, nil) + if err != nil { + return fmt.Errorf("error creating ACME plugin: %w", err) + } + + return nil +} + +// Update updates an existing ACME plugin. +func (c *Client) Update(ctx context.Context, id string, data *ACMEPluginsUpdateRequestBody) error { + err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(url.PathEscape(id)), data, nil) + if err != nil { + return fmt.Errorf("error updating ACME plugin: %w", err) + } + + return nil +} + +// Delete removes an ACME plugin. +func (c *Client) Delete(ctx context.Context, id string) error { + err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(url.PathEscape(id)), nil, nil) + if err != nil { + return fmt.Errorf("error deleting ACME plugin: %w", err) + } + + return nil +} diff --git a/proxmox/cluster/acme/plugins/acme_plugins_types.go b/proxmox/cluster/acme/plugins/acme_plugins_types.go new file mode 100644 index 00000000..d3660b9d --- /dev/null +++ b/proxmox/cluster/acme/plugins/acme_plugins_types.go @@ -0,0 +1,117 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package plugins + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "strings" +) + +// BaseACMEPluginData contains common fields for ACME plugin data. +type BaseACMEPluginData struct { + // ACME challenge type (dns, standalone). + Type string `json:"type,omitempty" url:"type,omitempty"` + // Prevent changes if current configuration file has a different digest. This can be used to prevent concurrent modifications. + Digest string `json:"digest,omitempty" url:"digest,omitempty"` + // API plugin name + API string `json:"api,omitempty" url:"api,omitempty"` + // Extra delay in seconds to wait before requesting validation. Allows to cope with a long TTL of DNS records (0 - 172800). + ValidationDelay int64 `json:"validation-delay,omitempty" url:"validation-delay,omitempty"` +} + +// ACMEPluginsListResponseBody contains the body from an ACME plugins list response. +type ACMEPluginsListResponseBody struct { + // Unique identifier for ACME plugin instance. + Data []*ACMEPluginsListResponseData `json:"data,omitempty"` +} + +// ACMEPluginsListResponseData contains the data from an ACME plugins list response. +type ACMEPluginsListResponseData struct { + BaseACMEPluginData + // ACME Plugin ID name + Plugin string `json:"plugin" url:"plugin"` + // DNS plugin data. + Data *DNSPluginData `json:"data,omitempty"` +} + +// ACMEPluginsGetResponseBody contains the body from an ACME plugins get response. +type ACMEPluginsGetResponseBody struct { + Data *ACMEPluginsGetResponseData `json:"data,omitempty"` +} + +// ACMEPluginsGetResponseData contains the data from an ACME plugins get response. +type ACMEPluginsGetResponseData struct { + BaseACMEPluginData + // ACME Plugin ID name + Plugin string `json:"plugin" url:"plugin"` + // DNS plugin data. + Data *DNSPluginData `json:"data"` +} + +// ACMEPluginsCreateRequestBody contains the body for creating a new ACME plugin. +type ACMEPluginsCreateRequestBody struct { + BaseACMEPluginData + // ACME Plugin ID name + Plugin string `json:"id" url:"id"` + // DNS plugin data. (base64 encoded) + Data *DNSPluginData `url:"data,omitempty"` + // Flag to disable the config. + Disable bool `url:"disable,omitempty,int"` +} + +// ACMEPluginsUpdateRequestBody contains the body for updating an existing ACME plugin. +type ACMEPluginsUpdateRequestBody struct { + BaseACMEPluginData + // DNS plugin data. (base64 encoded) + Data *DNSPluginData `url:"data,omitempty"` + // A list of settings you want to delete. + Delete string `url:"delete,omitempty"` + // Flag to disable the config. + Disable bool `url:"disable,omitempty,int"` +} + +// DNSPluginData is a map of DNS plugin data. +type DNSPluginData map[string]string + +// EncodeValues encodes the DNSPluginData into the URL values. +func (d DNSPluginData) EncodeValues(key string, v *url.Values) error { + values := make([]string, 0, len(d)) + + for key, value := range d { + values = append(values, fmt.Sprintf("%s=%s", key, value)) + } + + v.Add(key, base64.StdEncoding.EncodeToString([]byte(strings.Join(values, "\n")))) + + return nil +} + +// UnmarshalJSON unmarshals a DNSPluginData struct from JSON. +func (d *DNSPluginData) UnmarshalJSON(b []byte) error { + mapData := make(map[string]string) + + s := "" + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshaling json: %w", err) + } + + for _, line := range strings.Split(s, "\n") { + if line == "" { + continue + } + + before, after, _ := strings.Cut(line, "=") + mapData[before] = after + } + + *d = mapData + + return nil +} diff --git a/proxmox/cluster/acme/plugins/client.go b/proxmox/cluster/acme/plugins/client.go new file mode 100644 index 00000000..f40f59ab --- /dev/null +++ b/proxmox/cluster/acme/plugins/client.go @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package plugins + +import ( + "fmt" + + "github.com/bpg/terraform-provider-proxmox/proxmox/api" +) + +// Client is an interface for accessing the Proxmox ACME plugins API. +type Client struct { + api.Client +} + +// ExpandPath expands a relative path to the Proxmox ACME plugins API path. +func (c *Client) ExpandPath(path string) string { + return fmt.Sprintf("cluster/acme/plugins/%s", path) +} diff --git a/tools/tools.go b/tools/tools.go index d6744e9e..ba2ed9e1 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -30,6 +30,8 @@ import ( //go:generate cp -R ../build/docs-gen/guides/. ../docs/guides/ //go:generate cp ../build/docs-gen/data-sources/virtual_environment_acme_account.md ../docs/data-sources/ //go:generate cp ../build/docs-gen/data-sources/virtual_environment_acme_accounts.md ../docs/data-sources/ +//go:generate cp ../build/docs-gen/data-sources/virtual_environment_acme_plugin.md ../docs/data-sources/ +//go:generate cp ../build/docs-gen/data-sources/virtual_environment_acme_plugins.md ../docs/data-sources/ //go:generate cp ../build/docs-gen/data-sources/virtual_environment_apt_repository.md ../docs/data-sources/ //go:generate cp ../build/docs-gen/data-sources/virtual_environment_apt_standard_repository.md ../docs/data-sources/ //go:generate cp ../build/docs-gen/data-sources/virtual_environment_hagroup.md ../docs/data-sources/ @@ -43,6 +45,7 @@ import ( //go:generate cp ../build/docs-gen/data-sources/virtual_environment_vm2.md ../docs/data-sources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_acl.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_acme_account.md ../docs/resources/ +//go:generate cp ../build/docs-gen/resources/virtual_environment_acme_dns_plugin.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_apt_repository.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_apt_standard_repository.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_cluster_options.md ../docs/resources/