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

feat(ha): add support for Proxmox High Availability objects (#498)

* chore: fix a pair of typos in comments

* feat(api): list High Availability groups

  * New clients created for HA and HA groups (via
    `Cluster().HA().Groups()`)
  * `List(ctx)` method that lists the cluster's High Availability groups

* feat(ha): added the `proxmox_virtual_environment_hagroups` data source

  * This data source returns the list of HA groups in its value's
    `group_ids` field

* fix(api): changed incorrect copy-pasted error message

* feat(api): get a HA group's full information

  * Added a `Get()` method to the HA group client, which fetches a
    single group's information based on its identifier.

* feat(ha): added the `proxmox_virtual_environment_hagroup` data source

  * This data source can read information about a single Proxmox High
    Availabillity group from the cluster.

* chore(ha): fixed linter error

* test(ha): added schema tests for the HA groups data sources

* fix(ha): use -1 as a node's priority when no priority is defined

  * It used to default to 0, which is a valid value for priorities.

* chore(ha): converted the `hagroups` datasource to the Terraform plugin SDK

* chore(refactoring): common definition for `id` attributes

* chore(ha): ported the HA group datasource to the Terraform plugin framework

* feat(ha): return HA group identifiers as a set rather than a list

* docs(ha): added examples for the hagroups/hagroup datasources

* docs(ha): added documentation for the hagroup{,s} datasources

* chore(ha): fixed linter errors

* chore(ha): workaround for the linter's split personality disorder

* fix(ha): fixed reading the restricted flag

* chore(refactoring): use `ExpandPath` for paths to the HA groups API

Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>

* feat: CustomBool to Terraform attribute value conversion method

* chore(refactoring): use `CustomBool` for boolean fields in the API data

* chore(refactoring): renamed "members" to "nodes" in the HA group datasource

* fix: typo in comment

* chore(refactoring): split HA group API data and added the update request body

* fix(api): fixed copy-pasted error message

* feat(api): method to create/update a HA group

* feat(api): HA group deletion method

* fix(api): made the digest optional for HA groups

* feat(ha): added unimplemented hagroup resource

* fix(ha): fixed copy-pasted comment

* feat(ha): schema definition for the HA group resource

* feat: helper function that converts string attr values to string pointers

* fix(ha): ensure node priorities are <= 1000 in HA groups

* fix(ha): add the digest attribute to the schema

* feat(ha): model definition for the HA group resource

* fix(api): fixed incorrect error message

* fix(api): fixed HA group creation / update

  * I had somehow misunderstood the Proxmox API doc and thought creation
    and update went through the same endpoint. This has been fixed by
    adding separate data structures and separate methods for both
    actions.

* feat: Terraform/Proxmox API conversion utilities

* chore(refactoring): HA group model and reading code moved to separate file

* feat(ha): HA group creation

* fix(api): renamed method (missed during previous refactor)

* feat(ha): `Read()` method implemented for the `hagroup` resource

* chore(refactoring): more consistent variable naming

* fix(ha): fixed the behaviour of `Read()` when the resource is deleted externally

* feat(ha): implement HA group deletion

* feat(ha): HA group update implemented

* fix(ha): prevent empty or untrimmed HA group comments

* feat(ha): HA group import

* docs(ha): HA group resource examples

* docs(ha): generated documentation for the `hagroup` resource

* chore(ha): fixed linter errors

* chore(refactoring): updated the code based on changes to the datasource PR

* fix(api): fixed boolean fields in the HA group create/update structures

* fix(ha): removed digest from the HA group resource and datasource

  * The digest is generated by Proxmox from the *whole* HA groups
    configuration, so any update to one group causes changes in all
    other groups.
  * Because of that, using it causes failures when updating two or more
    HA groups.
  * It is also a pretty useless value to have in the datasource, as it
    is global and not actually related to the individual data items

* chore(refactoring): removed obsolete type conversion code

* chore(refactoring): use `ExpandPath` in the HA groups API client

* feat(ha): custom type for HA resource states

* feat(ha): custom type for HA resource types

* fix(api): fixed JSON decoding for HA resource states and types

  * Values were being decoded directly from the raw bytes.
  * Added tests for JSON marshaling/unmarshaling

* feat(api): custom type for HA resource identifiers

  * Structure with a type and name
  * Conversion to/from strings
  * Marshaling to/Unmarshaling from JSON
  * URL encoding

* feat(api): list and get HA resources

* feat(ha): HA resources list datasource

* feat(ha): added method that converts HA resource data to Terraform values

* fix(api): HA resource max relocation/restarts are optional

* feat(ha): Terraform validator for HA resource IDs

* feat(ha): HA resource datasource

* chore(refactoring): moved HA resource model to separate file

* feat(api): data structures for HA resource creation and update

* feat(api): HA resource creation, update and deletion

* fix(api): incorrect mapping in common HA resource data

* feat: utility function to create attribute validators based on parse functions

* feat: validators for HA resource identifiers, states and types

* fix(api): incorrect comment for the update request body

* feat(ha): Terraform resource for Proxmox HA resources

* chore(reafactoring): removed old HA resource ID validator

* docs: examples related to HA resources added

* docs: added documentation related to HA resources management

* fix: update doc generation, fix minor typos

* fix: rename & split utils package, replace `iota`

---------

Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Emmanuel Benoît 2023-08-20 23:42:12 +02:00 committed by GitHub
parent a2853606ad
commit 03c9b36b86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 3069 additions and 5 deletions

View File

@ -0,0 +1,44 @@
---
layout: page
title: proxmox_virtual_environment_hagroup
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves information about a specific High Availability group.
---
# Data Source: proxmox_virtual_environment_hagroup
Retrieves information about a specific High Availability group.
## Example Usage
```terraform
// This will fetch the set of HA group identifiers...
data "proxmox_virtual_environment_hagroups" "all" {}
// ...which we will go through in order to fetch the whole data on each group.
data "proxmox_virtual_environment_hagroup" "example" {
for_each = data.proxmox_virtual_environment_hagroups.all.group_ids
group = each.value
}
output "proxmox_virtual_environment_hagroups_full" {
value = data.proxmox_virtual_environment_hagroup.example
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `group` (String) The identifier of the High Availability group to read.
### Read-Only
- `comment` (String) The comment associated with this group
- `id` (String) The ID of this resource.
- `no_failback` (Boolean) A flag that indicates that failing back to a higher priority node is disabled for this HA group.
- `nodes` (Map of Number) The member nodes for this group. They are provided as a map, where the keys are the node names and the values represent their priority: integers for known priorities or `null` for unset priorities.
- `restricted` (Boolean) A flag that indicates that other nodes may not be used to run resources associated to this HA group.

View File

@ -0,0 +1,30 @@
---
layout: page
title: proxmox_virtual_environment_hagroups
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves the list of High Availability groups.
---
# Data Source: proxmox_virtual_environment_hagroups
Retrieves the list of High Availability groups.
## Example Usage
```terraform
data "proxmox_virtual_environment_hagroups" "example" {}
output "data_proxmox_virtual_environment_hagroups" {
value = data.proxmox_virtual_environment_hagroups.example.group_ids
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Read-Only
- `group_ids` (Set of String) The identifiers of the High Availability groups.
- `id` (String) The ID of this resource.

View File

@ -0,0 +1,46 @@
---
layout: page
title: proxmox_virtual_environment_haresource
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves the list of High Availability resources.
---
# Data Source: proxmox_virtual_environment_haresource
Retrieves the list of High Availability resources.
## Example Usage
```terraform
// This will fetch the set of all HA resource identifiers...
data "proxmox_virtual_environment_haresources" "all" {}
// ...which we will go through in order to fetch the whole record for each resource.
data "proxmox_virtual_environment_haresource" "example" {
for_each = data.proxmox_virtual_environment_haresources.all.resource_ids
resource_id = each.value
}
output "proxmox_virtual_environment_haresources_full" {
value = data.proxmox_virtual_environment_haresource.example
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `resource_id` (String) The identifier of the Proxmox HA resource to read.
### Read-Only
- `comment` (String) The comment associated with this resource.
- `group` (String) The identifier of the High Availability group this resource is a member of.
- `id` (String) The ID of this resource.
- `max_relocate` (Number) The maximal number of relocation attempts.
- `max_restart` (Number) The maximal number of restart attempts.
- `state` (String) The desired state of the resource.
- `type` (String) The type of High Availability resource (`vm` or `ct`).

View File

@ -0,0 +1,43 @@
---
layout: page
title: proxmox_virtual_environment_haresources
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves the list of High Availability resources.
---
# Data Source: proxmox_virtual_environment_haresources
Retrieves the list of High Availability resources.
## Example Usage
```terraform
// This will fetch the set of all HA resource identifiers.
data "proxmox_virtual_environment_haresources" "example_all" {}
// This will fetch the set of HA resource identifiers that correspond to virtual machines.
data "proxmox_virtual_environment_haresources" "example_vm" {
type = "vm"
}
output "data_proxmox_virtual_environment_haresources" {
value = {
all = data.proxmox_virtual_environment_haresources.example_all.resource_ids
vms = data.proxmox_virtual_environment_haresources.example_vm.resource_ids
}
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Optional
- `type` (String) The type of High Availability resources to fetch (`vm` or `ct`). All resources will be fetched if this option is unset.
### Read-Only
- `id` (String) The ID of this resource.
- `resource_ids` (Set of String) The identifiers of the High Availability resources.

View File

@ -0,0 +1,59 @@
---
layout: page
title: proxmox_virtual_environment_hagroup
parent: Resources
subcategory: Virtual Environment
description: |-
Manages a High Availability group in a Proxmox VE cluster.
---
# Resource: proxmox_virtual_environment_hagroup
Manages a High Availability group in a Proxmox VE cluster.
## Example Usage
```terraform
resource "proxmox_virtual_environment_hagroup" "example" {
group = "example"
comment = "This is a comment."
# Member nodes, with or without priority.
nodes = {
node1 = null
node2 = 2
node3 = 1
}
restricted = true
no_failback = false
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `group` (String) The identifier of the High Availability group to manage.
- `nodes` (Map of Number) The member nodes for this group. They are provided as a map, where the keys are the node names and the values represent their priority: integers for known priorities or `null` for unset priorities.
### Optional
- `comment` (String) The comment associated with this group
- `no_failback` (Boolean) A flag that indicates that failing back to a higher priority node is disabled for this HA group. Defaults to `false`.
- `restricted` (Boolean) A flag that indicates that other nodes may not be used to run resources associated to this HA group. Defaults to `false`.
### Read-Only
- `id` (String) The ID of this resource.
## Import
Import is supported using the following syntax:
```shell
#!/usr/bin/env sh
# HA groups can be imported using their name, e.g.:
terraform import proxmox_virtual_environment_hagroup.example example
```

View File

@ -0,0 +1,56 @@
---
layout: page
title: proxmox_virtual_environment_haresource
parent: Resources
subcategory: Virtual Environment
description: |-
Manages Proxmox HA resources.
---
# Resource: proxmox_virtual_environment_haresource
Manages Proxmox HA resources.
## Example Usage
```terraform
resource "proxmox_virtual_environment_haresource" "example" {
depends_on = [
proxmox_virtual_environment_hagroup.example
]
resource_id = "vm:123"
state = "started"
group = "example"
comment = "Managed by Terraform"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `resource_id` (String) The Proxmox HA resource identifier
### Optional
- `comment` (String) The comment associated with this resource.
- `group` (String) The identifier of the High Availability group this resource is a member of.
- `max_relocate` (Number) The maximal number of relocation attempts.
- `max_restart` (Number) The maximal number of restart attempts.
- `state` (String) The desired state of the resource.
- `type` (String) The type of HA resources to create. If unset, it will be deduced from the `resource_id`.
### Read-Only
- `id` (String) The ID of this resource.
## Import
Import is supported using the following syntax:
```shell
#!/usr/bin/env sh
# HA resources can be imported using their identifiers, e.g.:
terraform import proxmox_virtual_environment_haresource.example vm:123
```

View File

@ -0,0 +1,12 @@
// This will fetch the set of HA group identifiers...
data "proxmox_virtual_environment_hagroups" "all" {}
// ...which we will go through in order to fetch the whole data on each group.
data "proxmox_virtual_environment_hagroup" "example" {
for_each = data.proxmox_virtual_environment_hagroups.all.group_ids
group = each.value
}
output "proxmox_virtual_environment_hagroups_full" {
value = data.proxmox_virtual_environment_hagroup.example
}

View File

@ -0,0 +1,5 @@
data "proxmox_virtual_environment_hagroups" "example" {}
output "data_proxmox_virtual_environment_hagroups" {
value = data.proxmox_virtual_environment_hagroups.example.group_ids
}

View File

@ -0,0 +1,12 @@
// This will fetch the set of all HA resource identifiers...
data "proxmox_virtual_environment_haresources" "all" {}
// ...which we will go through in order to fetch the whole record for each resource.
data "proxmox_virtual_environment_haresource" "example" {
for_each = data.proxmox_virtual_environment_haresources.all.resource_ids
resource_id = each.value
}
output "proxmox_virtual_environment_haresources_full" {
value = data.proxmox_virtual_environment_haresource.example
}

View File

@ -0,0 +1,14 @@
// This will fetch the set of all HA resource identifiers.
data "proxmox_virtual_environment_haresources" "example_all" {}
// This will fetch the set of HA resource identifiers that correspond to virtual machines.
data "proxmox_virtual_environment_haresources" "example_vm" {
type = "vm"
}
output "data_proxmox_virtual_environment_haresources" {
value = {
all = data.proxmox_virtual_environment_haresources.example_all.resource_ids
vms = data.proxmox_virtual_environment_haresources.example_vm.resource_ids
}
}

View File

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

View File

@ -0,0 +1,14 @@
resource "proxmox_virtual_environment_hagroup" "example" {
group = "example"
comment = "This is a comment."
# Member nodes, with or without priority.
nodes = {
node1 = null
node2 = 2
node3 = 1
}
restricted = true
no_failback = false
}

View File

@ -0,0 +1,3 @@
#!/usr/bin/env sh
# HA resources can be imported using their identifiers, e.g.:
terraform import proxmox_virtual_environment_haresource.example vm:123

View File

@ -0,0 +1,9 @@
resource "proxmox_virtual_environment_haresource" "example" {
depends_on = [
proxmox_virtual_environment_hagroup.example
]
resource_id = "vm:123"
state = "started"
group = "example"
comment = "Managed by Terraform"
}

View File

@ -0,0 +1,131 @@
/*
* 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 cluster
import (
"context"
"fmt"
"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/internal/structure"
"github.com/bpg/terraform-provider-proxmox/proxmox"
hagroups "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/groups"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &hagroupDatasource{}
_ datasource.DataSourceWithConfigure = &hagroupDatasource{}
)
// NewHAGroupDataSource is a helper function to simplify the provider implementation.
func NewHAGroupDataSource() datasource.DataSource {
return &hagroupDatasource{}
}
// hagroupDatasource is the data source implementation for full information about
// specific High Availability groups.
type hagroupDatasource struct {
client *hagroups.Client
}
// Metadata returns the data source type name.
func (d *hagroupDatasource) Metadata(
_ context.Context,
req datasource.MetadataRequest,
resp *datasource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_hagroup"
}
// Schema returns the schema for the data source.
func (d *hagroupDatasource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Retrieves information about a specific High Availability group.",
Attributes: map[string]schema.Attribute{
"id": structure.IDAttribute(),
"group": schema.StringAttribute{
Description: "The identifier of the High Availability group to read.",
Required: true,
},
"comment": schema.StringAttribute{
Description: "The comment associated with this group",
Computed: true,
},
"nodes": schema.MapAttribute{
Description: "The member nodes for this group. They are provided as a map, where the keys are the node " +
"names and the values represent their priority: integers for known priorities or `null` for unset " +
"priorities.",
Computed: true,
ElementType: types.Int64Type,
},
"no_failback": schema.BoolAttribute{
Description: "A flag that indicates that failing back to a higher priority node is disabled for this HA group.",
Computed: true,
},
"restricted": schema.BoolAttribute{
Description: "A flag that indicates that other nodes may not be used to run resources associated to this HA group.",
Computed: true,
},
},
}
}
// Configure adds the provider-configured client to the data source.
func (d *hagroupDatasource) 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. Please report this issue to the provider developers.",
req.ProviderData),
)
return
}
d.client = client.Cluster().HA().Groups()
}
// Read fetches the list of HA groups from the Proxmox cluster then converts it to a list of strings.
func (d *hagroupDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var state hagroupModel
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
groupID := state.Group.ValueString()
group, err := d.client.Get(ctx, groupID)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to read High Availability group '%s'", groupID),
err.Error(),
)
return
}
state.ID = types.StringValue(groupID)
resp.Diagnostics.Append(state.importFromAPI(*group)...)
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 cluster
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/internal/structure"
"github.com/bpg/terraform-provider-proxmox/proxmox"
hagroups "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/groups"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &hagroupsDatasource{}
_ datasource.DataSourceWithConfigure = &hagroupsDatasource{}
)
// NewHAGroupsDataSource is a helper function to simplify the provider implementation.
func NewHAGroupsDataSource() datasource.DataSource {
return &hagroupsDatasource{}
}
// hagroupsDatasource is the data source implementation for High Availability groups.
type hagroupsDatasource struct {
client *hagroups.Client
}
// hagroupsModel maps the schema data for the High Availability groups data source.
type hagroupsModel struct {
Groups types.Set `tfsdk:"group_ids"`
ID types.String `tfsdk:"id"`
}
// Metadata returns the data source type name.
func (d *hagroupsDatasource) Metadata(
_ context.Context,
req datasource.MetadataRequest,
resp *datasource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_hagroups"
}
// Schema returns the schema for the data source.
func (d *hagroupsDatasource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Retrieves the list of High Availability groups.",
Attributes: map[string]schema.Attribute{
"id": structure.IDAttribute(),
"group_ids": schema.SetAttribute{
Description: "The identifiers of the High Availability groups.",
ElementType: types.StringType,
Computed: true,
},
},
}
}
// Configure adds the provider-configured client to the data source.
func (d *hagroupsDatasource) 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. Please report this issue to the provider developers.",
req.ProviderData),
)
return
}
d.client = client.Cluster().HA().Groups()
}
// Read fetches the list of HA groups from the Proxmox cluster then converts it to a list of strings.
func (d *hagroupsDatasource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
var state hagroupsModel
list, err := d.client.List(ctx)
if err != nil {
resp.Diagnostics.AddError(
"Unable to read High Availability groups",
err.Error(),
)
return
}
groups := make([]attr.Value, len(list))
for i, v := range list {
groups[i] = types.StringValue(v.ID)
}
groupsValue, diags := types.SetValue(types.StringType, groups)
resp.Diagnostics.Append(diags...)
state.ID = types.StringValue("hagroups")
state.Groups = groupsValue
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

View File

@ -0,0 +1,144 @@
/*
* 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 cluster
import (
"context"
"fmt"
"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/bpg/terraform-provider-proxmox/internal/structure"
customtypes "github.com/bpg/terraform-provider-proxmox/internal/types"
"github.com/bpg/terraform-provider-proxmox/proxmox"
haresources "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/resources"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &haresourceDatasource{}
_ datasource.DataSourceWithConfigure = &haresourceDatasource{}
)
// NewHAResourceDataSource is a helper function to simplify the provider implementation.
func NewHAResourceDataSource() datasource.DataSource {
return &haresourceDatasource{}
}
// haresourceDatasource is the data source implementation for High Availability resources.
type haresourceDatasource struct {
client *haresources.Client
}
// Metadata returns the data source type name.
func (d *haresourceDatasource) Metadata(
_ context.Context,
req datasource.MetadataRequest,
resp *datasource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_haresource"
}
// Schema returns the schema for the data source.
func (d *haresourceDatasource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Retrieves the list of High Availability resources.",
Attributes: map[string]schema.Attribute{
"id": structure.IDAttribute(),
"resource_id": schema.StringAttribute{
Description: "The identifier of the Proxmox HA resource to read.",
Required: true,
Validators: []validator.String{
customtypes.HAResourceIDValidator(),
},
},
"type": schema.StringAttribute{
Description: "The type of High Availability resource (`vm` or `ct`).",
Computed: true,
},
"comment": schema.StringAttribute{
Description: "The comment associated with this resource.",
Computed: true,
},
"group": schema.StringAttribute{
Description: "The identifier of the High Availability group this resource is a member of.",
Computed: true,
},
"max_relocate": schema.Int64Attribute{
Description: "The maximal number of relocation attempts.",
Computed: true,
},
"max_restart": schema.Int64Attribute{
Description: "The maximal number of restart attempts.",
Computed: true,
},
"state": schema.StringAttribute{
Description: "The desired state of the resource.",
Computed: true,
},
},
}
}
// Configure adds the provider-configured client to the data source.
func (d *haresourceDatasource) Configure(
_ context.Context,
req datasource.ConfigureRequest,
resp *datasource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(proxmox.Client)
if ok {
d.client = client.Cluster().HA().Resources()
} else {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T. Please report this issue to the provider developers.",
req.ProviderData),
)
}
}
// Read fetches the specified HA resource.
func (d *haresourceDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data haresourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resID, err := customtypes.ParseHAResourceID(data.ResourceID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse configuration into a valid HA resource identifier: %s. "+
"Please report this issue to the provider developers.", err.Error()),
)
return
}
resource, err := d.client.Get(ctx, resID)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to read High Availability resource %v", resID),
err.Error(),
)
return
}
data.importFromAPI(resource)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@ -0,0 +1,163 @@
/*
* 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 cluster
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/internal/structure"
customtypes "github.com/bpg/terraform-provider-proxmox/internal/types"
"github.com/bpg/terraform-provider-proxmox/proxmox"
haresources "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/resources"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &haresourcesDatasource{}
_ datasource.DataSourceWithConfigure = &haresourcesDatasource{}
)
// NewHAResourcesDataSource is a helper function to simplify the provider implementation.
func NewHAResourcesDataSource() datasource.DataSource {
return &haresourcesDatasource{}
}
// haresourcesDatasource is the data source implementation for High Availability resources.
type haresourcesDatasource struct {
client *haresources.Client
}
// haresourcesModel maps the schema data for the High Availability resources data source.
type haresourcesModel struct {
// The Terraform resource identifier
ID types.String `tfsdk:"id"`
// The type of HA resources to fetch. If unset, all resources will be fetched.
Type types.String `tfsdk:"type"`
// The set of HA resource identifiers
Resources types.Set `tfsdk:"resource_ids"`
}
// Metadata returns the data source type name.
func (d *haresourcesDatasource) Metadata(
_ context.Context,
req datasource.MetadataRequest,
resp *datasource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_haresources"
}
// Schema returns the schema for the data source.
func (d *haresourcesDatasource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Retrieves the list of High Availability resources.",
Attributes: map[string]schema.Attribute{
"id": structure.IDAttribute(),
"type": schema.StringAttribute{
Description: "The type of High Availability resources to fetch (`vm` or `ct`). All resources " +
"will be fetched if this option is unset.",
Optional: true,
Validators: []validator.String{
stringvalidator.OneOf("ct", "vm"),
},
},
"resource_ids": schema.SetAttribute{
Description: "The identifiers of the High Availability resources.",
ElementType: types.StringType,
Computed: true,
},
},
}
}
// Configure adds the provider-configured client to the data source.
func (d *haresourcesDatasource) Configure(
_ context.Context,
req datasource.ConfigureRequest,
resp *datasource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(proxmox.Client)
if ok {
d.client = client.Cluster().HA().Resources()
} else {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T. Please report this issue to the provider developers.",
req.ProviderData),
)
}
}
// Read fetches the list of HA resources from the Proxmox cluster then converts it to a list of strings.
func (d *haresourcesDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var (
data haresourcesModel
fetchType *customtypes.HAResourceType
)
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
if data.Type.IsNull() {
data.ID = types.StringValue("haresources")
} else {
confType, err := customtypes.ParseHAResourceType(data.Type.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unexpected HA resource type",
fmt.Sprintf(
"Couldn't parse configuration into a valid HA resource type: %s. Please report this issue to the "+
"provider developers.", err.Error(),
),
)
return
}
fetchType = &confType
data.ID = types.StringValue(fmt.Sprintf("haresources:%v", confType))
}
list, err := d.client.List(ctx, fetchType)
if err != nil {
resp.Diagnostics.AddError(
"Unable to read High Availability resources",
err.Error(),
)
return
}
resources := make([]attr.Value, len(list))
for i, v := range list {
resources[i] = types.StringValue(v.ID.String())
}
resourcesValue, diags := types.SetValue(types.StringType, resources)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Resources = resourcesValue
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@ -0,0 +1,84 @@
/*
* 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 cluster
import (
"fmt"
"strconv"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
hagroups "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/groups"
)
// hagroupModel is the model used to represent a High Availability group.
type hagroupModel struct {
ID types.String `tfsdk:"id"` // Identifier used by Terrraform
Group types.String `tfsdk:"group"` // HA group name
Comment types.String `tfsdk:"comment"` // Comment, if present
Nodes types.Map `tfsdk:"nodes"` // Map of member nodes associated with their priorities
NoFailback types.Bool `tfsdk:"no_failback"` // Flag that disables failback
Restricted types.Bool `tfsdk:"restricted"` // Flag that prevents execution on other member nodes
}
// Import the contents of a HA group model from the API's response data.
func (m *hagroupModel) importFromAPI(group hagroups.HAGroupGetResponseData) diag.Diagnostics {
m.Comment = types.StringPointerValue(group.Comment)
m.NoFailback = group.NoFailback.ToValue()
m.Restricted = group.Restricted.ToValue()
return m.parseHAGroupNodes(group.Nodes)
}
// Parse the list of member nodes. The list is received from the Proxmox API as a string. It must
// be converted into a map value. Errors will be returned as Terraform diagnostics.
func (m *hagroupModel) parseHAGroupNodes(nodes string) diag.Diagnostics {
var diags diag.Diagnostics
nodesIn := strings.Split(nodes, ",")
nodesOut := make(map[string]attr.Value)
for _, nodeDescStr := range nodesIn {
nodeDesc := strings.Split(nodeDescStr, ":")
if len(nodeDesc) > 2 {
diags.AddWarning(
"Could not parse HA group node",
fmt.Sprintf("Received group node '%s' for HA group '%s'",
nodeDescStr, m.Group.ValueString()),
)
continue
}
priority := types.Int64Null()
if len(nodeDesc) == 2 {
prio, err := strconv.Atoi(nodeDesc[1])
if err == nil {
priority = types.Int64Value(int64(prio))
} else {
diags.AddWarning(
"Could not parse HA group node priority",
fmt.Sprintf("Node priority string '%s' for node %s of HA group '%s'",
nodeDesc[1], nodeDesc[0], m.Group.ValueString()),
)
}
}
nodesOut[nodeDesc[0]] = priority
}
value, mbDiags := types.MapValue(types.Int64Type, nodesOut)
diags.Append(mbDiags...)
m.Nodes = value
return diags
}

View File

@ -0,0 +1,114 @@
/*
* 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 cluster
import (
"fmt"
customtypes "github.com/bpg/terraform-provider-proxmox/internal/types"
haresources "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/resources"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// haresourceModel maps the schema data for the High Availability resource data source.
type haresourceModel struct {
// The Terraform resource identifier
ID types.String `tfsdk:"id"`
// The Proxmox HA resource identifier
ResourceID types.String `tfsdk:"resource_id"`
// The type of HA resources to fetch. If unset, all resources will be fetched.
Type types.String `tfsdk:"type"`
// The desired state of the resource.
State types.String `tfsdk:"state"`
// The comment associated with this resource.
Comment types.String `tfsdk:"comment"`
// The identifier of the High Availability group this resource is a member of.
Group types.String `tfsdk:"group"`
// The maximal number of relocation attempts.
MaxRelocate types.Int64 `tfsdk:"max_relocate"`
// The maximal number of restart attempts.
MaxRestart types.Int64 `tfsdk:"max_restart"`
}
// importFromAPI imports the contents of a HA resource model from the API's response data.
func (d *haresourceModel) importFromAPI(data *haresources.HAResourceGetResponseData) {
d.ID = data.ID.ToValue()
d.ResourceID = data.ID.ToValue()
d.Type = data.Type.ToValue()
d.State = data.State.ToValue()
d.Comment = types.StringPointerValue(data.Comment)
d.Group = types.StringPointerValue(data.Group)
d.MaxRelocate = types.Int64PointerValue(data.MaxRelocate)
d.MaxRestart = types.Int64PointerValue(data.MaxRestart)
}
// toRequestBase builds the common request data structure for HA resource creation or update API calls.
func (d haresourceModel) toRequestBase() haresources.HAResourceDataBase {
var state customtypes.HAResourceState
if d.State.IsNull() {
state = customtypes.HAResourceStateStarted
} else {
var err error
state, err = customtypes.ParseHAResourceState(d.State.ValueString())
if err != nil {
panic(fmt.Errorf(
"state string '%s' wrongly assumed to be valid; error: %w",
d.State.ValueString(), err,
))
}
}
return haresources.HAResourceDataBase{
State: state,
Comment: d.Comment.ValueStringPointer(),
Group: d.Group.ValueStringPointer(),
MaxRelocate: d.MaxRelocate.ValueInt64Pointer(),
MaxRestart: d.MaxRestart.ValueInt64Pointer(),
}
}
// toCreateRequest builds the request data structure for creating a new HA resource.
func (d haresourceModel) toCreateRequest(resID customtypes.HAResourceID) *haresources.HAResourceCreateRequestBody {
return &haresources.HAResourceCreateRequestBody{
ID: resID,
Type: &resID.Type,
HAResourceDataBase: d.toRequestBase(),
}
}
// toUpdateRequest builds the request data structure for updating an existing HA resource.
func (d haresourceModel) toUpdateRequest(state *haresourceModel) *haresources.HAResourceUpdateRequestBody {
del := []string{}
if d.Comment.IsNull() && !state.Comment.IsNull() {
del = append(del, "comment")
}
if d.Group.IsNull() && !state.Group.IsNull() {
del = append(del, "group")
}
if d.MaxRelocate.IsNull() && !state.MaxRelocate.IsNull() {
del = append(del, "max_relocate")
}
if d.MaxRestart.IsNull() && !state.MaxRestart.IsNull() {
del = append(del, "max_restart")
}
if len(del) == 0 {
del = nil
}
return &haresources.HAResourceUpdateRequestBody{
HAResourceDataBase: d.toRequestBase(),
Delete: del,
}
}

View File

@ -0,0 +1,339 @@
/*
* 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 cluster
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/internal/structure"
"github.com/bpg/terraform-provider-proxmox/proxmox"
hagroups "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/groups"
)
var (
_ resource.Resource = &hagroupResource{}
_ resource.ResourceWithConfigure = &hagroupResource{}
_ resource.ResourceWithImportState = &hagroupResource{}
)
// NewHAGroupResource creates a new resource for managing Linux Bridge network interfaces.
func NewHAGroupResource() resource.Resource {
return &hagroupResource{}
}
// hagroupResource contains the resource's internal data.
type hagroupResource struct {
// The HA groups API client
client hagroups.Client
}
// Metadata defines the name of the resource.
func (r *hagroupResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_hagroup"
}
// Schema defines the schema for the resource.
func (r *hagroupResource) Schema(
_ context.Context,
_ resource.SchemaRequest,
resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Manages a High Availability group in a Proxmox VE cluster.",
Attributes: map[string]schema.Attribute{
"id": structure.IDAttribute(),
"group": schema.StringAttribute{
Description: "The identifier of the High Availability group to manage.",
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9\-_\.]*[a-zA-Z0-9]$`),
"must start with a letter, end with a letter or number, be composed of "+
"letters, numbers, '-', '_' and '.', and must be at least 2 characters long",
),
},
},
"comment": schema.StringAttribute{
Description: "The comment associated with this group",
Optional: true,
Validators: []validator.String{
stringvalidator.UTF8LengthAtLeast(1),
stringvalidator.RegexMatches(regexp.MustCompile(`^[^\s]|^$`), "must not start with whitespace"),
stringvalidator.RegexMatches(regexp.MustCompile(`[^\s]$|^$`), "must not end with whitespace"),
},
},
"nodes": schema.MapAttribute{
Description: "The member nodes for this group. They are provided as a map, where the keys are the node " +
"names and the values represent their priority: integers for known priorities or `null` for unset " +
"priorities.",
Required: true,
ElementType: types.Int64Type,
Validators: []validator.Map{
mapvalidator.SizeAtLeast(1),
mapvalidator.KeysAre(
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$`),
"must be a valid Proxmox node name",
),
),
mapvalidator.ValueInt64sAre(int64validator.Between(0, 1000)),
},
},
"no_failback": schema.BoolAttribute{
Description: "A flag that indicates that failing back to a higher priority node is disabled for this HA " +
"group. Defaults to `false`.",
Computed: true,
Optional: true,
Default: booldefault.StaticBool(false),
},
"restricted": schema.BoolAttribute{
Description: "A flag that indicates that other nodes may not be used to run resources associated to this HA " +
"group. Defaults to `false`.",
Computed: true,
Optional: true,
Default: booldefault.StaticBool(false),
},
},
}
}
// Configure accesses the provider-configured Proxmox API client on behalf of the resource.
func (r *hagroupResource) 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().HA().Groups()
} else {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T. Please report this issue to the provider developers.",
req.ProviderData),
)
}
}
// Create creates a new HA group on the Proxmox cluster.
func (r *hagroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data hagroupModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
groupID := data.Group.ValueString()
createRequest := &hagroups.HAGroupCreateRequestBody{}
createRequest.ID = groupID
createRequest.Comment = data.Comment.ValueStringPointer()
createRequest.Nodes = r.groupNodesToString(data.Nodes)
createRequest.NoFailback.FromValue(data.NoFailback)
createRequest.Restricted.FromValue(data.Restricted)
createRequest.Type = "group"
err := r.client.Create(ctx, createRequest)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not create HA group '%s'.", groupID),
err.Error(),
)
return
}
data.ID = types.StringValue(groupID)
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
}
// Read reads a HA group definition from the Proxmox cluster.
func (r *hagroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data hagroupModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
found, diags := r.read(ctx, &data)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
if found {
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
} else {
resp.State.RemoveResource(ctx)
}
}
}
// Update updates a HA group definition on the Proxmox cluster.
func (r *hagroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data, state hagroupModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
updateRequest := &hagroups.HAGroupUpdateRequestBody{}
updateRequest.Comment = data.Comment.ValueStringPointer()
updateRequest.Nodes = r.groupNodesToString(data.Nodes)
updateRequest.NoFailback.FromValue(data.NoFailback)
updateRequest.Restricted.FromValue(data.Restricted)
if updateRequest.Comment == nil && !state.Comment.IsNull() {
updateRequest.Delete = "comment"
}
err := r.client.Update(ctx, state.Group.ValueString(), updateRequest)
if err == nil {
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
} else {
resp.Diagnostics.AddError(
"Error updating HA group",
fmt.Sprintf("Could not update HA group '%s', unexpected error: %s",
state.Group.ValueString(), err.Error()),
)
}
}
// Delete deletes a HA group definition.
func (r *hagroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data hagroupModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
groupID := data.Group.ValueString()
err := r.client.Delete(ctx, groupID)
if err != nil {
if strings.Contains(err.Error(), "no such ha group") {
resp.Diagnostics.AddWarning(
"HA group does not exist",
fmt.Sprintf(
"Could not delete HA group '%s', it does not exist or has been deleted outside of Terraform.",
groupID,
),
)
} else {
resp.Diagnostics.AddError(
"Error deleting HA group",
fmt.Sprintf("Could not delete HA group '%s', unexpected error: %s",
groupID, err.Error()),
)
}
}
}
// ImportState imports a HA group from the Proxmox cluster.
func (r *hagroupResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
reqID := req.ID
data := hagroupModel{
ID: types.StringValue(reqID),
Group: types.StringValue(reqID),
}
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
}
// readBack reads information about a created or modified HA group from the cluster then updates the response
// state accordingly. It is assumed that the `state`'s identifier is set.
func (r *hagroupResource) readBack(
ctx context.Context,
data *hagroupModel,
respDiags *diag.Diagnostics,
respState *tfsdk.State,
) {
found, diags := r.read(ctx, data)
respDiags.Append(diags...)
if !found {
respDiags.AddError(
"HA group not found after update",
"Failed to find the group when trying to read back the updated HA group's data.",
)
}
if !respDiags.HasError() {
respDiags.Append(respState.Set(ctx, *data)...)
}
}
// read reads information about a HA group from the cluster. The group identifier must have been set in the
// `data`.
func (r *hagroupResource) read(ctx context.Context, data *hagroupModel) (bool, diag.Diagnostics) {
name := data.Group.ValueString()
group, err := r.client.Get(ctx, name)
if err != nil {
var diags diag.Diagnostics
if !strings.Contains(err.Error(), "no such ha group") {
diags.AddError("Could not read HA group", err.Error())
}
return false, diags
}
return true, data.importFromAPI(*group)
}
// groupNodesToString converts the map of group member nodes into a string.
func (r *hagroupResource) groupNodesToString(nodes types.Map) string {
mbElements := nodes.Elements()
mbNodes := make([]string, len(mbElements))
i := 0
for name, value := range mbElements {
if value.IsNull() {
mbNodes[i] = name
} else {
mbNodes[i] = fmt.Sprintf("%s:%d", name, value.(types.Int64).ValueInt64())
}
i++
}
return strings.Join(mbNodes, ",")
}

View File

@ -0,0 +1,372 @@
/*
* 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 cluster
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/bpg/terraform-provider-proxmox/internal/structure"
customtypes "github.com/bpg/terraform-provider-proxmox/internal/types"
"github.com/bpg/terraform-provider-proxmox/proxmox"
haresources "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/resources"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// haresourceResource contains the resource's internal data.
type haresourceResource struct {
// The HA resources API client
client haresources.Client
}
// Ensure the resource implements the expected interfaces.
var (
_ resource.Resource = &haresourceResource{}
_ resource.ResourceWithConfigure = &haresourceResource{}
_ resource.ResourceWithImportState = &haresourceResource{}
)
// NewHAResourceResource returns a new resource for managing High Availability resources.
func NewHAResourceResource() resource.Resource {
return &haresourceResource{}
}
// Metadata defines the name of the resource.
func (r *haresourceResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_haresource"
}
// Schema defines the schema for the resource.
func (r *haresourceResource) Schema(
_ context.Context,
_ resource.SchemaRequest,
resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Manages Proxmox HA resources.",
Attributes: map[string]schema.Attribute{
"id": structure.IDAttribute(),
"resource_id": schema.StringAttribute{
Description: "The Proxmox HA resource identifier",
Required: true,
Validators: []validator.String{
customtypes.HAResourceIDValidator(),
},
},
"state": schema.StringAttribute{
Description: "The desired state of the resource.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("started"),
Validators: []validator.String{
customtypes.HAResourceStateValidator(),
},
},
"type": schema.StringAttribute{
MarkdownDescription: "The type of HA resources to create. If unset, it will be deduced from the `resource_id`.",
Computed: true,
Optional: true,
Validators: []validator.String{
customtypes.HAResourceTypeValidator(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"comment": schema.StringAttribute{
Description: "The comment associated with this resource.",
Optional: true,
Validators: []validator.String{
stringvalidator.UTF8LengthAtLeast(1),
stringvalidator.RegexMatches(regexp.MustCompile(`^[^\s]|^$`), "must not start with whitespace"),
stringvalidator.RegexMatches(regexp.MustCompile(`[^\s]$|^$`), "must not end with whitespace"),
},
},
"group": schema.StringAttribute{
Description: "The identifier of the High Availability group this resource is a member of.",
Optional: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9\-_\.]*[a-zA-Z0-9]$`),
"must start with a letter, end with a letter or number, be composed of "+
"letters, numbers, '-', '_' and '.', and must be at least 2 characters long",
),
},
},
"max_relocate": schema.Int64Attribute{
Description: "The maximal number of relocation attempts.",
Optional: true,
Validators: []validator.Int64{
int64validator.Between(0, 10),
},
},
"max_restart": schema.Int64Attribute{
Description: "The maximal number of restart attempts.",
Optional: true,
Validators: []validator.Int64{
int64validator.Between(0, 10),
},
},
},
}
}
// Configure adds the provider-configured client to the resource.
func (r *haresourceResource) 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().HA().Resources()
} else {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T. Please report this issue to the provider developers.",
req.ProviderData),
)
}
}
// Create creates a new HA resource.
func (r *haresourceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data haresourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resID, err := customtypes.ParseHAResourceID(data.ResourceID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse the Terraform resource ID into a valid HA resource identifier: %s. "+
"Please report this issue to the provider developers.", err),
)
return
}
createRequest := data.toCreateRequest(resID)
err = r.client.Create(ctx, createRequest)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not create HA resource '%v'.", resID),
err.Error(),
)
return
}
data.ID = types.StringValue(resID.String())
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
}
// Update updates an existing HA resource.
func (r *haresourceResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
var data, state haresourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
resID, err := customtypes.ParseHAResourceID(state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse the Terraform resource ID into a valid HA resource identifier: %s. "+
"Please report this issue to the provider developers.", err),
)
return
}
updateRequest := data.toUpdateRequest(&state)
err = r.client.Update(ctx, resID, updateRequest)
if err == nil {
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
} else {
resp.Diagnostics.AddError(
"Error updating HA resource",
fmt.Sprintf("Could not update HA resource '%s', unexpected error: %s",
state.Group.ValueString(), err.Error()),
)
}
}
// Delete deletes an existing HA resource.
func (r *haresourceResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) {
var data haresourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resID, err := customtypes.ParseHAResourceID(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse the Terraform resource ID into a valid HA resource identifier: %s. "+
"Please report this issue to the provider developers.", err),
)
return
}
err = r.client.Delete(ctx, resID)
if err != nil {
if strings.Contains(err.Error(), "no such resource") {
resp.Diagnostics.AddWarning(
"HA resource does not exist",
fmt.Sprintf(
"Could not delete HA resource '%v', it does not exist or has been deleted outside of Terraform.",
resID,
),
)
} else {
resp.Diagnostics.AddError(
"Error deleting HA resource",
fmt.Sprintf("Could not delete HA resource '%v', unexpected error: %s",
resID, err.Error()),
)
}
}
}
// Read reads the HA resource.
func (r *haresourceResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) {
var data haresourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
found, diags := r.read(ctx, &data)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
if found {
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
} else {
resp.State.RemoveResource(ctx)
}
}
}
// ImportState imports a HA resource from the Proxmox cluster.
func (r *haresourceResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
reqID := req.ID
data := haresourceModel{
ID: types.StringValue(reqID),
ResourceID: types.StringValue(reqID),
}
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
}
// read reads information about a HA resource from the cluster. The Terraform resource identifier must have been set
// in the model before this function is called.
func (r *haresourceResource) read(ctx context.Context, data *haresourceModel) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
resID, err := customtypes.ParseHAResourceID(data.ID.ValueString())
if err != nil {
diags.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse the Terraform resource ID into a valid HA resource identifier: %s. "+
"Please report this issue to the provider developers.", err),
)
return false, diags
}
resource, err := r.client.Get(ctx, resID)
if err != nil {
if !strings.Contains(err.Error(), "no such resource") {
diags.AddError("Could not read HA resource", err.Error())
}
return false, diags
}
data.importFromAPI(resource)
return true, nil
}
// readBack reads information about a created or modified HA resource from the cluster then updates the response
// state accordingly. It is assumed that the `state`'s identifier is set.
func (r *haresourceResource) readBack(
ctx context.Context,
data *haresourceModel,
respDiags *diag.Diagnostics,
respState *tfsdk.State,
) {
found, diags := r.read(ctx, data)
respDiags.Append(diags...)
if !found {
respDiags.AddError(
"HA resource not found after update",
"Failed to find the resource when trying to read back the updated HA resource's data.",
)
}
if !respDiags.HasError() {
respDiags.Append(respState.Set(ctx, *data)...)
}
}

View File

@ -23,6 +23,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/internal/cluster"
"github.com/bpg/terraform-provider-proxmox/internal/network"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
@ -348,6 +349,8 @@ func (p *proxmoxProvider) Configure(
func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
cluster.NewHAGroupResource,
cluster.NewHAResourceResource,
network.NewLinuxBridgeResource,
network.NewLinuxVLANResource,
}
@ -356,6 +359,10 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewVersionDataSource,
cluster.NewHAGroupsDataSource,
cluster.NewHAGroupDataSource,
cluster.NewHAResourcesDataSource,
cluster.NewHAResourceDataSource,
}
}

View File

@ -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 structure
import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
)
// IDAttribute generates an attribute definition suitable for the always-present `id` attribute.
func IDAttribute() schema.StringAttribute {
return schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
}
}

View File

@ -12,6 +12,8 @@ import (
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// CustomBool allows a JSON boolean value to also be an integer.
@ -63,6 +65,16 @@ func (r *CustomBool) PointerBool() *bool {
return (*bool)(r)
}
// ToValue returns a Terraform attribute value.
func (r CustomBool) ToValue() types.Bool {
return types.BoolValue(bool(r))
}
// FromValue sets the numeric boolean based on the value of a Terraform attribute.
func (r *CustomBool) FromValue(tfValue types.Bool) {
*r = CustomBool(tfValue.ValueBool())
}
// MarshalJSON converts a boolean to a JSON value.
func (r *CustomCommaSeparatedList) MarshalJSON() ([]byte, error) {
s := strings.Join(*r, ",")

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 types
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/google/go-querystring/query"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/internal/validators"
)
// NOTE: the linter believes the `HAResourceID` structure below should be tagged with `json:` due to some values of it
// being passed to a JSON marshaler in the tests. As far as I can tell this is unnecessary, so I'm silencing the lint.
// HAResourceID represents a HA resource identifier, composed of a resource type and identifier.
//
//nolint:musttag
type HAResourceID struct {
Type HAResourceType // The type of this HA resource.
Name string // The name of the element the HA resource refers to.
}
// Ensure the HA resource identifier type implements various interfaces.
var (
_ fmt.Stringer = &HAResourceID{}
_ json.Marshaler = &HAResourceID{}
_ json.Unmarshaler = &HAResourceID{}
_ query.Encoder = &HAResourceID{}
)
// ParseHAResourceID parses a string that represents a HA resource identifier into a value of `HAResourceID`.
func ParseHAResourceID(input string) (HAResourceID, error) {
resID := HAResourceID{}
inParts := strings.SplitN(input, ":", 2)
if len(inParts) < 2 {
return resID, fmt.Errorf("'%s' is not a valid HA resource identifier", input)
}
resType, err := ParseHAResourceType(inParts[0])
if err != nil {
return resID, fmt.Errorf("could not extract type from HA resource identifier '%s': %w", input, err)
}
// For types VM and Container, we know the resource "name" should be a valid integer between 100
// and 999_999_999.
if resType == HAResourceTypeVM || resType == HAResourceTypeContainer {
id, err := strconv.Atoi(inParts[1])
if err != nil {
return resID, fmt.Errorf("invalid %s HA resource name '%s': %w", resType, inParts[1], err)
}
if id < 100 {
return resID, fmt.Errorf("invalid %s HA resource name '%s': minimum value is 100", resType, inParts[1])
}
if id > 999_999_999 {
return resID, fmt.Errorf("invalid %s HA resource name '%s': maximum value is 999999999", resType, inParts[1])
}
}
resID.Type = resType
resID.Name = inParts[1]
return resID, nil
}
// HAResourceIDValidator returns a new HA resource identifier validator.
func HAResourceIDValidator() validator.String {
return validators.NewParseValidator(ParseHAResourceID, "value must be a valid HA resource identifier")
}
// String converts a HAResourceID value into a string.
func (rid HAResourceID) String() string {
return fmt.Sprintf("%s:%s", rid.Type, rid.Name)
}
// MarshalJSON marshals a HA resource identifier into JSON value.
func (rid HAResourceID) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(rid.String())
if err != nil {
return nil, fmt.Errorf("cannot marshal HA resource identifier: %w", err)
}
return bytes, nil
}
// UnmarshalJSON unmarshals a Proxmox HA resource identifier.
func (rid *HAResourceID) UnmarshalJSON(b []byte) error {
var ridString string
err := json.Unmarshal(b, &ridString)
if err != nil {
return fmt.Errorf("cannot unmarshal HA resource type: %w", err)
}
resType, err := ParseHAResourceID(ridString)
if err == nil {
*rid = resType
}
return err
}
// EncodeValues encodes a HA resource ID field into an URL-encoded set of values.
func (rid HAResourceID) EncodeValues(key string, v *url.Values) error {
v.Add(key, rid.String())
return nil
}
// ToValue converts a HA resource ID into a Terraform value.
func (rid HAResourceID) ToValue() types.String {
return types.StringValue(rid.String())
}

View File

@ -0,0 +1,128 @@
/*
* 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 types
import (
"bytes"
"encoding/json"
"testing"
)
func TestParseHAResourceID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
want HAResourceID
wantErr bool
}{
{"VM value", "vm:123", HAResourceID{HAResourceTypeVM, "123"}, false},
{"container value", "ct:123", HAResourceID{HAResourceTypeContainer, "123"}, false},
{"no semicolon", "ct", HAResourceID{}, true},
{"invalid type", "blah:123", HAResourceID{}, true},
{"invalid VM name", "vm:moo", HAResourceID{}, true},
{"invalid container name", "ct:moo", HAResourceID{}, true},
{"VM name too low", "vm:99", HAResourceID{}, true},
{"VM name too high", "vm:1000000000", HAResourceID{}, true},
{"container name too low", "ct:99", HAResourceID{}, true},
{"container name too high", "ct:1000000000", HAResourceID{}, true},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := ParseHAResourceID(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseHAResourceID() error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("ParseHAResourceID() got = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceIDToString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
state HAResourceID
want string
}{
{"stringify VM", HAResourceID{HAResourceTypeVM, "123"}, "vm:123"},
{"stringify CT", HAResourceID{HAResourceTypeContainer, "123"}, "ct:123"},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := tt.state.String(); got != tt.want {
t.Errorf("HAResourceID.String() = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceIDToJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
state HAResourceID
want string
}{
{"jsonify VM", HAResourceID{HAResourceTypeVM, "123"}, `"vm:123"`},
{"jsonify CT", HAResourceID{HAResourceTypeContainer, "123"}, `"ct:123"`},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := json.Marshal(tt.state)
if err != nil {
t.Errorf("json.Marshal(HAResourceID): err = %v", err)
} else if !bytes.Equal(got, []byte(tt.want)) {
t.Errorf("json.Marshal(HAResourceID) = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceIDFromJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
want HAResourceID
wantErr bool
}{
{"VM", `"vm:123"`, HAResourceID{HAResourceTypeVM, "123"}, false},
{"container", `"ct:123"`, HAResourceID{HAResourceTypeContainer, "123"}, false},
{"invalid JSON", `\\/yo`, HAResourceID{}, true},
{"incompatible type", `["yo"]`, HAResourceID{}, true},
{"invalid content", `"nope:notatall"`, HAResourceID{}, true},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got HAResourceID
err := json.Unmarshal([]byte(tt.json), &got)
if (err != nil) != tt.wantErr {
t.Errorf("json.Unmarshal(HAResourceID) error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("json.Unmarshal(HAResourceID) got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,127 @@
/*
* 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 types
import (
"encoding/json"
"fmt"
"net/url"
"github.com/google/go-querystring/query"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/internal/validators"
)
// HAResourceState represents the requested state of a HA resource.
type HAResourceState int
// Ensure various interfaces are supported by the HA resource state type.
// NOTE: the global variable created here is only meant to be used in this block. There is, to my knowledge, no
// other way to enforce interface implementation at compile time unless the value is wrapped into a struct. Because
// of this, the linter is disabled.
var (
//nolint:gochecknoglobals
_haResourceStateValue HAResourceState
_ fmt.Stringer = &_haResourceStateValue
_ json.Marshaler = &_haResourceStateValue
_ json.Unmarshaler = &_haResourceStateValue
_ query.Encoder = &_haResourceStateValue
)
const (
// HAResourceStateStarted indicates that a HA resource should be started.
HAResourceStateStarted HAResourceState = 0
// HAResourceStateStopped indicates that a HA resource should be stopped, but that it should still be relocated
// on node failure.
HAResourceStateStopped HAResourceState = 1
// HAResourceStateDisabled indicates that a HA resource should be stopped. No relocation should occur on node failure.
HAResourceStateDisabled HAResourceState = 2
// HAResourceStateIgnored indicates that a HA resource is not managed by the cluster resource manager. No relocation
// or status change will occur.
HAResourceStateIgnored HAResourceState = 3
)
// ParseHAResourceState converts the string representation of a HA resource state into the corresponding
// enum value. An error is returned if the input string does not match any known state. This function also
// parses the `enabled` value which is an alias for `started`.
func ParseHAResourceState(input string) (HAResourceState, error) {
switch input {
case "started":
return HAResourceStateStarted, nil
case "enabled":
return HAResourceStateStarted, nil
case "stopped":
return HAResourceStateStopped, nil
case "disabled":
return HAResourceStateDisabled, nil
case "ignored":
return HAResourceStateIgnored, nil
default:
return HAResourceStateIgnored, fmt.Errorf("illegal HA resource state '%s'", input)
}
}
// HAResourceStateValidator returns a new HA resource state validator.
func HAResourceStateValidator() validator.String {
return validators.NewParseValidator(ParseHAResourceState, "value must be a valid HA resource state")
}
// String converts a HAResourceState value into a string.
func (s HAResourceState) String() string {
switch s {
case HAResourceStateStarted:
return "started"
case HAResourceStateStopped:
return "stopped"
case HAResourceStateDisabled:
return "disabled"
case HAResourceStateIgnored:
return "ignored"
default:
panic(fmt.Sprintf("unknown HA resource state value: %d", s))
}
}
// MarshalJSON marshals a HA resource state into JSON value.
func (s HAResourceState) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(s.String())
if err != nil {
return nil, fmt.Errorf("cannot marshal HA resource state: %w", err)
}
return bytes, nil
}
// UnmarshalJSON unmarshals a Proxmox HA resource state.
func (s *HAResourceState) UnmarshalJSON(b []byte) error {
var stateString string
err := json.Unmarshal(b, &stateString)
if err != nil {
return fmt.Errorf("cannot unmarshal HA resource state: %w", err)
}
state, err := ParseHAResourceState(stateString)
if err == nil {
*s = state
}
return err
}
// EncodeValues encodes a HA resource state field into an URL-encoded set of values.
func (s HAResourceState) EncodeValues(key string, v *url.Values) error {
v.Add(key, s.String())
return nil
}
// ToValue converts a HA resource state into a Terraform value.
func (s HAResourceState) ToValue() types.String {
return types.StringValue(s.String())
}

View File

@ -0,0 +1,131 @@
/*
* 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 types
import (
"bytes"
"encoding/json"
"testing"
)
func TestParseHAResourceState(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
want HAResourceState
wantErr bool
}{
{"valid value started", "started", HAResourceStateStarted, false},
{"valid value enabled", "enabled", HAResourceStateStarted, false},
{"valid value stopped", "stopped", HAResourceStateStopped, false},
{"valid value disabled", "disabled", HAResourceStateDisabled, false},
{"valid value ignored", "ignored", HAResourceStateIgnored, false},
{"empty value", "", HAResourceStateIgnored, true},
{"invalid value", "blah", HAResourceStateIgnored, true},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := ParseHAResourceState(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseHAResourceState() error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("ParseHAResourceState() got = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceStateToString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
state HAResourceState
want string
}{
{"stringify started", HAResourceStateStarted, "started"},
{"stringify stopped", HAResourceStateStopped, "stopped"},
{"stringify disabled", HAResourceStateDisabled, "disabled"},
{"stringify ignored", HAResourceStateIgnored, "ignored"},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := tt.state.String(); got != tt.want {
t.Errorf("HAResourceState.String() = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceStateToJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
state HAResourceState
want string
}{
{"jsonify started", HAResourceStateStarted, `"started"`},
{"jsonify stopped", HAResourceStateStopped, `"stopped"`},
{"jsonify disabled", HAResourceStateDisabled, `"disabled"`},
{"jsonify ignored", HAResourceStateIgnored, `"ignored"`},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := json.Marshal(tt.state)
if err != nil {
t.Errorf("json.Marshal(HAResourceState): err = %v", err)
} else if !bytes.Equal(got, []byte(tt.want)) {
t.Errorf("json.Marshal(HAResourceState) = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceStateFromJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
want HAResourceState
wantErr bool
}{
{"started", `"started"`, HAResourceStateStarted, false},
{"stopped", `"stopped"`, HAResourceStateStopped, false},
{"disabled", `"disabled"`, HAResourceStateDisabled, false},
{"ignored", `"ignored"`, HAResourceStateIgnored, false},
{"invalid JSON", `\\/yo`, HAResourceStateIgnored, true},
{"incompatible type", `["yo"]`, HAResourceStateIgnored, true},
{"invalid content", `"nope"`, HAResourceStateIgnored, true},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got HAResourceState
err := json.Unmarshal([]byte(tt.json), &got)
if (err != nil) != tt.wantErr {
t.Errorf("json.Unmarshal(HAResourceState) error = %v, wantErr %v", err, tt.wantErr)
}
if err == nil && got != tt.want {
t.Errorf("json.Unmarshal(HAResourceState) got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,108 @@
/*
* 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 types
import (
"encoding/json"
"fmt"
"net/url"
"github.com/google/go-querystring/query"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/internal/validators"
)
// HAResourceType represents the type of a HA resource.
type HAResourceType int
// Ensure various interfaces are supported by the HA resource type type.
// NOTE: to my knowledge, this "global" here is required for the static type checks to work.
var (
//nolint:gochecknoglobals
_haResourceTypeValue HAResourceType
_ fmt.Stringer = &_haResourceTypeValue
_ json.Marshaler = &_haResourceTypeValue
_ json.Unmarshaler = &_haResourceTypeValue
_ query.Encoder = &_haResourceTypeValue
)
const (
// HAResourceTypeVM indicates that a HA resource refers to a virtual machine.
HAResourceTypeVM HAResourceType = 0
// HAResourceTypeContainer indicates that a HA resource refers to a container.
HAResourceTypeContainer HAResourceType = 1
)
// ParseHAResourceType converts the string representation of a HA resource type into the corresponding
// enum value. An error is returned if the input string does not match any known type.
func ParseHAResourceType(input string) (HAResourceType, error) {
switch input {
case "vm":
return HAResourceTypeVM, nil
case "ct":
return HAResourceTypeContainer, nil
default:
return _haResourceTypeValue, fmt.Errorf("illegal HA resource type '%s'", input)
}
}
// HAResourceTypeValidator returns a new HA resource type validator.
func HAResourceTypeValidator() validator.String {
return validators.NewParseValidator(ParseHAResourceType, "value must be a valid HA resource type")
}
// String converts a HAResourceType value into a string.
func (t HAResourceType) String() string {
switch t {
case HAResourceTypeVM:
return "vm"
case HAResourceTypeContainer:
return "ct"
default:
panic(fmt.Sprintf("unknown HA resource type value: %d", t))
}
}
// MarshalJSON marshals a HA resource type into JSON value.
func (t HAResourceType) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(t.String())
if err != nil {
return nil, fmt.Errorf("cannot marshal HA resource type: %w", err)
}
return bytes, nil
}
// UnmarshalJSON unmarshals a Proxmox HA resource type.
func (t *HAResourceType) UnmarshalJSON(b []byte) error {
var rtString string
err := json.Unmarshal(b, &rtString)
if err != nil {
return fmt.Errorf("cannot unmarshal HA resource type: %w", err)
}
resType, err := ParseHAResourceType(rtString)
if err == nil {
*t = resType
}
return err
}
// EncodeValues encodes a HA resource type field into an URL-encoded set of values.
func (t HAResourceType) EncodeValues(key string, v *url.Values) error {
v.Add(key, t.String())
return nil
}
// ToValue converts a HA resource type into a Terraform value.
func (t HAResourceType) ToValue() types.String {
return types.StringValue(t.String())
}

View File

@ -0,0 +1,122 @@
/*
* 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 types
import (
"bytes"
"encoding/json"
"testing"
)
func TestParseHAResourceType(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
want HAResourceType
wantErr bool
}{
{"valid value vm", "vm", HAResourceTypeVM, false},
{"valid value ct", "ct", HAResourceTypeContainer, false},
{"empty value", "", _haResourceTypeValue, true},
{"invalid value", "blah", _haResourceTypeValue, true},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := ParseHAResourceType(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseHAResourceType() error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("ParseHAResourceType() got = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceTypeToString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
resType HAResourceType
want string
}{
{"stringify vm", HAResourceTypeVM, "vm"},
{"stringify ct", HAResourceTypeContainer, "ct"},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := tt.resType.String(); got != tt.want {
t.Errorf("HAResourceType.String() = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceTypeToJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
state HAResourceType
want string
}{
{"jsonify vm", HAResourceTypeVM, `"vm"`},
{"jsonify container", HAResourceTypeContainer, `"ct"`},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := json.Marshal(tt.state)
if err != nil {
t.Errorf("json.Marshal(HAResourceType): err = %v", err)
} else if !bytes.Equal(got, []byte(tt.want)) {
t.Errorf("json.Marshal(HAResourceType) = %v, want %v", got, tt.want)
}
})
}
}
func TestHAResourceTypeFromJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
want HAResourceType
wantErr bool
}{
{"started", `"vm"`, HAResourceTypeVM, false},
{"container", `"ct"`, HAResourceTypeContainer, false},
{"invalid JSON", `\\/yo`, HAResourceTypeVM, true},
{"incompatible type", `["yo"]`, HAResourceTypeVM, true},
{"invalid content", `"nope"`, HAResourceTypeVM, true},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got HAResourceType
err := json.Unmarshal([]byte(tt.json), &got)
if (err != nil) != tt.wantErr {
t.Errorf("json.Unmarshal(HAResourceType) error = %v, wantErr %v", err, tt.wantErr)
}
if err == nil && got != tt.want {
t.Errorf("json.Unmarshal(HAResourceType) got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,59 @@
/*
* 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 validators
import (
"context"
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)
// NewParseValidator creates a validator which uses a parsing function to validate a string. The function is expected
// to return a value of type `T` and an error. If the error is non-nil, the validator will fail. The `description`
// argument should contain a description of the validator's effect.
func NewParseValidator[T any](parseFunction func(string) (T, error), description string) validator.String {
return &parseValidator[T]{
parseFunction: parseFunction,
description: description,
}
}
// parseValidator is a validator which uses a parsing function to validate a string.
type parseValidator[T any] struct {
parseFunction func(string) (T, error)
description string
}
func (val *parseValidator[T]) Description(_ context.Context) string {
return val.description
}
func (val *parseValidator[T]) MarkdownDescription(_ context.Context) string {
return val.description
}
func (val *parseValidator[T]) ValidateString(
ctx context.Context,
request validator.StringRequest,
response *validator.StringResponse,
) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}
value := request.ConfigValue
_, err := val.parseFunction(value.ValueString())
if err != nil {
response.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic(
request.Path,
val.Description(ctx),
value.String(),
))
}
}

View File

@ -37,10 +37,10 @@ type Client interface {
// Version returns a client for getting the version of the Proxmox Virtual Environment API.
Version() *version.Client
// API returns a lower-lever REST API client.
// API returns a lower-level REST API client.
API() api.Client
// SSH returns a lower-lever SSH client.
// SSH returns a lower-level SSH client.
SSH() ssh.Client
}

View File

@ -11,6 +11,7 @@ import (
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
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/firewall"
)
@ -30,3 +31,8 @@ func (c *Client) Firewall() clusterfirewall.API {
Client: firewall.Client{Client: c},
}
}
// HA returns a client for managing the cluster's High Availability features.
func (c *Client) HA() *ha.Client {
return &ha.Client{Client: c}
}

View File

@ -0,0 +1,35 @@
/*
* 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 ha
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
hagroups "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/groups"
haresources "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/resources"
)
// Client is an interface for accessing the Proxmox High Availability 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/ha/%s", path)
}
// Groups returns a client for managing the cluster's High Availability groups.
func (c *Client) Groups() *hagroups.Client {
return &hagroups.Client{Client: c.Client}
}
// Resources returns a client for managing the cluster's High Availability resources.
func (c *Client) Resources() *haresources.Client {
return &haresources.Client{Client: c.Client}
}

View File

@ -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 groups
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Client is an interface for accessing the Proxmox High Availability groups API.
type Client struct {
api.Client
}
// ExpandPath expands a relative path to the HA groups management API path.
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("cluster/ha/groups/%s", path)
}

View File

@ -0,0 +1,86 @@
/*
* 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 groups
import (
"context"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// List retrieves the list of HA groups.
func (c *Client) List(ctx context.Context) ([]*HAGroupListResponseData, error) {
resBody := &HAGroupListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(""), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error listing HA groups: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
return resBody.Data, nil
}
// Get retrieves a single HA group based on its identifier.
func (c *Client) Get(ctx context.Context, groupID string) (*HAGroupGetResponseData, error) {
resBody := &HAGroupGetResponseBody{}
err := c.DoRequest(
ctx, http.MethodGet,
c.ExpandPath(url.PathEscape(groupID)), nil, resBody,
)
if err != nil {
return nil, fmt.Errorf("error reading HA group: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// Create creates a new HA group.
func (c *Client) Create(ctx context.Context, data *HAGroupCreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(""), data, nil)
if err != nil {
return fmt.Errorf("error creating HA group: %w", err)
}
return nil
}
// Update updates a HA group's configuration.
func (c *Client) Update(ctx context.Context, groupID string, data *HAGroupUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(url.PathEscape(groupID)), data, nil)
if err != nil {
return fmt.Errorf("error updating HA group: %w", err)
}
return nil
}
// Delete deletes a HA group.
func (c *Client) Delete(ctx context.Context, groupID string) error {
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(url.PathEscape(groupID)), nil, nil)
if err != nil {
return fmt.Errorf("error deleting HA group: %w", err)
}
return nil
}

View File

@ -0,0 +1,67 @@
/*
* 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 groups
import "github.com/bpg/terraform-provider-proxmox/internal/types"
// HAGroupListResponseBody contains the body from a HA group list response.
type HAGroupListResponseBody struct {
Data []*HAGroupListResponseData `json:"data,omitempty"`
}
// HAGroupListResponseData contains the data from a HA group list response.
type HAGroupListResponseData struct {
ID string `json:"group"`
}
// HAGroupGetResponseBody contains the body from a HA group get response.
type HAGroupGetResponseBody struct {
Data *HAGroupGetResponseData `json:"data,omitempty"`
}
// HAGroupDataBase contains fields which are both received from and send to the HA group API.
type HAGroupDataBase struct {
// A SHA1 digest of the group's configuration.
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
// The group's comment, if defined
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
// A comma-separated list of node fields. Each node field contains a node name, and may
// include a priority, with a semicolon acting as a separator.
Nodes string `json:"nodes" url:"nodes"`
// A boolean (0/1) indicating that failing back to the highest priority node is disabled.
NoFailback types.CustomBool `json:"nofailback" url:"nofailback,int"`
// A boolean (0/1) indicating that associated resources cannot run on other nodes.
Restricted types.CustomBool `json:"restricted" url:"restricted,int"`
}
// HAGroupGetResponseData contains the data from a HA group get response.
type HAGroupGetResponseData struct {
// The group's data
HAGroupDataBase
// The group's identifier
ID string `json:"group"`
// The type. Always set to `group`.
Type string `json:"type"`
}
// HAGroupCreateRequestBody contains the data which must be sent when creating a HA group.
type HAGroupCreateRequestBody struct {
// The group's data
HAGroupDataBase
// The group's identifier
ID string `url:"group"`
// The type. Always set to `group`.
Type string `url:"type"`
}
// HAGroupUpdateRequestBody contains the data which must be sent when updating a HA group.
type HAGroupUpdateRequestBody struct {
// The group's data
HAGroupDataBase
// A list of settings to delete
Delete string `url:"delete"`
}

View File

@ -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 resources
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Client is an interface for accessing the Proxmox High Availability resources management API.
type Client struct {
api.Client
}
// ExpandPath expands a relative path to the HA resources management API path.
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("cluster/ha/resources/%s", path)
}

View File

@ -0,0 +1,92 @@
/*
* 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 resources
import (
"context"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/internal/types"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
type haResourceTypeListQuery struct {
ResType *types.HAResourceType `url:"type"`
}
// List retrieves the list of HA resources. If the `resType` argument is `nil`, all resources will be returned;
// otherwise resources will be filtered by the specified type (either `ct` or `vm`).
func (c *Client) List(ctx context.Context, resType *types.HAResourceType) ([]*HAResourceListResponseData, error) {
options := &haResourceTypeListQuery{resType}
resBody := &HAResourceListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(""), options, resBody)
if err != nil {
return nil, fmt.Errorf("error listing HA resources: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID.Type < resBody.Data[j].ID.Type ||
(resBody.Data[i].ID.Type == resBody.Data[j].ID.Type &&
resBody.Data[i].ID.Name < resBody.Data[j].ID.Name)
})
return resBody.Data, nil
}
// Get retrieves the configuration of a single HA resource.
func (c *Client) Get(ctx context.Context, id types.HAResourceID) (*HAResourceGetResponseData, error) {
resBody := &HAResourceGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(url.PathEscape(id.String())), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error reading HA resource: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// Create creates a new HA resource.
func (c *Client) Create(ctx context.Context, data *HAResourceCreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(""), data, nil)
if err != nil {
return fmt.Errorf("error creating HA resource: %w", err)
}
return nil
}
// Update updates an existing HA resource.
func (c *Client) Update(ctx context.Context, id types.HAResourceID, data *HAResourceUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(url.PathEscape(id.String())), data, nil)
if err != nil {
return fmt.Errorf("error updating HA resource %v: %w", id, err)
}
return nil
}
// Delete deletes a HA resource.
func (c *Client) Delete(ctx context.Context, id types.HAResourceID) error {
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(url.PathEscape(id.String())), nil, nil)
if err != nil {
return fmt.Errorf("error deleting HA resource %v: %w", id, err)
}
return nil
}

View File

@ -0,0 +1,68 @@
/*
* 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 resources
import "github.com/bpg/terraform-provider-proxmox/internal/types"
// HAResourceListResponseBody contains the body from a HA resource list response.
type HAResourceListResponseBody struct {
Data []*HAResourceListResponseData `json:"data,omitempty"`
}
// HAResourceListResponseData contains the data from a HA resource list response.
type HAResourceListResponseData struct {
ID types.HAResourceID `json:"sid"`
}
// HAResourceGetResponseBody contains the body from a HA resource get response.
type HAResourceGetResponseBody struct {
Data *HAResourceGetResponseData `json:"data,omitempty"`
}
// HAResourceDataBase contains data common to all HA resource API calls.
type HAResourceDataBase struct {
// Resource comment, if defined
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
// HA group identifier, if the resource is part of one.
Group *string `json:"group,omitempty" url:"group,omitempty"`
// Maximal number of service relocation attempts.
MaxRelocate *int64 `json:"max_relocate,omitempty" url:"max_relocate,omitempty"`
// Maximal number of service restart attempts.
MaxRestart *int64 `json:"max_restart" url:"max_restart,omitempty"`
// Requested resource state.
State types.HAResourceState `json:"state" url:"state"`
}
// HAResourceGetResponseData contains data received from the HA resource API when requesting information about a single
// HA resource.
type HAResourceGetResponseData struct {
HAResourceDataBase
// Identifier of this resource
ID types.HAResourceID `json:"sid"`
// Type of this resource
Type types.HAResourceType `json:"type"`
// SHA-1 digest of the resources' configuration.
Digest *string `json:"digest,omitempty"`
}
// HAResourceCreateRequestBody contains data received from the HA resource API when creating a new HA resource.
type HAResourceCreateRequestBody struct {
HAResourceDataBase
// Identifier of this resource
ID types.HAResourceID `url:"sid"`
// Type of this resource
Type *types.HAResourceType `url:"type,omitempty"`
// SHA-1 digest of the resources' configuration.
Digest *string `url:"comment,omitempty"`
}
// HAResourceUpdateRequestBody contains data received from the HA resource API when updating an existing HA resource.
type HAResourceUpdateRequestBody struct {
HAResourceDataBase
// Settings that must be deleted from the resource's configuration
Delete []string `url:"delete,omitempty,comma"`
}

View File

@ -1,12 +1,12 @@
//go:build tools
// +build tools
/*
* 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/.
*/
//go:build tools
// +build tools
package tools
// Manage tool dependencies via go.mod.
@ -29,5 +29,11 @@ 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 ../build/docs-gen/data-sources/virtual_environment_version.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_hagroup.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_hagroups.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_haresource.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_haresources.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_network_linux_bridge.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_network_linux_vlan.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_hagroup.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_haresource.md ../docs/resources/