refactor: migrate USG setting resource to Terraform Plugin Framework (#42)

* refactor: migrate USG setting resource to Terraform Plugin Framework

* remove setting_usg from old provider

* add USG resource to provider v2
This commit is contained in:
Mateusz Filipowicz
2025-03-05 12:38:44 +01:00
committed by GitHub
parent eb8fc3871e
commit 35c74bf59d
16 changed files with 565 additions and 1045 deletions

View File

@@ -22,17 +22,17 @@ func TestAccSettingUsg_mdns_v6(t *testing.T) {
Config: testAccSettingUsgConfig_mdns(true),
Check: resource.ComposeTestCheckFunc(),
},
pt.ImportStep("unifi_setting_usg.test"),
pt.ImportStepWithSite("unifi_setting_usg.test"),
{
Config: testAccSettingUsgConfig_mdns(false),
Check: resource.ComposeTestCheckFunc(),
},
pt.ImportStep("unifi_setting_usg.test"),
pt.ImportStepWithSite("unifi_setting_usg.test"),
{
Config: testAccSettingUsgConfig_mdns(true),
Check: resource.ComposeTestCheckFunc(),
},
pt.ImportStep("unifi_setting_usg.test"),
pt.ImportStepWithSite("unifi_setting_usg.test"),
},
})
}
@@ -58,7 +58,7 @@ func TestAccSettingUsg_dhcpRelay(t *testing.T) {
Config: testAccSettingUsgConfig_dhcpRelay(),
Check: resource.ComposeTestCheckFunc(),
},
pt.ImportStep("unifi_setting_usg.test"),
pt.ImportStepWithSite("unifi_setting_usg.test"),
},
})
}

View File

@@ -11,6 +11,7 @@ import (
type Resource interface {
SetClient(client *Client)
SetVersionValidator(validator ControllerVersionValidator)
}
// ResourceModel defines the interface that all setting models must implement
@@ -69,6 +70,7 @@ func ConfigureDatasource(base Resource, req datasource.ConfigureRequest, resp *d
return
}
base.SetClient(cfg)
base.SetVersionValidator(NewControllerVersionValidator(cfg))
}
func ConfigureResource(base Resource, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
@@ -86,4 +88,5 @@ func ConfigureResource(base Resource, req resource.ConfigureRequest, resp *resou
return
}
base.SetClient(cfg)
base.SetVersionValidator(NewControllerVersionValidator(cfg))
}

View File

@@ -0,0 +1,188 @@
package base
import (
"context"
"fmt"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"strings"
)
func AsVersion(versionString string) *version.Version {
return version.Must(version.NewVersion(versionString))
}
// TODO remove this legacy
var (
ControllerV6 = AsVersion("6.0.0")
ControllerV7 = AsVersion("7.0.0")
ControllerV9 = AsVersion("9.0.0")
ControllerVersionApiKeyAuth = AsVersion("9.0.108")
// https://community.ui.com/releases/UniFi-Network-Application-8-2-93/fce86dc6-897a-4944-9c53-1eec7e37e738
ControllerVersionDnsRecords = AsVersion("8.2.93")
// https://community.ui.com/releases/UniFi-Network-Controller-6-1-61/62f1ad38-1ac5-430c-94b0-becbb8f71d7d
ControllerVersionWPA3 = AsVersion("6.1.61")
)
func (c *Client) IsControllerV6() bool {
return c.Version.GreaterThanOrEqual(ControllerV6)
}
func (c *Client) IsControllerV7() bool {
return c.Version.GreaterThanOrEqual(ControllerV7)
}
func (c *Client) IsControllerV9() bool {
return c.Version.GreaterThanOrEqual(ControllerV9)
}
func (c *Client) SupportsApiKeyAuthentication() bool {
return c.Version.GreaterThanOrEqual(ControllerVersionApiKeyAuth)
}
func (c *Client) SupportsWPA3() bool {
return c.Version.GreaterThanOrEqual(ControllerVersionWPA3)
}
func (c *Client) SupportsDnsRecords() bool {
return c.Version.GreaterThanOrEqual(ControllerVersionDnsRecords)
}
func CheckMinimumControllerVersion(versionString string) error {
v, err := version.NewVersion(versionString)
if err != nil {
return err
}
if v.LessThan(ControllerV6) {
return fmt.Errorf("Controller version %q or greater is required to use the provider, found %q.", ControllerV6, v)
}
return nil
}
// TODO remove until here
// ControllerVersionValidator is a validator that checks if the UniFi controller version
// matches the specified constraints.
type ControllerVersionValidator interface {
RequireMinVersion(min string) diag.Diagnostics
RequireMaxVersion(max string) diag.Diagnostics
RequireVersionBetween(min, max string) diag.Diagnostics
RequireMinVersionForPath(min string, attrPath path.Path, config tfsdk.Config) diag.Diagnostics
RequireMaxVersionForPath(max string, attrPath path.Path, config tfsdk.Config) diag.Diagnostics
RequireVersionBetweenForPath(min, max string, attrPath path.Path, config tfsdk.Config) diag.Diagnostics
}
var _ ControllerVersionValidator = &controllerVersionValidator{}
func NewControllerVersionValidator(client *Client) ControllerVersionValidator {
return &controllerVersionValidator{client: client}
}
type controllerVersionValidator struct {
client *Client
}
func (v controllerVersionValidator) RequireMinVersion(min string) diag.Diagnostics {
return v.requireVersion(minVersionRequirement(min), nil)
}
func (v controllerVersionValidator) RequireMaxVersion(max string) diag.Diagnostics {
return v.requireVersion(maxVersionRequirement(max), nil)
}
func (v controllerVersionValidator) RequireVersionBetween(min, max string) diag.Diagnostics {
return v.requireVersion(versionBetweenRequirement(min, max), nil)
}
func (v controllerVersionValidator) RequireMinVersionForPath(min string, attrPath path.Path, config tfsdk.Config) diag.Diagnostics {
return v.requireVersionForPath(minVersionRequirement(min), attrPath, config)
}
func (v controllerVersionValidator) RequireMaxVersionForPath(max string, attrPath path.Path, config tfsdk.Config) diag.Diagnostics {
return v.requireVersionForPath(maxVersionRequirement(max), attrPath, config)
}
func (v controllerVersionValidator) RequireVersionBetweenForPath(min, max string, attrPath path.Path, config tfsdk.Config) diag.Diagnostics {
return v.requireVersionForPath(versionBetweenRequirement(min, max), attrPath, config)
}
func minVersionRequirement(min string) versionRequirement {
return versionRequirement{minVersion: AsVersion(min)}
}
func maxVersionRequirement(max string) versionRequirement {
return versionRequirement{maxVersion: AsVersion(max)}
}
func versionBetweenRequirement(min, max string) versionRequirement {
return versionRequirement{minVersion: AsVersion(min), maxVersion: AsVersion(max)}
}
type versionRequirement struct {
minVersion *version.Version
maxVersion *version.Version
}
func (r versionRequirement) isBetweenRequirement() bool {
return r.minVersion != nil && r.maxVersion != nil
}
func (r versionRequirement) isMinRequirement() bool {
return r.minVersion != nil && r.maxVersion == nil
}
func (r versionRequirement) isMaxRequirement() bool {
return r.minVersion == nil && r.maxVersion != nil
}
const controllerVersionErrorMessage = "Controller version does not meet requirements"
func (v controllerVersionValidator) requireVersionForPath(req versionRequirement, attrPath path.Path, config tfsdk.Config) diag.Diagnostics {
diags := diag.Diagnostics{}
var val attr.Value
diags.Append(config.GetAttribute(context.Background(), attrPath, &val)...)
if diags.HasError() {
return diags
}
if !IsDefined(val) {
return diags
}
diags.Append(v.requireVersion(req, &attrPath)...)
return diags
}
// requireVersion checks if the controller version meets the constraints
func (v controllerVersionValidator) requireVersion(req versionRequirement, attrPath *path.Path) diag.Diagnostics {
diags := diag.Diagnostics{}
if v.client == nil || v.client.Version == nil {
diags.AddError("Controller version not available", "Provider was not initialized properly. UniFi client or controller version is not available")
return diags
}
controllerVersion := v.client.Version
errorBuilder := strings.Builder{}
if attrPath != nil {
errorBuilder.WriteString(fmt.Sprintf("%s is not supported. ", attrPath.String()))
}
errorBuilder.WriteString(fmt.Sprintf("Controller version %s", controllerVersion))
failed := false
if req.isBetweenRequirement() && (controllerVersion.LessThan(req.minVersion) || controllerVersion.GreaterThan(req.maxVersion)) {
failed = true
errorBuilder.WriteString(fmt.Sprintf(" is not between required %s and %s", req.minVersion, req.maxVersion))
} else if req.isMinRequirement() && controllerVersion.LessThan(req.minVersion) {
failed = true
errorBuilder.WriteString(fmt.Sprintf(" is less than minimum required version %s", req.minVersion))
} else if req.isMaxRequirement() && controllerVersion.GreaterThan(req.maxVersion) {
failed = true
errorBuilder.WriteString(fmt.Sprintf(" is greater than maximum required version %s", req.maxVersion))
}
if failed {
diags.AddError(controllerVersionErrorMessage, errorBuilder.String())
}
return diags
}

