mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-07-03 12:02:57 +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>
466 lines
16 KiB
Go
466 lines
16 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_test
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
|
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
|
|
"github.com/hashicorp/terraform-plugin-testing/statecheck"
|
|
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
|
|
|
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/nodes/apt"
|
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/test"
|
|
apitypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/nodes/apt/repositories"
|
|
)
|
|
|
|
// Note that some "hard-coded" values must be used because of the way how the Proxmox VE API for APT repositories works.
|
|
const (
|
|
testAccResourceRepoSelector = "proxmox_virtual_environment_" + apt.ResourceRepoIDPrefix + ".test"
|
|
|
|
// By default, this should be the main Debian package repository on any (new) Proxmox VE node.
|
|
testAccResourceRepoIndex = 0
|
|
|
|
testAccResourceStandardRepoSelector = "proxmox_virtual_environment_" + apt.ResourceStandardRepoIDPrefix + ".test"
|
|
|
|
// Use an APT standard repository handle that is not enabled by default on any new Proxmox VE node.
|
|
testAccResourceStandardRepoHandle = "no-subscription"
|
|
)
|
|
|
|
func testAccRepoInit(t *testing.T) *test.Environment {
|
|
t.Helper()
|
|
t.Parallel()
|
|
|
|
return test.InitEnvironment(t)
|
|
}
|
|
|
|
func TestAccDataSourceRepo(t *testing.T) {
|
|
te := testAccRepoInit(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
steps []resource.TestStep
|
|
}{
|
|
{
|
|
"read APT repository attributes",
|
|
[]resource.TestStep{
|
|
{
|
|
Config: fmt.Sprintf(
|
|
`
|
|
data %q %q {
|
|
%s = %q # file_path
|
|
%s = %d # index
|
|
%s = %q # node
|
|
}
|
|
`,
|
|
strings.Split(testAccResourceRepoSelector, ".")[0],
|
|
strings.Split(testAccResourceRepoSelector, ".")[1],
|
|
// To ensure stable acceptance tests we must use one of the Proxmox VE default source lists that always
|
|
// exists on any (new) Proxmox VE node.
|
|
apt.SchemaAttrNameFilePath, apitypes.StandardRepoFilePathMain,
|
|
apt.SchemaAttrNameIndex, testAccResourceRepoIndex,
|
|
apt.SchemaAttrNameNode, te.NodeName,
|
|
),
|
|
// The provided attributes and computed attributes should be set.
|
|
Check: resource.ComposeTestCheckFunc(
|
|
resource.TestMatchResourceAttr(
|
|
fmt.Sprintf("data.%s", testAccResourceRepoSelector),
|
|
apt.SchemaAttrNameComment,
|
|
// Expect any value or an empty string.
|
|
regexp.MustCompile(`(.*|^$)`),
|
|
),
|
|
resource.TestCheckResourceAttr(
|
|
fmt.Sprintf("data.%s", testAccResourceRepoSelector),
|
|
apt.SchemaAttrNameTerraformID,
|
|
fmt.Sprintf(
|
|
"%s_%s_%s_%d",
|
|
apt.ResourceRepoIDPrefix,
|
|
strings.ToLower(te.NodeName),
|
|
apt.RepoIDCharReplaceRegEx.ReplaceAllString(
|
|
strings.TrimPrefix(apitypes.StandardRepoFilePathMain, "/"),
|
|
"_",
|
|
),
|
|
testAccResourceRepoIndex,
|
|
),
|
|
),
|
|
test.ResourceAttributesSet(
|
|
fmt.Sprintf("data.%s", testAccResourceRepoSelector),
|
|
[]string{
|
|
fmt.Sprintf("%s.#", apt.SchemaAttrNameComponents),
|
|
apt.SchemaAttrNameEnabled,
|
|
apt.SchemaAttrNameFilePath,
|
|
apt.SchemaAttrNameIndex,
|
|
apt.SchemaAttrNameNode,
|
|
fmt.Sprintf("%s.#", apt.SchemaAttrNamePackageTypes),
|
|
fmt.Sprintf("%s.#", apt.SchemaAttrNameSuites),
|
|
fmt.Sprintf("%s.#", apt.SchemaAttrNameURIs),
|
|
},
|
|
),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
resource.ParallelTest(
|
|
t, resource.TestCase{
|
|
ProtoV6ProviderFactories: te.AccProviders,
|
|
Steps: tt.steps,
|
|
},
|
|
)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestAccDataSourceStandardRepo(t *testing.T) {
|
|
t.Helper()
|
|
t.Parallel()
|
|
|
|
te := test.InitEnvironment(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
steps []resource.TestStep
|
|
}{
|
|
{
|
|
"read APT standard repository attributes",
|
|
[]resource.TestStep{
|
|
{
|
|
Config: fmt.Sprintf(
|
|
`
|
|
data %q %q {
|
|
%s = %q # handle
|
|
%s = %q # node
|
|
}
|
|
`,
|
|
strings.Split(testAccResourceStandardRepoSelector, ".")[0],
|
|
strings.Split(testAccResourceStandardRepoSelector, ".")[1],
|
|
apt.SchemaAttrNameStandardHandle, apitypes.StandardRepoHandleKindNoSubscription,
|
|
apt.SchemaAttrNameNode, te.NodeName,
|
|
),
|
|
// The provided attributes and computed attributes should be set.
|
|
Check: resource.ComposeTestCheckFunc(
|
|
resource.TestCheckResourceAttr(
|
|
fmt.Sprintf("data.%s", testAccResourceStandardRepoSelector),
|
|
apt.SchemaAttrNameTerraformID,
|
|
fmt.Sprintf(
|
|
"%s_%s_%s",
|
|
apt.ResourceStandardRepoIDPrefix,
|
|
strings.ToLower(te.NodeName),
|
|
apt.RepoIDCharReplaceRegEx.ReplaceAllString(
|
|
strings.TrimPrefix(apitypes.StandardRepoHandleKindNoSubscription.String(), "/"),
|
|
"_",
|
|
),
|
|
),
|
|
),
|
|
test.ResourceAttributesSet(
|
|
fmt.Sprintf("data.%s", testAccResourceStandardRepoSelector),
|
|
// Note that we can not check for the following attributes because they are only available when the
|
|
// standard repository has been added to a source list:
|
|
//
|
|
// - apt.SchemaAttrNameFilePath (file_path) - will be set when parsing all configured repositories in all
|
|
// source list files.
|
|
// - apt.SchemaAttrNameIndex (index) - will be set when finding the repository within a source list file,
|
|
// based on the detected file path.
|
|
// - apt.SchemaAttrNameStandardStatus (status) - is only available when the standard has been configured.
|
|
[]string{
|
|
apt.SchemaAttrNameStandardDescription,
|
|
apt.SchemaAttrNameStandardHandle,
|
|
apt.SchemaAttrNameStandardName,
|
|
apt.SchemaAttrNameNode,
|
|
},
|
|
),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name, func(t *testing.T) {
|
|
resource.ParallelTest(
|
|
t, resource.TestCase{
|
|
ProtoV6ProviderFactories: te.AccProviders,
|
|
Steps: tt.steps,
|
|
},
|
|
)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// Run tests for APT repository resource definitions with valid input where all required attributes are specified.
|
|
// Only the [Create], [Read] and [Update] method implementations of the
|
|
// [github.com/hashicorp/terraform-plugin-framework/resource.Resource] interface are tested in sequential steps because
|
|
// [Delete] is no-op due to the non-existing capability of the Proxmox VE API of deleting configured APT repository.
|
|
//
|
|
// [Create]: https://developer.hashicorp.com/terraform/plugin/framework/resources/create
|
|
// [Delete]: https://developer.hashicorp.com/terraform/plugin/framework/resources/delete
|
|
// [Read]: https://developer.hashicorp.com/terraform/plugin/framework/resources/read
|
|
// [Update]: https://developer.hashicorp.com/terraform/plugin/framework/resources/update
|
|
func TestAccResourceRepoValidInput(t *testing.T) {
|
|
t.Helper()
|
|
t.Parallel()
|
|
|
|
te := test.InitEnvironment(t)
|
|
|
|
resource.Test(
|
|
t, resource.TestCase{
|
|
ProtoV6ProviderFactories: te.AccProviders,
|
|
Steps: []resource.TestStep{
|
|
// Test the "Create" and "Read" implementations.
|
|
{
|
|
Config: fmt.Sprintf(
|
|
`
|
|
resource %q %q {
|
|
%s = %t # enabled
|
|
%s = %q # file_path
|
|
%s = %d # index
|
|
%s = %q # node
|
|
}
|
|
`,
|
|
strings.Split(testAccResourceRepoSelector, ".")[0],
|
|
strings.Split(testAccResourceRepoSelector, ".")[1],
|
|
apt.SchemaAttrNameEnabled, apt.ResourceRepoActivationStatus,
|
|
// To ensure stable acceptance tests we must use one of the Proxmox VE default source lists that always
|
|
// exists on any (new) Proxmox VE node.
|
|
apt.SchemaAttrNameFilePath, apitypes.StandardRepoFilePathMain,
|
|
apt.SchemaAttrNameIndex, testAccResourceRepoIndex,
|
|
apt.SchemaAttrNameNode, te.NodeName,
|
|
),
|
|
// The computed attributes should be set.
|
|
ConfigStateChecks: []statecheck.StateCheck{
|
|
statecheck.ExpectKnownValue(
|
|
testAccResourceRepoSelector,
|
|
tfjsonpath.New(apt.SchemaAttrNameComponents),
|
|
knownvalue.ListPartial(
|
|
map[int]knownvalue.Check{
|
|
// Use the same check for both entries because the sort order cannot be guaranteed.
|
|
0: knownvalue.StringRegexp(regexp.MustCompile(`(contrib|main)`)),
|
|
1: knownvalue.StringRegexp(regexp.MustCompile(`(contrib|main)`)),
|
|
},
|
|
),
|
|
),
|
|
statecheck.ExpectKnownValue(
|
|
testAccResourceRepoSelector,
|
|
tfjsonpath.New(apt.SchemaAttrNamePackageTypes),
|
|
knownvalue.ListPartial(
|
|
map[int]knownvalue.Check{
|
|
0: knownvalue.StringRegexp(regexp.MustCompile(`(deb)`)),
|
|
},
|
|
),
|
|
),
|
|
statecheck.ExpectKnownValue(
|
|
testAccResourceRepoSelector,
|
|
tfjsonpath.New(apt.SchemaAttrNameSuites),
|
|
knownvalue.ListPartial(
|
|
map[int]knownvalue.Check{
|
|
// The possible Debian version is based on the official table of the Proxmox VE FAQ page:
|
|
// - https://pve.proxmox.com/wiki/FAQ#faq-support-table
|
|
// - https://www.thomas-krenn.com/en/wiki/Proxmox_VE#Proxmox_VE_8.x
|
|
//
|
|
// The required Proxmox VE version for this provider is of course also taken into account:
|
|
// - https://github.com/bpg/terraform-provider-proxmox?tab=readme-ov-file#requirements
|
|
0: knownvalue.StringRegexp(regexp.MustCompile(`(bookworm)`)),
|
|
},
|
|
),
|
|
),
|
|
statecheck.ExpectKnownValue(
|
|
testAccResourceRepoSelector,
|
|
tfjsonpath.New(apt.SchemaAttrNameURIs),
|
|
knownvalue.ListPartial(
|
|
map[int]knownvalue.Check{
|
|
0: knownvalue.StringRegexp(regexp.MustCompile(`https?://ftp\.([a-z]+\.)?debian\.org/debian`)),
|
|
},
|
|
),
|
|
),
|
|
},
|
|
// The provided attributes and computed attributes should be set.
|
|
Check: resource.ComposeTestCheckFunc(
|
|
resource.TestMatchResourceAttr(
|
|
testAccResourceRepoSelector,
|
|
apt.SchemaAttrNameComment,
|
|
// Expect any value or an empty string.
|
|
regexp.MustCompile(`(.*|^$)`),
|
|
),
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceRepoSelector,
|
|
apt.SchemaAttrNameEnabled,
|
|
strconv.FormatBool(apt.ResourceRepoActivationStatus),
|
|
),
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceRepoSelector,
|
|
apt.SchemaAttrNameFilePath,
|
|
apitypes.StandardRepoFilePathMain,
|
|
),
|
|
resource.TestCheckResourceAttrSet(testAccResourceRepoSelector, apt.SchemaAttrNameFileType),
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceRepoSelector,
|
|
apt.SchemaAttrNameIndex,
|
|
strconv.FormatInt(testAccResourceRepoIndex, 10),
|
|
),
|
|
resource.TestCheckResourceAttr(testAccResourceRepoSelector, apt.SchemaAttrNameNode, te.NodeName),
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceRepoSelector,
|
|
apt.SchemaAttrNameTerraformID,
|
|
fmt.Sprintf(
|
|
"%s_%s_%s_%d",
|
|
apt.ResourceRepoIDPrefix,
|
|
strings.ToLower(te.NodeName),
|
|
apt.RepoIDCharReplaceRegEx.ReplaceAllString(
|
|
strings.TrimPrefix(apitypes.StandardRepoFilePathMain, "/"),
|
|
"_",
|
|
),
|
|
testAccResourceRepoIndex,
|
|
),
|
|
),
|
|
),
|
|
},
|
|
|
|
// Test the "ImportState" implementation.
|
|
{
|
|
ImportState: true,
|
|
ImportStateId: fmt.Sprintf(
|
|
"%s,%s,%d",
|
|
strings.ToLower(te.NodeName),
|
|
apitypes.StandardRepoFilePathMain,
|
|
testAccResourceRepoIndex,
|
|
),
|
|
ImportStateVerify: true,
|
|
ResourceName: testAccResourceRepoSelector,
|
|
},
|
|
|
|
// Test the "Update" implementation by toggling the activation status.
|
|
{
|
|
Config: fmt.Sprintf(
|
|
`
|
|
resource %q %q {
|
|
%s = %t # enabled
|
|
%s = %q # file_path
|
|
%s = %d # index
|
|
%s = %q # node
|
|
}
|
|
`,
|
|
strings.Split(testAccResourceRepoSelector, ".")[0],
|
|
strings.Split(testAccResourceRepoSelector, ".")[1],
|
|
// Disable the repository which is enabled by default for created or imported resources.
|
|
apt.SchemaAttrNameEnabled, !apt.ResourceRepoActivationStatus,
|
|
// To ensure stable acceptance tests we must use one of the Proxmox VE default source lists that always
|
|
// exists on any (new) Proxmox VE node.s
|
|
apt.SchemaAttrNameFilePath, apitypes.StandardRepoFilePathMain,
|
|
apt.SchemaAttrNameIndex, testAccResourceRepoIndex,
|
|
apt.SchemaAttrNameNode, te.NodeName,
|
|
),
|
|
// The provides attributes and some computed attributes should be set.
|
|
Check: resource.ComposeTestCheckFunc(
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceRepoSelector,
|
|
apt.SchemaAttrNameEnabled,
|
|
strconv.FormatBool(!apt.ResourceRepoActivationStatus),
|
|
),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// Run tests for APT standard repository resource definitions with valid input where all required attributes are
|
|
// specified.
|
|
// Only the [Create] and [Read] method implementations of the
|
|
// [github.com/hashicorp/terraform-plugin-framework/resource.Resource] interface are tested in sequential steps because
|
|
// [Delete] and [Update] are no-op due to the non-existing capability of the Proxmox VE API of deleting or updating a
|
|
// configured APT standard repository.
|
|
//
|
|
// [Create]: https://developer.hashicorp.com/terraform/plugin/framework/resources/create
|
|
// [Delete]: https://developer.hashicorp.com/terraform/plugin/framework/resources/delete
|
|
// [Read]: https://developer.hashicorp.com/terraform/plugin/framework/resources/read
|
|
// [Update]: https://developer.hashicorp.com/terraform/plugin/framework/resources/update
|
|
func TestAccResourceStandardRepoValidInput(t *testing.T) {
|
|
t.Helper()
|
|
t.Parallel()
|
|
|
|
te := test.InitEnvironment(t)
|
|
|
|
resource.Test(
|
|
t, resource.TestCase{
|
|
ProtoV6ProviderFactories: te.AccProviders,
|
|
Steps: []resource.TestStep{
|
|
// Test the "Create" and "Read" implementations.
|
|
{
|
|
Config: fmt.Sprintf(
|
|
`
|
|
resource %q %q {
|
|
%s = %q # handle
|
|
%s = %q # node
|
|
}
|
|
`,
|
|
strings.Split(testAccResourceStandardRepoSelector, ".")[0],
|
|
strings.Split(testAccResourceStandardRepoSelector, ".")[1],
|
|
apt.SchemaAttrNameStandardHandle, testAccResourceStandardRepoHandle,
|
|
apt.SchemaAttrNameNode, te.NodeName,
|
|
),
|
|
// The provided attributes and computed attributes should be set.
|
|
Check: resource.ComposeTestCheckFunc(
|
|
resource.TestCheckResourceAttrSet(
|
|
testAccResourceStandardRepoSelector,
|
|
apt.SchemaAttrNameStandardDescription,
|
|
),
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceStandardRepoSelector,
|
|
apt.SchemaAttrNameFilePath,
|
|
apitypes.StandardRepoFilePathMain,
|
|
),
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceStandardRepoSelector,
|
|
apt.SchemaAttrNameStandardHandle,
|
|
testAccResourceStandardRepoHandle,
|
|
),
|
|
resource.TestCheckResourceAttrSet(testAccResourceStandardRepoSelector, apt.SchemaAttrNameIndex),
|
|
resource.TestCheckResourceAttrSet(testAccResourceStandardRepoSelector, apt.SchemaAttrNameStandardName),
|
|
resource.TestCheckResourceAttr(testAccResourceStandardRepoSelector, apt.SchemaAttrNameNode, te.NodeName),
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceStandardRepoSelector,
|
|
apt.SchemaAttrNameStandardStatus,
|
|
// By default, newly added APT standard repositories are enabled.
|
|
strconv.Itoa(1),
|
|
),
|
|
resource.TestCheckResourceAttr(
|
|
testAccResourceStandardRepoSelector,
|
|
apt.SchemaAttrNameTerraformID,
|
|
fmt.Sprintf(
|
|
"%s_%s_%s",
|
|
apt.ResourceStandardRepoIDPrefix,
|
|
strings.ToLower(te.NodeName),
|
|
apt.RepoIDCharReplaceRegEx.ReplaceAllString(testAccResourceStandardRepoHandle, "_"),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
|
|
// Test the "ImportState" implementation.
|
|
{
|
|
ImportState: true,
|
|
ImportStateId: fmt.Sprintf("%s,%s", strings.ToLower(te.NodeName), testAccResourceStandardRepoHandle),
|
|
ImportStateVerify: true,
|
|
ResourceName: testAccResourceStandardRepoSelector,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
}
|