0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 02:31:10 +00:00

feat(acme): implement resources and data sources for ACME accounts (#1455)

* feat(acme): implement CRUD API for proxmox cluster ACME
* feat(acme): implement acme_accounts data source
* feat(acme): implement acme_account data source
* fix(acme): wait for task status on account creation
* feat(acme): implement account resource creation
* feat(acme): implement account read
* fix(acme): wait for task status on account update
* feat(acme): implement account update
* fix(acme): wait for task status on account deletion
* feat(acme): implement account deletion
* feat(acme): implement account import
* feat(acme): provide correctly typed API response for `account` field
* feat(acme): implement account schema for acme_account data source
* fix(acme): read `location` into state in acme_account resource
* fix(acme): ensure `name` of acme_account resource can't be changed
* docs(acme): generate documentation
* feat(acme): read back ACME account details from API
* Revert "fix(acme): ensure `name` of acme_account resource can't be changed"
* fix(acme): provide default for acme account name
* fix(acme): acme account name can't be changed
* chore(acme): update resource doc to clarify PVE auth requirements
* chore(acme): add `created_at` attr to the resource, sort model fields & schema attributes alphabetically

---------

Signed-off-by: Björn Brauer <zaubernerd@zaubernerd.de>
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Björn Brauer 2024-08-08 05:16:31 +02:00 committed by GitHub
parent b589083a1a
commit 9de4037a82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1077 additions and 1 deletions

View File

@ -0,0 +1,52 @@
---
layout: page
title: proxmox_virtual_environment_acme_account
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves information about a specific ACME account.
---
# Data Source: proxmox_virtual_environment_acme_account
Retrieves information about a specific ACME account.
## Example Usage
```terraform
// This will fetch all ACME accounts...
data "proxmox_virtual_environment_acme_accounts" "all" {}
// ...which we will go through in order to fetch the whole data on each account.
data "proxmox_virtual_environment_acme_account" "example" {
for_each = data.proxmox_virtual_environment_acme_accounts.all.accounts
name = each.value
}
output "data_proxmox_virtual_environment_acme_account" {
value = data.proxmox_virtual_environment_acme_account.example
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Optional
- `name` (String) The identifier of the ACME account to read.
### Read-Only
- `account` (Attributes) The ACME account information. (see [below for nested schema](#nestedatt--account))
- `directory` (String) The directory URL of the ACME account.
- `location` (String) The location URL of the ACME account.
- `tos` (String) The URL of the terms of service of the ACME account.
<a id="nestedatt--account"></a>
### Nested Schema for `account`
Read-Only:
- `contact` (List of String) An array of contact email addresses.
- `created_at` (String) The timestamp of the account creation.
- `status` (String) The status of the account. Can be one of `valid`, `deactivated` or `revoked`.

View File

@ -0,0 +1,29 @@
---
layout: page
title: proxmox_virtual_environment_acme_accounts
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves the list of ACME accounts.
---
# Data Source: proxmox_virtual_environment_acme_accounts
Retrieves the list of ACME accounts.
## Example Usage
```terraform
data "proxmox_virtual_environment_acme_accounts" "example" {}
output "data_proxmox_virtual_environment_acme_accounts" {
value = data.proxmox_virtual_environment_acme_accounts.example.accounts
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Read-Only
- `accounts` (Set of String) The identifiers of the ACME accounts.

View File

@ -320,7 +320,10 @@ provider "proxmox" {
-> The token authentication is taking precedence over the password authentication.
-> Not all Proxmox API operations are supported via API Token.
You may see errors like `error creating container: received an HTTP 403 response - Reason: Permission check failed (changing feature flags for privileged container is only allowed for root@pam)` or `error creating VM: received an HTTP 500 response - Reason: only root can set 'arch' config` when using API Token authentication, even when `Administrator` role or the `root@pam` user is used with the token.
You may see errors like
`error creating container: received an HTTP 403 response - Reason: Permission check failed (changing feature flags for privileged container is only allowed for root@pam)` or
`error creating VM: received an HTTP 500 response - Reason: only root can set 'arch' config` or
`Permission check failed (user != root@pam)` when using API Token authentication, even when `Administrator` role or the `root@pam` user is used with the token.
The workaround is to use password authentication for those operations.
-> You can also configure additional Proxmox users and roles using [`virtual_environment_user`](https://registry.terraform.io/providers/bpg/proxmox/latest/docs/data-sources/virtual_environment_user) and [`virtual_environment_role`](https://registry.terraform.io/providers/bpg/proxmox/latest/docs/data-sources/virtual_environment_role) resources of the provider.

View File

@ -0,0 +1,56 @@
---
layout: page
title: proxmox_virtual_environment_acme_account
parent: Resources
subcategory: Virtual Environment
description: |-
Manages an ACME account in a Proxmox VE cluster.
~> This resource requires root@pam authentication.
---
# Resource: proxmox_virtual_environment_acme_account
Manages an ACME account in a Proxmox VE cluster.
~> This resource requires `root@pam` authentication.
## Example Usage
```terraform
resource "proxmox_virtual_environment_acme_account" "example" {
name = "example"
contact = "example@email.com"
directory = "https://acme-staging-v02.api.letsencrypt.org/directory"
tos = "https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `contact` (String) The contact email addresses.
### Optional
- `directory` (String) The URL of the ACME CA directory endpoint.
- `eab_hmac_key` (String) The HMAC key for External Account Binding.
- `eab_kid` (String) The Key Identifier for External Account Binding.
- `name` (String) The ACME account config file name.
- `tos` (String) The URL of CA TermsOfService - setting this indicates agreement.
### Read-Only
- `created_at` (String) The timestamp of the ACME account creation.
- `location` (String) The location of the ACME account.
## 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_account.example example
```

View File

@ -0,0 +1,12 @@
// This will fetch all ACME accounts...
data "proxmox_virtual_environment_acme_accounts" "all" {}
// ...which we will go through in order to fetch the whole data on each account.
data "proxmox_virtual_environment_acme_account" "example" {
for_each = data.proxmox_virtual_environment_acme_accounts.all.accounts
name = each.value
}
output "data_proxmox_virtual_environment_acme_account" {
value = data.proxmox_virtual_environment_acme_account.example
}

View File

@ -0,0 +1,5 @@
data "proxmox_virtual_environment_acme_accounts" "example" {}
output "data_proxmox_virtual_environment_acme_accounts" {
value = data.proxmox_virtual_environment_acme_accounts.example.accounts
}

View File

@ -0,0 +1,3 @@
#!/usr/bin/env sh
# ACME accounts can be imported using their name, e.g.:
terraform import proxmox_virtual_environment_acme_account.example example

View File

@ -0,0 +1,6 @@
resource "proxmox_virtual_environment_acme_account" "example" {
name = "example"
contact = "example@email.com"
directory = "https://acme-staging-v02.api.letsencrypt.org/directory"
tos = "https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf"
}

View File

@ -0,0 +1,191 @@
/*
* 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/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/account"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &acmeAccountDatasource{}
_ datasource.DataSourceWithConfigure = &acmeAccountDatasource{}
)
// NewACMEAccountDataSource is a helper function to simplify the provider implementation.
func NewACMEAccountDataSource() datasource.DataSource {
return &acmeAccountDatasource{}
}
// acmeAccountDatasource is the data source implementation for ACME accounts.
type acmeAccountDatasource struct {
client *account.Client
}
type accountDataModel struct {
Contact []types.String `tfsdk:"contact"`
CreatedAt types.String `tfsdk:"created_at"`
Status types.String `tfsdk:"status"`
}
func (m *accountDataModel) attrTypes() map[string]attr.Type {
return map[string]attr.Type{
"contact": types.ListType{ElemType: types.StringType},
"created_at": types.StringType,
"status": types.StringType,
}
}
// accountModel is the model used to represent an ACME account.
type accountModel struct {
// Name is the ACME account config file name.
Name types.String `tfsdk:"name"`
// Account is the ACME account information.
Account types.Object `tfsdk:"account"`
// Directory is the URL of the ACME CA directory endpoint.
Directory types.String `tfsdk:"directory"`
// Location is the location of the ACME account.
Location types.String `tfsdk:"location"`
// URL of CA TermsOfService - setting this indicates agreement.
TOS types.String `tfsdk:"tos"`
}
// Metadata returns the data source type name.
func (d *acmeAccountDatasource) Metadata(
_ context.Context,
req datasource.MetadataRequest,
resp *datasource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_acme_account"
}
// Schema returns the schema for the data source.
func (d *acmeAccountDatasource) Schema(
_ context.Context,
_ datasource.SchemaRequest,
resp *datasource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Retrieves information about a specific ACME account.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The identifier of the ACME account to read.",
Optional: true,
},
"account": schema.SingleNestedAttribute{
Description: "The ACME account information.",
Computed: true,
Attributes: map[string]schema.Attribute{
"contact": schema.ListAttribute{
Description: "An array of contact email addresses.",
ElementType: types.StringType,
Computed: true,
},
"created_at": schema.StringAttribute{
Description: "The timestamp of the account creation.",
Computed: true,
},
"status": schema.StringAttribute{
Description: "The status of the account.",
MarkdownDescription: "The status of the account. Can be one of `valid`, `deactivated` or `revoked`.",
Computed: true,
},
},
},
"directory": schema.StringAttribute{
Description: "The directory URL of the ACME account.",
Computed: true,
},
"location": schema.StringAttribute{
Description: "The location URL of the ACME account.",
Computed: true,
},
"tos": schema.StringAttribute{
Description: "The URL of the terms of service of the ACME account.",
Computed: true,
},
},
}
}
// Configure adds the provider-configured client to the data source.
func (d *acmeAccountDatasource) 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().Account()
}
// Read retrieves the ACME account information.
func (d *acmeAccountDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var state accountModel
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
name := state.Name.ValueString()
account, err := d.client.Get(ctx, name)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to read ACME account '%s'", name),
err.Error(),
)
return
}
contactList := make([]types.String, len(account.Account.Contact))
for i, contact := range account.Account.Contact {
contactList[i] = types.StringValue(contact)
}
data := &accountDataModel{
Contact: contactList,
CreatedAt: types.StringValue(account.Account.CreatedAt),
Status: types.StringValue(account.Account.Status),
}
accountObject, diags := types.ObjectValueFrom(ctx, data.attrTypes(), data)
resp.Diagnostics.Append(diags...)
state.Account = accountObject
state.Directory = types.StringValue(account.Directory)
state.Location = types.StringValue(account.Location)
state.TOS = types.StringValue(account.TOS)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

View File

@ -0,0 +1,119 @@
/*
* 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/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/account"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &acmeAccountsDatasource{}
_ datasource.DataSourceWithConfigure = &acmeAccountsDatasource{}
)
// NewACMEAccountsDataSource is a helper function to simplify the provider implementation.
func NewACMEAccountsDataSource() datasource.DataSource {
return &acmeAccountsDatasource{}
}
// acmeAccountsDatasource is the data source implementation for ACME accounts.
type acmeAccountsDatasource struct {
client *account.Client
}
// acmeAccountsModel maps the schema data for the ACME accounts data source.
type acmeAccountsModel struct {
Accounts types.Set `tfsdk:"accounts"`
}
// Metadata returns the data source type name.
func (d *acmeAccountsDatasource) Metadata(
_ context.Context,
req datasource.MetadataRequest,
resp *datasource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_acme_accounts"
}
// Schema returns the schema for the data source.
func (d *acmeAccountsDatasource) Schema(
_ context.Context,
_ datasource.SchemaRequest,
resp *datasource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Retrieves the list of ACME accounts.",
Attributes: map[string]schema.Attribute{
"accounts": schema.SetAttribute{
Description: "The identifiers of the ACME accounts.",
ElementType: types.StringType,
Computed: true,
},
},
}
}
// Configure adds the provider-configured client to the data source.
func (d *acmeAccountsDatasource) 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().Account()
}
// Read fetches the list of ACME Accounts from the Proxmox cluster then converts it to a list of strings.
func (d *acmeAccountsDatasource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
var state acmeAccountsModel
list, err := d.client.List(ctx)
if err != nil {
resp.Diagnostics.AddError(
"Unable to read ACME accounts",
err.Error(),
)
return
}
accounts := make([]attr.Value, len(list))
for i, v := range list {
accounts[i] = types.StringValue(v.Name)
}
accountsValue, diags := types.SetValue(types.StringType, accounts)
resp.Diagnostics.Append(diags...)
state.Accounts = accountsValue
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

View File

@ -0,0 +1,319 @@
/*
* 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"
"regexp"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"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/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/account"
)
var (
_ resource.Resource = &acmeAccountResource{}
_ resource.ResourceWithConfigure = &acmeAccountResource{}
_ resource.ResourceWithImportState = &acmeAccountResource{}
)
// NewACMEAccountResource creates a new resource for managing ACME accounts.
func NewACMEAccountResource() resource.Resource {
return &acmeAccountResource{}
}
// acmeAccountResource contains the resource's internal data.
type acmeAccountResource struct {
// The ACME account API client
client account.Client
}
// acmeAccountModel maps the schema data for the ACME account resource.
type acmeAccountModel struct {
// Contact email addresses.
Contact types.String `tfsdk:"contact"`
// CreatedAt timestamp of the account creation.
CreatedAt types.String `tfsdk:"created_at"`
// URL of ACME CA directory endpoint.
Directory types.String `tfsdk:"directory"`
// HMAC key for External Account Binding.
EABHMACKey types.String `tfsdk:"eab_hmac_key"`
// Key Identifier for External Account Binding.
EABKID types.String `tfsdk:"eab_kid"`
// Location of the ACME account.
Location types.String `tfsdk:"location"`
// ACME account config file name.
Name types.String `tfsdk:"name"`
// URL of CA TermsOfService - setting this indicates agreement.
TOS types.String `tfsdk:"tos"`
}
// Metadata defines the name of the resource.
func (r *acmeAccountResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_acme_account"
}
// Schema defines the schema for the resource.
func (r *acmeAccountResource) Schema(
_ context.Context,
_ resource.SchemaRequest,
resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Manages an ACME account in a Proxmox VE cluster.",
MarkdownDescription: "Manages an ACME account in a Proxmox VE cluster.\n\n" +
"~> This resource requires `root@pam` authentication.",
Attributes: map[string]schema.Attribute{
"contact": schema.StringAttribute{
Description: "The contact email addresses.",
Required: true,
},
"created_at": schema.StringAttribute{
Description: "The timestamp of the ACME account creation.",
Computed: true,
},
"directory": schema.StringAttribute{
Description: "The URL of the ACME CA directory endpoint.",
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^https?://.*$`),
"must be a valid URL",
),
},
Optional: true,
},
"eab_hmac_key": schema.StringAttribute{
Description: "The HMAC key for External Account Binding.",
Optional: true,
},
"eab_kid": schema.StringAttribute{
Description: "The Key Identifier for External Account Binding.",
Optional: true,
},
"location": schema.StringAttribute{
Description: "The location of the ACME account.",
Computed: true,
},
"name": schema.StringAttribute{
Description: "The ACME account config file name.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("default"),
},
"tos": schema.StringAttribute{
Description: "The URL of CA TermsOfService - setting this indicates agreement.",
Optional: true,
},
},
}
}
// Configure accesses the provider-configured Proxmox API client on behalf of the resource.
func (r *acmeAccountResource) 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().Account()
} else {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T",
req.ProviderData),
)
}
}
// Create creates a new ACME account on the Proxmox cluster.
func (r *acmeAccountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan acmeAccountModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
createRequest := &account.ACMEAccountCreateRequestBody{}
createRequest.Contact = plan.Contact.ValueString()
createRequest.Directory = plan.Directory.ValueString()
createRequest.EABHMACKey = plan.EABHMACKey.ValueString()
createRequest.EABKID = plan.EABKID.ValueString()
createRequest.Name = plan.Name.ValueString()
createRequest.TOS = plan.TOS.ValueString()
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 account '%s'", plan.Name),
err.Error(),
)
return
}
resp.Diagnostics.AddError(
fmt.Sprintf("ACME account '%s' already exists", plan.Name),
err.Error(),
)
}
r.readBack(ctx, &plan, &resp.Diagnostics, &resp.State)
}
// Read retrieves the current state of the ACME account from the Proxmox cluster.
func (r *acmeAccountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state acmeAccountModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
found, diags := r.read(ctx, &state)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
if found {
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
} else {
resp.State.RemoveResource(ctx)
}
}
}
// Update modifies an existing ACME account on the Proxmox cluster.
func (r *acmeAccountResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan acmeAccountModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
updateRequest := &account.ACMEAccountUpdateRequestBody{}
updateRequest.Contact = plan.Contact.ValueString()
err := r.client.Update(ctx, plan.Name.ValueString(), updateRequest)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to update ACME account '%s'", plan.Name),
err.Error(),
)
return
}
r.readBack(ctx, &plan, &resp.Diagnostics, &resp.State)
}
// Delete removes an existing ACME account from the Proxmox cluster.
func (r *acmeAccountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state acmeAccountModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.Delete(ctx, state.Name.ValueString())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to delete ACME account '%s'", state.Name),
err.Error(),
)
return
}
}
// ImportState retrieves the current state of an existing ACME account from the Proxmox cluster.
func (r *acmeAccountResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
}
func (r *acmeAccountResource) readBack(
ctx context.Context,
data *acmeAccountModel,
respDiags *diag.Diagnostics,
respState *tfsdk.State,
) {
found, diags := r.read(ctx, data)
respDiags.Append(diags...)
if !found {
respDiags.AddError(
fmt.Sprintf("ACME account '%s' not found after update", data.Name),
"Failed to find ACME account when trying to read back the updated ACME account's data.",
)
}
if !respDiags.HasError() {
respDiags.Append(respState.Set(ctx, data)...)
}
}
func (r *acmeAccountResource) read(ctx context.Context, data *acmeAccountModel) (bool, diag.Diagnostics) {
name := data.Name.ValueString()
acc, err := r.client.Get(ctx, name)
if err != nil {
var diags diag.Diagnostics
if !strings.Contains(err.Error(), "does not exist") {
diags.AddError(
fmt.Sprintf("Unable to read ACME account '%s'", name),
err.Error(),
)
}
return false, diags
}
var contact string
if len(acc.Account.Contact) > 0 {
contact = strings.Replace(acc.Account.Contact[0], "mailto:", "", 1)
}
data.Directory = types.StringValue(acc.Directory)
data.TOS = types.StringValue(acc.TOS)
data.Location = types.StringValue(acc.Location)
data.Contact = types.StringValue(contact)
data.CreatedAt = types.StringValue(acc.Account.CreatedAt)
return true, nil
}

View File

@ -26,6 +26,7 @@ import (
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/fwprovider/access"
"github.com/bpg/terraform-provider-proxmox/fwprovider/acme"
"github.com/bpg/terraform-provider-proxmox/fwprovider/ha"
"github.com/bpg/terraform-provider-proxmox/fwprovider/hardwaremapping"
"github.com/bpg/terraform-provider-proxmox/fwprovider/network"
@ -444,6 +445,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
return []func() resource.Resource{
NewClusterOptionsResource,
NewDownloadFileResource,
acme.NewACMEAccountResource,
apt.NewResourceRepo,
apt.NewResourceStandardRepo,
access.NewACLResource,
@ -461,6 +463,8 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewVersionDataSource,
acme.NewACMEAccountsDataSource,
acme.NewACMEAccountDataSource,
apt.NewDataSourceRepo,
apt.NewDataSourceStandardRepo,
ha.NewHAGroupDataSource,

View File

@ -0,0 +1,125 @@
/*
* 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 account
import (
"context"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// List returns a list of ACME accounts.
func (c *Client) List(ctx context.Context) ([]*ACMEAccountListResponseData, error) {
resBody := &ACMEAccountListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(""), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error listing ACME accounts: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].Name < resBody.Data[j].Name
})
return resBody.Data, nil
}
// Get retrieves a single ACME account based on its identifier.
func (c *Client) Get(ctx context.Context, name string) (*ACMEAccountGetResponseData, error) {
resBody := &ACMEAccountGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(url.PathEscape(name)), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error reading ACME account: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// Create creates a new ACME account.
func (c *Client) Create(ctx context.Context, data *ACMEAccountCreateRequestBody) error {
resBody := &ACMEAccountCreateResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(""), data, resBody)
if err != nil {
return fmt.Errorf("error creating ACME account: %w", err)
}
if resBody.Data == nil {
return api.ErrNoDataObjectInResponse
}
err = c.Tasks().WaitForTask(ctx, *resBody.Data)
if err != nil {
return fmt.Errorf(
"error updating ACME account: failed waiting for task: %w",
err,
)
}
return nil
}
// Update updates an existing ACME account.
func (c *Client) Update(ctx context.Context, accountName string, data *ACMEAccountUpdateRequestBody) error {
resBody := &ACMEAccountUpdateResponseBody{}
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(url.PathEscape(accountName)), data, resBody)
if err != nil {
return fmt.Errorf("error updating ACME account: %w", err)
}
if resBody.Data == nil {
return api.ErrNoDataObjectInResponse
}
err = c.Tasks().WaitForTask(ctx, *resBody.Data)
if err != nil {
return fmt.Errorf(
"error updating ACME account: failed waiting for task: %w",
err,
)
}
return nil
}
// Delete removes an ACME account.
func (c *Client) Delete(ctx context.Context, accountName string) error {
resBody := &ACMEAccountDeleteResponseBody{}
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(url.PathEscape(accountName)), nil, resBody)
if err != nil {
return fmt.Errorf("error deleting ACME account: %w", err)
}
if resBody.Data == nil {
return api.ErrNoDataObjectInResponse
}
err = c.Tasks().WaitForTask(ctx, *resBody.Data)
if err != nil {
return fmt.Errorf(
"error deleting ACME account: failed waiting for task: %w",
err,
)
}
return nil
}

View File

@ -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 account
// ACMEAccountListResponseBody contains the body from a ACME account list response.
type ACMEAccountListResponseBody struct {
Data []*ACMEAccountListResponseData `json:"data,omitempty"`
}
// ACMEAccountListResponseData contains the data from a ACME account list response.
type ACMEAccountListResponseData struct {
Name string `json:"name"`
}
// ACMEAccountGetResponseBody contains the body from a ACME account get response.
type ACMEAccountGetResponseBody struct {
Data *ACMEAccountGetResponseData `json:"data,omitempty"`
}
// ACMEAccountData contains the data from a ACME account.
type ACMEAccountData struct {
// An array of contact email addresses.
Contact []string `json:"contact"`
// Timestamp of the account creation.
CreatedAt string `json:"createdAt"`
// Status of the account. Can be one of "valid", "deactivated" or "revoked".
Status string `json:"status"`
}
// ACMEAccountGetResponseData contains the data from a ACME account get response.
type ACMEAccountGetResponseData struct {
// Account is the ACME account data.
Account ACMEAccountData `json:"account"`
// Directory is the URL of the ACME CA directory endpoint.
Directory string `json:"directory"`
// Location is the location of the ACME account.
Location string `json:"location"`
// TOS is the terms of service URL.
TOS string `json:"tos"`
}
// ACMEAccountCreateRequestBody contains the body for creating a new ACME account.
type ACMEAccountCreateRequestBody struct {
// Contact is the contact email addresses.
Contact string `url:"contact"`
// Directory is the URL of the ACME CA directory endpoint.
Directory string `url:"directory,omitempty"`
// EABHMACKey is the HMAC key for External Account Binding.
EABHMACKey string `url:"eab-hmac-key,omitempty"`
// EABKID is the Key Identifier for External Account Binding.
EABKID string `url:"eab-kid,omitempty"`
// Name is the ACME account config file name.
Name string `url:"name,omitempty"`
// TOS is the URL of CA TermsOfService - setting this indicates agreement.
TOS string `url:"tos_url,omitempty"`
}
// ACMEAccountCreateResponseBody contains the body from an ACME account create request.
type ACMEAccountCreateResponseBody struct {
Data *string `json:"data,omitempty"`
}
// ACMEAccountUpdateRequestBody contains the body for updating an existing ACME account.
type ACMEAccountUpdateRequestBody struct {
// Contact is the contact email addresses.
Contact string `url:"contact,omitempty"`
// Name is the ACME account config file name.
Name string `url:"name,omitempty"`
}
// ACMEAccountUpdateResponseBody contains the body from an ACME account update request.
type ACMEAccountUpdateResponseBody struct {
Data *string `json:"data,omitempty"`
}
// ACMEAccountDeleteResponseBody contains the body from an ACME account delete request.
type ACMEAccountDeleteResponseBody struct {
Data *string `json:"data,omitempty"`
}

View File

@ -0,0 +1,31 @@
/*
* 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 account
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes/tasks"
)
// Client is an interface for accessing the Proxmox ACME management API.
type Client struct {
api.Client
}
// ExpandPath expands a relative path to the Proxmox ACME management API path.
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("cluster/acme/account/%s", path)
}
// Tasks returns a client for managing ACME account tasks.
func (c *Client) Tasks() *tasks.Client {
return &tasks.Client{
Client: c.Client,
}
}

View File

@ -0,0 +1,29 @@
/*
* 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 (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme/account"
)
// Client is an interface for accessing the Proxmox ACME API.
type Client struct {
api.Client
}
// ExpandPath expands a relative path to a full cluster API path.
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("cluster/acme/%s", path)
}
// Account returns a client for managing the cluster's ACME account.
func (c *Client) Account() *account.Client {
return &account.Client{Client: c.Client}
}

View File

@ -10,6 +10,7 @@ import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/acme"
clusterfirewall "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/firewall"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
@ -42,3 +43,8 @@ func (c *Client) HA() *ha.Client {
func (c *Client) HardwareMapping() *mapping.Client {
return &mapping.Client{Client: c}
}
// ACME returns a client for managing the cluster's ACME features.
func (c *Client) ACME() *acme.Client {
return &acme.Client{Client: c}
}

View File

@ -28,6 +28,8 @@ import (
// Temporary: while migrating to the TF framework, we need to copy the generated docs to the right place
// for the resources / data sources that have been migrated.
//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_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/
@ -40,6 +42,7 @@ import (
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_version.md ../docs/data-sources/
//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_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/