diff --git a/internal/provider/acctest/resource_setting_usg_test.go b/internal/provider/acctest/resource_setting_usg_test.go index d7b3cb0..2f06eb4 100644 --- a/internal/provider/acctest/resource_setting_usg_test.go +++ b/internal/provider/acctest/resource_setting_usg_test.go @@ -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"), }, }) } diff --git a/internal/provider/base/base.go b/internal/provider/base/base.go index 216d5eb..420bfb5 100644 --- a/internal/provider/base/base.go +++ b/internal/provider/base/base.go @@ -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)) } diff --git a/internal/provider/base/controller_version.go b/internal/provider/base/controller_version.go new file mode 100644 index 0000000..d4fe270 --- /dev/null +++ b/internal/provider/base/controller_version.go @@ -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 +} diff --git a/internal/provider/base/controller_version_test.go b/internal/provider/base/controller_version_test.go new file mode 100644 index 0000000..c2c2f57 --- /dev/null +++ b/internal/provider/base/controller_version_test.go @@ -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") +} diff --git a/internal/provider/base/controller_versions.go b/internal/provider/base/controller_versions.go deleted file mode 100644 index 97899f9..0000000 --- a/internal/provider/base/controller_versions.go +++ /dev/null @@ -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 -} diff --git a/internal/provider/dns/datasource_dns_record.go b/internal/provider/dns/datasource_dns_record.go index b213f22..9063a2d 100644 --- a/internal/provider/dns/datasource_dns_record.go +++ b/internal/provider/dns/datasource_dns_record.go @@ -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) } diff --git a/internal/provider/dns/datasource_dns_records.go b/internal/provider/dns/datasource_dns_records.go index bcc4c3c..e2fff86 100644 --- a/internal/provider/dns/datasource_dns_records.go +++ b/internal/provider/dns/datasource_dns_records.go @@ -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) } diff --git a/internal/provider/dns/resource_dns_record.go b/internal/provider/dns/resource_dns_record.go index 9a20640..ce0281f 100644 --- a/internal/provider/dns/resource_dns_record.go +++ b/internal/provider/dns/resource_dns_record.go @@ -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{} } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 3626e08..6102ebe 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -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(), }, diff --git a/internal/provider/provider_v2.go b/internal/provider/provider_v2.go index 0c53d06..525bff8 100644 --- a/internal/provider/provider_v2.go +++ b/internal/provider/provider_v2.go @@ -104,8 +104,8 @@ func (p *unifiProvider) Configure(ctx context.Context, req provider.ConfigureReq path.Root("api_url"), "Unknown UniFi Controller API URL", "The provider cannot create the UniFi Controller API client as there is an unknown configuration value "+ - "for the API endpoint. Either target apply the source of the value first, set the value statically in "+ - "the configuration, or use the UNIFI_API environment variable.", + "for the API endpoint. Either target apply the source of the value first, set the value statically in "+ + "the configuration, or use the UNIFI_API environment variable.", ) } @@ -182,6 +182,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource settings.NewNtpResource, settings.NewSslInspectionResource, settings.NewTeleportResource, + settings.NewUsgResource, } } diff --git a/internal/provider/settings/base_setting_resource.go b/internal/provider/settings/base_setting_resource.go index 0621f9e..4164509 100644 --- a/internal/provider/settings/base_setting_resource.go +++ b/internal/provider/settings/base_setting_resource.go @@ -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) } diff --git a/internal/provider/settings/resource_setting_teleport.go b/internal/provider/settings/resource_setting_teleport.go index deb7cc6..9db775c 100644 --- a/internal/provider/settings/resource_setting_teleport.go +++ b/internal/provider/settings/resource_setting_teleport.go @@ -48,17 +48,21 @@ func (d *teleportModel) Merge(other interface{}) diag.Diagnostics { } var ( - _ base.ResourceModel = &teleportModel{} - _ resource.Resource = &teleportResource{} - _ resource.ResourceWithConfigure = &teleportResource{} - _ resource.ResourceWithImportState = &teleportResource{} - _ resource.ResourceWithConfigValidators = &teleportResource{} + _ base.ResourceModel = &teleportModel{} + _ resource.Resource = &teleportResource{} + _ resource.ResourceWithConfigure = &teleportResource{} + _ resource.ResourceWithImportState = &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( diff --git a/internal/provider/settings/resource_setting_usg.go b/internal/provider/settings/resource_setting_usg.go index 4cda9dd..e4a1a54 100644 --- a/internal/provider/settings/resource_setting_usg.go +++ b/internal/provider/settings/resource_setting_usg.go @@ -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, - 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), - ), + ElementType: types.StringType, + Optional: true, + Computed: true, + 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 } diff --git a/internal/provider/validators/controller_version.go b/internal/provider/validators/controller_version.go deleted file mode 100644 index bbb817c..0000000 --- a/internal/provider/validators/controller_version.go +++ /dev/null @@ -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, - } -} diff --git a/internal/provider/validators/controller_version_test.go b/internal/provider/validators/controller_version_test.go deleted file mode 100644 index 73dd0c0..0000000 --- a/internal/provider/validators/controller_version_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/internal/utils/lists.go b/internal/utils/lists.go new file mode 100644 index 0000000..899deb1 --- /dev/null +++ b/internal/utils/lists.go @@ -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 +}