View File

@@ -0,0 +1,192 @@
package base_test
import (
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/stretchr/testify/require"
"testing"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/stretchr/testify/assert"
)
func TestAsVersion(t *testing.T) {
t.Parallel()
tests := []struct {
name string
versionString string
expected string
}{
{
name: "simple version",
versionString: "1.0.0",
expected: "1.0.0",
},
{
name: "complex version",
versionString: "7.2.95",
expected: "7.2.95",
},
{
name: "version with prerelease",
versionString: "6.0.0-beta1",
expected: "6.0.0-beta1",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := base.AsVersion(tt.versionString)
assert.Equal(t, tt.expected, result.String())
})
}
}
func TestCheckMinimumControllerVersion(t *testing.T) {
t.Parallel()
tests := []struct {
name string
versionString string
expectError bool
}{
{
name: "version equal to minimum",
versionString: "6.0.0",
expectError: false,
},
{
name: "version greater than minimum",
versionString: "7.0.0",
expectError: false,
},
{
name: "version less than minimum",
versionString: "5.9.9",
expectError: true,
},
{
name: "invalid version",
versionString: "invalid",
expectError: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := base.CheckMinimumControllerVersion(tt.versionString)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestControllerVersionValidator(t *testing.T) {
t.Parallel()
tests := []struct {
name string
clientVer string
testFunc func(v base.ControllerVersionValidator) diag.Diagnostics
expectError bool
errorMessage string
}{
{
name: "min version satisfied",
clientVer: "7.0.0",
testFunc: func(v base.ControllerVersionValidator) diag.Diagnostics {
return v.RequireMinVersion("6.0.0")
},
expectError: false,
},
{
name: "min version not satisfied",
clientVer: "6.0.0",
testFunc: func(v base.ControllerVersionValidator) diag.Diagnostics {
return v.RequireMinVersion("7.0.0")
},
expectError: true,
errorMessage: "Controller version 6.0.0 is less than minimum required version 7.0.0",
},
{
name: "max version satisfied",
clientVer: "6.0.0",
testFunc: func(v base.ControllerVersionValidator) diag.Diagnostics {
return v.RequireMaxVersion("7.0.0")
},
expectError: false,
},
{
name: "max version not satisfied",
clientVer: "8.0.0",
testFunc: func(v base.ControllerVersionValidator) diag.Diagnostics {
return v.RequireMaxVersion("7.0.0")
},
expectError: true,
errorMessage: "Controller version 8.0.0 is greater than maximum required version 7.0.0",
},
{
name: "between version satisfied",
clientVer: "7.0.0",
testFunc: func(v base.ControllerVersionValidator) diag.Diagnostics {
return v.RequireVersionBetween("6.0.0", "8.0.0")
},
expectError: false,
},
{
name: "between version not satisfied - too low",
clientVer: "5.0.0",
testFunc: func(v base.ControllerVersionValidator) diag.Diagnostics {
return v.RequireVersionBetween("6.0.0", "8.0.0")
},
expectError: true,
errorMessage: "Controller version 5.0.0 is not between required 6.0.0 and 8.0.0",
},
{
name: "between version not satisfied - too high",
clientVer: "9.0.0",
testFunc: func(v base.ControllerVersionValidator) diag.Diagnostics {
return v.RequireVersionBetween("6.0.0", "8.0.0")
},
expectError: true,
errorMessage: "Controller version 9.0.0 is not between required 6.0.0 and 8.0.0",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
client := &base.Client{
Version: base.AsVersion(tt.clientVer),
}
validator := base.NewControllerVersionValidator(client)
diags := tt.testFunc(validator)
if tt.expectError {
assert.True(t, diags.HasError())
assert.Contains(t, diags.Errors()[0].Detail(), tt.errorMessage)
} else {
assert.False(t, diags.HasError())
}
})
}
}
func TestControllerVersionValidatorNilClient(t *testing.T) {
t.Parallel()
validator := base.NewControllerVersionValidator(nil)
diags := validator.RequireMinVersion("6.0.0")
assert.True(t, diags.HasError())
assert.Contains(t, diags.Errors()[0].Summary(), "Controller version not available")
}

View File

@@ -1,63 +0,0 @@
package base
import (
"fmt"
"github.com/hashicorp/go-version"
)
func asVersion(versionString string) *version.Version {
return version.Must(version.NewVersion(versionString))
}
// AsVersion converts a string version to a *version.Version
// This is a utility function for consumers of this package
func AsVersion(versionString string) *version.Version {
return asVersion(versionString)
}
var (
ControllerV6 = asVersion("6.0.0")
ControllerV7 = asVersion("7.0.0")
ControllerV9 = asVersion("9.0.0")
ControllerVersionApiKeyAuth = asVersion("9.0.108")
// https://community.ui.com/releases/UniFi-Network-Application-8-2-93/fce86dc6-897a-4944-9c53-1eec7e37e738
ControllerVersionDnsRecords = asVersion("8.2.93")
// https://community.ui.com/releases/UniFi-Network-Controller-6-1-61/62f1ad38-1ac5-430c-94b0-becbb8f71d7d
ControllerVersionWPA3 = asVersion("6.1.61")
)
func (c *Client) IsControllerV6() bool {
return c.Version.GreaterThanOrEqual(ControllerV6)
}
func (c *Client) IsControllerV7() bool {
return c.Version.GreaterThanOrEqual(ControllerV7)
}
func (c *Client) IsControllerV9() bool {
return c.Version.GreaterThanOrEqual(ControllerV9)
}
func (c *Client) SupportsApiKeyAuthentication() bool {
return c.Version.GreaterThanOrEqual(ControllerVersionApiKeyAuth)
}
func (c *Client) SupportsWPA3() bool {
return c.Version.GreaterThanOrEqual(ControllerVersionWPA3)
}
func (c *Client) SupportsDnsRecords() bool {
return c.Version.GreaterThanOrEqual(ControllerVersionDnsRecords)
}
func CheckMinimumControllerVersion(versionString string) error {
v, err := version.NewVersion(versionString)
if err != nil {
return err
}
if v.LessThan(ControllerV6) {
return fmt.Errorf("Controller version %q or greater is required to use the provider, found %q.", ControllerV6, v)
}
return nil
}

View File

@@ -23,6 +23,7 @@ var (
)
type dnsRecordDatasource struct {
base.ControllerVersionValidator
client *base.Client
}
@@ -43,6 +44,10 @@ func (d *dnsRecordDatasource) SetClient(client *base.Client) {
d.client = client
}
func (d *dnsRecordDatasource) SetVersionValidator(validator base.ControllerVersionValidator) {
d.ControllerVersionValidator = validator
}
func (d *dnsRecordDatasource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
base.ConfigureDatasource(d, req, resp)
}

View File

@@ -16,6 +16,7 @@ var (
)
type dnsRecordsDatasource struct {
base.ControllerVersionValidator
client *base.Client
}
@@ -27,6 +28,10 @@ func (d *dnsRecordsDatasource) SetClient(client *base.Client) {
d.client = client
}
func (d *dnsRecordsDatasource) SetVersionValidator(validator base.ControllerVersionValidator) {
d.ControllerVersionValidator = validator
}
func (d *dnsRecordsDatasource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
base.ConfigureDatasource(d, req, resp)
}

View File

@@ -23,6 +23,7 @@ var (
)
type dnsRecordResource struct {
base.ControllerVersionValidator
client *base.Client
}
@@ -30,6 +31,10 @@ func (d *dnsRecordResource) SetClient(client *base.Client) {
d.client = client
}
func (d *dnsRecordResource) SetVersionValidator(validator base.ControllerVersionValidator) {
d.ControllerVersionValidator = validator
}
func NewDnsRecordResource() resource.Resource {
return &dnsRecordResource{}
}

View File

@@ -115,10 +115,8 @@ func New(version string) func() *schema.Provider {
"unifi_site": site.ResourceSite(),
"unifi_account": radius.ResourceAccount(),
"unifi_radius_profile": radius.ResourceRadiusProfile(),
"unifi_setting_mgmt": settings.ResourceSettingMgmt(),
"unifi_setting_radius": settings.ResourceSettingRadius(),
"unifi_setting_usg": settings.ResourceSettingUsg(),
"unifi_user_group": user.ResourceUserGroup(),
"unifi_user": user.ResourceUser(),
},

View File

@@ -182,6 +182,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource
settings.NewNtpResource,
settings.NewSslInspectionResource,
settings.NewTeleportResource,
settings.NewUsgResource,
}
}

