0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-07-02 03:22:59 +00:00
terraform-provider-proxmox/fwprovider/nodes/apt/models.go
Sven Greb 357f7c70a7
feat(node): implement initial support to manage APT repositories (#1325)
* feat(nodes): Initial support to manage APT repositories

> Summary

This commit implements initial support for managing APT repositories
which is (currently) limited to…

- …adding "standard" repositories to allow to configure it.
- toggling the activation status (enabled/disabled) of any configured
  repository.

+ !WARNING!
+ Note that deleting or modifying a repository in any other way is
+ (sadly) not possible (yet?)!
+ The limited functionality is due to the (current) capabilities of
+ the Proxmox VE APT repository API [1] itself.

>> Why are there two resources for one API entity?

Even though an APT repository should be seen as a single API entity, it
was required to implement standard repositories as dedicated
`proxmox_virtual_environment_apt_standard_repository`. This is because
standard repositories must be configured (added) first to the default
source list files because their activation status can be toggled. This
is handled by the HTTP `PUT` request, but the modifying request is
`POST` which would require two calls within the same Terraform execution
cycle. I tried to implement it in a single resource and it worked out
mostly after some handling some edges cases, but in the end there were
still too many situations an edge cases where it might break due to
Terraform state drifts between states. In the end the dedicated
resources are way cleaner and easier to use without no complexity and
conditional attribute juggling for practitioners.

>> Other "specialties"

Unfortunately the Proxmox VE API responses to HTTP `GET` requests with
four larger arrays which are, more or less, kind of connected to each
other, but they also somehow stand on their own. This means that there
is a `files` array that contains the `repositories` again which again
contains all repositories with their metadata of every source file. On
the other hand available standard repositories are listed in the
`standard-repos` array, but their activation status is only stored when
they have already been added through a `PUT` request. The `infos` array
is more less useless.

So in order to get the required data and store them in the state the
`importFromAPI` methods of the models must loop through all the
deep-nested arrays and act based on specific attributes like a matching
file path, comparing it to the activation status and so on.

In the end the implementation is really stable after testing it with all
possible conditions and state combinations.

@bpg if you'd like me to create a small data logic flow chart to make it
easier to understand some parts of the code let me know. I can make my
local notes "shareable" which I created to not loose track of the logic.

>> What is the way to manage the activation status of a "standard" repository?

Because the two resources are modular and scoped they can be simply
combined to manage an APT "standard" repository, e.g. toggling its
activation status. The following examples are also included in the
documentations.

```hcl
// This resource ensure that the "no-subscription" standard repository
// is added to the source list.
// It represents the `PUT` API request.
resource "proxmox_virtual_environment_apt_standard_repository" "example" {
  handle = "no-subscription"
  node   = "pve"
}

// This resource allows to actually modify the activation status of the
// standard repository as it represents the `POST`.
// Using the values from the dedicated standard repository resource
// makes sure that Terraform correctly resolves dependency order.
resource "proxmox_virtual_environment_apt_repository" "example" {
  enabled   = true
  file_path = proxmox_virtual_environment_apt_standard_repository.example.file_path
  index     = proxmox_virtual_environment_apt_standard_repository.example.index
  node      = proxmox_virtual_environment_apt_standard_repository.example.node
}
```

[1]: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/apt/repositories

---------

Signed-off-by: Sven Greb <development@svengreb.de>
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
2024-07-05 18:48:35 -04:00

282 lines
10 KiB
Go

/*
* 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 apt
import (
"context"
"fmt"
"regexp"
"slices"
"strings"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/nodes/apt"
api "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/apt/repositories"
)
// Note that most constants are exported to allow the usage in (acceptance) tests.
const (
// SchemaAttrNameComment is the name of the APT repository schema attribute for the associated comment.
SchemaAttrNameComment = "comment"
// SchemaAttrNameComponents is the name of the APT repository schema attribute for the list of components.
SchemaAttrNameComponents = "components"
// SchemaAttrNameEnabled is the name of the APT repository schema attribute that indicates the activation status.
SchemaAttrNameEnabled = "enabled"
// SchemaAttrNameFilePath is the name of the APT repository schema attribute for the path of the defining source list
// file.
SchemaAttrNameFilePath = "file_path"
// SchemaAttrNameFileType is the name of the APT repository schema attribute for the format of the defining source
// list file.
SchemaAttrNameFileType = "file_type"
// SchemaAttrNameIndex is the name of the APT repository schema attribute for the index within the defining source
// list file.
SchemaAttrNameIndex = "index"
// SchemaAttrNameNode is the name of the APT repository schema attribute for the name of the Proxmox VE node.
SchemaAttrNameNode = "node"
// SchemaAttrNamePackageTypes is the name of the APT repository schema attribute for the list of package types.
SchemaAttrNamePackageTypes = "package_types"
// SchemaAttrNameStandardDescription is the name of the APT repository schema attribute for the description.
SchemaAttrNameStandardDescription = "description"
// SchemaAttrNameStandardHandle is the name of the APT repository schema attribute for the standard repository
// handle.
SchemaAttrNameStandardHandle = "handle"
// SchemaAttrNameStandardName is the name of the APT repository schema attribute for the human-readable name.
SchemaAttrNameStandardName = "name"
// SchemaAttrNameStandardStatus is the name of the APT standard repository schema attribute that indicates the
// configuration and activation status.
SchemaAttrNameStandardStatus = "status"
// SchemaAttrNameSuites is the name of the APT repository schema attribute for the list of package distributions.
SchemaAttrNameSuites = "suites"
// SchemaAttrNameTerraformID is the name of the APT repository schema attribute for the Terraform ID.
SchemaAttrNameTerraformID = "id"
// SchemaAttrNameURIs is the name of the APT repository schema attribute for the list of repository URIs.
SchemaAttrNameURIs = "uris"
)
// RepoIDCharReplaceRegEx is a regular expression to replace characters in a Terraform resource/data source ID.
// The "^" at the beginning of the character group selects all characters not matching the group.
var RepoIDCharReplaceRegEx = regexp.MustCompile(`([^a-zA-Z1-9_])`)
// modelRepo maps the schema data for an APT repository from a parsed source list file.
type modelRepo struct {
// Comment is the comment of the APT repository.
Comment types.String `tfsdk:"comment"`
// Components is the list of repository components.
Components types.List `tfsdk:"components"`
// Enabled indicates whether the APT repository is enabled.
Enabled types.Bool `tfsdk:"enabled"`
// FilePath is the path of the source list file that contains the APT repository.
FilePath types.String `tfsdk:"file_path"`
// FileType is the format of the packages.
FileType types.String `tfsdk:"file_type"`
// ID is the Terraform identifier of the APT repository.
ID types.String `tfsdk:"id"`
// Index is the index of the APT repository within the defining source list.
Index types.Int64 `tfsdk:"index"`
// Node is the name of the Proxmox VE node for the APT repository.
Node types.String `tfsdk:"node"`
// PackageTypes is the list of package types.
PackageTypes types.List `tfsdk:"package_types"`
// Suites is the list of package distributions.
Suites types.List `tfsdk:"suites"`
// URIs is the list of repository URIs.
URIs types.List `tfsdk:"uris"`
}
// modelStandardRepo maps the schema data for an APT standard repository.
type modelStandardRepo struct {
// Description is the description of the APT standard repository.
Description types.String `tfsdk:"description"`
// FilePath is the path of the source list file that contains the APT standard repository.
FilePath types.String `tfsdk:"file_path"`
// ID is the Terraform identifier of the APT standard repository.
ID types.String `tfsdk:"id"`
// Index is the index of the APT standard repository within the defining source list file.
Index types.Int64 `tfsdk:"index"`
// Handle is the handle of the APT standard repository.
Handle customtypes.StandardRepoHandleValue `tfsdk:"handle"`
// Name is the name of the APT standard repository.
Name types.String `tfsdk:"name"`
// Node is the name of the Proxmox VE node for the APT standard repository.
Node types.String `tfsdk:"node"`
// Status is the configuration and activation status of the APT standard repository.
Status types.Int64 `tfsdk:"status"`
}
// importFromAPI imports the contents of an APT repository model from the Proxmox VE API's response data.
func (rp *modelRepo) importFromAPI(ctx context.Context, data *api.GetResponseData) diag.Diagnostics {
var diags diag.Diagnostics
// We can only ensure a unique ID by using the name of the Proxmox VE node and the absolute file path because custom
// source list files can be loaded by Proxmox VE from every path on a node.
rp.ID = types.StringValue(
fmt.Sprintf(
"%s_%s_%s_%d",
ResourceRepoIDPrefix,
strings.ToLower(rp.Node.ValueString()),
strings.ToLower(RepoIDCharReplaceRegEx.ReplaceAllString(strings.TrimPrefix(rp.FilePath.ValueString(), "/"), "_")),
rp.Index.ValueInt64(),
),
)
// We must ensure that the type definitions for lists and other attributes are set since Terraform must know these
// during the planning phase. This is important when the resource was imported where only the ID is known.
rp.Comment = types.StringNull()
rp.Enabled = types.BoolNull()
rp.FileType = types.StringNull()
rp.Components = types.ListNull(types.StringType)
rp.PackageTypes = types.ListNull(types.StringType)
rp.Suites = types.ListNull(types.StringType)
rp.URIs = types.ListNull(types.StringType)
// Iterate through all repository files…
for _, repoFile := range data.Files {
// …and the defined repositories when the file path matches.
if repoFile.Path == rp.FilePath.ValueString() {
// Handle situations where an APT repository might have been removed manually which is currently the only way to
// solve this with the capabilities of the Proxmox VE API.
if int64(len(repoFile.Repositories)) > rp.Index.ValueInt64() {
repo := repoFile.Repositories[rp.Index.ValueInt64()]
// Strip the unnecessary new line control character (\n) from the end of the comment that is, for whatever
// reason, returned this way by the Proxmox VE API.
if repo.Comment != nil {
rp.Comment = types.StringValue(strings.TrimSuffix(*repo.Comment, "\n"))
}
rp.Enabled = repo.Enabled.ToValue()
rp.FileType = types.StringValue(repo.FileType)
components, convDiags := types.ListValueFrom(ctx, types.StringType, repo.Components)
if convDiags.HasError() {
diags.AddError("Terraform list value conversion", "Convert list of APT repository components")
} else {
rp.Components = components
}
pkgTypes, convDiags := types.ListValueFrom(ctx, types.StringType, repo.PackageTypes)
if convDiags.HasError() {
diags.AddError("Terraform list value conversion", "Convert list of APT repository package types")
} else {
rp.PackageTypes = pkgTypes
}
suites, convDiags := types.ListValueFrom(ctx, types.StringType, repo.Suites)
if convDiags.HasError() {
diags.AddError("Terraform list value conversion", "Convert list of APT repository suites")
} else {
rp.Suites = suites
}
uris, convDiags := types.ListValueFrom(ctx, types.StringType, repo.URIs)
if convDiags.HasError() {
diags.AddError("Terraform list value conversion", "Convert list of APT repository URIs")
} else {
rp.URIs = uris
}
}
}
}
return diags
}
// importFromAPI imports the contents of an APT standard repository from the Proxmox VE API's response data.
func (srp *modelStandardRepo) importFromAPI(_ context.Context, data *api.GetResponseData) {
for _, repo := range data.StandardRepos {
if repo.Handle == srp.Handle.ValueString() {
srp.Description = types.StringPointerValue(repo.Description)
// We can only ensure a unique ID by using the name of the Proxmox VE node in combination with the unique standard
// handle.
srp.ID = types.StringValue(
fmt.Sprintf(
"%s_%s_%s",
ResourceStandardRepoIDPrefix,
strings.ToLower(srp.Node.ValueString()),
RepoIDCharReplaceRegEx.ReplaceAllString(srp.Handle.ValueString(), "_"),
),
)
srp.Name = types.StringValue(repo.Name)
srp.Status = types.Int64PointerValue(repo.Status)
}
}
// Set the index…
srp.setIndex(data)
// … and then the file path when the index is valid…
if !srp.Index.IsNull() {
// …by iterating through all repository files…
for _, repoFile := range data.Files {
// …and get the repository when the file path matches.
if srp.Handle.IsSupportedFilePath(repoFile.Path) {
srp.FilePath = types.StringValue(repoFile.Path)
}
}
}
}
// setIndex sets the index of the APT standard repository derived from the defining source list file.
func (srp *modelStandardRepo) setIndex(data *api.GetResponseData) {
for _, file := range data.Files {
for idx, repo := range file.Repositories {
if slices.Contains(repo.Components, srp.Handle.ComponentName()) {
// Return early for non-Ceph repositories…
if !srp.Handle.IsCephHandle() {
srp.Index = types.Int64Value(int64(idx))
return
}
// …and find the index for Ceph repositories based on the version name within the list of URIs.
for _, uri := range repo.URIs {
if strings.Contains(uri, srp.Handle.CephVersionName().String()) {
srp.Index = types.Int64Value(int64(idx))
return
}
}
}
}
}
srp.Index = types.Int64Null()
}