feat: add country setting resource support with unifi_setting_country resource (#31)

* feat: add country setting resource support with `unifi_setting_country` resource

* linting
This commit is contained in:
Mateusz Filipowicz
2025-02-27 02:56:07 +01:00
committed by GitHub
parent ccac6edebe
commit a36940b019
33 changed files with 625 additions and 60 deletions

1
go.mod
View File

@@ -8,6 +8,7 @@ go 1.23.5
require ( require (
github.com/apparentlymart/go-cidr v1.1.0 github.com/apparentlymart/go-cidr v1.1.0
github.com/biter777/countries v1.7.5
github.com/deckarep/golang-set/v2 v2.7.0 github.com/deckarep/golang-set/v2 v2.7.0
github.com/filipowm/go-unifi v1.4.0 github.com/filipowm/go-unifi v1.4.0
github.com/golangci/golangci-lint v1.64.5 github.com/golangci/golangci-lint v1.64.5

2
go.sum
View File

@@ -122,6 +122,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E=
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=

View File

@@ -162,7 +162,7 @@ func TestDNSRecord_Update(t *testing.T) {
{ {
Config: testAccDnsRecordConfig(updated), Config: testAccDnsRecordConfig(updated),
Check: testAccDnsRecordCheckAttrs(updated), Check: testAccDnsRecordCheckAttrs(updated),
ConfigPlanChecks: pt.CheckResourceAction(testDnsRecordResourceName, plancheck.ResourceActionUpdate), ConfigPlanChecks: pt.CheckResourceActions(testDnsRecordResourceName, plancheck.ResourceActionUpdate),
}, },
}, },
}) })

View File

@@ -0,0 +1,69 @@
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 settingCountryLock = &sync.Mutex{}
func TestAccSettingCountry(t *testing.T) {
AcceptanceTest(t, AcceptanceTestCase{
Lock: settingCountryLock,
Steps: []resource.TestStep{
{
Config: testAccSettingCountryConfig("US"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("unifi_setting_country.test", "code", "US"),
resource.TestCheckResourceAttr("unifi_setting_country.test", "code_numeric", "840"),
),
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_country.test", plancheck.ResourceActionCreate),
},
pt.ImportStepWithSite("unifi_setting_country.test"),
{
Config: testAccSettingCountryConfig("PL"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("unifi_setting_country.test", "code", "PL"),
resource.TestCheckResourceAttr("unifi_setting_country.test", "code_numeric", "616"),
),
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_country.test", plancheck.ResourceActionUpdate),
},
},
})
}
var invalidCountryCodeErrorRegex = regexp.MustCompile("ISO 3166-1 alpha-2")
var stringLengthExactly2Regex = regexp.MustCompile("string length must be exactly 2")
func TestAccSettingCountry_invalidCode(t *testing.T) {
AcceptanceTest(t, AcceptanceTestCase{
Lock: settingCountryLock,
Steps: []resource.TestStep{
{
Config: testAccSettingCountryConfig("WP"),
ExpectError: invalidCountryCodeErrorRegex,
},
{
Config: testAccSettingCountryConfig("Too long"),
ExpectError: stringLengthExactly2Regex,
},
{
Config: testAccSettingCountryConfig(""),
ExpectError: stringLengthExactly2Regex,
},
},
})
}
func testAccSettingCountryConfig(code string) string {
return fmt.Sprintf(`
resource "unifi_setting_country" "test" {
code = %q
}
`, code)
}

View File

