mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-07-02 03:22:59 +00:00
* 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>
282 lines
10 KiB
Go
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()
|
|
}
|