View File

@@ -13,6 +13,7 @@ import (
// BaseSettingResource provides common functionality for all setting resources
type BaseSettingResource[T base.ResourceModel] struct {
base.ControllerVersionValidator
client *base.Client
typeName string
modelFactory func() T
@@ -45,6 +46,10 @@ func (b *BaseSettingResource[T]) SetClient(client *base.Client) {
b.client = client
}
func (b *BaseSettingResource[T]) SetVersionValidator(validator base.ControllerVersionValidator) {
b.ControllerVersionValidator = validator
}
func (b *BaseSettingResource[T]) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
base.ConfigureResource(b, req, resp)
}

View File

@@ -52,13 +52,17 @@ var (
_ resource.Resource = &teleportResource{}
_ resource.ResourceWithConfigure = &teleportResource{}
_ resource.ResourceWithImportState = &teleportResource{}
_ resource.ResourceWithConfigValidators = &teleportResource{}
_ resource.ResourceWithModifyPlan = &teleportResource{}
)
type teleportResource struct {
*BaseSettingResource[*teleportModel]
}
func (r *teleportResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
resp.Diagnostics.Append(r.RequireMinVersion("7.1")...)
}
func (r *teleportResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages Teleport settings for a UniFi site. Teleport is a secure remote access technology that allows authorized users to connect to UniFi devices from anywhere.",
@@ -81,12 +85,6 @@ func (r *teleportResource) Schema(_ context.Context, _ resource.SchemaRequest, r
}
}
func (r *teleportResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
return []resource.ConfigValidator{
validators.ResourceRequireMinVersion(r.GetClient(), "7.1", "Teleport requires UniFi controller version 7.1 or higher"),
}
}
func NewTeleportResource() resource.Resource {
r := &teleportResource{}
r.BaseSettingResource = NewBaseSettingResource(

View File

@@ -2,32 +2,110 @@ package settings
import (
"context"
"errors"
"fmt"
"sync"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/filipowm/terraform-provider-unifi/internal/utils"
"github.com/filipowm/go-unifi/unifi"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
"github.com/filipowm/terraform-provider-unifi/internal/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var resourceSettingUsgLock = sync.Mutex{}
func resourceSettingUsgLocker(f func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
resourceSettingUsgLock.Lock()
defer resourceSettingUsgLock.Unlock()
return f(ctx, d, meta)
}
// usgModel represents the data model for USG (UniFi Security Gateway) settings.
// It defines how USG features like mDNS and DHCP relay are configured for a UniFi site.
type usgModel struct {
base.Model
MulticastDnsEnabled types.Bool `tfsdk:"multicast_dns_enabled"`
DhcpRelayServers types.List `tfsdk:"dhcp_relay_servers"`
}
func ResourceSettingUsg() *schema.Resource {
return &schema.Resource{
Description: "The `unifi_setting_usg` resource manages advanced settings for UniFi Security Gateways (USG) and UniFi Dream Machines (UDM/UDM-Pro).\n\n" +
func (d *usgModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
diags := diag.Diagnostics{}
model := &unifi.SettingUsg{
ID: d.ID.ValueString(),
MdnsEnabled: d.MulticastDnsEnabled.ValueBool(),
}
// Extract DHCP relay servers from the list
var dhcpRelayServers []string
diags.Append(utils.ListElementsAs(d.DhcpRelayServers, &dhcpRelayServers)...)
if diags.HasError() {
return nil, diags
}
// Assign DHCP relay servers to the model (up to 5)
model.DHCPRelayServer1 = append(dhcpRelayServers, "")[0]
model.DHCPRelayServer2 = append(dhcpRelayServers, "", "")[1]
model.DHCPRelayServer3 = append(dhcpRelayServers, "", "", "")[2]
model.DHCPRelayServer4 = append(dhcpRelayServers, "", "", "", "")[3]
model.DHCPRelayServer5 = append(dhcpRelayServers, "", "", "", "", "")[4]
return model, diags
}
func (d *usgModel) Merge(other interface{}) diag.Diagnostics {
diags := diag.Diagnostics{}
model, ok := other.(*unifi.SettingUsg)
if !ok {
diags.AddError("Cannot merge", "Cannot merge type that is not *unifi.SettingUsg")
return diags
}
d.ID = types.StringValue(model.ID)
d.MulticastDnsEnabled = types.BoolValue(model.MdnsEnabled)
// Extract non-empty DHCP relay servers
dhcpRelay := []string{}
for _, s := range []string{
model.DHCPRelayServer1,
model.DHCPRelayServer2,
model.DHCPRelayServer3,
model.DHCPRelayServer4,
model.DHCPRelayServer5,
} {
if s == "" {
continue
}
dhcpRelay = append(dhcpRelay, s)
}
// Set the DHCP relay servers list
dhcpRelayServers, diags := types.ListValueFrom(context.Background(), types.StringType, dhcpRelay)
if diags.HasError() {
return diags
}
d.DhcpRelayServers = dhcpRelayServers
return diags
}
var (
_ base.ResourceModel = &usgModel{}
_ resource.Resource = &usgResource{}
_ resource.ResourceWithConfigure = &usgResource{}
_ resource.ResourceWithImportState = &usgResource{}
_ resource.ResourceWithModifyPlan = &usgResource{}
)
type usgResource struct {
*BaseSettingResource[*usgModel]
}
func (r *usgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
resp.Diagnostics.Append(r.RequireMaxVersionForPath("7.0", path.Root("multicast_dns_enabled"), req.Config)...)
}
func (r *usgResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "The `unifi_setting_usg` resource manages advanced settings for UniFi Security Gateways (USG) and UniFi Dream Machines (UDM/UDM-Pro).\n\n" +
"This resource allows you to configure gateway-specific features including:\n" +
" * Multicast DNS (mDNS) for service discovery\n" +
" * DHCP relay for forwarding DHCP requests to external servers\n\n" +
@@ -36,146 +114,44 @@ func ResourceSettingUsg() *schema.Resource {
" * Centralizing DHCP management in enterprise environments\n" +
" * Integration with existing network infrastructure\n\n" +
"Note: Some settings may not be available on all controller versions. For example, multicast_dns_enabled is not supported on UniFi OS v7+.",
CreateContext: resourceSettingUsgLocker(resourceSettingUsgUpsert),
ReadContext: resourceSettingUsgLocker(resourceSettingUsgRead),
UpdateContext: resourceSettingUsgLocker(resourceSettingUsgUpsert),
DeleteContext: schema.NoopContext,
Importer: &schema.ResourceImporter{
StateContext: base.ImportSiteAndID,
},
Schema: map[string]*schema.Schema{
"id": {
Description: "The unique identifier of the USG settings configuration in the UniFi controller.",
Type: schema.TypeString,
Computed: true,
},
"site": {
Description: "The name of the UniFi site where these USG settings should be applied. If not specified, the default site will be used.",
Type: schema.TypeString,
Computed: true,
Optional: true,
ForceNew: true,
},
"multicast_dns_enabled": {
Description: "Enable multicast DNS (mDNS/Bonjour/Avahi) forwarding across VLANs. This allows devices to discover services " +
Attributes: map[string]schema.Attribute{
"id": base.ID(),
"site": base.SiteAttribute(),
"multicast_dns_enabled": schema.BoolAttribute{
MarkdownDescription: "Enable multicast DNS (mDNS/Bonjour/Avahi) forwarding across VLANs. This allows devices to discover services " +
"(like printers, Chromecasts, etc.) even when they are on different networks. Note: Not supported on UniFi OS v7+.",
Type: schema.TypeBool,
Optional: true,
Computed: true,
},
"dhcp_relay_servers": {
Description: "List of up to 5 DHCP relay servers (specified by IP address) that will receive forwarded DHCP requests. " +
"dhcp_relay_servers": schema.ListAttribute{
MarkdownDescription: "List of up to 5 DHCP relay servers (specified by IP address) that will receive forwarded DHCP requests. " +
"This is useful when you want to use external DHCP servers instead of the built-in DHCP server. " +
"Example: ['192.168.1.5', '192.168.2.5']",
Type: schema.TypeList,
ElementType: types.StringType,
Optional: true,
Computed: true,
MaxItems: 5,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.All(
validation.IsIPv4Address,
// this doesn't let blank through
validation.StringLenBetween(1, 50),
),
Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
Validators: []validator.List{
listvalidator.SizeAtMost(5),
listvalidator.ValueStringsAre(validators.IPv4()),
},
},
},
}
}
func resourceSettingUsgUpdateResourceData(d *schema.ResourceData, meta interface{}, setting *unifi.SettingUsg) error {
c := meta.(*base.Client)
//nolint // GetOkExists is deprecated, but using here:
if mdns, hasMdns := d.GetOkExists("multicast_dns_enabled"); hasMdns {
if c.IsControllerV7() {
return fmt.Errorf("multicast_dns_enabled is not supported on controller version %v", c.Version)
}
setting.MdnsEnabled = mdns.(bool)
}
dhcpRelay, err := utils.ListToStringSlice(d.Get("dhcp_relay_servers").([]interface{}))
if err != nil {
return fmt.Errorf("unable to convert dhcp_relay_servers to string slice: %w", err)
}
setting.DHCPRelayServer1 = append(dhcpRelay, "")[0]
setting.DHCPRelayServer2 = append(dhcpRelay, "", "")[1]
setting.DHCPRelayServer3 = append(dhcpRelay, "", "", "")[2]
setting.DHCPRelayServer4 = append(dhcpRelay, "", "", "", "")[3]
setting.DHCPRelayServer5 = append(dhcpRelay, "", "", "", "", "")[4]
return nil
}
func resourceSettingUsgUpsert(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*base.Client)
site := d.Get("site").(string)
if site == "" {
site = c.Site
}
req, err := c.GetSettingUsg(ctx, c.Site)
if err != nil {
return diag.FromErr(err)
}
err = resourceSettingUsgUpdateResourceData(d, meta, req)
if err != nil {
return diag.FromErr(err)
}
resp, err := c.UpdateSettingUsg(ctx, site, req)
if err != nil {
return diag.FromErr(err)
}
d.SetId(resp.ID)
return resourceSettingUsgSetResourceData(resp, d, meta, site)
}
func resourceSettingUsgSetResourceData(resp *unifi.SettingUsg, d *schema.ResourceData, meta interface{}, site string) diag.Diagnostics {
d.Set("site", site)
d.Set("multicast_dns_enabled", resp.MdnsEnabled)
dhcpRelay := []string{}
for _, s := range []string{
resp.DHCPRelayServer1,
resp.DHCPRelayServer2,
resp.DHCPRelayServer3,
resp.DHCPRelayServer4,
resp.DHCPRelayServer5,
} {
if s == "" {
continue
}
dhcpRelay = append(dhcpRelay, s)
}
d.Set("dhcp_relay_servers", dhcpRelay)
return nil
}
func resourceSettingUsgRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*base.Client)
site := d.Get("site").(string)
if site == "" {
site = c.Site
}
resp, err := c.GetSettingUsg(ctx, site)
if errors.Is(err, unifi.ErrNotFound) {
d.SetId("")
return nil
}
if err != nil {
return diag.FromErr(err)
}
return resourceSettingUsgSetResourceData(resp, d, meta, site)
// NewUsgResource creates a new instance of the USG resource.
func NewUsgResource() resource.Resource {
r := &usgResource{}
r.BaseSettingResource = NewBaseSettingResource(
"unifi_setting_usg",
func() *usgModel { return &usgModel{} },
func(ctx context.Context, client *base.Client, site string) (interface{}, error) {
return client.GetSettingUsg(ctx, site)
},
func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) {
return client.UpdateSettingUsg(ctx, site, body.(*unifi.SettingUsg))
},
)
return r
}

View File

@@ -1,437 +0,0 @@
package validators
import (
"context"
"fmt"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)
var (
_ resource.ConfigValidator = &ControllerVersionValidator{}
_ datasource.ConfigValidator = &ControllerVersionValidator{}
_ validator.String = &ControllerVersionValidator{}
_ validator.Bool = &ControllerVersionValidator{}
_ validator.Int64 = &ControllerVersionValidator{}
_ validator.Float64 = &ControllerVersionValidator{}
_ validator.List = &ControllerVersionValidator{}
_ validator.Map = &ControllerVersionValidator{}
_ validator.Object = &ControllerVersionValidator{}
_ validator.Set = &ControllerVersionValidator{}
)
// ControllerVersionValidator is a validator that checks if the UniFi controller version
// matches the specified constraints.
type ControllerVersionValidator struct {
client *base.Client
minVersion *version.Version
maxVersion *version.Version
exactVersion *version.Version
conditionMessage string
}
// Description returns a description of the validator.
func (v ControllerVersionValidator) Description(ctx context.Context) string {
return v.MarkdownDescription(ctx)
}
// MarkdownDescription returns a markdown description of the validator.
func (v ControllerVersionValidator) MarkdownDescription(_ context.Context) string {
if v.exactVersion != nil {
return fmt.Sprintf("Validates that the controller version is exactly %s", v.exactVersion)
}
if v.minVersion != nil && v.maxVersion != nil {
return fmt.Sprintf("Validates that the controller version is between %s and %s", v.minVersion, v.maxVersion)
}
if v.minVersion != nil {
return fmt.Sprintf("Validates that the controller version is at least %s", v.minVersion)
}
if v.maxVersion != nil {
return fmt.Sprintf("Validates that the controller version is at most %s", v.maxVersion)
}
return "Validates the controller version"
}
// ValidateResource validates the resource configuration.
func (v ControllerVersionValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
if v.client == nil || v.client.Version == nil {
resp.Diagnostics.AddWarning("Controller version not available", "Provider was not initialized properly. UniFi client or controller version is not available")
return
}
v.validateVersion(ctx, &resp.Diagnostics)
}
// ValidateDataSource validates the datasource configuration.
func (v ControllerVersionValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
if v.client == nil || v.client.Version == nil {
resp.Diagnostics.AddWarning("Controller version not available", "Provider was not initialized properly. UniFi client or controller version is not available")
return
}
v.validateVersion(ctx, &resp.Diagnostics)
}
// validateVersion checks if the controller version meets the constraints
func (v ControllerVersionValidator) validateVersion(_ context.Context, diags *diag.Diagnostics) {
controllerVersion := v.client.Version
message := v.conditionMessage
if message == "" {
message = "Controller version does not meet requirements"
}
if v.exactVersion != nil && !controllerVersion.Equal(v.exactVersion) {
diags.AddError(
message,
fmt.Sprintf("Controller version %s does not match required version %s", controllerVersion, v.exactVersion),
)
return
}
if v.minVersion != nil && controllerVersion.LessThan(v.minVersion) {
diags.AddError(
message,
fmt.Sprintf("Controller version %s is less than minimum required version %s", controllerVersion, v.minVersion),
)
return
}
if v.maxVersion != nil && controllerVersion.GreaterThan(v.maxVersion) {
diags.AddError(
message,
fmt.Sprintf("Controller version %s is greater than maximum allowed version %s", controllerVersion, v.maxVersion),
)
return
}
}
// validateAttributeVersion is a helper function for attribute validators
func (v ControllerVersionValidator) validateAttributeVersion(ctx context.Context, req path.Path) diag.Diagnostics {
diags := diag.Diagnostics{}
if v.client == nil || v.client.Version == nil {
diags.AddWarning("Controller version not available", "Provider was not initialized properly. UniFi client or controller version is not available")
return diags
}
controllerVersion := v.client.Version
message := v.conditionMessage
if message == "" {
message = "Controller version does not meet requirements"
}
if v.exactVersion != nil && !controllerVersion.Equal(v.exactVersion) {
diags.Append(validatordiag.InvalidAttributeValueDiagnostic(
req,
message,
fmt.Sprintf("Controller version %s does not match required version %s to use given attribute", controllerVersion, v.exactVersion),
))
return diags
}
if v.minVersion != nil && controllerVersion.LessThan(v.minVersion) {
diags.Append(validatordiag.InvalidAttributeValueDiagnostic(
req,
message,
fmt.Sprintf("Controller version %s is less than minimum required version %s to use given attribute", controllerVersion, v.minVersion),
))
return diags
}
if v.maxVersion != nil && controllerVersion.GreaterThan(v.maxVersion) {
diags.Append(validatordiag.InvalidAttributeValueDiagnostic(
req,
message,
fmt.Sprintf("Controller version %s is greater than maximum allowed version %s to use given attribute", controllerVersion, v.maxVersion),
))
return diags
}
return diags
}
// ValidateString implements validator.String
func (v ControllerVersionValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...)
}
// ValidateBool implements validator.Bool
func (v ControllerVersionValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...)
}
// ValidateInt64 implements validator.Int64
func (v ControllerVersionValidator) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...)
}
// ValidateFloat64 implements validator.Float64
func (v ControllerVersionValidator) ValidateFloat64(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...)
}
// ValidateList implements validator.List
func (v ControllerVersionValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...)
}
// ValidateMap implements validator.Map
func (v ControllerVersionValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...)
}
// ValidateObject implements validator.Object
func (v ControllerVersionValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...)
}
// ValidateSet implements validator.Set
func (v ControllerVersionValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...)
}
// ResourceRequireMinVersion returns a resource validator that checks if the controller version
// is at least the specified version.
func ResourceRequireMinVersion(client *base.Client, minVersion string, conditionMessage string) resource.ConfigValidator {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
conditionMessage: conditionMessage,
}
}
// ResourceRequireMaxVersion returns a resource validator that checks if the controller version
// is at most the specified version.
func ResourceRequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) resource.ConfigValidator {
return ControllerVersionValidator{
client: client,
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// ResourceRequireVersionRange returns a resource validator that checks if the controller version
// is within the specified range (inclusive).
func ResourceRequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) resource.ConfigValidator {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// ResourceRequireExactVersion returns a resource validator that checks if the controller version
// matches the specified version exactly.
func ResourceRequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) resource.ConfigValidator {
return ControllerVersionValidator{
client: client,
exactVersion: base.AsVersion(exactVersion),
conditionMessage: conditionMessage,
}
}
// DatasourceRequireMinVersion returns a datasource validator that checks if the controller version
// is at least the specified version.
func DatasourceRequireMinVersion(client *base.Client, minVersion string, conditionMessage string) datasource.ConfigValidator {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
conditionMessage: conditionMessage,
}
}
// DatasourceRequireMaxVersion returns a datasource validator that checks if the controller version
// is at most the specified version.
func DatasourceRequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) datasource.ConfigValidator {
return ControllerVersionValidator{
client: client,
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// DatasourceRequireVersionRange returns a datasource validator that checks if the controller version
// is within the specified range (inclusive).
func DatasourceRequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) datasource.ConfigValidator {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// DatasourceRequireExactVersion returns a datasource validator that checks if the controller version
// matches the specified version exactly.
func DatasourceRequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) datasource.ConfigValidator {
return ControllerVersionValidator{
client: client,
exactVersion: base.AsVersion(exactVersion),
conditionMessage: conditionMessage,
}
}
// StringRequireMinVersion returns a string validator that checks if the controller version
// is at least the specified version.
func StringRequireMinVersion(client *base.Client, minVersion string, conditionMessage string) validator.String {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
conditionMessage: conditionMessage,
}
}
// StringRequireMaxVersion returns a string validator that checks if the controller version
// is at most the specified version.
func StringRequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) validator.String {
return ControllerVersionValidator{
client: client,
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// StringRequireVersionRange returns a string validator that checks if the controller version
// is within the specified range (inclusive).
func StringRequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) validator.String {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// StringRequireExactVersion returns a string validator that checks if the controller version
// matches the specified version exactly.
func StringRequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) validator.String {
return ControllerVersionValidator{
client: client,
exactVersion: base.AsVersion(exactVersion),
conditionMessage: conditionMessage,
}
}
// BoolRequireMinVersion returns a bool validator that checks if the controller version
// is at least the specified version.
func BoolRequireMinVersion(client *base.Client, minVersion string, conditionMessage string) validator.Bool {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
conditionMessage: conditionMessage,
}
}
// BoolRequireMaxVersion returns a bool validator that checks if the controller version
// is at most the specified version.
func BoolRequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) validator.Bool {
return ControllerVersionValidator{
client: client,
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// BoolRequireVersionRange returns a bool validator that checks if the controller version
// is within the specified range (inclusive).
func BoolRequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) validator.Bool {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// BoolRequireExactVersion returns a bool validator that checks if the controller version
// matches the specified version exactly.
func BoolRequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) validator.Bool {
return ControllerVersionValidator{
client: client,
exactVersion: base.AsVersion(exactVersion),
conditionMessage: conditionMessage,
}
}
// Int64RequireMinVersion returns an int64 validator that checks if the controller version
// is at least the specified version.
func Int64RequireMinVersion(client *base.Client, minVersion string, conditionMessage string) validator.Int64 {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
conditionMessage: conditionMessage,
}
}
// Int64RequireMaxVersion returns an int64 validator that checks if the controller version
// is at most the specified version.
func Int64RequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) validator.Int64 {
return ControllerVersionValidator{
client: client,
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// Int64RequireVersionRange returns an int64 validator that checks if the controller version
// is within the specified range (inclusive).
func Int64RequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) validator.Int64 {
return ControllerVersionValidator{
client: client,
minVersion: base.AsVersion(minVersion),
maxVersion: base.AsVersion(maxVersion),
conditionMessage: conditionMessage,
}
}
// Int64RequireExactVersion returns an int64 validator that checks if the controller version
// matches the specified version exactly.
func Int64RequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) validator.Int64 {
return ControllerVersionValidator{
client: client,
exactVersion: base.AsVersion(exactVersion),
conditionMessage: conditionMessage,
}
}

