feat: support complete USG resource (#44)
* feat: add support for UPNP and Geo IP filtering to USG settings resource * feat: support complete USG settings resource * fix messages in required_together_if.go * improve docs of USG resource * tests: require version at least 9.0 for unbind_wan_monitors * feat: require version at least 8.5 for dns_verification * fix: use go-unifi 1.5.2 to fix NTP * require 7.0 or later for timeout preference * require 7.0 or later for geo IP filtering
This commit is contained in:
committed by
GitHub
parent
35c74bf59d
commit
fcea1e0ba4
10
go.mod
10
go.mod
@@ -10,8 +10,8 @@ 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/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.5.0
|
github.com/filipowm/go-unifi v1.5.2
|
||||||
github.com/golangci/golangci-lint v1.64.5
|
github.com/golangci/golangci-lint v1.64.6
|
||||||
github.com/hashicorp/go-version v1.7.0
|
github.com/hashicorp/go-version v1.7.0
|
||||||
github.com/hashicorp/terraform-plugin-docs v0.21.0
|
github.com/hashicorp/terraform-plugin-docs v0.21.0
|
||||||
github.com/hashicorp/terraform-plugin-framework v1.14.1
|
github.com/hashicorp/terraform-plugin-framework v1.14.1
|
||||||
@@ -27,7 +27,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
||||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
github.com/4meepo/tagalign v1.4.2 // indirect
|
github.com/4meepo/tagalign v1.4.2 // indirect
|
||||||
@@ -219,7 +219,7 @@ require (
|
|||||||
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
|
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/kisielk/errcheck v1.9.0 // indirect
|
github.com/kisielk/errcheck v1.9.0 // indirect
|
||||||
github.com/kkHAIKE/contextcheck v1.1.5 // indirect
|
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/kulti/thelper v0.6.3 // indirect
|
github.com/kulti/thelper v0.6.3 // indirect
|
||||||
github.com/kunwardeep/paralleltest v1.0.10 // indirect
|
github.com/kunwardeep/paralleltest v1.0.10 // indirect
|
||||||
@@ -299,7 +299,7 @@ require (
|
|||||||
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc // indirect
|
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc // indirect
|
||||||
github.com/raeperd/recvcheck v0.2.0 // indirect
|
github.com/raeperd/recvcheck v0.2.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.0 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/ryancurrah/gomodguard v1.3.5 // indirect
|
github.com/ryancurrah/gomodguard v1.3.5 // indirect
|
||||||
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
|
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -1,5 +1,6 @@
|
|||||||
4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA=
|
4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA=
|
||||||
4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs=
|
4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs=
|
||||||
|
4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
|
||||||
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
|
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
|
||||||
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
|
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
@@ -278,6 +279,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/filipowm/go-unifi v1.5.0 h1:DgnPKA23urg4jG3k2jaoigbk1Mt22QB0nNE/ItgNZTU=
|
github.com/filipowm/go-unifi v1.5.0 h1:DgnPKA23urg4jG3k2jaoigbk1Mt22QB0nNE/ItgNZTU=
|
||||||
github.com/filipowm/go-unifi v1.5.0/go.mod h1:dr+YNQ1Y2EjOTttAkWAMWMU/J8iv+FueAxZKmptnVEI=
|
github.com/filipowm/go-unifi v1.5.0/go.mod h1:dr+YNQ1Y2EjOTttAkWAMWMU/J8iv+FueAxZKmptnVEI=
|
||||||
|
github.com/filipowm/go-unifi v1.5.1 h1:dXdu4ei3ta+r8TAXc2CxPGEdBW01Bxhf1sxBgDreeWk=
|
||||||
|
github.com/filipowm/go-unifi v1.5.1/go.mod h1:ldtL5szykvR9fPWB9GlGZqaJGxKd8IWLQY5M8O1PDQ8=
|
||||||
|
github.com/filipowm/go-unifi v1.5.2 h1:S974Fi3BWt7qm0P8G/SwUVOjzgWTSjNJGV0jW5yD6K0=
|
||||||
|
github.com/filipowm/go-unifi v1.5.2/go.mod h1:ldtL5szykvR9fPWB9GlGZqaJGxKd8IWLQY5M8O1PDQ8=
|
||||||
github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=
|
github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=
|
||||||
github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
|
github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
@@ -391,6 +396,7 @@ github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0a
|
|||||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
|
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
|
||||||
github.com/golangci/golangci-lint v1.64.5 h1:5omC86XFBKXZgCrVdUWU+WNHKd+CWCxNx717KXnzKZY=
|
github.com/golangci/golangci-lint v1.64.5 h1:5omC86XFBKXZgCrVdUWU+WNHKd+CWCxNx717KXnzKZY=
|
||||||
github.com/golangci/golangci-lint v1.64.5/go.mod h1:WZnwq8TF0z61h3jLQ7Sk5trcP7b3kUFxLD6l1ivtdvU=
|
github.com/golangci/golangci-lint v1.64.5/go.mod h1:WZnwq8TF0z61h3jLQ7Sk5trcP7b3kUFxLD6l1ivtdvU=
|
||||||
|
github.com/golangci/golangci-lint v1.64.6/go.mod h1:Wz9q+6EVuqGQ94GQ96RB2mjpcZYTOGhBhbt4O7REPu4=
|
||||||
github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
|
github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
|
||||||
github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
|
github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
|
||||||
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
|
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
|
||||||
@@ -565,6 +571,7 @@ github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0
|
|||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg=
|
github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg=
|
||||||
github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA=
|
github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA=
|
||||||
|
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
@@ -809,6 +816,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.0 h1:unbRd941gNa8SS77YznHXOYVBDgWcF9xhzECdm8juZc=
|
github.com/rogpeppe/go-internal v1.14.0 h1:unbRd941gNa8SS77YznHXOYVBDgWcF9xhzECdm8juZc=
|
||||||
github.com/rogpeppe/go-internal v1.14.0/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.0/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU=
|
github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU=
|
||||||
github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE=
|
github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE=
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func TestAccSettingUsg_mdns_v7(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccSettingUsg_dhcpRelay(t *testing.T) {
|
func TestAccSettingUsg_dhcpRelayServers(t *testing.T) {
|
||||||
AcceptanceTest(t, AcceptanceTestCase{
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
Lock: &settingUsgLock,
|
Lock: &settingUsgLock,
|
||||||
Steps: []resource.TestStep{
|
Steps: []resource.TestStep{
|
||||||
@@ -81,6 +81,495 @@ func TestAccSettingUsg_site(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_geoIpFiltering(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 7",
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_geoIpFilteringBasic(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.block", "block"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.traffic_direction", "both"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.#", "3"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "RU"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "CN"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "KP"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_geoIpFilteringAllow(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.block", "allow"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.traffic_direction", "both"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.#", "3"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "US"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "CA"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "GB"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_geoIpFilteringDirections(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.block", "block"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.traffic_direction", "ingress"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.#", "2"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "RU"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "CN"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_geoIpFilteringDisabled(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_geoIpFilteringBasic(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.block", "block"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.traffic_direction", "both"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.#", "3"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "RU"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "CN"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "KP"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_upnp(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_upnpBasic(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.enabled", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_upnpAdvanced(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.enabled", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.nat_pmp_enabled", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.secure_mode", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.wan_interface", "WAN"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_upnpDisabled(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.enabled", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_dnsVerification(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.5",
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_dnsVerification(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_usg.test", "dns_verification.domain"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_usg.test", "dns_verification.primary_dns_server"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_usg.test", "dns_verification.secondary_dns_server"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.setting_preference", "auto"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_dnsVerificationUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.domain", "example.com"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.primary_dns_server", "1.1.1.1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.secondary_dns_server", "1.0.0.1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.setting_preference", "manual"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func TestAccSettingUsg_tcpTimeouts(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_tcpTimeouts(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.close_timeout", "10"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.established_timeout", "3600"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.close_wait_timeout", "20"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.fin_wait_timeout", "30"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.last_ack_timeout", "30"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.syn_recv_timeout", "60"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.syn_sent_timeout", "120"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.time_wait_timeout", "120"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_tcpTimeoutsUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.close_timeout", "20"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.established_timeout", "7200"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.close_wait_timeout", "40"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.fin_wait_timeout", "60"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.last_ack_timeout", "60"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.syn_recv_timeout", "120"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.syn_sent_timeout", "240"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.time_wait_timeout", "240"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_arpCache(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_arpCache(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_base_reachable", "60"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_timeout", "custom"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_arpCacheUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_base_reachable", "120"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_timeout", "normal"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_dhcpConfig(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_dhcpConfig(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "broadcast_ping", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_hostfile_update", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_use_dnsmasq", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dnsmasq_all_servers", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_dhcpConfigUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "broadcast_ping", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_hostfile_update", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_use_dnsmasq", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dnsmasq_all_servers", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_dhcpRelayConfig(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_dhcpRelayConfig(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.agents_packets", "forward"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.hop_count", "5"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.max_size", "1400"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.port", "67"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay_servers.#", "2"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.3"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.4"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_dhcpRelayConfigUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.agents_packets", "replace"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.hop_count", "10"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.max_size", "64"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.port", "68"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay_servers.#", "3"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.5"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.6"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.7"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_networkTools(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_networkTools(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "echo_server", "echo.example.com"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_protocolModules(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_protocolModules(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "ftp_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "gre_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "h323_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "pptp_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "sip_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tftp_module", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_protocolModulesUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "ftp_module", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "gre_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "h323_module", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "pptp_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "sip_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tftp_module", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_icmpAndLldp(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_icmpAndLldp(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "icmp_timeout", "60"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "lldp_enable_all", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_icmpAndLldpUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "icmp_timeout", "120"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "lldp_enable_all", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_mssClamp(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_mssClamp(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "mss_clamp", "auto"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "mss_clamp_mss", "1452"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_mssClampUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "mss_clamp", "custom"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "mss_clamp_mss", "1400"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_offloadSettings(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_offloadSettings(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_accounting", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_l2_blocking", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_sch", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_offloadSettingsUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_accounting", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_l2_blocking", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_sch", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_timeoutSettings(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 7",
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_timeoutSettings(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "other_timeout", "600"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "timeout_setting_preference", "auto"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_timeoutSettingsUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "other_timeout", "1200"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "timeout_setting_preference", "manual"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_redirectsAndSecurity(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_redirectsAndSecurity(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "receive_redirects", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "send_redirects", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "syn_cookies", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_redirectsAndSecurityUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "receive_redirects", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "send_redirects", "false"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "syn_cookies", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_udp(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_udp(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_other_timeout", "30"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_stream_timeout", "120"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_udpUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_other_timeout", "60"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_stream_timeout", "240"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_unbindWanMonitor(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 9",
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_unbindWanMonitor(true),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "unbind_wan_monitors", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_unbindWanMonitor(false),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "unbind_wan_monitors", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingUsg_comprehensive(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 7",
|
||||||
|
Lock: &settingUsgLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingUsgConfig_comprehensive(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
// ARP Cache
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_base_reachable", "60"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_timeout", "custom"),
|
||||||
|
|
||||||
|
// DHCP Config
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "broadcast_ping", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_hostfile_update", "true"),
|
||||||
|
|
||||||
|
// Protocol Modules (sample)
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "ftp_module", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tftp_module", "true"),
|
||||||
|
|
||||||
|
// Timeouts
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "other_timeout", "600"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_stream_timeout", "120"),
|
||||||
|
|
||||||
|
// Security
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_usg.test", "syn_cookies", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testAccSettingUsgConfig_mdns(mdns bool) string {
|
func testAccSettingUsgConfig_mdns(mdns bool) string {
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
resource "unifi_setting_usg" "test" {
|
resource "unifi_setting_usg" "test" {
|
||||||
@@ -111,3 +600,443 @@ resource "unifi_setting_usg" "test" {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_geoIpFilteringBasic() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
geo_ip_filtering = {
|
||||||
|
enabled = true
|
||||||
|
countries = ["RU", "CN", "KP"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_geoIpFilteringAllow() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
geo_ip_filtering = {
|
||||||
|
enabled = true
|
||||||
|
block = "allow"
|
||||||
|
countries = ["US", "CA", "GB"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_geoIpFilteringDirections() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
geo_ip_filtering = {
|
||||||
|
enabled = true
|
||||||
|
traffic_direction = "ingress"
|
||||||
|
countries = ["RU", "CN"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_geoIpFilteringDisabled() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
geo_ip_filtering = {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_upnpBasic() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
upnp = {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_upnpAdvanced() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
upnp = {
|
||||||
|
enabled = true
|
||||||
|
nat_pmp_enabled = true
|
||||||
|
secure_mode = true
|
||||||
|
wan_interface = "WAN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_upnpDisabled() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
upnp = {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_dnsVerification() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
dns_verification = {
|
||||||
|
setting_preference = "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_dnsVerificationUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
dns_verification = {
|
||||||
|
domain = "example.com"
|
||||||
|
primary_dns_server = "1.1.1.1"
|
||||||
|
secondary_dns_server = "1.0.0.1"
|
||||||
|
setting_preference = "manual"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_tcpTimeouts() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
tcp_timeouts = {
|
||||||
|
close_timeout = 10
|
||||||
|
established_timeout = 3600
|
||||||
|
close_wait_timeout = 20
|
||||||
|
fin_wait_timeout = 30
|
||||||
|
last_ack_timeout = 30
|
||||||
|
syn_recv_timeout = 60
|
||||||
|
syn_sent_timeout = 120
|
||||||
|
time_wait_timeout = 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_tcpTimeoutsUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
tcp_timeouts = {
|
||||||
|
close_timeout = 20
|
||||||
|
established_timeout = 7200
|
||||||
|
close_wait_timeout = 40
|
||||||
|
fin_wait_timeout = 60
|
||||||
|
last_ack_timeout = 60
|
||||||
|
syn_recv_timeout = 120
|
||||||
|
syn_sent_timeout = 240
|
||||||
|
time_wait_timeout = 240
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_arpCache() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
arp_cache_base_reachable = 60
|
||||||
|
arp_cache_timeout = "custom"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_dhcpConfig() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
broadcast_ping = true
|
||||||
|
dhcpd_hostfile_update = true
|
||||||
|
dhcpd_use_dnsmasq = true
|
||||||
|
dnsmasq_all_servers = true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_dhcpRelayConfig() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
dhcp_relay = {
|
||||||
|
agents_packets = "forward"
|
||||||
|
hop_count = 5
|
||||||
|
max_size = 1400
|
||||||
|
port = 67
|
||||||
|
}
|
||||||
|
dhcp_relay_servers = ["10.1.2.3","10.1.2.4"]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_dhcpRelayConfigUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
dhcp_relay = {
|
||||||
|
agents_packets = "replace"
|
||||||
|
hop_count = 10
|
||||||
|
max_size = 64
|
||||||
|
port = 68
|
||||||
|
}
|
||||||
|
dhcp_relay_servers = ["10.1.2.5","10.1.2.6","10.1.2.7"]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_networkTools() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
echo_server = "echo.example.com"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_protocolModules() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
ftp_module = true
|
||||||
|
gre_module = true
|
||||||
|
h323_module = true
|
||||||
|
pptp_module = true
|
||||||
|
sip_module = true
|
||||||
|
tftp_module = true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_icmpAndLldp() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
icmp_timeout = 60
|
||||||
|
lldp_enable_all = true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_mssClamp() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
mss_clamp = "auto"
|
||||||
|
mss_clamp_mss = 1452
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_offloadSettings() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
offload_accounting = true
|
||||||
|
offload_l2_blocking = true
|
||||||
|
offload_sch = true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_timeoutSettings() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
other_timeout = 600
|
||||||
|
timeout_setting_preference = "auto"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_redirectsAndSecurity() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
receive_redirects = false
|
||||||
|
send_redirects = true
|
||||||
|
syn_cookies = true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_udp() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
udp_other_timeout = 30
|
||||||
|
udp_stream_timeout = 120
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_comprehensive() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
// ARP Cache Configuration
|
||||||
|
arp_cache_base_reachable = 60
|
||||||
|
arp_cache_timeout = "custom"
|
||||||
|
|
||||||
|
// DHCP Configuration
|
||||||
|
broadcast_ping = true
|
||||||
|
dhcpd_hostfile_update = true
|
||||||
|
dhcpd_use_dnsmasq = true
|
||||||
|
dnsmasq_all_servers = true
|
||||||
|
|
||||||
|
// DHCP Relay
|
||||||
|
dhcp_relay = {
|
||||||
|
agents_packets = "forward"
|
||||||
|
hop_count = 5
|
||||||
|
}
|
||||||
|
dhcp_relay_servers = ["10.1.2.3", "10.1.2.4"]
|
||||||
|
|
||||||
|
// Network Tools
|
||||||
|
echo_server = "echo.example.com"
|
||||||
|
|
||||||
|
// Protocol Modules
|
||||||
|
ftp_module = true
|
||||||
|
gre_module = true
|
||||||
|
tftp_module = true
|
||||||
|
|
||||||
|
// ICMP & LLDP
|
||||||
|
icmp_timeout = 20
|
||||||
|
lldp_enable_all = true
|
||||||
|
|
||||||
|
// MSS Clamp
|
||||||
|
mss_clamp = "auto"
|
||||||
|
mss_clamp_mss = 1452
|
||||||
|
|
||||||
|
// Offload Settings
|
||||||
|
offload_accounting = true
|
||||||
|
offload_l2_blocking = true
|
||||||
|
|
||||||
|
// Timeout Settings
|
||||||
|
other_timeout = 600
|
||||||
|
timeout_setting_preference = "auto"
|
||||||
|
|
||||||
|
// TCP Settings
|
||||||
|
tcp_timeouts = {
|
||||||
|
close_timeout = 10
|
||||||
|
established_timeout = 3600
|
||||||
|
close_wait_timeout = 20
|
||||||
|
fin_wait_timeout = 30
|
||||||
|
last_ack_timeout = 30
|
||||||
|
syn_recv_timeout = 60
|
||||||
|
syn_sent_timeout = 120
|
||||||
|
time_wait_timeout = 120
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirects & Security
|
||||||
|
receive_redirects = false
|
||||||
|
send_redirects = true
|
||||||
|
syn_cookies = true
|
||||||
|
|
||||||
|
// UDP
|
||||||
|
udp_other_timeout = 30
|
||||||
|
udp_stream_timeout = 120
|
||||||
|
|
||||||
|
// Geo IP Filtering
|
||||||
|
geo_ip_filtering = {
|
||||||
|
enabled = true
|
||||||
|
block = "block"
|
||||||
|
countries = ["RU", "CN"]
|
||||||
|
traffic_direction = "both"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPNP Settings
|
||||||
|
upnp = {
|
||||||
|
enabled = true
|
||||||
|
nat_pmp_enabled = true
|
||||||
|
secure_mode = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_arpCacheUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
arp_cache_base_reachable = 120
|
||||||
|
arp_cache_timeout = "normal"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_dhcpConfigUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
broadcast_ping = false
|
||||||
|
dhcpd_hostfile_update = false
|
||||||
|
dhcpd_use_dnsmasq = false
|
||||||
|
dnsmasq_all_servers = false
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_protocolModulesUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
ftp_module = false
|
||||||
|
gre_module = true
|
||||||
|
h323_module = false
|
||||||
|
pptp_module = true
|
||||||
|
sip_module = true
|
||||||
|
tftp_module = false
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_icmpAndLldpUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
icmp_timeout = 120
|
||||||
|
lldp_enable_all = false
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_mssClampUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
mss_clamp = "custom"
|
||||||
|
mss_clamp_mss = 1400
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_offloadSettingsUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
offload_accounting = false
|
||||||
|
offload_l2_blocking = false
|
||||||
|
offload_sch = false
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_timeoutSettingsUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
other_timeout = 1200
|
||||||
|
timeout_setting_preference = "manual"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_redirectsAndSecurityUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
receive_redirects = true
|
||||||
|
send_redirects = false
|
||||||
|
syn_cookies = false
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_udpUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
udp_other_timeout = 60
|
||||||
|
udp_stream_timeout = 240
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingUsgConfig_unbindWanMonitor(enabled bool) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_setting_usg" "test" {
|
||||||
|
unbind_wan_monitors = %t
|
||||||
|
}
|
||||||
|
`, enabled)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
|
||||||
@@ -22,8 +23,8 @@ type ResourceModel interface {
|
|||||||
GetID() string
|
GetID() string
|
||||||
GetRawID() types.String
|
GetRawID() types.String
|
||||||
SetID(string)
|
SetID(string)
|
||||||
AsUnifiModel() (interface{}, diag.Diagnostics)
|
AsUnifiModel(context.Context) (interface{}, diag.Diagnostics)
|
||||||
Merge(interface{}) diag.Diagnostics
|
Merge(context.Context, interface{}) diag.Diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ func (p *unifiProvider) Configure(ctx context.Context, req provider.ConfigureReq
|
|||||||
path.Root("api_url"),
|
path.Root("api_url"),
|
||||||
"Unknown UniFi Controller API URL",
|
"Unknown UniFi Controller API URL",
|
||||||
"The provider cannot create the UniFi Controller API client as there is an unknown configuration value "+
|
"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 "+
|
"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.",
|
"the configuration, or use the UNIFI_API environment variable.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,13 +87,14 @@ func (b *BaseSettingResource[T]) Create(ctx context.Context, req resource.Create
|
|||||||
if resp.Diagnostics.HasError() {
|
if resp.Diagnostics.HasError() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body, diags := plan.AsUnifiModel()
|
site := b.client.ResolveSite(plan)
|
||||||
|
|
||||||
|
body, diags := plan.AsUnifiModel(ctx)
|
||||||
|
|
||||||
if diags.HasError() {
|
if diags.HasError() {
|
||||||
resp.Diagnostics.Append(diags...)
|
resp.Diagnostics.Append(diags...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
site := b.client.ResolveSite(plan)
|
|
||||||
|
|
||||||
res, err := b.updater(ctx, b.client, site, body)
|
res, err := b.updater(ctx, b.client, site, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,7 +105,7 @@ func (b *BaseSettingResource[T]) Create(ctx context.Context, req resource.Create
|
|||||||
resp.Diagnostics.AddError("Error creating settings", fmt.Sprintf("No %[1]s settings returned from the UniFi controller. %[1]s might not be supported on this controller", b.typeName))
|
resp.Diagnostics.AddError("Error creating settings", fmt.Sprintf("No %[1]s settings returned from the UniFi controller. %[1]s might not be supported on this controller", b.typeName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
plan.Merge(res)
|
plan.Merge(ctx, res)
|
||||||
plan.SetSite(site)
|
plan.SetSite(site)
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
}
|
}
|
||||||
@@ -128,7 +129,7 @@ func (b *BaseSettingResource[T]) read(ctx context.Context, site string, state T,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if res != nil {
|
if res != nil {
|
||||||
state.Merge(res)
|
state.Merge(ctx, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ func (b *BaseSettingResource[T]) Update(ctx context.Context, req resource.Update
|
|||||||
if resp.Diagnostics.HasError() {
|
if resp.Diagnostics.HasError() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body, diags := plan.AsUnifiModel()
|
body, diags := plan.AsUnifiModel(ctx)
|
||||||
if diags.HasError() {
|
if diags.HasError() {
|
||||||
resp.Diagnostics.Append(diags...)
|
resp.Diagnostics.Append(diags...)
|
||||||
return
|
return
|
||||||
@@ -184,7 +185,7 @@ func (b *BaseSettingResource[T]) Update(ctx context.Context, req resource.Update
|
|||||||
resp.Diagnostics.AddError("Error updating settings", err.Error())
|
resp.Diagnostics.AddError("Error updating settings", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.Merge(res)
|
state.Merge(ctx, res)
|
||||||
state.SetSite(site)
|
state.SetSite(site)
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type autoSpeedtestModel struct {
|
|||||||
Enabled types.Bool `tfsdk:"enabled"`
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *autoSpeedtestModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
func (d *autoSpeedtestModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||||
return &unifi.SettingAutoSpeedtest{
|
return &unifi.SettingAutoSpeedtest{
|
||||||
ID: d.ID.ValueString(),
|
ID: d.ID.ValueString(),
|
||||||
CronExpr: d.CronExpression.ValueString(),
|
CronExpr: d.CronExpression.ValueString(),
|
||||||
@@ -32,7 +32,7 @@ func (d *autoSpeedtestModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
|||||||
}, diag.Diagnostics{}
|
}, diag.Diagnostics{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *autoSpeedtestModel) Merge(other interface{}) diag.Diagnostics {
|
func (d *autoSpeedtestModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||||
if typed, ok := other.(*unifi.SettingAutoSpeedtest); ok {
|
if typed, ok := other.(*unifi.SettingAutoSpeedtest); ok {
|
||||||
d.ID = types.StringValue(typed.ID)
|
d.ID = types.StringValue(typed.ID)
|
||||||
d.CronExpression = types.StringValue(typed.CronExpr)
|
d.CronExpression = types.StringValue(typed.CronExpr)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type countryModel struct {
|
|||||||
CodeNumeric types.Int32 `tfsdk:"code_numeric"`
|
CodeNumeric types.Int32 `tfsdk:"code_numeric"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *countryModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
func (d *countryModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||||
code := countries.ByName(d.Code.ValueString())
|
code := countries.ByName(d.Code.ValueString())
|
||||||
return &unifi.SettingCountry{
|
return &unifi.SettingCountry{
|
||||||
ID: d.ID.ValueString(),
|
ID: d.ID.ValueString(),
|
||||||
@@ -34,7 +34,7 @@ func (d *countryModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
|||||||
}, diag.Diagnostics{}
|
}, diag.Diagnostics{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *countryModel) Merge(other interface{}) diag.Diagnostics {
|
func (d *countryModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||||
if typed, ok := other.(*unifi.SettingCountry); ok {
|
if typed, ok := other.(*unifi.SettingCountry); ok {
|
||||||
d.ID = types.StringValue(typed.ID)
|
d.ID = types.StringValue(typed.ID)
|
||||||
code := countries.ByNumeric(typed.Code)
|
code := countries.ByNumeric(typed.Code)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"github.com/filipowm/go-unifi/unifi"
|
"github.com/filipowm/go-unifi/unifi"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -26,7 +27,7 @@ func TestSettingCountry_ProperCountryCodeMappingFromModel(t *testing.T) {
|
|||||||
model := countryModel{
|
model := countryModel{
|
||||||
Code: types.StringValue(tc.code),
|
Code: types.StringValue(tc.code),
|
||||||
}
|
}
|
||||||
unifiModel, _ := model.AsUnifiModel()
|
unifiModel, _ := model.AsUnifiModel(context.Background())
|
||||||
typed, ok := unifiModel.(*unifi.SettingCountry)
|
typed, ok := unifiModel.(*unifi.SettingCountry)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, tc.expectedNumericCode, typed.Code)
|
assert.Equal(t, tc.expectedNumericCode, typed.Code)
|
||||||
@@ -54,7 +55,7 @@ func TestSettingCountry_ProperCountryCodeMappingToModel(t *testing.T) {
|
|||||||
Code: tc.numericCode,
|
Code: tc.numericCode,
|
||||||
}
|
}
|
||||||
model := countryModel{}
|
model := countryModel{}
|
||||||
model.Merge(unifiModel)
|
model.Merge(context.Background(), unifiModel)
|
||||||
assert.Equal(t, tc.expectedCode, model.Code.ValueString())
|
assert.Equal(t, tc.expectedCode, model.Code.ValueString())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type localeModel struct {
|
|||||||
Timezone types.String `tfsdk:"timezone"`
|
Timezone types.String `tfsdk:"timezone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *localeModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
func (d *localeModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model := &unifi.SettingLocale{
|
model := &unifi.SettingLocale{
|
||||||
@@ -29,7 +29,7 @@ func (d *localeModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
|||||||
return model, diags
|
return model, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *localeModel) Merge(other interface{}) diag.Diagnostics {
|
func (d *localeModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model, ok := other.(*unifi.SettingLocale)
|
model, ok := other.(*unifi.SettingLocale)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type magicSiteToSiteVpnModel struct {
|
|||||||
Enabled types.Bool `tfsdk:"enabled"`
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *magicSiteToSiteVpnModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
func (d *magicSiteToSiteVpnModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model := &unifi.SettingMagicSiteToSiteVpn{
|
model := &unifi.SettingMagicSiteToSiteVpn{
|
||||||
@@ -27,7 +27,7 @@ func (d *magicSiteToSiteVpnModel) AsUnifiModel() (interface{}, diag.Diagnostics)
|
|||||||
return model, diags
|
return model, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *magicSiteToSiteVpnModel) Merge(other interface{}) diag.Diagnostics {
|
func (d *magicSiteToSiteVpnModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model, ok := other.(*unifi.SettingMagicSiteToSiteVpn)
|
model, ok := other.(*unifi.SettingMagicSiteToSiteVpn)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type networkOptimizationModel struct {
|
|||||||
Enabled types.Bool `tfsdk:"enabled"`
|
Enabled types.Bool `tfsdk:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *networkOptimizationModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
func (d *networkOptimizationModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model := &unifi.SettingNetworkOptimization{
|
model := &unifi.SettingNetworkOptimization{
|
||||||
@@ -27,7 +27,7 @@ func (d *networkOptimizationModel) AsUnifiModel() (interface{}, diag.Diagnostics
|
|||||||
return model, diags
|
return model, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *networkOptimizationModel) Merge(other interface{}) diag.Diagnostics {
|
func (d *networkOptimizationModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model, ok := other.(*unifi.SettingNetworkOptimization)
|
model, ok := other.(*unifi.SettingNetworkOptimization)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type ntpModel struct {
|
|||||||
Mode types.String `tfsdk:"mode"`
|
Mode types.String `tfsdk:"mode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ntpModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
func (d *ntpModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model := &unifi.SettingNtp{
|
model := &unifi.SettingNtp{
|
||||||
@@ -56,7 +56,7 @@ func (d *ntpModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
|||||||
return model, diags
|
return model, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ntpModel) Merge(other interface{}) diag.Diagnostics {
|
func (d *ntpModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model, ok := other.(*unifi.SettingNtp)
|
model, ok := other.(*unifi.SettingNtp)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"github.com/filipowm/go-unifi/unifi"
|
"github.com/filipowm/go-unifi/unifi"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -21,7 +22,7 @@ func TestNtpModel_AsUnifiModel_Auto(t *testing.T) {
|
|||||||
model.ID = types.StringValue("test-id")
|
model.ID = types.StringValue("test-id")
|
||||||
|
|
||||||
// Convert to UnifiModel
|
// Convert to UnifiModel
|
||||||
unifiModel, diags := model.AsUnifiModel()
|
unifiModel, diags := model.AsUnifiModel(context.Background())
|
||||||
|
|
||||||
// Verify no diagnostics errors
|
// Verify no diagnostics errors
|
||||||
assert.False(t, diags.HasError())
|
assert.False(t, diags.HasError())
|
||||||
@@ -138,7 +139,7 @@ func TestNtpModel_AsUnifiModel_Manual(t *testing.T) {
|
|||||||
model.ID = types.StringValue("test-id")
|
model.ID = types.StringValue("test-id")
|
||||||
|
|
||||||
// Convert to UnifiModel
|
// Convert to UnifiModel
|
||||||
unifiModel, diags := model.AsUnifiModel()
|
unifiModel, diags := model.AsUnifiModel(context.Background())
|
||||||
|
|
||||||
// Verify no diagnostics errors
|
// Verify no diagnostics errors
|
||||||
assert.False(t, diags.HasError())
|
assert.False(t, diags.HasError())
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type sslInspectionModel struct {
|
|||||||
State types.String `tfsdk:"state"`
|
State types.String `tfsdk:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *sslInspectionModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
func (d *sslInspectionModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model := &unifi.SettingSslInspection{
|
model := &unifi.SettingSslInspection{
|
||||||
@@ -29,7 +29,7 @@ func (d *sslInspectionModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
|||||||
return model, diags
|
return model, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *sslInspectionModel) Merge(other interface{}) diag.Diagnostics {
|
func (d *sslInspectionModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model, ok := other.(*unifi.SettingSslInspection)
|
model, ok := other.(*unifi.SettingSslInspection)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type teleportModel struct {
|
|||||||
Subnet types.String `tfsdk:"subnet"`
|
Subnet types.String `tfsdk:"subnet"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *teleportModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
func (d *teleportModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model := &unifi.SettingTeleport{
|
model := &unifi.SettingTeleport{
|
||||||
@@ -31,7 +31,7 @@ func (d *teleportModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
|||||||
return model, diags
|
return model, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *teleportModel) Merge(other interface{}) diag.Diagnostics {
|
func (d *teleportModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
model, ok := other.(*unifi.SettingTeleport)
|
model, ok := other.(*unifi.SettingTeleport)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ var (
|
|||||||
_ datasource.ConfigValidator = &RequiredNoneIfValidator{}
|
_ datasource.ConfigValidator = &RequiredNoneIfValidator{}
|
||||||
_ provider.ConfigValidator = &RequiredNoneIfValidator{}
|
_ provider.ConfigValidator = &RequiredNoneIfValidator{}
|
||||||
_ resource.ConfigValidator = &RequiredNoneIfValidator{}
|
_ resource.ConfigValidator = &RequiredNoneIfValidator{}
|
||||||
|
_ validator.Object = &RequiredNoneIfValidator{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequiredNoneIfValidator struct {
|
type RequiredNoneIfValidator struct {
|
||||||
@@ -27,6 +28,10 @@ type RequiredNoneIfValidator struct {
|
|||||||
TargetExpressions path.Expressions
|
TargetExpressions path.Expressions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v RequiredNoneIfValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
func (v RequiredNoneIfValidator) Description(ctx context.Context) string {
|
func (v RequiredNoneIfValidator) Description(ctx context.Context) string {
|
||||||
return v.MarkdownDescription(ctx)
|
return v.MarkdownDescription(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
194
internal/provider/validators/required_together_if.go
Normal file
194
internal/provider/validators/required_together_if.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.ConfigValidator = &RequiredTogetherIfValidator{}
|
||||||
|
_ provider.ConfigValidator = &RequiredTogetherIfValidator{}
|
||||||
|
_ resource.ConfigValidator = &RequiredTogetherIfValidator{}
|
||||||
|
_ validator.Object = &RequiredTogetherIfValidator{}
|
||||||
|
_ validator.String = &RequiredTogetherIfValidator{}
|
||||||
|
_ validator.Bool = &RequiredTogetherIfValidator{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequiredTogetherIfValidator struct {
|
||||||
|
ConditionPath path.Expression
|
||||||
|
ConditionValue attr.Value
|
||||||
|
TargetExpressions path.Expressions
|
||||||
|
CheckOnlyIfSet bool // When true, only checks if the condition value is set (not null), not its actual value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) Description(ctx context.Context) string {
|
||||||
|
return v.MarkdownDescription(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) MarkdownDescription(_ context.Context) string {
|
||||||
|
if v.CheckOnlyIfSet {
|
||||||
|
return fmt.Sprintf("If %s is set, these attributes must be configured together: %s", v.ConditionPath, v.TargetExpressions)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("If %s equals %s, these attributes must be configured together: %s", v.ConditionPath, v.ConditionValue, v.TargetExpressions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
|
||||||
|
resp.Diagnostics.Append(v.Validate(ctx, req.Config)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) shouldValidate(ctx context.Context, config tfsdk.Config) bool {
|
||||||
|
// First check the condition attribute's value
|
||||||
|
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, v.ConditionPath)
|
||||||
|
if matchedPathsDiags.HasError() || len(matchedPaths) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value of the condition attribute
|
||||||
|
var conditionValue attr.Value
|
||||||
|
getConditionDiags := config.GetAttribute(ctx, matchedPaths[0], &conditionValue)
|
||||||
|
if getConditionDiags.HasError() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the condition attribute is null or unknown, skip validation
|
||||||
|
if conditionValue.IsNull() || conditionValue.IsUnknown() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the condition matches
|
||||||
|
if v.CheckOnlyIfSet {
|
||||||
|
return !conditionValue.IsNull()
|
||||||
|
}
|
||||||
|
return conditionValueMatches(ctx, conditionValue, v.ConditionValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredTogetherIfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
if !v.shouldValidate(ctx, config) {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition matched, now apply the RequiredTogether validation
|
||||||
|
configuredPaths := path.Paths{}
|
||||||
|
foundPaths := path.Paths{}
|
||||||
|
unknownPaths := path.Paths{}
|
||||||
|
|
||||||
|
// Check that all target attributes are present
|
||||||
|
for _, expression := range v.TargetExpressions {
|
||||||
|
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, expression)
|
||||||
|
diags.Append(matchedPathsDiags...)
|
||||||
|
|
||||||
|
// Collect all errors
|
||||||
|
if matchedPathsDiags.HasError() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture all matched paths
|
||||||
|
foundPaths.Append(matchedPaths...)
|
||||||
|
|
||||||
|
for _, matchedPath := range matchedPaths {
|
||||||
|
var value attr.Value
|
||||||
|
getAttributeDiags := config.GetAttribute(ctx, matchedPath, &value)
|
||||||
|
diags.Append(getAttributeDiags...)
|
||||||
|
|
||||||
|
// Collect all errors
|
||||||
|
if getAttributeDiags.HasError() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If value is unknown, collect the path to skip validation later
|
||||||
|
if value.IsUnknown() {
|
||||||
|
unknownPaths.Append(matchedPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If value is null, move onto the next one
|
||||||
|
if value.IsNull() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value is known and not null, it is configured
|
||||||
|
configuredPaths.Append(matchedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return early if all paths were null
|
||||||
|
//if len(configuredPaths) == 0 {
|
||||||
|
// return diags
|
||||||
|
//}
|
||||||
|
|
||||||
|
// If there are unknown values, we cannot know if the validator should
|
||||||
|
// succeed or not
|
||||||
|
if len(unknownPaths) > 0 {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// If configured paths does not equal all matched paths, then something
|
||||||
|
// was missing
|
||||||
|
if len(configuredPaths) != len(foundPaths) {
|
||||||
|
diags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
|
||||||
|
foundPaths[0],
|
||||||
|
v.Description(ctx),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateString method to implement the validator.String interface
|
||||||
|
func (v RequiredTogetherIfValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||||
|
resp.Diagnostics.Append(v.Validate(ctx, req.Config)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredTogetherIf creates a validator for string type attributes that ensures
|
||||||
|
// a set of target attributes are configured together if a condition attribute equals a specific value.
|
||||||
|
func RequiredTogetherIf(conditionPath path.Expression, conditionValue attr.Value, targetExpressions ...path.Expression) RequiredTogetherIfValidator {
|
||||||
|
return RequiredTogetherIfValidator{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
ConditionValue: conditionValue,
|
||||||
|
TargetExpressions: targetExpressions,
|
||||||
|
CheckOnlyIfSet: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredTogetherIfSet creates a validator that ensures a set of target attributes
|
||||||
|
// are configured together if a condition attribute is set (not null), regardless of its value.
|
||||||
|
func RequiredTogetherIfSet(conditionPath path.Expression, targetExpressions ...path.Expression) RequiredTogetherIfValidator {
|
||||||
|
return RequiredTogetherIfValidator{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
ConditionValue: nil, // Not used for this validator
|
||||||
|
TargetExpressions: targetExpressions,
|
||||||
|
CheckOnlyIfSet: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
875
internal/provider/validators/required_together_if_test.go
Normal file
875
internal/provider/validators/required_together_if_test.go
Normal file
@@ -0,0 +1,875 @@
|
|||||||
|
package validators_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"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/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to convert types.Int32 to tftypes.Value
|
||||||
|
func numberToTfValue(value types.Int32) tftypes.Value {
|
||||||
|
if value.IsNull() {
|
||||||
|
return tftypes.NewValue(tftypes.Number, nil)
|
||||||
|
} else if value.IsUnknown() {
|
||||||
|
return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tftypes.NewValue(tftypes.Number, float64(value.ValueInt32()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common test case structure for string conditions
|
||||||
|
type requiredTogetherIfStringConditionTestCase struct {
|
||||||
|
condition types.String
|
||||||
|
field1 types.String
|
||||||
|
field2 types.String
|
||||||
|
expectError bool
|
||||||
|
expectErrorText string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common test case structure for bool conditions
|
||||||
|
type requiredTogetherIfBoolConditionTestCase struct {
|
||||||
|
condition types.Bool
|
||||||
|
field1 types.String
|
||||||
|
field2 types.String
|
||||||
|
expectError bool
|
||||||
|
expectErrorText string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common test case structure for int32 conditions
|
||||||
|
type int32ConditionTestCase struct {
|
||||||
|
condition types.Int32
|
||||||
|
field1 types.String
|
||||||
|
field2 types.String
|
||||||
|
expectError bool
|
||||||
|
expectErrorText string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a schema object with string condition
|
||||||
|
func createRequiredIfStringConditionSchema() schema.Schema {
|
||||||
|
return schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"field2": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a schema object with bool condition
|
||||||
|
func createRequiredIfBoolConditionSchema() schema.Schema {
|
||||||
|
return schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.BoolAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field2": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a schema object with int32 condition
|
||||||
|
func createInt32ConditionSchema() schema.Schema {
|
||||||
|
return schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.Int32Attribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field2": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a config with string condition
|
||||||
|
func createRequiredIfStringConditionConfig(schema schema.Schema, testCase requiredTogetherIfStringConditionTestCase) tfsdk.Config {
|
||||||
|
return tfsdk.Config{
|
||||||
|
Schema: schema,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.String,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
"field2": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": stringToTfValue(testCase.condition),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
"field2": stringToTfValue(testCase.field2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a config with bool condition
|
||||||
|
func createRequiredIfBoolConditionConfig(schema schema.Schema, testCase requiredTogetherIfBoolConditionTestCase) tfsdk.Config {
|
||||||
|
return tfsdk.Config{
|
||||||
|
Schema: schema,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.Bool,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
"field2": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": boolToTfValue(testCase.condition),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
"field2": stringToTfValue(testCase.field2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a config with int32 condition
|
||||||
|
func createInt32ConditionConfig(schema schema.Schema, testCase int32ConditionTestCase) tfsdk.Config {
|
||||||
|
return tfsdk.Config{
|
||||||
|
Schema: schema,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.Number,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
"field2": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": numberToTfValue(testCase.condition),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
"field2": stringToTfValue(testCase.field2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to verify validation results
|
||||||
|
func verifyValidationResults(t *testing.T, response interface{}, testCase interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var hasDiagnosticError bool
|
||||||
|
var diagnosticErrors []interface{}
|
||||||
|
|
||||||
|
// Extract diagnostic errors based on the type
|
||||||
|
switch d := response.(type) {
|
||||||
|
case resource.ValidateConfigResponse:
|
||||||
|
hasDiagnosticError = d.Diagnostics.HasError()
|
||||||
|
if hasDiagnosticError {
|
||||||
|
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||||
|
}
|
||||||
|
case *resource.ValidateConfigResponse:
|
||||||
|
hasDiagnosticError = d.Diagnostics.HasError()
|
||||||
|
if hasDiagnosticError {
|
||||||
|
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||||
|
}
|
||||||
|
case datasource.ValidateConfigResponse:
|
||||||
|
hasDiagnosticError = d.Diagnostics.HasError()
|
||||||
|
if hasDiagnosticError {
|
||||||
|
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||||
|
}
|
||||||
|
case *datasource.ValidateConfigResponse:
|
||||||
|
hasDiagnosticError = d.Diagnostics.HasError()
|
||||||
|
if hasDiagnosticError {
|
||||||
|
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||||
|
}
|
||||||
|
case provider.ValidateConfigResponse:
|
||||||
|
hasDiagnosticError = d.Diagnostics.HasError()
|
||||||
|
if hasDiagnosticError {
|
||||||
|
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||||
|
}
|
||||||
|
case *provider.ValidateConfigResponse:
|
||||||
|
hasDiagnosticError = d.Diagnostics.HasError()
|
||||||
|
if hasDiagnosticError {
|
||||||
|
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||||
|
}
|
||||||
|
case validator.StringResponse:
|
||||||
|
hasDiagnosticError = d.Diagnostics.HasError()
|
||||||
|
if hasDiagnosticError {
|
||||||
|
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||||
|
}
|
||||||
|
case *validator.StringResponse:
|
||||||
|
hasDiagnosticError = d.Diagnostics.HasError()
|
||||||
|
if hasDiagnosticError {
|
||||||
|
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unsupported response type: %T", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify results based on test case type
|
||||||
|
switch tc := testCase.(type) {
|
||||||
|
case requiredTogetherIfStringConditionTestCase:
|
||||||
|
if tc.expectError {
|
||||||
|
assert.True(t, hasDiagnosticError)
|
||||||
|
if hasDiagnosticError {
|
||||||
|
assert.Contains(t, fmt.Sprintf("%v", diagnosticErrors[0]), tc.expectErrorText)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.False(t, hasDiagnosticError)
|
||||||
|
}
|
||||||
|
case requiredTogetherIfBoolConditionTestCase:
|
||||||
|
if tc.expectError {
|
||||||
|
assert.True(t, hasDiagnosticError)
|
||||||
|
if hasDiagnosticError {
|
||||||
|
assert.Contains(t, fmt.Sprintf("%v", diagnosticErrors[0]), tc.expectErrorText)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.False(t, hasDiagnosticError)
|
||||||
|
}
|
||||||
|
case int32ConditionTestCase:
|
||||||
|
if tc.expectError {
|
||||||
|
require.True(t, hasDiagnosticError)
|
||||||
|
if hasDiagnosticError {
|
||||||
|
assert.Contains(t, fmt.Sprintf("%v", diagnosticErrors[0]), tc.expectErrorText)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.False(t, hasDiagnosticError)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unsupported test case type: %T", testCase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredTogetherIf(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||||
|
"condition-matches-all-fields-set": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition-matches-field1-missing": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||||
|
},
|
||||||
|
"condition-matches-field2-missing": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||||
|
},
|
||||||
|
"condition-matches-both-fields-missing": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"condition-does-not-match": {
|
||||||
|
condition: types.StringValue("different"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition-is-null": {
|
||||||
|
condition: types.StringNull(),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition-is-unknown": {
|
||||||
|
condition: types.StringUnknown(),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
name, testCase := name, testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schemaObject := createRequiredIfStringConditionSchema()
|
||||||
|
val := validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("expected"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
config := createRequiredIfStringConditionConfig(schemaObject, testCase)
|
||||||
|
request := validator.StringRequest{
|
||||||
|
ConfigValue: testCase.condition,
|
||||||
|
Config: config,
|
||||||
|
Path: path.Root("condition"),
|
||||||
|
}
|
||||||
|
|
||||||
|
response := validator.StringResponse{}
|
||||||
|
val.ValidateString(ctx, request, &response)
|
||||||
|
|
||||||
|
verifyValidationResults(t, response, testCase)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredTogetherIfWithBoolCondition(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]requiredTogetherIfBoolConditionTestCase{
|
||||||
|
"condition-true-all-fields-set": {
|
||||||
|
condition: types.BoolValue(true),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition-true-field1-missing": {
|
||||||
|
condition: types.BoolValue(true),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition equals true, these attributes must be configured together",
|
||||||
|
},
|
||||||
|
"condition-false-fields-missing": {
|
||||||
|
condition: types.BoolValue(false),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
name, testCase := name, testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
schemaObject := createRequiredIfBoolConditionSchema()
|
||||||
|
|
||||||
|
// Create a validator with Bool condition
|
||||||
|
val := validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.BoolValue(true),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
config := createRequiredIfBoolConditionConfig(schemaObject, testCase)
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := resource.ValidateConfigResponse{}
|
||||||
|
val.ValidateResource(context.Background(), request, &response)
|
||||||
|
|
||||||
|
verifyValidationResults(t, response, testCase)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredTogetherIfWithNumberCondition(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]int32ConditionTestCase{
|
||||||
|
"condition-matches-all-fields-set": {
|
||||||
|
condition: types.Int32Value(42),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition-matches-field-missing": {
|
||||||
|
condition: types.Int32Value(42),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition equals 42, these attributes must be configured together",
|
||||||
|
},
|
||||||
|
"condition-does-not-match": {
|
||||||
|
condition: types.Int32Value(24),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
name, testCase := name, testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schemaObject := createInt32ConditionSchema()
|
||||||
|
val := validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.Int32Value(42),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
config := createInt32ConditionConfig(schemaObject, testCase)
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := resource.ValidateConfigResponse{}
|
||||||
|
val.ValidateResource(ctx, request, &response)
|
||||||
|
|
||||||
|
verifyValidationResults(t, response, testCase)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceRequiredTogetherIf(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||||
|
"condition-matches-all-fields-set": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition-matches-field-missing": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
name, testCase := name, testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schemaObject := createRequiredIfStringConditionSchema()
|
||||||
|
|
||||||
|
// Test the RequiredTogetherIf function
|
||||||
|
val := validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("expected"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
config := createRequiredIfStringConditionConfig(schemaObject, testCase)
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := resource.ValidateConfigResponse{}
|
||||||
|
val.ValidateResource(ctx, request, &response)
|
||||||
|
|
||||||
|
verifyValidationResults(t, response, testCase)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataSourceRequiredTogetherIf(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||||
|
"condition-matches-all-fields-set": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition-matches-field-missing": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
name, testCase := name, testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schemaObject := createRequiredIfStringConditionSchema()
|
||||||
|
|
||||||
|
// Test the RequiredTogetherIf function with a datasource validator
|
||||||
|
val := validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("expected"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
config := createRequiredIfStringConditionConfig(schemaObject, testCase)
|
||||||
|
request := datasource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := datasource.ValidateConfigResponse{}
|
||||||
|
val.ValidateDataSource(ctx, request, &response)
|
||||||
|
|
||||||
|
verifyValidationResults(t, response, testCase)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderRequiredTogetherIf(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||||
|
"condition-matches-all-fields-set": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition-matches-field-missing": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
name, testCase := name, testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schemaObject := createRequiredIfStringConditionSchema()
|
||||||
|
|
||||||
|
// Test the RequiredTogetherIf function with a provider validator
|
||||||
|
val := validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("expected"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
config := createRequiredIfStringConditionConfig(schemaObject, testCase)
|
||||||
|
request := provider.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := provider.ValidateConfigResponse{}
|
||||||
|
val.ValidateProvider(ctx, request, &response)
|
||||||
|
|
||||||
|
verifyValidationResults(t, response, testCase)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredTogetherIfWithNonBooleanCondition(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type customType struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
testStruct := customType{Name: "test"}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
condValue types.String
|
||||||
|
expectedValue any
|
||||||
|
field1 types.String
|
||||||
|
field2 types.String
|
||||||
|
expectError bool
|
||||||
|
matcherChanged bool
|
||||||
|
}{
|
||||||
|
"custom-object-not-equal": {
|
||||||
|
condValue: types.StringValue("test"),
|
||||||
|
expectedValue: testStruct,
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true, // Changed to true - we expect validation to fail when the condition matches
|
||||||
|
matcherChanged: true,
|
||||||
|
},
|
||||||
|
"same-string-different-value": {
|
||||||
|
condValue: types.StringValue("test"),
|
||||||
|
expectedValue: "different",
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
matcherChanged: false,
|
||||||
|
},
|
||||||
|
"different-types": {
|
||||||
|
condValue: types.StringValue("123"),
|
||||||
|
expectedValue: 123,
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true, // Changed to true - condition equals 123 and field1 is null, should cause validation to fail
|
||||||
|
matcherChanged: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
name, testCase := name, testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
schemaObject := schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field2": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedValue := testCase.expectedValue
|
||||||
|
var v validators.RequiredTogetherIfValidator
|
||||||
|
|
||||||
|
if testCase.matcherChanged {
|
||||||
|
// Using the string "test" as the expected condition value
|
||||||
|
v = validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Normal string comparison using the string representation of expectedValue
|
||||||
|
v = validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue(fmt.Sprintf("%v", expectedValue)),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := tfsdk.Config{
|
||||||
|
Schema: schemaObject,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.String,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
"field2": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": stringToTfValue(testCase.condValue),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
"field2": stringToTfValue(testCase.field2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := resource.ValidateConfigResponse{}
|
||||||
|
v.ValidateResource(ctx, request, &response)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, response.Diagnostics.HasError())
|
||||||
|
} else {
|
||||||
|
assert.False(t, response.Diagnostics.HasError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredTogetherIfWithUnknownTargetPaths(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
condition types.String
|
||||||
|
field1 types.String
|
||||||
|
field2 types.String
|
||||||
|
fieldPath string // Path that will be used in the validator but doesn't exist in schema
|
||||||
|
expectError bool
|
||||||
|
expectErrorText string
|
||||||
|
}{
|
||||||
|
"unknown-path-condition-matches": {
|
||||||
|
condition: types.StringValue("expected"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
fieldPath: "unknown_field",
|
||||||
|
expectError: true, // Changed to true since the validator errors when a path doesn't exist
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
name, testCase := name, testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
schemaObject := schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field2": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
val := validators.RequiredTogetherIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("expected"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
path.MatchRoot(testCase.fieldPath),
|
||||||
|
)
|
||||||
|
|
||||||
|
config := tfsdk.Config{
|
||||||
|
Schema: schemaObject,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.String,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
"field2": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": stringToTfValue(testCase.condition),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
"field2": stringToTfValue(testCase.field2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := resource.ValidateConfigResponse{}
|
||||||
|
val.ValidateResource(ctx, request, &response)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, response.Diagnostics.HasError())
|
||||||
|
assert.Contains(t, response.Diagnostics.Errors()[0].Detail(), testCase.expectErrorText)
|
||||||
|
} else {
|
||||||
|
assert.False(t, response.Diagnostics.HasError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredTogetherIfSet(t *testing.T) {
|
||||||
|
schema := createRequiredIfStringConditionSchema()
|
||||||
|
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||||
|
"No condition value specified": {
|
||||||
|
condition: types.StringNull(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"Unknown condition value": {
|
||||||
|
condition: types.StringUnknown(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"Condition set, both fields set": {
|
||||||
|
condition: types.StringValue("any_value"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"Condition set, neither field set": {
|
||||||
|
condition: types.StringValue("any_value"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"Condition set, only field1 set": {
|
||||||
|
condition: types.StringValue("any_value"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition is set, these attributes must be configured together: [field1,field2]",
|
||||||
|
},
|
||||||
|
"Condition set, only field2 set": {
|
||||||
|
condition: types.StringValue("any_value"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If condition is set, these attributes must be configured together: [field1,field2]",
|
||||||
|
},
|
||||||
|
"Condition set, field1 set, field2 unknown": {
|
||||||
|
condition: types.StringValue("any_value"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringUnknown(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"Condition set, field1 unknown, field2 set": {
|
||||||
|
condition: types.StringValue("any_value"),
|
||||||
|
field1: types.StringUnknown(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
config := createRequiredIfStringConditionConfig(schema, testCase)
|
||||||
|
v := validators.RequiredTogetherIfSet(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
response := &resource.ValidateConfigResponse{}
|
||||||
|
|
||||||
|
v.ValidateResource(context.Background(), request, response)
|
||||||
|
verifyValidationResults(t, response, testCase)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
126
internal/provider/validators/required_value_if.go
Normal file
126
internal/provider/validators/required_value_if.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.ConfigValidator = &RequiredValueIfValidator{}
|
||||||
|
_ provider.ConfigValidator = &RequiredValueIfValidator{}
|
||||||
|
_ resource.ConfigValidator = &RequiredValueIfValidator{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequiredValueIfValidator validates that if a condition attribute is set to a specific value,
|
||||||
|
// then a target attribute must be set to a specific value.
|
||||||
|
type RequiredValueIfValidator struct {
|
||||||
|
ConditionPath path.Expression
|
||||||
|
ConditionValue attr.Value
|
||||||
|
TargetPath path.Expression
|
||||||
|
TargetValue attr.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredValueIfValidator) Description(ctx context.Context) string {
|
||||||
|
return v.MarkdownDescription(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredValueIfValidator) MarkdownDescription(_ context.Context) string {
|
||||||
|
return fmt.Sprintf("If %s equals %v, then %s must equal %v", v.ConditionPath, v.ConditionValue, v.TargetPath, v.TargetValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredValueIfValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredValueIfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredValueIfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredValueIfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredValueIfValidator) shouldValidate(ctx context.Context, config tfsdk.Config) bool {
|
||||||
|
// First check the condition attribute's value
|
||||||
|
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, v.ConditionPath)
|
||||||
|
if matchedPathsDiags.HasError() || len(matchedPaths) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value of the condition attribute
|
||||||
|
var conditionValue attr.Value
|
||||||
|
getConditionDiags := config.GetAttribute(ctx, matchedPaths[0], &conditionValue)
|
||||||
|
if getConditionDiags.HasError() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the condition attribute is null or unknown, skip validation
|
||||||
|
if conditionValue.IsNull() || conditionValue.IsUnknown() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the condition matches
|
||||||
|
return conditionValueMatches(ctx, conditionValue, v.ConditionValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredValueIfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
if !v.shouldValidate(ctx, config) {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition matched, now validate that the target attribute has the required value
|
||||||
|
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, v.TargetPath)
|
||||||
|
diags.Append(matchedPathsDiags...)
|
||||||
|
if diags.HasError() || len(matchedPaths) == 0 {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value of the target attribute
|
||||||
|
var targetValue attr.Value
|
||||||
|
getTargetDiags := config.GetAttribute(ctx, matchedPaths[0], &targetValue)
|
||||||
|
diags.Append(getTargetDiags...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip validation if the target value is unknown
|
||||||
|
if targetValue.IsUnknown() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the target value is null or doesn't match the required value, add a diagnostic
|
||||||
|
if targetValue.IsNull() || !conditionValueMatches(ctx, targetValue, v.TargetValue) {
|
||||||
|
diags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
|
||||||
|
matchedPaths[0],
|
||||||
|
fmt.Sprintf("When %s is set to %v, %s must be set to %v", v.ConditionPath, v.ConditionValue, v.TargetPath, v.TargetValue),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredValueIf creates a validator that ensures if a condition attribute equals a specific value,
|
||||||
|
// then a target attribute must equal a specific value.
|
||||||
|
func RequiredValueIf(conditionPath path.Expression, conditionValue attr.Value, targetPath path.Expression, targetValue attr.Value) RequiredValueIfValidator {
|
||||||
|
return RequiredValueIfValidator{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
ConditionValue: conditionValue,
|
||||||
|
TargetPath: targetPath,
|
||||||
|
TargetValue: targetValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
201
internal/provider/validators/required_value_if_test.go
Normal file
201
internal/provider/validators/required_value_if_test.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package validators_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"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/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common test case structure for string conditions
|
||||||
|
type requiredValueIfTestCase struct {
|
||||||
|
conditionValue types.String
|
||||||
|
targetValue types.String
|
||||||
|
expectError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a schema object for RequiredValueIf tests
|
||||||
|
func createRequiredValueIfSchema() schema.Schema {
|
||||||
|
return schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition_attr": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"target_attr": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a config for RequiredValueIf tests
|
||||||
|
func createRequiredValueIfConfig(schema schema.Schema, testCase requiredValueIfTestCase) tfsdk.Config {
|
||||||
|
var conditionValue, targetValue tftypes.Value
|
||||||
|
|
||||||
|
if testCase.conditionValue.IsNull() {
|
||||||
|
conditionValue = tftypes.NewValue(tftypes.String, nil)
|
||||||
|
} else if testCase.conditionValue.IsUnknown() {
|
||||||
|
conditionValue = tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
|
||||||
|
} else {
|
||||||
|
conditionValue = tftypes.NewValue(tftypes.String, testCase.conditionValue.ValueString())
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.targetValue.IsNull() {
|
||||||
|
targetValue = tftypes.NewValue(tftypes.String, nil)
|
||||||
|
} else if testCase.targetValue.IsUnknown() {
|
||||||
|
targetValue = tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
|
||||||
|
} else {
|
||||||
|
targetValue = tftypes.NewValue(tftypes.String, testCase.targetValue.ValueString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return tfsdk.Config{
|
||||||
|
Schema: schema,
|
||||||
|
Raw: tftypes.NewValue(tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition_attr": tftypes.String,
|
||||||
|
"target_attr": tftypes.String,
|
||||||
|
},
|
||||||
|
}, map[string]tftypes.Value{
|
||||||
|
"condition_attr": conditionValue,
|
||||||
|
"target_attr": targetValue,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredValueIf(t *testing.T) {
|
||||||
|
schema := createRequiredValueIfSchema()
|
||||||
|
testCases := map[string]requiredValueIfTestCase{
|
||||||
|
"condition match target match": {
|
||||||
|
conditionValue: types.StringValue("active"),
|
||||||
|
targetValue: types.StringValue("enabled"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition match target mismatch": {
|
||||||
|
conditionValue: types.StringValue("active"),
|
||||||
|
targetValue: types.StringValue("disabled"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"condition match target null": {
|
||||||
|
conditionValue: types.StringValue("active"),
|
||||||
|
targetValue: types.StringNull(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"condition match target unknown": {
|
||||||
|
conditionValue: types.StringValue("active"),
|
||||||
|
targetValue: types.StringUnknown(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition mismatch": {
|
||||||
|
conditionValue: types.StringValue("inactive"),
|
||||||
|
targetValue: types.StringValue("disabled"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition null": {
|
||||||
|
conditionValue: types.StringNull(),
|
||||||
|
targetValue: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition unknown": {
|
||||||
|
conditionValue: types.StringUnknown(),
|
||||||
|
targetValue: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
config := createRequiredValueIfConfig(schema, testCase)
|
||||||
|
validator := validators.RequiredValueIf(
|
||||||
|
path.MatchRoot("condition_attr"),
|
||||||
|
types.StringValue("active"),
|
||||||
|
path.MatchRoot("target_attr"),
|
||||||
|
types.StringValue("enabled"),
|
||||||
|
)
|
||||||
|
diags := validator.Validate(context.Background(), config)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, diags.HasError(), "expected error, but got none")
|
||||||
|
} else {
|
||||||
|
assert.False(t, diags.HasError(), "expected no error, but got: %v", diags)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceRequiredValueIf(t *testing.T) {
|
||||||
|
schema := createRequiredValueIfSchema()
|
||||||
|
testCase := requiredValueIfTestCase{
|
||||||
|
conditionValue: types.StringValue("active"),
|
||||||
|
targetValue: types.StringValue("disabled"),
|
||||||
|
expectError: true,
|
||||||
|
}
|
||||||
|
config := createRequiredValueIfConfig(schema, testCase)
|
||||||
|
validator := validators.RequiredValueIf(
|
||||||
|
path.MatchRoot("condition_attr"),
|
||||||
|
types.StringValue("active"),
|
||||||
|
path.MatchRoot("target_attr"),
|
||||||
|
types.StringValue("enabled"),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp := &resource.ValidateConfigResponse{}
|
||||||
|
validator.ValidateResource(context.Background(), resource.ValidateConfigRequest{Config: config}, resp)
|
||||||
|
assert.True(t, resp.Diagnostics.HasError(), "expected error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataSourceRequiredValueIf(t *testing.T) {
|
||||||
|
schema := createRequiredValueIfSchema()
|
||||||
|
testCase := requiredValueIfTestCase{
|
||||||
|
conditionValue: types.StringValue("active"),
|
||||||
|
targetValue: types.StringValue("disabled"),
|
||||||
|
expectError: true,
|
||||||
|
}
|
||||||
|
config := createRequiredValueIfConfig(schema, testCase)
|
||||||
|
validator := validators.RequiredValueIf(
|
||||||
|
path.MatchRoot("condition_attr"),
|
||||||
|
types.StringValue("active"),
|
||||||
|
path.MatchRoot("target_attr"),
|
||||||
|
types.StringValue("enabled"),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp := &datasource.ValidateConfigResponse{}
|
||||||
|
validator.ValidateDataSource(context.Background(), datasource.ValidateConfigRequest{Config: config}, resp)
|
||||||
|
assert.True(t, resp.Diagnostics.HasError(), "expected error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderRequiredValueIf(t *testing.T) {
|
||||||
|
schema := createRequiredValueIfSchema()
|
||||||
|
testCase := requiredValueIfTestCase{
|
||||||
|
conditionValue: types.StringValue("active"),
|
||||||
|
targetValue: types.StringValue("disabled"),
|
||||||
|
expectError: true,
|
||||||
|
}
|
||||||
|
config := createRequiredValueIfConfig(schema, testCase)
|
||||||
|
validator := validators.RequiredValueIf(
|
||||||
|
path.MatchRoot("condition_attr"),
|
||||||
|
types.StringValue("active"),
|
||||||
|
path.MatchRoot("target_attr"),
|
||||||
|
types.StringValue("enabled"),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp := &provider.ValidateConfigResponse{}
|
||||||
|
validator.ValidateProvider(context.Background(), provider.ValidateConfigRequest{Config: config}, resp)
|
||||||
|
assert.True(t, resp.Diagnostics.HasError(), "expected error, but got none")
|
||||||
|
}
|
||||||
@@ -3,10 +3,21 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func DefaultEmptyList(elementType attr.Type) defaults.List {
|
||||||
|
return listdefault.StaticValue(EmptyList(elementType))
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmptyList(elementType attr.Type) types.List {
|
||||||
|
return types.ListValueMust(elementType, []attr.Value{})
|
||||||
|
}
|
||||||
|
|
||||||
func ListElementsAs(list types.List, target interface{}) diag.Diagnostics {
|
func ListElementsAs(list types.List, target interface{}) diag.Diagnostics {
|
||||||
diags := diag.Diagnostics{}
|
diags := diag.Diagnostics{}
|
||||||
if !base.IsDefined(list) {
|
if !base.IsDefined(list) {
|
||||||
@@ -17,3 +28,32 @@ func ListElementsAs(list types.List, target interface{}) diag.Diagnostics {
|
|||||||
}
|
}
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListElementsToString(ctx context.Context, list types.List) (string, diag.Diagnostics) {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
if !base.IsDefined(list) {
|
||||||
|
return "", diags
|
||||||
|
}
|
||||||
|
if list.ElementType(ctx) == types.StringType {
|
||||||
|
var target []string
|
||||||
|
diags.Append(ListElementsAs(list, &target)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return "", diags
|
||||||
|
}
|
||||||
|
return JoinNonEmpty(target, ","), diags
|
||||||
|
}
|
||||||
|
diags.AddError("List is not a list of types.StringType", "List is not a list of strings")
|
||||||
|
return "", diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringToListElements(ctx context.Context, value string) (types.List, diag.Diagnostics) {
|
||||||
|
countries := SplitAndTrim(value, ",")
|
||||||
|
if len(countries) == 0 {
|
||||||
|
return types.ListNull(types.StringType), diag.Diagnostics{}
|
||||||
|
}
|
||||||
|
list, diags := types.ListValueFrom(ctx, types.StringType, countries)
|
||||||
|
if diags.HasError() {
|
||||||
|
return types.ListNull(types.StringType), diags
|
||||||
|
}
|
||||||
|
return list, diags
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||||
)
|
)
|
||||||
@@ -38,3 +39,35 @@ func StringSliceToSet(src []string) *schema.Set {
|
|||||||
func IsStringValueNotEmpty(s basetypes.StringValue) bool {
|
func IsStringValueNotEmpty(s basetypes.StringValue) bool {
|
||||||
return !s.IsUnknown() && !s.IsNull() && s.ValueString() != ""
|
return !s.IsUnknown() && !s.IsNull() && s.ValueString() != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JoinNonEmpty joins non-empty strings from a slice with the specified separator.
|
||||||
|
// Empty strings in the slice are filtered out.
|
||||||
|
func JoinNonEmpty(elements []string, separator string) string {
|
||||||
|
var nonEmpty []string
|
||||||
|
for _, elem := range elements {
|
||||||
|
if elem != "" {
|
||||||
|
nonEmpty = append(nonEmpty, elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(nonEmpty, separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitAndTrim splits a string by the specified separator and trims whitespace from each element.
|
||||||
|
// Empty strings after trimming are filtered out.
|
||||||
|
func SplitAndTrim(s string, separator string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(s, separator)
|
||||||
|
var result []string
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
trimmed := strings.TrimSpace(part)
|
||||||
|
if trimmed != "" {
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user