feat: add locale setting resource support with unifi_setting_locale resource (#34)
* feat: add locale setting resource support with `unifi_setting_locale` resource * lint
This commit is contained in:
committed by
GitHub
parent
273d0daddd
commit
f815ffef79
70
internal/provider/acctest/resource_setting_locale_test.go
Normal file
70
internal/provider/acctest/resource_setting_locale_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package acctest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
pt "github.com/filipowm/terraform-provider-unifi/internal/provider/testing"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
"github.com/hashicorp/terraform-plugin-testing/plancheck"
|
||||
"regexp"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var settingLocaleLock = &sync.Mutex{}
|
||||
|
||||
func TestAccSettingLocale(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
VersionConstraint: ">= 7.3",
|
||||
Lock: settingLocaleLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingLocaleConfig("America/New_York"),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet("unifi_setting_locale.test", "id"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_locale.test", "site", "default"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_locale.test", "timezone", "America/New_York"),
|
||||
),
|
||||
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_locale.test", plancheck.ResourceActionCreate),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_locale.test"),
|
||||
{
|
||||
Config: testAccSettingLocaleConfig("Europe/London"),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet("unifi_setting_locale.test", "id"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_locale.test", "site", "default"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_locale.test", "timezone", "Europe/London"),
|
||||
),
|
||||
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_locale.test", plancheck.ResourceActionUpdate),
|
||||
},
|
||||
{
|
||||
Config: testAccSettingLocaleConfig("UTC"),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet("unifi_setting_locale.test", "id"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_locale.test", "site", "default"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_locale.test", "timezone", "UTC"),
|
||||
),
|
||||
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_locale.test", plancheck.ResourceActionUpdate),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
func TestAccSettingLocaleInvalid(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
VersionConstraint: ">= 7.3",
|
||||
Lock: settingLocaleLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingLocaleConfig("Invalid/Timezone"),
|
||||
ExpectError: regexp.MustCompile("must be a valid IANA timezone identifier"),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccSettingLocaleConfig(timezone string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "unifi_setting_locale" "test" {
|
||||
timezone = %q
|
||||
}
|
||||
`, timezone)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/dns"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/settings"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/utils"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
@@ -25,6 +26,10 @@ func NewV2(version string) func() provider.Provider {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ provider.Provider = &unifiProvider{}
|
||||
)
|
||||
|
||||
type unifiProvider struct {
|
||||
version string
|
||||
}
|
||||
@@ -64,6 +69,7 @@ func (p *unifiProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp
|
||||
MarkdownDescription: ProviderAPIURLDescription,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1), // workaround for `required: true`, because it fails on doc generation due to incorrectly detected difference between v1 and v2
|
||||
validators.HTTPSUrl(),
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
@@ -170,6 +176,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource
|
||||
dns.NewDnsRecordResource,
|
||||
settings.NewAutoSpeedtestResource,
|
||||
settings.NewCountryResource,
|
||||
settings.NewLocaleResource,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
88
internal/provider/settings/resource_setting_locale.go
Normal file
88
internal/provider/settings/resource_setting_locale.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
|
||||
"github.com/filipowm/go-unifi/unifi"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
type localeModel struct {
|
||||
base.Model
|
||||
Timezone types.String `tfsdk:"timezone"`
|
||||
}
|
||||
|
||||
func (d *localeModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model := &unifi.SettingLocale{
|
||||
ID: d.ID.ValueString(),
|
||||
Timezone: d.Timezone.ValueString(),
|
||||
}
|
||||
|
||||
return model, diags
|
||||
}
|
||||
|
||||
func (d *localeModel) Merge(other interface{}) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model, ok := other.(*unifi.SettingLocale)
|
||||
if !ok {
|
||||
diags.AddError("Cannot merge", "Cannot merge type that is not *unifi.SettingLocale")
|
||||
return diags
|
||||
}
|
||||
|
||||
d.ID = types.StringValue(model.ID)
|
||||
d.Timezone = types.StringValue(model.Timezone)
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
var (
|
||||
_ base.ResourceModel = &localeModel{}
|
||||
_ resource.Resource = &localeResource{}
|
||||
_ resource.ResourceWithConfigure = &localeResource{}
|
||||
_ resource.ResourceWithImportState = &localeResource{}
|
||||
)
|
||||
|
||||
type localeResource struct {
|
||||
*BaseSettingResource[*localeModel]
|
||||
}
|
||||
|
||||
func (r *localeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: "Manages locale settings for a UniFi site.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": base.ID(),
|
||||
"site": base.SiteAttribute(),
|
||||
"timezone": schema.StringAttribute{
|
||||
Required: true,
|
||||
MarkdownDescription: "Timezone for the UniFi controller, e.g., `America/Los_Angeles`",
|
||||
Validators: []validator.String{
|
||||
validators.Timezone(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewLocaleResource() resource.Resource {
|
||||
r := &localeResource{}
|
||||
r.BaseSettingResource = NewBaseSettingResource(
|
||||
"unifi_setting_locale",
|
||||
func() *localeModel { return &localeModel{} },
|
||||
func(ctx context.Context, client *base.Client, site string) (interface{}, error) {
|
||||
return client.GetSettingLocale(ctx, site)
|
||||
},
|
||||
func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) {
|
||||
return client.UpdateSettingLocale(ctx, site, body.(*unifi.SettingLocale))
|
||||
},
|
||||
)
|
||||
return r
|
||||
}
|
||||
93
internal/provider/validators/timezone.go
Normal file
93
internal/provider/validators/timezone.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
)
|
||||
|
||||
// Timezone returns a validator which ensures that the string value is a valid IANA timezone identifier
|
||||
// according to the time.LoadLocation function.
|
||||
func Timezone() validator.String {
|
||||
return timezoneValidator{}
|
||||
}
|
||||
|
||||
type timezoneValidator struct{}
|
||||
|
||||
func (v timezoneValidator) Description(_ context.Context) string {
|
||||
return "must be a valid IANA timezone identifier (e.g., 'America/New_York')"
|
||||
}
|
||||
|
||||
func (v timezoneValidator) MarkdownDescription(ctx context.Context) string {
|
||||
return v.Description(ctx)
|
||||
}
|
||||
|
||||
func (v timezoneValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||
value := req.ConfigValue
|
||||
if !base.IsDefined(value) {
|
||||
return
|
||||
}
|
||||
|
||||
val := value.ValueString()
|
||||
|
||||
// Check for empty string
|
||||
if val == "" {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
"Timezone cannot be empty. Use a valid IANA timezone identifier like 'America/New_York'",
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for proper case (IANA timezone identifiers are case-sensitive)
|
||||
// Regions should start with uppercase
|
||||
if val[0] >= 'a' && val[0] <= 'z' {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q has incorrect case. IANA timezone regions should start with uppercase (e.g., 'America/New_York')", val),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to load the timezone location
|
||||
_, err := time.LoadLocation(val)
|
||||
if err != nil {
|
||||
// For better error messages, check common mistakes
|
||||
if strings.Contains(val, "UTC") && val != "UTC" {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q is not a valid timezone. For UTC offset use the standard 'UTC' timezone instead.", val),
|
||||
),
|
||||
)
|
||||
} else if strings.Contains(val, " ") {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q is not a valid timezone. Timezones should not contain spaces.", val),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q is not a valid IANA timezone identifier. Use a value like 'America/New_York'", val),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
internal/provider/validators/timezone_test.go
Normal file
85
internal/provider/validators/timezone_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
func TestTimezoneValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
val types.String
|
||||
expectError bool
|
||||
}
|
||||
tests := map[string]testCase{
|
||||
"unknown": {
|
||||
val: types.StringUnknown(),
|
||||
},
|
||||
"null": {
|
||||
val: types.StringNull(),
|
||||
},
|
||||
"valid-america": {
|
||||
val: types.StringValue("America/Los_Angeles"),
|
||||
},
|
||||
"valid-europe": {
|
||||
val: types.StringValue("Europe/London"),
|
||||
},
|
||||
"valid-asia": {
|
||||
val: types.StringValue("Asia/Tokyo"),
|
||||
},
|
||||
"valid-australia": {
|
||||
val: types.StringValue("Australia/Sydney"),
|
||||
},
|
||||
"valid-utc": {
|
||||
val: types.StringValue("UTC"),
|
||||
},
|
||||
"invalid-with-space": {
|
||||
val: types.StringValue("America/New York"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-nonexistent": {
|
||||
val: types.StringValue("NonExistent/Timezone"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-empty-string": {
|
||||
val: types.StringValue(""),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-just-region": {
|
||||
val: types.StringValue("America"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-lowercase": {
|
||||
val: types.StringValue("america/los_angeles"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-utc-offset": {
|
||||
val: types.StringValue("UTC+01:00"),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
name, test := name, test
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
request := validator.StringRequest{
|
||||
ConfigValue: test.val,
|
||||
}
|
||||
response := validator.StringResponse{}
|
||||
Timezone().ValidateString(context.Background(), request, &response)
|
||||
|
||||
if !response.Diagnostics.HasError() && test.expectError {
|
||||
t.Fatal("expected error, got no error")
|
||||
}
|
||||
|
||||
if response.Diagnostics.HasError() && !test.expectError {
|
||||
t.Fatalf("got unexpected error: %s", response.Diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
92
internal/provider/validators/url.go
Normal file
92
internal/provider/validators/url.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
)
|
||||
|
||||
// URL returns a validator which ensures that the string value is a valid URL.
|
||||
func URL() validator.String {
|
||||
return urlValidator{requireHTTPS: false}
|
||||
}
|
||||
|
||||
// HTTPSUrl returns a validator which ensures that the string value is a valid HTTPS URL.
|
||||
func HTTPSUrl() validator.String {
|
||||
return urlValidator{requireHTTPS: true}
|
||||
}
|
||||
|
||||
type urlValidator struct {
|
||||
requireHTTPS bool
|
||||
}
|
||||
|
||||
func (v urlValidator) Description(_ context.Context) string {
|
||||
if v.requireHTTPS {
|
||||
return "must be a valid HTTPS URL"
|
||||
}
|
||||
return "must be a valid URL"
|
||||
}
|
||||
|
||||
func (v urlValidator) MarkdownDescription(ctx context.Context) string {
|
||||
return v.Description(ctx)
|
||||
}
|
||||
|
||||
func (v urlValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||
value := req.ConfigValue
|
||||
if !base.IsDefined(value) {
|
||||
return
|
||||
}
|
||||
|
||||
val := value.ValueString()
|
||||
parsedURL, err := url.Parse(val)
|
||||
|
||||
if err != nil {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q is not a valid URL: %s", val, err),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if URL has a scheme
|
||||
if parsedURL.Scheme == "" {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q is missing a scheme (e.g., http:// or https://)", val),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if HTTPS is required
|
||||
if v.requireHTTPS && parsedURL.Scheme != "https" {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q must use HTTPS scheme", val),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if URL has a host
|
||||
if parsedURL.Host == "" {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q is missing a host", val),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
128
internal/provider/validators/url_test.go
Normal file
128
internal/provider/validators/url_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
func TestURLValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
val types.String
|
||||
expectError bool
|
||||
}
|
||||
tests := map[string]testCase{
|
||||
"unknown": {
|
||||
val: types.StringUnknown(),
|
||||
},
|
||||
"null": {
|
||||
val: types.StringNull(),
|
||||
},
|
||||
"valid-http": {
|
||||
val: types.StringValue("http://example.com"),
|
||||
},
|
||||
"valid-https": {
|
||||
val: types.StringValue("https://example.com"),
|
||||
},
|
||||
"valid-with-path": {
|
||||
val: types.StringValue("https://example.com/path"),
|
||||
},
|
||||
"valid-with-query": {
|
||||
val: types.StringValue("https://example.com/path?query=value"),
|
||||
},
|
||||
"valid-with-port": {
|
||||
val: types.StringValue("https://example.com:8443"),
|
||||
},
|
||||
"invalid-no-scheme": {
|
||||
val: types.StringValue("example.com"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-no-host": {
|
||||
val: types.StringValue("https://"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-malformed": {
|
||||
val: types.StringValue("htt ps://example.com"),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
name, test := name, test
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
request := validator.StringRequest{
|
||||
ConfigValue: test.val,
|
||||
}
|
||||
response := validator.StringResponse{}
|
||||
URL().ValidateString(context.Background(), request, &response)
|
||||
|
||||
if !response.Diagnostics.HasError() && test.expectError {
|
||||
t.Fatal("expected error, got no error")
|
||||
}
|
||||
|
||||
if response.Diagnostics.HasError() && !test.expectError {
|
||||
t.Fatalf("got unexpected error: %s", response.Diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPSURLValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
val types.String
|
||||
expectError bool
|
||||
}
|
||||
tests := map[string]testCase{
|
||||
"unknown": {
|
||||
val: types.StringUnknown(),
|
||||
},
|
||||
"null": {
|
||||
val: types.StringNull(),
|
||||
},
|
||||
"valid-https": {
|
||||
val: types.StringValue("https://example.com"),
|
||||
},
|
||||
"valid-with-path": {
|
||||
val: types.StringValue("https://example.com/path"),
|
||||
},
|
||||
"invalid-http": {
|
||||
val: types.StringValue("http://example.com"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-no-scheme": {
|
||||
val: types.StringValue("example.com"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-no-host": {
|
||||
val: types.StringValue("https://"),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
name, test := name, test
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
request := validator.StringRequest{
|
||||
ConfigValue: test.val,
|
||||
}
|
||||
response := validator.StringResponse{}
|
||||
HTTPSUrl().ValidateString(context.Background(), request, &response)
|
||||
|
||||
if !response.Diagnostics.HasError() && test.expectError {
|
||||
t.Fatal("expected error, got no error")
|
||||
}
|
||||
|
||||
if response.Diagnostics.HasError() && !test.expectError {
|
||||
t.Fatalf("got unexpected error: %s", response.Diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user