View File

@@ -1,375 +0,0 @@
package validators
import (
"context"
"testing"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/stretchr/testify/assert"
)
func TestControllerVersionValidator_Description(t *testing.T) {
tests := []struct {
name string
validator ControllerVersionValidator
expected string
description string
}{
{
name: "exact version",
validator: ControllerVersionValidator{
exactVersion: base.AsVersion("7.0.0"),
},
expected: "Validates that the controller version is exactly 7.0.0",
description: "Should describe exact version check",
},
{
name: "min version",
validator: ControllerVersionValidator{
minVersion: base.AsVersion("7.0.0"),
},
expected: "Validates that the controller version is at least 7.0.0",
description: "Should describe minimum version check",
},
{
name: "max version",
validator: ControllerVersionValidator{
maxVersion: base.AsVersion("7.0.0"),
},
expected: "Validates that the controller version is at most 7.0.0",
description: "Should describe maximum version check",
},
{
name: "version range",
validator: ControllerVersionValidator{
minVersion: base.AsVersion("7.0.0"),
maxVersion: base.AsVersion("8.0.0"),
},
expected: "Validates that the controller version is between 7.0.0 and 8.0.0",
description: "Should describe version range check",
},
{
name: "no constraint",
validator: ControllerVersionValidator{},
expected: "Validates the controller version",
description: "Should provide generic description when no constraints",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
actual := test.validator.Description(ctx)
assert.Equal(t, test.expected, actual, test.description)
})
}
}
func TestControllerVersionValidator_ValidateResource(t *testing.T) {
tests := []struct {
name string
controllerVersion string
validator ControllerVersionValidator
expectError bool
description string
}{
{
name: "exact version match",
controllerVersion: "7.0.0",
validator: ControllerVersionValidator{
exactVersion: base.AsVersion("7.0.0"),
},
expectError: false,
description: "Should pass when exact version matches",
},
{
name: "exact version mismatch",
controllerVersion: "7.0.0",
validator: ControllerVersionValidator{
exactVersion: base.AsVersion("7.1.0"),
},
expectError: true,
description: "Should fail when exact version doesn't match",
},
{
name: "min version satisfied",
controllerVersion: "7.5.0",
validator: ControllerVersionValidator{
minVersion: base.AsVersion("7.0.0"),
},
expectError: false,
description: "Should pass when version meets minimum",
},
{
name: "min version not satisfied",
controllerVersion: "6.5.0",
validator: ControllerVersionValidator{
minVersion: base.AsVersion("7.0.0"),
},
expectError: true,
description: "Should fail when version doesn't meet minimum",
},
{
name: "max version satisfied",
controllerVersion: "6.5.0",
validator: ControllerVersionValidator{
maxVersion: base.AsVersion("7.0.0"),
},
expectError: false,
description: "Should pass when version is below maximum",
},
{
name: "max version not satisfied",
controllerVersion: "7.5.0",
validator: ControllerVersionValidator{
maxVersion: base.AsVersion("7.0.0"),
},
expectError: true,
description: "Should fail when version exceeds maximum",
},
{
name: "version in range",
controllerVersion: "7.5.0",
validator: ControllerVersionValidator{
minVersion: base.AsVersion("7.0.0"),
maxVersion: base.AsVersion("8.0.0"),
},
expectError: false,
description: "Should pass when version is in range",
},
{
name: "version below range",
controllerVersion: "6.5.0",
validator: ControllerVersionValidator{
minVersion: base.AsVersion("7.0.0"),
maxVersion: base.AsVersion("8.0.0"),
},
expectError: true,
description: "Should fail when version is below range",
},
{
name: "version above range",
controllerVersion: "8.5.0",
validator: ControllerVersionValidator{
minVersion: base.AsVersion("7.0.0"),
maxVersion: base.AsVersion("8.0.0"),
},
expectError: true,
description: "Should fail when version is above range",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
// Create a mock client with the specified version
mockClient := &base.Client{
Version: version.Must(version.NewVersion(test.controllerVersion)),
}
// Update the validator with the mock client
test.validator.client = mockClient
// Create request and response objects
req := resource.ValidateConfigRequest{}
resp := resource.ValidateConfigResponse{
Diagnostics: diag.Diagnostics{},
}
// Call the validator
test.validator.ValidateResource(ctx, req, &resp)
// Check if the result matches expectations
if test.expectError {
assert.True(t, resp.Diagnostics.HasError(), test.description)
} else {
assert.False(t, resp.Diagnostics.HasError(), test.description)
}
})
}
}
func TestResourceHelperFunctions(t *testing.T) {
mockClient := &base.Client{
Version: base.AsVersion("7.5.0"),
}
tests := []struct {
name string
validator resource.ConfigValidator
expectError bool
description string
}{
{
name: "ResourceRequireMinVersion passing",
validator: ResourceRequireMinVersion(mockClient, "7.0.0", ""),
expectError: false,
description: "ResourceRequireMinVersion should pass with sufficient version",
},
{
name: "ResourceRequireMinVersion failing",
validator: ResourceRequireMinVersion(mockClient, "8.0.0", ""),
expectError: true,
description: "ResourceRequireMinVersion should fail with insufficient version",
},
{
name: "ResourceRequireMaxVersion passing",
validator: ResourceRequireMaxVersion(mockClient, "8.0.0", ""),
expectError: false,
description: "ResourceRequireMaxVersion should pass with acceptable version",
},
{
name: "ResourceRequireMaxVersion failing",
validator: ResourceRequireMaxVersion(mockClient, "7.0.0", ""),
expectError: true,
description: "ResourceRequireMaxVersion should fail with too high version",
},
{
name: "ResourceRequireVersionRange passing",
validator: ResourceRequireVersionRange(mockClient, "7.0.0", "8.0.0", ""),
expectError: false,
description: "ResourceRequireVersionRange should pass with version in range",
},
{
name: "ResourceRequireVersionRange failing (below)",
validator: ResourceRequireVersionRange(mockClient, "7.6.0", "8.0.0", ""),
expectError: true,
description: "ResourceRequireVersionRange should fail with version below range",
},
{
name: "ResourceRequireVersionRange failing (above)",
validator: ResourceRequireVersionRange(mockClient, "6.0.0", "7.0.0", ""),
expectError: true,
description: "ResourceRequireVersionRange should fail with version above range",
},
{
name: "ResourceRequireExactVersion passing",
validator: ResourceRequireExactVersion(mockClient, "7.5.0", ""),
expectError: false,
description: "ResourceRequireExactVersion should pass with exact version match",
},
{
name: "ResourceRequireExactVersion failing",
validator: ResourceRequireExactVersion(mockClient, "7.5.1", ""),
expectError: true,
description: "ResourceRequireExactVersion should fail with version mismatch",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
// Create request and response objects
req := resource.ValidateConfigRequest{}
resp := resource.ValidateConfigResponse{
Diagnostics: diag.Diagnostics{},
}
// Call the validator
test.validator.(ControllerVersionValidator).ValidateResource(ctx, req, &resp)
// Check if the result matches expectations
if test.expectError {
assert.True(t, resp.Diagnostics.HasError(), test.description)
} else {
assert.False(t, resp.Diagnostics.HasError(), test.description)
}
})
}
}
func TestDatasourceHelperFunctions(t *testing.T) {
mockClient := &base.Client{
Version: base.AsVersion("7.5.0"),
}
tests := []struct {
name string
validator datasource.ConfigValidator
expectError bool
description string
}{
{
name: "DatasourceRequireMinVersion passing",
validator: DatasourceRequireMinVersion(mockClient, "7.0.0", ""),
expectError: false,
description: "DatasourceRequireMinVersion should pass with sufficient version",
},
{
name: "DatasourceRequireMinVersion failing",
validator: DatasourceRequireMinVersion(mockClient, "8.0.0", ""),
expectError: true,
description: "DatasourceRequireMinVersion should fail with insufficient version",
},
{
name: "DatasourceRequireMaxVersion passing",
validator: DatasourceRequireMaxVersion(mockClient, "8.0.0", ""),
expectError: false,
description: "DatasourceRequireMaxVersion should pass with acceptable version",
},
{
name: "DatasourceRequireMaxVersion failing",
validator: DatasourceRequireMaxVersion(mockClient, "7.0.0", ""),
expectError: true,
description: "DatasourceRequireMaxVersion should fail with too high version",
},
{
name: "DatasourceRequireVersionRange passing",
validator: DatasourceRequireVersionRange(mockClient, "7.0.0", "8.0.0", ""),
expectError: false,
description: "DatasourceRequireVersionRange should pass with version in range",
},
{
name: "DatasourceRequireVersionRange failing (below)",
validator: DatasourceRequireVersionRange(mockClient, "7.6.0", "8.0.0", ""),
expectError: true,
description: "DatasourceRequireVersionRange should fail with version below range",
},
{
name: "DatasourceRequireVersionRange failing (above)",
validator: DatasourceRequireVersionRange(mockClient, "6.0.0", "7.0.0", ""),
expectError: true,
description: "DatasourceRequireVersionRange should fail with version above range",
},
{
name: "DatasourceRequireExactVersion passing",
validator: DatasourceRequireExactVersion(mockClient, "7.5.0", ""),
expectError: false,
description: "DatasourceRequireExactVersion should pass with exact version match",
},
{
name: "DatasourceRequireExactVersion failing",
validator: DatasourceRequireExactVersion(mockClient, "7.5.1", ""),
expectError: true,
description: "DatasourceRequireExactVersion should fail with version mismatch",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
// Create request and response objects
req := datasource.ValidateConfigRequest{}
resp := datasource.ValidateConfigResponse{
Diagnostics: diag.Diagnostics{},
}
// Call the validator
test.validator.(ControllerVersionValidator).ValidateDataSource(ctx, req, &resp)
// Check if the result matches expectations
if test.expectError {
assert.True(t, resp.Diagnostics.HasError(), test.description)
} else {
assert.False(t, resp.Diagnostics.HasError(), test.description)
}
})
}
}

19
internal/utils/lists.go Normal file
View File

@@ -0,0 +1,19 @@
package utils
import (
"context"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
)
func ListElementsAs(list types.List, target interface{}) diag.Diagnostics {
diags := diag.Diagnostics{}
if !base.IsDefined(list) {
return diags
}
if diagErr := list.ElementsAs(context.Background(), target, false); diagErr != nil {
diags = append(diags, diagErr...)
}
return diags
}