@@ -4,12 +4,31 @@ import (
"fmt" "fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
) )
type BaseData interface { type BaseData interface {
SetClient(client *Client) SetClient(client *Client)
} }
type Site struct {
Site types.String `tfsdk:"site"`
}
func NewSite(str string) Site {
return Site{
Site: types.StringValue(str),
}
}
func (s *Site) SetSite(site string) {
s.Site = types.StringValue(site)
}
func (s *Site) AsString() string {
return s.Site.ValueString()
}
func ConfigureDatasource(base BaseData, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { func ConfigureDatasource(base BaseData, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil { if req.ProviderData == nil {
return return

View File

@@ -80,6 +80,13 @@ type Client struct {
Version *version.Version Version *version.Version
} }
func (c *Client) ResolveSite(site *Site) string {
if site == nil || IsEmptyString(site.Site) {
return c.Site
}
return site.AsString()
}
func CreateHttpTransport(insecure bool) http.RoundTripper { func CreateHttpTransport(insecure bool) http.RoundTripper {
return &http.Transport{ return &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,

View File

@@ -0,0 +1,36 @@
package base
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"strings"
)
func ImportSiteAndID(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
if id := d.Id(); strings.Contains(id, ":") {
importParts := strings.SplitN(id, ":", 2)
d.SetId(importParts[1])
d.Set("site", importParts[0])
}
return []*schema.ResourceData{d}, nil
}
func ImportIDWithSite(req resource.ImportStateRequest, resp *resource.ImportStateResponse) (string, string) {
id := req.ID
if id == "" {
resp.Diagnostics.AddError("Invalid ID", "ID is required")
return "", ""
}
if strings.Contains(id, ":") {
importParts := strings.SplitN(id, ":", 2)
if len(importParts) == 2 {
return importParts[1], importParts[0]
}
resp.Diagnostics.AddError("Invalid ID", "ID contains too many colon-separated parts. Format should be 'site:id'")
return "", ""
}
resp.Diagnostics.AddError("Invalid ID", "ID does not contain site part. Format should be 'site:id'")
return id, ""
}

View File

@@ -1,17 +1,18 @@
package utils package base
import ( import (
"github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
) )
// ID generates an attribute definition suitable for the always-present `id` attribute. // ID generates an attribute definition suitable for the always-present `id` attribute.
func ID(desc ...string) schema.StringAttribute { func ID(desc ...string) schema.StringAttribute {
a := schema.StringAttribute{ a := schema.StringAttribute{
Computed: true,
Description: "The unique identifier of this resource.", Description: "The unique identifier of this resource.",
Computed: true,
PlanModifiers: []planmodifier.String{ PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(), stringplanmodifier.UseStateForUnknown(),
}, },
@@ -24,6 +25,23 @@ func ID(desc ...string) schema.StringAttribute {
return a return a
} }
func SiteAttribute(desc ...string) schema.StringAttribute {
s := schema.StringAttribute{
MarkdownDescription: "The name of the UniFi site where this resource should be applied. If not specified, the default site will be used.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
}
if len(desc) > 0 {
s.Description = desc[0]
}
return s
}
// ShouldBeRemoved evaluates if an attribute should be removed from the plan during update. // ShouldBeRemoved evaluates if an attribute should be removed from the plan during update.
func ShouldBeRemoved(plan attr.Value, state attr.Value, isClone bool) bool { func ShouldBeRemoved(plan attr.Value, state attr.Value, isClone bool) bool {
return !IsDefined(plan) && IsDefined(state) && !isClone return !IsDefined(plan) && IsDefined(state) && !isClone
@@ -33,3 +51,7 @@ func ShouldBeRemoved(plan attr.Value, state attr.Value, isClone bool) bool {
func IsDefined(v attr.Value) bool { func IsDefined(v attr.Value) bool {
return !v.IsNull() && !v.IsUnknown() return !v.IsNull() && !v.IsUnknown()
} }
func IsEmptyString(s types.String) bool {
return s.IsNull() || s.IsUnknown() || s.ValueString() == ""
}

View File

@@ -29,7 +29,7 @@ func ResourcePortProfile() *schema.Resource {
UpdateContext: resourcePortProfileUpdate, UpdateContext: resourcePortProfileUpdate,
DeleteContext: resourcePortProfileDelete, DeleteContext: resourcePortProfileDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -2,7 +2,7 @@ package dns
import ( import (
"github.com/filipowm/go-unifi/unifi" "github.com/filipowm/go-unifi/unifi"
"github.com/filipowm/terraform-provider-unifi/internal/utils" "github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types"
) )
@@ -32,8 +32,8 @@ type dnsRecordsDatasourceModel struct {
} }
var dnsRecordDatasourceAttributes = map[string]schema.Attribute{ var dnsRecordDatasourceAttributes = map[string]schema.Attribute{
"id": utils.ID(), "id": base.ID(),
"site_id": utils.ID("The site ID where the DNS record is located."), "site_id": base.ID("The site ID where the DNS record is located."),
"name": schema.StringAttribute{ "name": schema.StringAttribute{
Description: "DNS record name.", Description: "DNS record name.",
Computed: true, Computed: true,

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base" "github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/filipowm/terraform-provider-unifi/internal/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/int32validator" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/diag"
@@ -53,8 +52,8 @@ func (d *dnsRecordResource) Schema(_ context.Context, _ resource.SchemaRequest,
" * Adding TXT records for service verification\n\n", " * Adding TXT records for service verification\n\n",
Attributes: map[string]schema.Attribute{ Attributes: map[string]schema.Attribute{
"id": utils.ID(), "id": base.ID(),
"site_id": utils.ID("The site ID where the DNS record is located."), "site_id": base.ID("The site ID where the DNS record is located."),
"name": schema.StringAttribute{ "name": schema.StringAttribute{
MarkdownDescription: "DNS record name.", MarkdownDescription: "DNS record name.",
Required: true, Required: true,
@@ -244,11 +243,6 @@ func (d *dnsRecordResource) ImportState(ctx context.Context, req resource.Import
} }
d.read(ctx, &state, &resp.Diagnostics) d.read(ctx, &state, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
d.read(ctx, &state, &resp.Diagnostics)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return
} }

View File

@@ -4,10 +4,8 @@ import (
"context" "context"
"errors" "errors"
"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/filipowm/go-unifi/unifi"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" "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/schema"
) )
@@ -31,7 +29,7 @@ func ResourceDynamicDNS() *schema.Resource {
UpdateContext: resourceDynamicDNSUpdate, UpdateContext: resourceDynamicDNSUpdate,
DeleteContext: resourceDynamicDNSDelete, DeleteContext: resourceDynamicDNSDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -31,7 +31,7 @@ func ResourceFirewallGroup() *schema.Resource {
UpdateContext: resourceFirewallGroupUpdate, UpdateContext: resourceFirewallGroupUpdate,
DeleteContext: resourceFirewallGroupDelete, DeleteContext: resourceFirewallGroupDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -32,7 +32,7 @@ func ResourceFirewallRule() *schema.Resource {
UpdateContext: resourceFirewallRuleUpdate, UpdateContext: resourceFirewallRuleUpdate,
DeleteContext: resourceFirewallRuleDelete, DeleteContext: resourceFirewallRuleDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -33,7 +33,7 @@ func ResourceWLAN() *schema.Resource {
UpdateContext: resourceWLANUpdate, UpdateContext: resourceWLANUpdate,
DeleteContext: resourceWLANDelete, DeleteContext: resourceWLANDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base" "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/dns"
"github.com/filipowm/terraform-provider-unifi/internal/provider/settings"
"github.com/filipowm/terraform-provider-unifi/internal/utils" "github.com/filipowm/terraform-provider-unifi/internal/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource"
@@ -167,6 +168,7 @@ func (p *unifiProvider) Configure(ctx context.Context, req provider.ConfigureReq
func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource { func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{ return []func() resource.Resource{
dns.NewDnsRecordResource, dns.NewDnsRecordResource,
settings.NewCountryResource,
} }
} }

View File

@@ -4,10 +4,8 @@ import (
"context" "context"
"errors" "errors"
"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/filipowm/go-unifi/unifi"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" "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/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@@ -38,7 +36,7 @@ func ResourceAccount() *schema.Resource {
UpdateContext: resourceAccountUpdate, UpdateContext: resourceAccountUpdate,
DeleteContext: resourceAccountDelete, DeleteContext: resourceAccountDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -28,7 +28,7 @@ func ResourcePortForward() *schema.Resource {
UpdateContext: resourcePortForwardUpdate, UpdateContext: resourcePortForwardUpdate,
DeleteContext: resourcePortForwardDelete, DeleteContext: resourcePortForwardDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -28,7 +28,7 @@ func ResourceStaticRoute() *schema.Resource {
UpdateContext: resourceStaticRouteUpdate, UpdateContext: resourceStaticRouteUpdate,
DeleteContext: resourceStaticRouteDelete, DeleteContext: resourceStaticRouteDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -0,0 +1,176 @@
package settings
import (
"context"
"errors"
"github.com/biter777/countries"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type countryModel struct {
base.Site
ID types.String `tfsdk:"id"`
Code types.String `tfsdk:"code"`
CodeNumeric types.Int32 `tfsdk:"code_numeric"`
}
func (d *countryModel) asUnifiModel() *unifi.SettingCountry {
code := countries.ByName(d.Code.ValueString())
return &unifi.SettingCountry{
ID: d.ID.ValueString(),
Code: int(code),
}
}
func (d *countryModel) merge(other *unifi.SettingCountry) {
d.ID = types.StringValue(other.ID)
// UniFi uses numeric codes, so we need to convert the alpha-2 code to the numeric code, but we store both
code := countries.ByNumeric(other.Code)
d.Code = types.StringValue(code.Alpha2())
d.CodeNumeric = types.Int32Value(int32(code))
}
var (
_ resource.Resource = &countryResource{}
_ resource.ResourceWithConfigure = &countryResource{}
_ resource.ResourceWithImportState = &countryResource{}
_ base.BaseData = &countryResource{}
)
type countryResource struct {
client *base.Client
}
func (c *countryResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id, site := base.ImportIDWithSite(req, resp)
if resp.Diagnostics.HasError() {
return
}
state := countryModel{
ID: types.StringValue(id),
Site: base.NewSite(site),
}
c.read(ctx, site, &state, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func NewCountryResource() resource.Resource {
return &countryResource{}
}
func (c *countryResource) SetClient(client *base.Client) {
c.client = client
}
func (c *countryResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
base.ConfigureResource(c, req, resp)
}
func (c *countryResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "unifi_setting_country"
}
func (c *countryResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "The `unifi_setting_country` resource allows you to configure the country settings for your UniFi network. ",
Attributes: map[string]schema.Attribute{
"id": base.ID(),
"site": base.SiteAttribute(),
"code": schema.StringAttribute{
Description: "The country code to set for the UniFi site. The country code must be a valid ISO 3166-1 alpha-2 code.",
Required: true,
Validators: []validator.String{
validators.StringLengthExactly(2),
validators.CountryCodeAlpha2(),
},
},
"code_numeric": schema.Int32Attribute{
Description: "The numeric representation in ISO 3166-1 of the country code.",
Computed: true,
},
},
}
}
func (c *countryResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan countryModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
body := plan.asUnifiModel()
site := c.client.ResolveSite(&plan.Site)
res, err := c.client.UpdateSettingCountry(ctx, site, body)
if err != nil {
resp.Diagnostics.AddError("Error creating country settings", err.Error())
return
}
plan.merge(res)
plan.Site.SetSite(site)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (c *countryResource) read(ctx context.Context, site string, state *countryModel, diag *diag.Diagnostics) {
res, err := c.client.GetSettingCountry(ctx, site)
if err != nil {
if errors.Is(err, unifi.ErrNotFound) {
diag.AddError("Country settings not found", "The country settings were not found in the UniFi controller")
} else {
diag.AddError("Error reading country settings", err.Error())
}
return
}
state.merge(res)
}
func (c *countryResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state countryModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
site := c.client.ResolveSite(&state.Site)
c.read(ctx, site, &state, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
(&state).Site.SetSite(site)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (c *countryResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state countryModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
body := plan.asUnifiModel()
site := c.client.ResolveSite(&plan.Site)
res, err := c.client.UpdateSettingCountry(ctx, site, body)
if err != nil {
resp.Diagnostics.AddError("Error updating country settings", err.Error())
return
}
state.merge(res)
state.Site.SetSite(site)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (c *countryResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// Not supported
}

View File

@@ -0,0 +1,59 @@
package settings
import (
"github.com/filipowm/go-unifi/unifi"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSettingCountry_ProperCountryCodeMappingFromModel(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
code string
expectedNumericCode int
}{
{"Poland", "PL", 616},
{"United States", "US", 840},
{"Unknown", "WP", 0},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
model := countryModel{
Code: types.StringValue(tc.code),
}
unifiModel := model.asUnifiModel()
assert.Equal(t, tc.expectedNumericCode, unifiModel.Code)
})
}
}
func TestSettingCountry_ProperCountryCodeMappingToModel(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
numericCode int
expectedCode string
}{
{"Poland", 616, "PL"},
{"United States", 840, "US"},
{"Unknown", 0, "Unknown"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
unifiModel := &unifi.SettingCountry{
Code: tc.numericCode,
}
model := countryModel{}
model.merge(unifiModel)
assert.Equal(t, tc.expectedCode, model.Code.ValueString())
})
}
}

View File

@@ -5,10 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"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/filipowm/go-unifi/unifi"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" "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/schema"
) )
@@ -34,9 +32,10 @@ func ResourceSettingMgmt() *schema.Resource {
UpdateContext: resourceSettingMgmtUpdate, UpdateContext: resourceSettingMgmtUpdate,
DeleteContext: resourceSettingMgmtDelete, DeleteContext: resourceSettingMgmtDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
// TODO add more
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"id": { "id": {
Description: "The unique identifier of the management settings configuration in the UniFi controller.", Description: "The unique identifier of the management settings configuration in the UniFi controller.",

View File

@@ -4,10 +4,8 @@ import (
"context" "context"
"errors" "errors"
"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/filipowm/go-unifi/unifi"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" "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/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@@ -31,7 +29,7 @@ func ResourceSettingRadius() *schema.Resource {
UpdateContext: resourceSettingRadiusUpdate, UpdateContext: resourceSettingRadiusUpdate,
DeleteContext: schema.NoopContext, DeleteContext: schema.NoopContext,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -42,7 +42,7 @@ func ResourceSettingUsg() *schema.Resource {
UpdateContext: resourceSettingUsgLocker(resourceSettingUsgUpsert), UpdateContext: resourceSettingUsgLocker(resourceSettingUsgUpsert),
DeleteContext: schema.NoopContext, DeleteContext: schema.NoopContext,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -20,6 +20,21 @@ func MarkAccTest(t *testing.T) {
} }
} }
func ImportStepWithSite(name string, ignore ...string) resource.TestStep {
step := &resource.TestStep{
ResourceName: name,
ImportState: true,
ImportStateVerify: true,
ImportStateIdFunc: SiteAndIDImportStateIDFunc(name),
}
if len(ignore) > 0 {
step.ImportStateVerifyIgnore = ignore
}
return *step
}
func ImportStep(name string, ignore ...string) resource.TestStep { func ImportStep(name string, ignore ...string) resource.TestStep {
step := resource.TestStep{ step := resource.TestStep{
ResourceName: name, ResourceName: name,
@@ -69,8 +84,12 @@ func CheckPlanPreApply(checks ...plancheck.PlanCheck) resource.ConfigPlanChecks
} }
} }
func CheckResourceAction(resourceAddress string, action plancheck.ResourceActionType) resource.ConfigPlanChecks { func CheckResourceActions(resourceAddress string, actions ...plancheck.ResourceActionType) resource.ConfigPlanChecks {
return CheckPlanPreApply(plancheck.ExpectResourceAction(resourceAddress, action)) var checks []plancheck.PlanCheck
for _, a := range actions {
checks = append(checks, plancheck.ExpectResourceAction(resourceAddress, a))
}
return CheckPlanPreApply(checks...)
} }
func ComposeConfig(configs ...string) string { func ComposeConfig(configs ...string) string {

View File

@@ -35,7 +35,7 @@ func ResourceUser() *schema.Resource {
UpdateContext: resourceUserUpdate, UpdateContext: resourceUserUpdate,
DeleteContext: resourceUserDelete, DeleteContext: resourceUserDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -4,10 +4,8 @@ import (
"context" "context"
"errors" "errors"
"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/filipowm/go-unifi/unifi"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" "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/schema"
) )
@@ -34,7 +32,7 @@ func ResourceUserGroup() *schema.Resource {
UpdateContext: resourceUserGroupUpdate, UpdateContext: resourceUserGroupUpdate,
DeleteContext: resourceUserGroupDelete, DeleteContext: resourceUserGroupDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
StateContext: utils.ImportSiteAndID, StateContext: base.ImportSiteAndID,
}, },
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{

View File

@@ -0,0 +1,39 @@
package validators
import (
"context"
"github.com/biter777/countries"
"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"
)
func CountryCodeAlpha2() validator.String {
return countryCodeAlpha2Validator{}
}
type countryCodeAlpha2Validator struct{}
func (c countryCodeAlpha2Validator) Description(_ context.Context) string {
return "The country code must be a valid ISO 3166-1 alpha-2 code."
}
func (c countryCodeAlpha2Validator) MarkdownDescription(ctx context.Context) string {
return c.Description(ctx)
}
func (c countryCodeAlpha2Validator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
code := req.ConfigValue
if base.IsEmptyString(code) {
return
}
codeString := code.ValueString()
if len(codeString) != 2 || countries.ByName(codeString) == countries.Unknown {
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
c.Description(ctx),
codeString,
))
}
}

View File

@@ -0,0 +1,34 @@
package validators
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
)
func TestCountryCodeValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
code string
validationFailed bool
}{
{"Poland", "PL", false},
{"United States", "US", false},
{"Empty", "", false},
{"Too long", "ABC", true},
{"Too short", "A", true},
{"Unknown", "WP", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
v := countryCodeAlpha2Validator{}
req, resp := newStringValidatorRequestResponse(tc.code)
v.ValidateString(context.Background(), req, resp)
assert.Equal(t, tc.validationFailed, resp.Diagnostics.HasError())
})
}
}

View File

@@ -0,0 +1,19 @@
package validators
import (
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
func newStringValidatorRequestResponse(value string) (validator.StringRequest, *validator.StringResponse) {
req := validator.StringRequest{
ConfigValue: types.StringValue(value),
Path: path.Empty(),
}
resp := validator.StringResponse{
Diagnostics: []diag.Diagnostic{},
}
return req, &resp
}

View File

@@ -0,0 +1,57 @@
package validators
import (
"context"
"fmt"
"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"
)
func StringLengthExactly(len int) validator.String {
return stringLengthExactlyValidator{len: len}
}
type stringLengthExactlyValidator struct {
len int
}
func (v stringLengthExactlyValidator) invalidUsageMessage() string {
return "length cannot be less than zero"
}
func (v stringLengthExactlyValidator) Description(_ context.Context) string {
return fmt.Sprintf("string length must be exactly %d", v.len)
}
func (v stringLengthExactlyValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringLengthExactlyValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if v.len < 0 {
resp.Diagnostics.Append(
validatordiag.InvalidValidatorUsageDiagnostic(
req.Path,
"StringLengthExactly",
v.invalidUsageMessage(),
),
)
return
}
value := req.ConfigValue
if !base.IsDefined(value) {
return
}
val := value.ValueString()
if len(val) != v.len {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%s (length: %d)", val, len(val)),
),
)
}
}

View File

@@ -0,0 +1,35 @@
package validators
import (
"context"
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestStringLengthExactlyValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
value string
length int
validationFailed bool
}{
{"", 0, false},
{"", 1, true},
{"a", 0, true},
{"a", 1, false},
{"a", 2, true},
{"ab", 2, false},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s-expected-length-%d", tc.value, tc.length), func(t *testing.T) {
t.Parallel()
v := StringLengthExactly(tc.length)
req, resp := newStringValidatorRequestResponse(tc.value)
v.ValidateString(context.Background(), req, resp)
assert.Equal(t, tc.validationFailed, resp.Diagnostics.HasError())
})
}
}

View File

@@ -1,16 +0,0 @@
package utils
import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"strings"
)
func ImportSiteAndID(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
if id := d.Id(); strings.Contains(id, ":") {
importParts := strings.SplitN(id, ":", 2)
d.SetId(importParts[1])
d.Set("site", importParts[0])
}
return []*schema.ResourceData{d}, nil
}