Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 23 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f0f0a94
feat(acme): implement CRUD API for proxmox cluster ACME
ZauberNerd Jul 24, 2024
4478376
feat(acme): implement acme_accounts data source
ZauberNerd Jul 24, 2024
a6ce9aa
feat(acme): implement acme_account data source
ZauberNerd Jul 24, 2024
faed7d2
fix(acme): wait for task status on account creation
ZauberNerd Jul 26, 2024
1bfb52e
feat(acme): implement account resource creation
ZauberNerd Jul 26, 2024
48ad5a8
feat(acme): implement account read
ZauberNerd Jul 26, 2024
5886f3e
fix(acme): wait for task status on account update
ZauberNerd Jul 26, 2024
e9b0787
feat(acme): implement account update
ZauberNerd Jul 26, 2024
626ee85
fix(acme): wait for task status on account deletion
ZauberNerd Jul 26, 2024
4c8d581
feat(acme): implement account deletion
ZauberNerd Jul 26, 2024
6bdfe1c
feat(acme): implement account import
ZauberNerd Jul 26, 2024
7c8f70f
feat(acme): provide correctly typed API response for `account` field
ZauberNerd Aug 2, 2024
06373ac
feat(acme): implement account schema for acme_account data source
ZauberNerd Aug 2, 2024
db3db08
fix(acme): read `location` into state in acme_account resource
ZauberNerd Aug 2, 2024
af4a881
fix(acme): ensure `name` of acme_account resource can't be changed
ZauberNerd Aug 2, 2024
266484f
docs(acme): generate documentation
ZauberNerd Aug 2, 2024
fcea655
feat(acme): read back ACME account details from API
ZauberNerd Aug 5, 2024
93ad80a
Revert "fix(acme): ensure `name` of acme_account resource can't be ch…
ZauberNerd Aug 5, 2024
e5d1674
fix(acme): provide default for acme account name
ZauberNerd Aug 5, 2024
8c1fdb4
fix(acme): acme account name can't be changed
ZauberNerd Aug 5, 2024
d18c8c6
chore(acme): update resource doc to clarify PVE auth requirements
bpg Aug 7, 2024
5442a13
chore(acme): add `created_at` attr to the resource, sort model fields…
bpg Aug 8, 2024
7a552ce
Merge branch 'main' into acme
bpg Aug 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/data-sources/virtual_environment_acme_account.md
Original file line number Diff line number Diff line change
@@ -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`.
29 changes: 29 additions & 0 deletions docs/data-sources/virtual_environment_acme_accounts.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 56 additions & 0 deletions docs/resources/virtual_environment_acme_account.md
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
191 changes: 191 additions & 0 deletions fwprovider/acme/datasource_acme_account.go
Original file line number Diff line number Diff line change
@@ -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)...)
}
Loading