feat: add support for Intrution Prevention System (IPS) settings with unifi_setting_ips resource (#56)
* feat: add support for Intrution Prevention System (IPS) settings with `unifi_setting_ips` resource * require IPS features enabled on controller * require version 7.4 * require version 7.5 for advanced_filtering_preference * feat: use Remember Me to prolong session for user/pass authentication * run some setting mgmt tests on 7.0+ due to auto_upgrade_hour not working until device is adopted and auto upgrade logic is different and not supported
This commit is contained in:
committed by
GitHub
parent
8b6ff55a18
commit
e9600c6e06
4
go.mod
4
go.mod
@@ -2,7 +2,7 @@ module github.com/filipowm/terraform-provider-unifi
|
|||||||
|
|
||||||
go 1.23.5
|
go 1.23.5
|
||||||
|
|
||||||
// replace github.com/filipowm/go-unifi v1.5.3 => ../go-unifi
|
//replace github.com/filipowm/go-unifi v1.5.3 => ../go-unifi
|
||||||
// replace github.com/hashicorp/terraform-plugin-docs => ../../hashicorp/terraform-plugin-docs
|
// replace github.com/hashicorp/terraform-plugin-docs => ../../hashicorp/terraform-plugin-docs
|
||||||
// replace github.com/hashicorp/terraform-plugin-sdk/v2 => ../../hashicorp/terraform-plugin-sdk
|
// replace github.com/hashicorp/terraform-plugin-sdk/v2 => ../../hashicorp/terraform-plugin-sdk
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ 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.3
|
github.com/filipowm/go-unifi v1.6.0
|
||||||
github.com/golangci/golangci-lint v1.64.7
|
github.com/golangci/golangci-lint v1.64.7
|
||||||
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
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -280,6 +280,10 @@ github.com/filipowm/go-unifi v1.5.2 h1:S974Fi3BWt7qm0P8G/SwUVOjzgWTSjNJGV0jW5yD6
|
|||||||
github.com/filipowm/go-unifi v1.5.2/go.mod h1:ldtL5szykvR9fPWB9GlGZqaJGxKd8IWLQY5M8O1PDQ8=
|
github.com/filipowm/go-unifi v1.5.2/go.mod h1:ldtL5szykvR9fPWB9GlGZqaJGxKd8IWLQY5M8O1PDQ8=
|
||||||
github.com/filipowm/go-unifi v1.5.3 h1:9e+V5xzgDtQ6ZmQvIiz8sCuwcDyM06nzbhBQxzvPBbo=
|
github.com/filipowm/go-unifi v1.5.3 h1:9e+V5xzgDtQ6ZmQvIiz8sCuwcDyM06nzbhBQxzvPBbo=
|
||||||
github.com/filipowm/go-unifi v1.5.3/go.mod h1:NQgqx3ylLVDqxVgY3uJ3ZMIecSl46fvqCCXzwHRuVDc=
|
github.com/filipowm/go-unifi v1.5.3/go.mod h1:NQgqx3ylLVDqxVgY3uJ3ZMIecSl46fvqCCXzwHRuVDc=
|
||||||
|
github.com/filipowm/go-unifi v1.5.4 h1:kc1jWx0Rht+tiEyFsDjPAYJ6h2EV+Atq7eIoTA6fH0Y=
|
||||||
|
github.com/filipowm/go-unifi v1.5.4/go.mod h1:NQgqx3ylLVDqxVgY3uJ3ZMIecSl46fvqCCXzwHRuVDc=
|
||||||
|
github.com/filipowm/go-unifi v1.6.0 h1:0oLOrsLWcaU8sUsyMyjyGwaAWNC9Ee4YZ1ehtijXahg=
|
||||||
|
github.com/filipowm/go-unifi v1.6.0/go.mod h1:hB5XyhjtnnU9GC6lYPYxuNmYq4J/SyjmElRVazCKT0U=
|
||||||
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=
|
||||||
|
|||||||
704
internal/provider/acctest/resource_setting_ips_test.go
Normal file
704
internal/provider/acctest/resource_setting_ips_test.go
Normal file
@@ -0,0 +1,704 @@
|
|||||||
|
package acctest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
pt "github.com/filipowm/terraform-provider-unifi/internal/provider/testing"
|
||||||
|
"github.com/hashicorp/go-version"
|
||||||
|
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-testing/plancheck"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Using dedicated lock for IPS settings to avoid interference with other tests
|
||||||
|
var settingIpsLock = &sync.Mutex{}
|
||||||
|
|
||||||
|
func TestAccSettingIps_basic(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_basic(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "ips_mode", "ips"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "enabled_networks.#", "1"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "enabled_networks.*", "LAN"),
|
||||||
|
),
|
||||||
|
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_ips.test", plancheck.ResourceActionCreate),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_updated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "ips_mode", "ids"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "enabled_networks.#", "1"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "enabled_networks.*", "LAN"),
|
||||||
|
),
|
||||||
|
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_ips.test", plancheck.ResourceActionUpdate),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_enabledCategories(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_enabledCategories(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "enabled_categories.#", "3"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "enabled_categories.*", "emerging-dos"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "enabled_categories.*", "emerging-exploit"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "enabled_categories.*", "emerging-malware"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_enabledCategoriesUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "enabled_categories.#", "2"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "enabled_categories.*", "emerging-scan"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "enabled_categories.*", "emerging-worm"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_adBlocking(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_adBlocking(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "ad_blocked_networks.#", "2"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "ad_blocked_networks.*", "network1"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "ad_blocked_networks.*", "network2"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_adBlockingUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "ad_blocked_networks.#", "1"),
|
||||||
|
resource.TestCheckTypeSetElemAttr("unifi_setting_ips.test", "ad_blocked_networks.*", "network3"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_honeypot(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_honeypot(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.0.ip_address", "192.168.1.10"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.0.network_id", "network1"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_honeypotUpdated(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.0.ip_address", "192.168.2.20"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.0.network_id", "network2"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_honeypotDisabled(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.#", "0"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_dnsFilters(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_dnsFilters(t),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.0.name", "Test Filter"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.0.filter", "work"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.0.description", "Test description"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.0.allowed_sites.#", "2"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.0.blocked_sites.#", "2"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.0.blocked_tld.#", "1"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_dnsFiltersUpdated(t),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.#", "2"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.0.name", "Test Filter Updated"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.0.filter", "family"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.1.name", "Second Filter"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.1.filter", "none"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_suppression(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_suppression(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.0.category", "emerging-dos"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.0.signature", "Test Signature"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.0.type", "all"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.whitelist.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.whitelist.0.direction", "src"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.whitelist.0.mode", "ip"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.whitelist.0.value", "192.168.1.100"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_suppressionUpdated(t),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.#", "2"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.0.type", "track"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.0.tracking.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.0.tracking.0.direction", "dest"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.0.tracking.0.mode", "subnet"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.0.tracking.0.value", "192.168.0.0/24"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.whitelist.#", "2"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_comprehensive(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_comprehensive(t),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "ips_mode", "ids"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "restrict_torrents", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "advanced_filtering_preference", "manual"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "enabled_categories.#", "2"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "enabled_networks.#", "2"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "ad_blocked_networks.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.whitelist.#", "1"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_comprehensiveBefore8(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: "< 8.0",
|
||||||
|
MinVersion: version.Must(version.NewVersion("7.4")),
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_comprehensiveBefore8(t),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "ips_mode", "ids"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "restrict_torrents", "true"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "enabled_categories.#", "2"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "ad_blocked_networks.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "honeypots.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "dns_filters.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.alerts.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "suppression.whitelist.#", "1"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_restrictTorrents(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 8.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_restrictTorrents(true),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "restrict_torrents", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_restrictTorrents(false),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "restrict_torrents", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingIps_memoryOptimized(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 9.0",
|
||||||
|
Lock: settingIpsLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_memoryOptimized(true),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "memory_optimized", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingIpsConfig_memoryOptimized(false),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ips.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ips.test", "memory_optimized", "false"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ips.test"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_basic() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ips"
|
||||||
|
enabled_networks = ["LAN"]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_updated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["LAN"]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_enabledCategories() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
enabled_categories = [
|
||||||
|
"emerging-dos",
|
||||||
|
"emerging-exploit",
|
||||||
|
"emerging-malware"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_enabledCategoriesUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
enabled_categories = [
|
||||||
|
"emerging-scan",
|
||||||
|
"emerging-worm",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_adBlocking() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
ad_blocked_networks = [
|
||||||
|
"network1",
|
||||||
|
"network2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_adBlockingUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
ad_blocked_networks = [
|
||||||
|
"network3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_honeypot() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
honeypots = [{
|
||||||
|
ip_address = "192.168.1.10"
|
||||||
|
network_id = "network1"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_honeypotUpdated() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
honeypots = [{
|
||||||
|
ip_address = "192.168.2.20"
|
||||||
|
network_id = "network2"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_honeypotDisabled() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
honeypots = []
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_dnsFilters(t *testing.T) string {
|
||||||
|
subnet, vlanId := pt.GetTestVLAN(t)
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
|
||||||
|
resource "unifi_network" "test" {
|
||||||
|
name = "Test"
|
||||||
|
purpose = "corporate"
|
||||||
|
subnet = %q
|
||||||
|
vlan_id = %d
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
dns_filters = [{
|
||||||
|
name = "Test Filter"
|
||||||
|
filter = "work"
|
||||||
|
description = "Test description"
|
||||||
|
network_id = unifi_network.test.id
|
||||||
|
allowed_sites = [
|
||||||
|
"example.com",
|
||||||
|
"allowed.org"
|
||||||
|
]
|
||||||
|
blocked_sites = [
|
||||||
|
"blocked1.com",
|
||||||
|
"blocked2.com"
|
||||||
|
]
|
||||||
|
blocked_tld = [
|
||||||
|
"xyz"
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`, subnet.String(), vlanId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_dnsFiltersUpdated(t *testing.T) string {
|
||||||
|
subnet, vlanId := pt.GetTestVLAN(t)
|
||||||
|
subnet2, vlanId2 := pt.GetTestVLAN(t)
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
|
||||||
|
resource "unifi_network" "test" {
|
||||||
|
name = "Test"
|
||||||
|
purpose = "corporate"
|
||||||
|
subnet = %q
|
||||||
|
vlan_id = %d
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
resource "unifi_network" "test2" {
|
||||||
|
name = "Test"
|
||||||
|
purpose = "corporate"
|
||||||
|
subnet = %q
|
||||||
|
vlan_id = %d
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
dns_filters = [
|
||||||
|
{
|
||||||
|
name = "Test Filter Updated"
|
||||||
|
filter = "family"
|
||||||
|
description = "Updated description"
|
||||||
|
network_id = unifi_network.test.id
|
||||||
|
allowed_sites = [
|
||||||
|
"example.com",
|
||||||
|
"allowed.org",
|
||||||
|
"new-allowed.com"
|
||||||
|
]
|
||||||
|
blocked_sites = [
|
||||||
|
"blocked1.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "Second Filter"
|
||||||
|
filter = "none"
|
||||||
|
network_id = unifi_network.test2.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`, subnet.String(), vlanId, subnet2.String(), vlanId2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_suppression() string {
|
||||||
|
return `
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
suppression = {
|
||||||
|
alerts = [{
|
||||||
|
category = "emerging-dos"
|
||||||
|
signature = "Test Signature"
|
||||||
|
type = "all"
|
||||||
|
}]
|
||||||
|
whitelist = [{
|
||||||
|
direction = "src"
|
||||||
|
mode = "ip"
|
||||||
|
value = "192.168.1.100"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_suppressionUpdated(t *testing.T) string {
|
||||||
|
subnet, vlanId := pt.GetTestVLAN(t)
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_network" "test" {
|
||||||
|
name = "Test"
|
||||||
|
purpose = "corporate"
|
||||||
|
subnet = %q
|
||||||
|
vlan_id = %d
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
suppression = {
|
||||||
|
alerts = [
|
||||||
|
{
|
||||||
|
category = "emerging-dos"
|
||||||
|
signature = "Test Signature"
|
||||||
|
type = "track"
|
||||||
|
tracking = [{
|
||||||
|
direction = "dest"
|
||||||
|
mode = "subnet"
|
||||||
|
value = "192.168.0.0/24"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category = "emerging-exploit"
|
||||||
|
signature = "Another Signature"
|
||||||
|
type = "track"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
whitelist = [
|
||||||
|
{
|
||||||
|
direction = "src"
|
||||||
|
mode = "subnet"
|
||||||
|
value = "192.168.1.0/24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction = "both"
|
||||||
|
mode = "network"
|
||||||
|
value = unifi_network.test.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, subnet.String(), vlanId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_comprehensive(t *testing.T) string {
|
||||||
|
subnet, vlanId := pt.GetTestVLAN(t)
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_network" "test" {
|
||||||
|
name = "Test"
|
||||||
|
purpose = "corporate"
|
||||||
|
subnet = %q
|
||||||
|
vlan_id = %d
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
restrict_torrents = true
|
||||||
|
advanced_filtering_preference = "manual"
|
||||||
|
|
||||||
|
enabled_categories = [
|
||||||
|
"emerging-dos",
|
||||||
|
"emerging-exploit"
|
||||||
|
]
|
||||||
|
|
||||||
|
enabled_networks = [
|
||||||
|
"network1",
|
||||||
|
"network2"
|
||||||
|
]
|
||||||
|
|
||||||
|
ad_blocked_networks = [
|
||||||
|
"network1"
|
||||||
|
]
|
||||||
|
|
||||||
|
honeypots = [{
|
||||||
|
ip_address = "192.168.1.10"
|
||||||
|
network_id = "network1"
|
||||||
|
}]
|
||||||
|
|
||||||
|
dns_filters = [{
|
||||||
|
name = "Comprehensive Filter"
|
||||||
|
filter = "work"
|
||||||
|
description = "Comprehensive test filter"
|
||||||
|
network_id = unifi_network.test.id
|
||||||
|
allowed_sites = ["allowed.com"]
|
||||||
|
blocked_sites = ["blocked.com"]
|
||||||
|
}]
|
||||||
|
|
||||||
|
suppression = {
|
||||||
|
alerts = [{
|
||||||
|
category = "emerging-dos"
|
||||||
|
signature = "Test Signature"
|
||||||
|
type = "all"
|
||||||
|
}]
|
||||||
|
whitelist = [{
|
||||||
|
direction = "src"
|
||||||
|
mode = "ip"
|
||||||
|
value = "192.168.1.100"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, subnet.String(), vlanId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_comprehensiveBefore8(t *testing.T) string {
|
||||||
|
subnet, vlanId := pt.GetTestVLAN(t)
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_network" "test" {
|
||||||
|
name = "Test"
|
||||||
|
purpose = "corporate"
|
||||||
|
subnet = %q
|
||||||
|
vlan_id = %d
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
restrict_torrents = true
|
||||||
|
|
||||||
|
enabled_categories = [
|
||||||
|
"emerging-dos",
|
||||||
|
"emerging-exploit"
|
||||||
|
]
|
||||||
|
|
||||||
|
ad_blocked_networks = [
|
||||||
|
"network1"
|
||||||
|
]
|
||||||
|
|
||||||
|
honeypots = [{
|
||||||
|
ip_address = "192.168.1.10"
|
||||||
|
network_id = "network1"
|
||||||
|
}]
|
||||||
|
|
||||||
|
dns_filters = [{
|
||||||
|
name = "Comprehensive Filter"
|
||||||
|
filter = "work"
|
||||||
|
description = "Comprehensive test filter"
|
||||||
|
network_id = unifi_network.test.id
|
||||||
|
allowed_sites = ["allowed.com"]
|
||||||
|
blocked_sites = ["blocked.com"]
|
||||||
|
}]
|
||||||
|
|
||||||
|
suppression = {
|
||||||
|
alerts = [{
|
||||||
|
category = "emerging-dos"
|
||||||
|
signature = "Test Signature"
|
||||||
|
type = "all"
|
||||||
|
}]
|
||||||
|
whitelist = [{
|
||||||
|
direction = "src"
|
||||||
|
mode = "ip"
|
||||||
|
value = "192.168.1.100"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, subnet.String(), vlanId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_restrictTorrents(enabled bool) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
restrict_torrents = %t
|
||||||
|
}
|
||||||
|
`, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingIpsConfig_memoryOptimized(enabled bool) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_setting_ips" "test" {
|
||||||
|
ips_mode = "ids"
|
||||||
|
enabled_networks = ["network1"]
|
||||||
|
memory_optimized = %t
|
||||||
|
}
|
||||||
|
`, enabled)
|
||||||
|
}
|
||||||
@@ -107,7 +107,8 @@ func TestAccSettingMgmt_fullConfig(t *testing.T) {
|
|||||||
|
|
||||||
func TestAccSettingMgmt_update(t *testing.T) {
|
func TestAccSettingMgmt_update(t *testing.T) {
|
||||||
AcceptanceTest(t, AcceptanceTestCase{
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
Lock: &settingMgmtLock,
|
VersionConstraint: ">= 7.0",
|
||||||
|
Lock: &settingMgmtLock,
|
||||||
Steps: []resource.TestStep{
|
Steps: []resource.TestStep{
|
||||||
{
|
{
|
||||||
Config: testAccSettingMgmtConfig_initialConfig(),
|
Config: testAccSettingMgmtConfig_initialConfig(),
|
||||||
@@ -155,7 +156,8 @@ func TestAccSettingMgmt_sshCredentials(t *testing.T) {
|
|||||||
|
|
||||||
func TestAccSettingMgmt_cornerCases(t *testing.T) {
|
func TestAccSettingMgmt_cornerCases(t *testing.T) {
|
||||||
AcceptanceTest(t, AcceptanceTestCase{
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
Lock: &settingMgmtLock,
|
VersionConstraint: ">= 7.0",
|
||||||
|
Lock: &settingMgmtLock,
|
||||||
Steps: []resource.TestStep{
|
Steps: []resource.TestStep{
|
||||||
{
|
{
|
||||||
// Initial configuration with specific values
|
// Initial configuration with specific values
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Identifiable interface {
|
|||||||
type Resource interface {
|
type Resource interface {
|
||||||
SetClient(client *Client)
|
SetClient(client *Client)
|
||||||
SetVersionValidator(validator ControllerVersionValidator)
|
SetVersionValidator(validator ControllerVersionValidator)
|
||||||
|
SetFeatureValidator(validator FeatureValidator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceModel defines the interface that all setting models must implement
|
// ResourceModel defines the interface that all setting models must implement
|
||||||
@@ -35,6 +36,12 @@ type ResourceModel interface {
|
|||||||
AsUnifiModel(context.Context) (interface{}, diag.Diagnostics)
|
AsUnifiModel(context.Context) (interface{}, diag.Diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResourceModel defines the interface that all setting models must implement
|
||||||
|
type DatasourceModel interface {
|
||||||
|
SiteAware
|
||||||
|
Merge(context.Context, interface{}) diag.Diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
ID types.String `tfsdk:"id"`
|
ID types.String `tfsdk:"id"`
|
||||||
Site types.String `tfsdk:"site"`
|
Site types.String `tfsdk:"site"`
|
||||||
@@ -80,6 +87,7 @@ func ConfigureDatasource(base Resource, req datasource.ConfigureRequest, resp *d
|
|||||||
}
|
}
|
||||||
base.SetClient(cfg)
|
base.SetClient(cfg)
|
||||||
base.SetVersionValidator(NewControllerVersionValidator(cfg))
|
base.SetVersionValidator(NewControllerVersionValidator(cfg))
|
||||||
|
base.SetFeatureValidator(NewFeatureValidator(cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfigureResource(base Resource, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
func ConfigureResource(base Resource, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
@@ -98,4 +106,5 @@ func ConfigureResource(base Resource, req resource.ConfigureRequest, resp *resou
|
|||||||
}
|
}
|
||||||
base.SetClient(cfg)
|
base.SetClient(cfg)
|
||||||
base.SetVersionValidator(NewControllerVersionValidator(cfg))
|
base.SetVersionValidator(NewControllerVersionValidator(cfg))
|
||||||
|
base.SetFeatureValidator(NewFeatureValidator(cfg))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/filipowm/go-unifi/unifi"
|
"github.com/filipowm/go-unifi/unifi"
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -23,7 +27,7 @@ type ClientConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(cfg *ClientConfig) (*Client, error) {
|
func NewClient(cfg *ClientConfig) (*Client, error) {
|
||||||
unifiClient, err := unifi.NewClient(&unifi.ClientConfig{
|
config := &unifi.ClientConfig{
|
||||||
URL: cfg.Url,
|
URL: cfg.Url,
|
||||||
User: cfg.Username,
|
User: cfg.Username,
|
||||||
Password: cfg.Password,
|
Password: cfg.Password,
|
||||||
@@ -31,7 +35,15 @@ func NewClient(cfg *ClientConfig) (*Client, error) {
|
|||||||
HttpRoundTripperProvider: cfg.HttpConfigurer,
|
HttpRoundTripperProvider: cfg.HttpConfigurer,
|
||||||
ValidationMode: unifi.DisableValidation,
|
ValidationMode: unifi.DisableValidation,
|
||||||
Logger: unifi.NewDefaultLogger(unifi.WarnLevel),
|
Logger: unifi.NewDefaultLogger(unifi.WarnLevel),
|
||||||
})
|
}
|
||||||
|
if cfg.Username != "" && cfg.Password != "" {
|
||||||
|
config.User = cfg.Username
|
||||||
|
config.Password = cfg.Password
|
||||||
|
config.RememberMe = true
|
||||||
|
} else {
|
||||||
|
config.APIKey = cfg.ApiKey
|
||||||
|
}
|
||||||
|
unifiClient, err := unifi.NewClient(config)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -65,6 +77,18 @@ func (c *Client) ResolveSite(res SiteAware) string {
|
|||||||
return res.GetSite()
|
return res.GetSite()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) ResolveSiteFromConfig(ctx context.Context, config tfsdk.Config) (string, diag.Diagnostics) {
|
||||||
|
var site types.String
|
||||||
|
diags := config.GetAttribute(ctx, path.Root("site"), &site)
|
||||||
|
if diags.HasError() {
|
||||||
|
return "", diags
|
||||||
|
}
|
||||||
|
if IsEmptyString(site) {
|
||||||
|
return c.Site, diags
|
||||||
|
}
|
||||||
|
return site.ValueString(), diags
|
||||||
|
}
|
||||||
|
|
||||||
func CreateHttpTransport(insecure bool) http.RoundTripper {
|
func CreateHttpTransport(insecure bool) http.RoundTripper {
|
||||||
return &http.Transport{
|
return &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func AsVersion(versionString string) *version.Version {
|
|||||||
var (
|
var (
|
||||||
ControllerV6 = AsVersion("6.0.0")
|
ControllerV6 = AsVersion("6.0.0")
|
||||||
ControllerV7 = AsVersion("7.0.0")
|
ControllerV7 = AsVersion("7.0.0")
|
||||||
|
ControllerV8 = AsVersion("8.0.0")
|
||||||
ControllerV9 = AsVersion("9.0.0")
|
ControllerV9 = AsVersion("9.0.0")
|
||||||
ControllerVersionApiKeyAuth = AsVersion("9.0.108")
|
ControllerVersionApiKeyAuth = AsVersion("9.0.108")
|
||||||
// https://community.ui.com/releases/UniFi-Network-Application-8-2-93/fce86dc6-897a-4944-9c53-1eec7e37e738
|
// https://community.ui.com/releases/UniFi-Network-Application-8-2-93/fce86dc6-897a-4944-9c53-1eec7e37e738
|
||||||
|
|||||||
127
internal/provider/base/features.go
Normal file
127
internal/provider/base/features.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
featureEnabled featureStatus = iota
|
||||||
|
featureDisabled
|
||||||
|
)
|
||||||
|
|
||||||
|
type featureStatus int
|
||||||
|
|
||||||
|
type FeatureValidator interface {
|
||||||
|
RequireFeaturesEnabled(ctx context.Context, site string, features ...string) diag.Diagnostics
|
||||||
|
RequireFeaturesEnabledForPath(ctx context.Context, site string, attrPath path.Path, config tfsdk.Config, features ...string) diag.Diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
type Features map[string]featureStatus
|
||||||
|
|
||||||
|
func (v Features) IsEnabled(feature string) bool {
|
||||||
|
return !v.IsUnavailable(feature) && v[feature] == featureEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Features) IsDisabled(feature string) bool {
|
||||||
|
return !v.IsUnavailable(feature) && v[feature] == featureDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Features) IsUnavailable(feature string) bool {
|
||||||
|
if _, ok := v[feature]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type featureEnabledValidator struct {
|
||||||
|
client *Client
|
||||||
|
cache map[string]Features
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFeatureValidator(client *Client) FeatureValidator {
|
||||||
|
return &featureEnabledValidator{client: client, cache: make(map[string]Features), lock: sync.Mutex{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *featureEnabledValidator) getFeatures(ctx context.Context, site string) Features {
|
||||||
|
if v.cache[site] != nil {
|
||||||
|
return v.cache[site]
|
||||||
|
}
|
||||||
|
v.lock.Lock()
|
||||||
|
defer v.lock.Unlock()
|
||||||
|
if v.cache[site] != nil {
|
||||||
|
return v.cache[site]
|
||||||
|
}
|
||||||
|
cache := make(map[string]featureStatus)
|
||||||
|
features, err := v.client.ListFeatures(ctx, site)
|
||||||
|
if err != nil {
|
||||||
|
// Return an empty Features map instead of nil to avoid potential nil pointer dereference
|
||||||
|
return Features{}
|
||||||
|
}
|
||||||
|
for _, feature := range features {
|
||||||
|
if feature.FeatureExists {
|
||||||
|
cache[feature.Name] = featureEnabled
|
||||||
|
} else {
|
||||||
|
cache[feature.Name] = featureDisabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.cache[site] = cache
|
||||||
|
return v.cache[site]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *featureEnabledValidator) requireFeatures(ctx context.Context, site string, attrPath *path.Path, features ...string) diag.Diagnostics {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
if len(features) == 0 {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
f := v.getFeatures(ctx, site)
|
||||||
|
var unavailableFeatures, disabledFeatures []string
|
||||||
|
for _, feature := range features {
|
||||||
|
if f.IsUnavailable(feature) {
|
||||||
|
unavailableFeatures = append(unavailableFeatures, feature)
|
||||||
|
}
|
||||||
|
if f.IsDisabled(feature) {
|
||||||
|
disabledFeatures = append(disabledFeatures, feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pathInfo := ""
|
||||||
|
if attrPath != nil {
|
||||||
|
pathInfo = fmt.Sprintf("%s is not supported. ", attrPath.String())
|
||||||
|
}
|
||||||
|
if len(unavailableFeatures) > 0 {
|
||||||
|
diags.AddError("Controller features not available", fmt.Sprintf("%sFeatures %s must be available on controller, but %s are not", pathInfo, strings.Join(features, ", "), strings.Join(unavailableFeatures, ", ")))
|
||||||
|
}
|
||||||
|
if len(disabledFeatures) > 0 {
|
||||||
|
diags.AddError("Controller features not disabled", fmt.Sprintf("%sFeatures %s must be enabled on controller, but %s are disabled", pathInfo, strings.Join(features, ", "), strings.Join(disabledFeatures, ", ")))
|
||||||
|
}
|
||||||
|
return diags
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *featureEnabledValidator) RequireFeaturesEnabled(ctx context.Context, site string, features ...string) diag.Diagnostics {
|
||||||
|
return v.requireFeatures(ctx, site, nil, features...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *featureEnabledValidator) RequireFeaturesEnabledForPath(ctx context.Context, site string, attrPath path.Path, config tfsdk.Config, features ...string) diag.Diagnostics {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
var val attr.Value
|
||||||
|
diags.Append(config.GetAttribute(context.Background(), attrPath, &val)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
if !IsDefined(val) {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
diags.Append(v.requireFeatures(ctx, site, &attrPath, features...)...)
|
||||||
|
return diags
|
||||||
|
}
|
||||||
670
internal/provider/base/features_test.go
Normal file
670
internal/provider/base/features_test.go
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/filipowm/go-unifi/unifi"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockUnifiClient provides a minimal implementation of unifi.Client for testing
|
||||||
|
type MockUnifiClient struct {
|
||||||
|
unifi.Client // Embed the interface to satisfy all methods (they'll panic if called)
|
||||||
|
featuresFunc func(ctx context.Context, site string) ([]unifi.DescribedFeature, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFeatures implements the only unifi.Client method we care about for testing
|
||||||
|
func (m *MockUnifiClient) ListFeatures(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
if m.featuresFunc != nil {
|
||||||
|
return m.featuresFunc(ctx, site)
|
||||||
|
}
|
||||||
|
return nil, errors.New("ListFeatures not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFeaturesIsEnabled tests the IsEnabled method of Features
|
||||||
|
func TestFeaturesIsEnabled(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
features Features
|
||||||
|
feature string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "feature is enabled",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureDisabled},
|
||||||
|
feature: "feature1",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature is disabled",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureDisabled},
|
||||||
|
feature: "feature2",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature does not exist",
|
||||||
|
features: Features{"feature1": featureEnabled},
|
||||||
|
feature: "feature2",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty features map",
|
||||||
|
features: Features{},
|
||||||
|
feature: "feature1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.features.IsEnabled(tt.feature)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFeaturesIsDisabled tests the IsDisabled method of Features
|
||||||
|
func TestFeaturesIsDisabled(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
features Features
|
||||||
|
feature string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "feature is enabled",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureDisabled},
|
||||||
|
feature: "feature1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature is disabled",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureDisabled},
|
||||||
|
feature: "feature2",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature does not exist",
|
||||||
|
features: Features{"feature1": featureEnabled},
|
||||||
|
feature: "feature2",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty features map",
|
||||||
|
features: Features{},
|
||||||
|
feature: "feature1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.features.IsDisabled(tt.feature)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFeaturesIsUnavailable tests the IsUnavailable method of Features
|
||||||
|
func TestFeaturesIsUnavailable(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
features Features
|
||||||
|
feature string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "feature is enabled",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureDisabled},
|
||||||
|
feature: "feature2",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature is disabled",
|
||||||
|
features: Features{"feature1": featureEnabled},
|
||||||
|
feature: "feature2",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature does not exist",
|
||||||
|
features: Features{},
|
||||||
|
feature: "feature2",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.features.IsUnavailable(tt.feature)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewFeatureValidator tests the NewFeatureValidator function
|
||||||
|
func TestNewFeatureValidator(t *testing.T) {
|
||||||
|
mockUnifiClient := &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return []unifi.DescribedFeature{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
Client: mockUnifiClient,
|
||||||
|
Site: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := NewFeatureValidator(client)
|
||||||
|
|
||||||
|
assert.NotNil(t, validator, "Validator should not be nil")
|
||||||
|
featureValidator, ok := validator.(*featureEnabledValidator)
|
||||||
|
assert.True(t, ok, "Validator should be of type *featureEnabledValidator")
|
||||||
|
assert.Equal(t, client, featureValidator.client, "Client should be set correctly")
|
||||||
|
assert.NotNil(t, featureValidator.cache, "Cache should be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetFeatures tests the getFeatures method of featureEnabledValidator
|
||||||
|
func TestGetFeatures(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() *MockUnifiClient
|
||||||
|
site string
|
||||||
|
expected Features
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successfully get features",
|
||||||
|
setup: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return []unifi.DescribedFeature{
|
||||||
|
{Name: "feature1", FeatureExists: true},
|
||||||
|
{Name: "feature2", FeatureExists: false},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
site: "site1",
|
||||||
|
expected: Features{
|
||||||
|
"feature1": featureEnabled,
|
||||||
|
"feature2": featureDisabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error getting features",
|
||||||
|
setup: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return nil, errors.New("error listing features")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
site: "site2",
|
||||||
|
expected: Features{}, // Now returns empty Features instead of nil
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no features returned",
|
||||||
|
setup: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return []unifi.DescribedFeature{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
site: "site3",
|
||||||
|
expected: Features{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockUnifiClient := tt.setup()
|
||||||
|
client := &Client{
|
||||||
|
Client: mockUnifiClient,
|
||||||
|
Site: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := &featureEnabledValidator{
|
||||||
|
client: client,
|
||||||
|
cache: make(map[string]Features),
|
||||||
|
lock: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := validator.getFeatures(context.Background(), tt.site)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
|
||||||
|
// Test caching - if we call again, we should get the cached result
|
||||||
|
if tt.expected != nil {
|
||||||
|
// Replace the test client with one that fails
|
||||||
|
client.Client = &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return nil, errors.New("should not be called")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result = validator.getFeatures(context.Background(), tt.site)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetFeaturesConcurrent tests the concurrency safety of getFeatures
|
||||||
|
func TestGetFeaturesConcurrent(t *testing.T) {
|
||||||
|
callCount := 0
|
||||||
|
mockUnifiClient := &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
callCount++
|
||||||
|
return []unifi.DescribedFeature{
|
||||||
|
{Name: "feature1", FeatureExists: true},
|
||||||
|
{Name: "feature2", FeatureExists: false},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
Client: mockUnifiClient,
|
||||||
|
Site: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := &featureEnabledValidator{
|
||||||
|
client: client,
|
||||||
|
cache: make(map[string]Features),
|
||||||
|
lock: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
// Launch 10 concurrent goroutines to call getFeatures
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
features := validator.getFeatures(context.Background(), "site1")
|
||||||
|
assert.NotNil(t, features)
|
||||||
|
assert.True(t, features.IsEnabled("feature1"))
|
||||||
|
assert.False(t, features.IsEnabled("feature2"))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Verify ListFeatures was called exactly once
|
||||||
|
assert.Equal(t, 1, callCount, "ListFeatures should be called exactly once")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireFeatures tests the requireFeatures method of featureEnabledValidator
|
||||||
|
func TestRequireFeatures(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
features Features
|
||||||
|
site string
|
||||||
|
attrPath *path.Path
|
||||||
|
requiredFeatures []string
|
||||||
|
expectedHasErrors bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all features enabled",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureEnabled},
|
||||||
|
site: "site1",
|
||||||
|
attrPath: nil,
|
||||||
|
requiredFeatures: []string{"feature1", "feature2"},
|
||||||
|
expectedHasErrors: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one feature disabled",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureDisabled},
|
||||||
|
site: "site1",
|
||||||
|
attrPath: nil,
|
||||||
|
requiredFeatures: []string{"feature1", "feature2"},
|
||||||
|
expectedHasErrors: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all features disabled",
|
||||||
|
features: Features{"feature1": featureDisabled, "feature2": featureDisabled},
|
||||||
|
site: "site1",
|
||||||
|
attrPath: nil,
|
||||||
|
requiredFeatures: []string{"feature1", "feature2"},
|
||||||
|
expectedHasErrors: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty required features",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureEnabled},
|
||||||
|
site: "site1",
|
||||||
|
attrPath: nil,
|
||||||
|
requiredFeatures: []string{},
|
||||||
|
expectedHasErrors: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil required features",
|
||||||
|
features: Features{"feature1": featureEnabled, "feature2": featureEnabled},
|
||||||
|
site: "site1",
|
||||||
|
attrPath: nil,
|
||||||
|
requiredFeatures: nil,
|
||||||
|
expectedHasErrors: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with attribute path",
|
||||||
|
features: Features{"feature1": featureDisabled},
|
||||||
|
site: "site1",
|
||||||
|
attrPath: &path.Path{},
|
||||||
|
requiredFeatures: []string{"feature1"},
|
||||||
|
expectedHasErrors: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature not in map",
|
||||||
|
features: Features{"feature1": featureEnabled},
|
||||||
|
site: "site1",
|
||||||
|
attrPath: nil,
|
||||||
|
requiredFeatures: []string{"feature2"},
|
||||||
|
expectedHasErrors: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockUnifiClient := &MockUnifiClient{}
|
||||||
|
client := &Client{
|
||||||
|
Client: mockUnifiClient,
|
||||||
|
Site: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := &featureEnabledValidator{
|
||||||
|
client: client,
|
||||||
|
cache: map[string]Features{tt.site: tt.features},
|
||||||
|
lock: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
diags := validator.requireFeatures(context.Background(), tt.site, tt.attrPath, tt.requiredFeatures...)
|
||||||
|
assert.Equal(t, tt.expectedHasErrors, diags.HasError())
|
||||||
|
|
||||||
|
if tt.expectedHasErrors {
|
||||||
|
// Verify error message contains appropriate information
|
||||||
|
assert.Contains(t, diags[0].Detail(), "Features", "Error detail should mention 'Features'")
|
||||||
|
|
||||||
|
if tt.attrPath != nil {
|
||||||
|
assert.Contains(t, diags[0].Detail(), "is not supported", "Error should mention path is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireFeaturesEnabledForPath tests the RequireFeaturesEnabledForPath method
|
||||||
|
func TestRequireFeaturesEnabledForPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupClient func() *MockUnifiClient
|
||||||
|
attrValue attr.Value
|
||||||
|
attrPath path.Path
|
||||||
|
requiredFeatures []string
|
||||||
|
configError bool
|
||||||
|
expectedHasErrors bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "attribute not set",
|
||||||
|
setupClient: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{}
|
||||||
|
},
|
||||||
|
attrValue: types.StringNull(),
|
||||||
|
attrPath: path.Root("test"),
|
||||||
|
requiredFeatures: []string{"feature1"},
|
||||||
|
configError: false,
|
||||||
|
expectedHasErrors: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error getting attribute",
|
||||||
|
setupClient: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{}
|
||||||
|
},
|
||||||
|
attrValue: types.StringNull(),
|
||||||
|
attrPath: path.Root("test"),
|
||||||
|
requiredFeatures: []string{"feature1"},
|
||||||
|
configError: true,
|
||||||
|
expectedHasErrors: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute set, feature enabled",
|
||||||
|
setupClient: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return []unifi.DescribedFeature{
|
||||||
|
{Name: "feature1", FeatureExists: true},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attrValue: types.StringValue("test"),
|
||||||
|
attrPath: path.Root("test"),
|
||||||
|
requiredFeatures: []string{"feature1"},
|
||||||
|
configError: false,
|
||||||
|
expectedHasErrors: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute set, feature disabled",
|
||||||
|
setupClient: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return []unifi.DescribedFeature{
|
||||||
|
{Name: "feature1", FeatureExists: false},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attrValue: types.StringValue("test"),
|
||||||
|
attrPath: path.Root("test"),
|
||||||
|
requiredFeatures: []string{"feature1"},
|
||||||
|
configError: false,
|
||||||
|
expectedHasErrors: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockUnifiClient := tt.setupClient()
|
||||||
|
client := &Client{
|
||||||
|
Client: mockUnifiClient,
|
||||||
|
Site: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a wrapper FeatureValidator that provides a minimal implementation
|
||||||
|
// of RequireFeaturesEnabledForPath without needing a real tfsdk.Config
|
||||||
|
validator := &testFeatureValidator{
|
||||||
|
base: NewFeatureValidator(client),
|
||||||
|
attrValue: tt.attrValue,
|
||||||
|
configErr: tt.configError,
|
||||||
|
}
|
||||||
|
|
||||||
|
diags := validator.TestRequireFeaturesEnabledForPath(context.Background(), "site1", tt.attrPath, tt.requiredFeatures...)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedHasErrors, diags.HasError())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireFeaturesEnabled tests the RequireFeaturesEnabled method
|
||||||
|
func TestRequireFeaturesEnabled(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupClient func() *MockUnifiClient
|
||||||
|
requiredFeatures []string
|
||||||
|
expectedHasErrors bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "feature enabled",
|
||||||
|
setupClient: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return []unifi.DescribedFeature{
|
||||||
|
{Name: "feature1", FeatureExists: true},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requiredFeatures: []string{"feature1"},
|
||||||
|
expectedHasErrors: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature disabled",
|
||||||
|
setupClient: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return []unifi.DescribedFeature{
|
||||||
|
{Name: "feature1", FeatureExists: false},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requiredFeatures: []string{"feature1"},
|
||||||
|
expectedHasErrors: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature error",
|
||||||
|
setupClient: func() *MockUnifiClient {
|
||||||
|
return &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
return nil, errors.New("error listing features")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requiredFeatures: []string{"feature1"},
|
||||||
|
expectedHasErrors: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockUnifiClient := tt.setupClient()
|
||||||
|
client := &Client{
|
||||||
|
Client: mockUnifiClient,
|
||||||
|
Site: "default",
|
||||||
|
}
|
||||||
|
validator := NewFeatureValidator(client)
|
||||||
|
diags := validator.RequireFeaturesEnabled(context.Background(), "site1", tt.requiredFeatures...)
|
||||||
|
assert.Equal(t, tt.expectedHasErrors, diags.HasError())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsDefined is used in RequireFeaturesEnabledForPath to check if a value is defined
|
||||||
|
func TestIsDefined(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value attr.Value
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "null",
|
||||||
|
value: types.StringNull(),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown",
|
||||||
|
value: types.StringUnknown(),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "null list",
|
||||||
|
value: types.ListNull(types.StringType),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty list",
|
||||||
|
value: types.ListValueMust(types.StringType, []attr.Value{}),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "defined value",
|
||||||
|
value: types.StringValue("test"),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.expected, IsDefined(tt.value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFeatureValidatorCache specifically tests the caching behavior of the FeatureValidator
|
||||||
|
// It verifies that multiple calls with the same site only result in one API call
|
||||||
|
func TestFeatureValidatorCache(t *testing.T) {
|
||||||
|
// Create a mock client with a counter for API calls
|
||||||
|
callCount := 0
|
||||||
|
mockUnifiClient := &MockUnifiClient{
|
||||||
|
featuresFunc: func(ctx context.Context, site string) ([]unifi.DescribedFeature, error) {
|
||||||
|
callCount++
|
||||||
|
return []unifi.DescribedFeature{
|
||||||
|
{Name: "feature1", FeatureExists: true},
|
||||||
|
{Name: "feature2", FeatureExists: false},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
Client: mockUnifiClient,
|
||||||
|
Site: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := NewFeatureValidator(client)
|
||||||
|
|
||||||
|
// First call to check features should trigger an API call
|
||||||
|
diags1 := validator.RequireFeaturesEnabled(context.Background(), "site1", "feature1")
|
||||||
|
assert.Equal(t, 1, callCount, "First call should trigger an API call")
|
||||||
|
assert.False(t, diags1.HasError(), "Feature1 should be enabled")
|
||||||
|
|
||||||
|
// Second call with the same site should use the cache
|
||||||
|
diags2 := validator.RequireFeaturesEnabled(context.Background(), "site1", "feature2")
|
||||||
|
assert.Equal(t, 1, callCount, "Second call should use cached data")
|
||||||
|
assert.True(t, diags2.HasError(), "Feature2 should be disabled")
|
||||||
|
|
||||||
|
// Call with a different site should trigger another API call
|
||||||
|
diags3 := validator.RequireFeaturesEnabled(context.Background(), "site2", "feature1")
|
||||||
|
assert.Equal(t, 2, callCount, "Call with different site should trigger an API call")
|
||||||
|
assert.False(t, diags3.HasError(), "Feature1 should be enabled")
|
||||||
|
|
||||||
|
// Multiple calls using the same site should still use the cache
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
validator.RequireFeaturesEnabled(context.Background(), "site1", "feature1")
|
||||||
|
}
|
||||||
|
assert.Equal(t, 2, callCount, "Multiple calls with same site should use cached data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// testFeatureValidator wraps a real FeatureValidator but has a special method for testing
|
||||||
|
// that doesn't require a real tfsdk.Config
|
||||||
|
type testFeatureValidator struct {
|
||||||
|
base FeatureValidator
|
||||||
|
attrValue attr.Value
|
||||||
|
configErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireFeaturesEnabledForPath is a test-specific version that doesn't need a real tfsdk.Config
|
||||||
|
func (v *testFeatureValidator) TestRequireFeaturesEnabledForPath(ctx context.Context, site string,
|
||||||
|
attrPath path.Path, features ...string) diag.Diagnostics {
|
||||||
|
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
|
// This simulates what happens in RequireFeaturesEnabledForPath without needing a real Config
|
||||||
|
if v.configErr {
|
||||||
|
diags.AddError("Error", "Error getting attribute")
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsDefined(v.attrValue) {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the underlying validator's RequireFeaturesEnabled
|
||||||
|
fv, ok := v.base.(*featureEnabledValidator)
|
||||||
|
if !ok {
|
||||||
|
diags.AddError("Error", "Invalid validator type")
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
diags.Append(fv.requireFeatures(ctx, site, &attrPath, features...)...)
|
||||||
|
return diags
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ type ResourceFunctions struct {
|
|||||||
// GenericResource provides common functionality for all resources
|
// GenericResource provides common functionality for all resources
|
||||||
type GenericResource[T ResourceModel] struct {
|
type GenericResource[T ResourceModel] struct {
|
||||||
ControllerVersionValidator
|
ControllerVersionValidator
|
||||||
|
FeatureValidator
|
||||||
client *Client
|
client *Client
|
||||||
typeName string
|
typeName string
|
||||||
modelFactory func() T
|
modelFactory func() T
|
||||||
@@ -52,6 +53,10 @@ func (b *GenericResource[T]) SetVersionValidator(validator ControllerVersionVali
|
|||||||
b.ControllerVersionValidator = validator
|
b.ControllerVersionValidator = validator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *GenericResource[T]) SetFeatureValidator(validator FeatureValidator) {
|
||||||
|
b.FeatureValidator = validator
|
||||||
|
}
|
||||||
|
|
||||||
func (b *GenericResource[T]) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
func (b *GenericResource[T]) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
ConfigureResource(b, req, resp)
|
ConfigureResource(b, req, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,14 @@ var (
|
|||||||
|
|
||||||
type dnsRecordDatasource struct {
|
type dnsRecordDatasource struct {
|
||||||
base.ControllerVersionValidator
|
base.ControllerVersionValidator
|
||||||
|
base.FeatureValidator
|
||||||
client *base.Client
|
client *base.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *dnsRecordDatasource) SetFeatureValidator(validator base.FeatureValidator) {
|
||||||
|
d.FeatureValidator = validator
|
||||||
|
}
|
||||||
|
|
||||||
func NewDnsRecordDatasource() datasource.DataSource {
|
func NewDnsRecordDatasource() datasource.DataSource {
|
||||||
return &dnsRecordDatasource{}
|
return &dnsRecordDatasource{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ var (
|
|||||||
|
|
||||||
type dnsRecordsDatasource struct {
|
type dnsRecordsDatasource struct {
|
||||||
base.ControllerVersionValidator
|
base.ControllerVersionValidator
|
||||||
|
base.FeatureValidator
|
||||||
client *base.Client
|
client *base.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +33,10 @@ func (d *dnsRecordsDatasource) SetVersionValidator(validator base.ControllerVers
|
|||||||
d.ControllerVersionValidator = validator
|
d.ControllerVersionValidator = validator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *dnsRecordsDatasource) SetFeatureValidator(validator base.FeatureValidator) {
|
||||||
|
d.FeatureValidator = validator
|
||||||
|
}
|
||||||
|
|
||||||
func (d *dnsRecordsDatasource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
func (d *dnsRecordsDatasource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||||
base.ConfigureDatasource(d, req, resp)
|
base.ConfigureDatasource(d, req, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,10 +174,12 @@ func (p *unifiProvider) Configure(ctx context.Context, req provider.ConfigureReq
|
|||||||
func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource {
|
func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource {
|
||||||
return []func() resource.Resource{
|
return []func() resource.Resource{
|
||||||
dns.NewDnsRecordResource,
|
dns.NewDnsRecordResource,
|
||||||
|
//firewall.NewFirewallZoneResource,
|
||||||
|
//firewall.NewFirewallZonePolicyResource,
|
||||||
settings.NewAutoSpeedtestResource,
|
settings.NewAutoSpeedtestResource,
|
||||||
settings.NewCountryResource,
|
settings.NewCountryResource,
|
||||||
settings.NewDpiResource,
|
settings.NewDpiResource,
|
||||||
//settings.NewIpsResource,
|
settings.NewIpsResource,
|
||||||
settings.NewLcmResource,
|
settings.NewLcmResource,
|
||||||
settings.NewLocaleResource,
|
settings.NewLocaleResource,
|
||||||
settings.NewMagicSiteToSiteVpnResource,
|
settings.NewMagicSiteToSiteVpnResource,
|
||||||
|
|||||||
865
internal/provider/settings/resource_setting_ips.go
Normal file
865
internal/provider/settings/resource_setting_ips.go
Normal file
@@ -0,0 +1,865 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/filipowm/go-unifi/unifi"
|
||||||
|
"github.com/filipowm/go-unifi/unifi/features"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/utils"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS Filter model
|
||||||
|
type DNSFilterModel struct {
|
||||||
|
AllowedSites types.List `tfsdk:"allowed_sites"`
|
||||||
|
BlockedSites types.List `tfsdk:"blocked_sites"`
|
||||||
|
BlockedTld types.List `tfsdk:"blocked_tld"`
|
||||||
|
Description types.String `tfsdk:"description"`
|
||||||
|
Filter types.String `tfsdk:"filter"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
NetworkID types.String `tfsdk:"network_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DNSFilterModel) AttributeTypes() map[string]attr.Type {
|
||||||
|
return map[string]attr.Type{
|
||||||
|
"allowed_sites": types.ListType{
|
||||||
|
ElemType: types.StringType,
|
||||||
|
},
|
||||||
|
"blocked_sites": types.ListType{
|
||||||
|
ElemType: types.StringType,
|
||||||
|
},
|
||||||
|
"blocked_tld": types.ListType{
|
||||||
|
ElemType: types.StringType,
|
||||||
|
},
|
||||||
|
"description": types.StringType,
|
||||||
|
"filter": types.StringType,
|
||||||
|
"name": types.StringType,
|
||||||
|
"network_id": types.StringType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Honeypots model
|
||||||
|
type HoneypotModel struct {
|
||||||
|
IPAddress types.String `tfsdk:"ip_address"`
|
||||||
|
NetworkID types.String `tfsdk:"network_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *HoneypotModel) AttributeTypes() map[string]attr.Type {
|
||||||
|
return map[string]attr.Type{
|
||||||
|
"ip_address": types.StringType,
|
||||||
|
"network_id": types.StringType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracking model
|
||||||
|
type TrackingModel struct {
|
||||||
|
Direction types.String `tfsdk:"direction"`
|
||||||
|
Mode types.String `tfsdk:"mode"`
|
||||||
|
Value types.String `tfsdk:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TrackingModel) AttributeTypes() map[string]attr.Type {
|
||||||
|
return map[string]attr.Type{
|
||||||
|
"direction": types.StringType,
|
||||||
|
"mode": types.StringType,
|
||||||
|
"value": types.StringType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alerts model
|
||||||
|
type AlertsModel struct {
|
||||||
|
Category types.String `tfsdk:"category"`
|
||||||
|
Signature types.String `tfsdk:"signature"`
|
||||||
|
Tracking types.List `tfsdk:"tracking"`
|
||||||
|
Type types.String `tfsdk:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AlertsModel) AttributeTypes() map[string]attr.Type {
|
||||||
|
return map[string]attr.Type{
|
||||||
|
"category": types.StringType,
|
||||||
|
"signature": types.StringType,
|
||||||
|
"tracking": types.ListType{
|
||||||
|
ElemType: types.ObjectType{
|
||||||
|
AttrTypes: (&TrackingModel{}).AttributeTypes(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": types.StringType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitelist model
|
||||||
|
type WhitelistModel struct {
|
||||||
|
Direction types.String `tfsdk:"direction"`
|
||||||
|
Mode types.String `tfsdk:"mode"`
|
||||||
|
Value types.String `tfsdk:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WhitelistModel) AttributeTypes() map[string]attr.Type {
|
||||||
|
return map[string]attr.Type{
|
||||||
|
"direction": types.StringType,
|
||||||
|
"mode": types.StringType,
|
||||||
|
"value": types.StringType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppression model
|
||||||
|
type SuppressionModel struct {
|
||||||
|
Alerts types.List `tfsdk:"alerts"`
|
||||||
|
Whitelist types.List `tfsdk:"whitelist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SuppressionModel) AttributeTypes() map[string]attr.Type {
|
||||||
|
return map[string]attr.Type{
|
||||||
|
"alerts": types.ListType{
|
||||||
|
ElemType: types.ObjectType{
|
||||||
|
AttrTypes: (&AlertsModel{}).AttributeTypes(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"whitelist": types.ListType{
|
||||||
|
ElemType: types.ObjectType{
|
||||||
|
AttrTypes: (&WhitelistModel{}).AttributeTypes(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main IPS model
|
||||||
|
type ipsModel struct {
|
||||||
|
base.Model
|
||||||
|
AdBlockedNetworks types.List `tfsdk:"ad_blocked_networks"`
|
||||||
|
AdvancedFilteringPreference types.String `tfsdk:"advanced_filtering_preference"`
|
||||||
|
DNSFilters types.List `tfsdk:"dns_filters"`
|
||||||
|
EnabledCategories types.List `tfsdk:"enabled_categories"`
|
||||||
|
EnabledNetworks types.List `tfsdk:"enabled_networks"`
|
||||||
|
Honeypots types.List `tfsdk:"honeypots"`
|
||||||
|
Mode types.String `tfsdk:"ips_mode"`
|
||||||
|
MemoryOptimized types.Bool `tfsdk:"memory_optimized"`
|
||||||
|
RestrictTorrents types.Bool `tfsdk:"restrict_torrents"`
|
||||||
|
Suppression types.Object `tfsdk:"suppression"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ipsModel) AsUnifiModel(ctx context.Context) (interface{}, diag.Diagnostics) {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
|
model := &unifi.SettingIps{
|
||||||
|
AdvancedFilteringPreference: d.AdvancedFilteringPreference.ValueString(),
|
||||||
|
IPsMode: d.Mode.ValueString(),
|
||||||
|
MemoryOptimized: d.MemoryOptimized.ValueBool(),
|
||||||
|
RestrictTorrents: d.RestrictTorrents.ValueBool(),
|
||||||
|
// Initialize empty slices for arrays to avoid null values in JSON
|
||||||
|
AdBlockingConfigurations: []unifi.SettingIpsAdBlockingConfigurations{},
|
||||||
|
DNSFilters: []unifi.SettingIpsDNSFilters{},
|
||||||
|
EnabledCategories: []string{},
|
||||||
|
EnabledNetworks: []string{},
|
||||||
|
Honeypot: []unifi.SettingIpsHoneypot{},
|
||||||
|
// Initialize suppression with empty arrays
|
||||||
|
Suppression: unifi.SettingIpsSuppression{
|
||||||
|
Alerts: []unifi.SettingIpsAlerts{},
|
||||||
|
Whitelist: []unifi.SettingIpsWhitelist{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.AdvancedFilteringPreference == "" {
|
||||||
|
if model.IPsMode != "disabled" {
|
||||||
|
model.AdvancedFilteringPreference = "manual"
|
||||||
|
} else {
|
||||||
|
model.AdvancedFilteringPreference = "disabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var enabledCategories []string
|
||||||
|
diags.Append(utils.ListElementsAs(d.EnabledCategories, &enabledCategories)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
model.EnabledCategories = enabledCategories
|
||||||
|
|
||||||
|
var enabledNetworks []string
|
||||||
|
diags.Append(utils.ListElementsAs(d.EnabledNetworks, &enabledNetworks)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
model.EnabledNetworks = enabledNetworks
|
||||||
|
|
||||||
|
// Handle AdBlockedNetworks - if any networks are configured, set AdBlockingEnabled to true
|
||||||
|
if base.IsDefined(d.AdBlockedNetworks) {
|
||||||
|
var adBlockedNetworks []string
|
||||||
|
diags.Append(utils.ListElementsAs(d.AdBlockedNetworks, &adBlockedNetworks)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(adBlockedNetworks) > 0 {
|
||||||
|
model.AdBlockingEnabled = true
|
||||||
|
model.AdBlockingConfigurations = make([]unifi.SettingIpsAdBlockingConfigurations, 0, len(adBlockedNetworks))
|
||||||
|
for _, networkID := range adBlockedNetworks {
|
||||||
|
model.AdBlockingConfigurations = append(model.AdBlockingConfigurations, unifi.SettingIpsAdBlockingConfigurations{
|
||||||
|
NetworkID: networkID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model.AdBlockingEnabled = false
|
||||||
|
model.AdBlockingConfigurations = []unifi.SettingIpsAdBlockingConfigurations{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle DNSFilters - if any filters are configured, set DNSFiltering to true
|
||||||
|
if base.IsDefined(d.DNSFilters) {
|
||||||
|
var dnsFiltersObjects []DNSFilterModel
|
||||||
|
diags.Append(utils.ListElementsAs(d.DNSFilters, &dnsFiltersObjects)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dnsFiltersObjects) > 0 {
|
||||||
|
model.DNSFiltering = true
|
||||||
|
model.DNSFilters = make([]unifi.SettingIpsDNSFilters, 0, len(dnsFiltersObjects))
|
||||||
|
|
||||||
|
for _, filterObj := range dnsFiltersObjects {
|
||||||
|
version := "v4"
|
||||||
|
if utils.IsIPv6(filterObj.NetworkID.ValueString()) {
|
||||||
|
version = "v6"
|
||||||
|
}
|
||||||
|
filter := unifi.SettingIpsDNSFilters{
|
||||||
|
Description: filterObj.Description.ValueString(),
|
||||||
|
Filter: filterObj.Filter.ValueString(),
|
||||||
|
Name: filterObj.Name.ValueString(),
|
||||||
|
NetworkID: filterObj.NetworkID.ValueString(),
|
||||||
|
Version: version,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle allowed sites
|
||||||
|
|
||||||
|
var allowedSites, blockedSites, blockedTlds []string
|
||||||
|
diags.Append(utils.ListElementsAs(filterObj.AllowedSites, &allowedSites)...)
|
||||||
|
diags.Append(utils.ListElementsAs(filterObj.BlockedSites, &blockedSites)...)
|
||||||
|
diags.Append(utils.ListElementsAs(filterObj.BlockedTld, &blockedTlds)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
filter.AllowedSites = allowedSites
|
||||||
|
filter.BlockedSites = blockedSites
|
||||||
|
filter.BlockedTld = blockedTlds
|
||||||
|
model.DNSFilters = append(model.DNSFilters, filter)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model.DNSFiltering = false
|
||||||
|
model.DNSFilters = []unifi.SettingIpsDNSFilters{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle honeypot
|
||||||
|
if base.IsDefined(d.Honeypots) {
|
||||||
|
var honeypotObjects []HoneypotModel
|
||||||
|
diags.Append(utils.ListElementsAs(d.Honeypots, &honeypotObjects)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
model.Honeypot = make([]unifi.SettingIpsHoneypot, 0)
|
||||||
|
for _, honeypotObj := range honeypotObjects {
|
||||||
|
version := "v4"
|
||||||
|
if utils.IsIPv6(honeypotObj.IPAddress.ValueString()) {
|
||||||
|
version = "v6"
|
||||||
|
}
|
||||||
|
model.Honeypot = append(model.Honeypot, unifi.SettingIpsHoneypot{
|
||||||
|
IPAddress: honeypotObj.IPAddress.ValueString(),
|
||||||
|
NetworkID: honeypotObj.NetworkID.ValueString(),
|
||||||
|
Version: version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(model.Honeypot) > 0 {
|
||||||
|
model.HoneypotEnabled = true
|
||||||
|
} else {
|
||||||
|
model.HoneypotEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle suppression
|
||||||
|
if base.IsDefined(d.Suppression) {
|
||||||
|
var suppressionObj SuppressionModel
|
||||||
|
diags.Append(d.Suppression.As(ctx, &suppressionObj, basetypes.ObjectAsOptions{})...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
var alerts []AlertsModel
|
||||||
|
diags.Append(utils.ListElementsAs(suppressionObj.Alerts, &alerts)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
model.Suppression.Alerts = make([]unifi.SettingIpsAlerts, 0)
|
||||||
|
for idx, alertObj := range alerts {
|
||||||
|
alert := unifi.SettingIpsAlerts{
|
||||||
|
Category: alertObj.Category.ValueString(),
|
||||||
|
Signature: alertObj.Signature.ValueString(),
|
||||||
|
Type: alertObj.Type.ValueString(),
|
||||||
|
ID: 100 + idx,
|
||||||
|
Gid: 200 + idx,
|
||||||
|
}
|
||||||
|
// Handle tracking
|
||||||
|
|
||||||
|
var trackings []TrackingModel
|
||||||
|
diags.Append(utils.ListElementsAs(alertObj.Tracking, &trackings)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
alert.Tracking = make([]unifi.SettingIpsTracking, 0)
|
||||||
|
for _, trackingObj := range trackings {
|
||||||
|
if base.IsEmptyString(trackingObj.Direction) || base.IsEmptyString(trackingObj.Mode) || base.IsEmptyString(trackingObj.Value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
alert.Tracking = append(alert.Tracking, unifi.SettingIpsTracking{
|
||||||
|
Direction: trackingObj.Direction.ValueString(),
|
||||||
|
Mode: trackingObj.Mode.ValueString(),
|
||||||
|
Value: trackingObj.Value.ValueString()})
|
||||||
|
}
|
||||||
|
model.Suppression.Alerts = append(model.Suppression.Alerts, alert)
|
||||||
|
}
|
||||||
|
|
||||||
|
var whitelists []WhitelistModel
|
||||||
|
diags.Append(utils.ListElementsAs(suppressionObj.Whitelist, &whitelists)...)
|
||||||
|
if diags.HasError() {
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
model.Suppression.Whitelist = make([]unifi.SettingIpsWhitelist, 0, len(whitelists))
|
||||||
|
for _, whitelistObj := range whitelists {
|
||||||
|
model.Suppression.Whitelist = append(model.Suppression.Whitelist, unifi.SettingIpsWhitelist{
|
||||||
|
Direction: whitelistObj.Direction.ValueString(),
|
||||||
|
Mode: whitelistObj.Mode.ValueString(),
|
||||||
|
Value: whitelistObj.Value.ValueString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ipsModel) Merge(ctx context.Context, other interface{}) diag.Diagnostics {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
|
model, ok := other.(*unifi.SettingIps)
|
||||||
|
if !ok {
|
||||||
|
diags.AddError("Invalid model type", "Expected *unifi.SettingIps")
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
d.ID = types.StringValue(model.ID)
|
||||||
|
|
||||||
|
// Only set values for fields that were explicitly set in the configuration
|
||||||
|
// or returned by the API with non-default values
|
||||||
|
|
||||||
|
// Set basic fields if they were defined in the plan
|
||||||
|
d.AdvancedFilteringPreference = types.StringValue(model.AdvancedFilteringPreference)
|
||||||
|
|
||||||
|
d.Mode = types.StringValue(model.IPsMode)
|
||||||
|
|
||||||
|
d.MemoryOptimized = types.BoolValue(model.MemoryOptimized)
|
||||||
|
d.RestrictTorrents = types.BoolValue(model.RestrictTorrents)
|
||||||
|
|
||||||
|
// Handle enabled categories
|
||||||
|
enabledCategoriesList, diags := types.ListValueFrom(ctx, types.StringType, model.EnabledCategories)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
if base.IsDefined(enabledCategoriesList) {
|
||||||
|
d.EnabledCategories = enabledCategoriesList
|
||||||
|
} else {
|
||||||
|
d.EnabledCategories = utils.EmptyList(types.StringType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle enabled networks
|
||||||
|
enabledNetworksList, diags := types.ListValueFrom(ctx, types.StringType, model.EnabledNetworks)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
if base.IsDefined(enabledNetworksList) {
|
||||||
|
d.EnabledNetworks = enabledNetworksList
|
||||||
|
} else {
|
||||||
|
d.EnabledNetworks = utils.EmptyList(types.StringType)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Handle AdBlockedNetworks - extract network IDs from AdBlockingConfigurations
|
||||||
|
adBlockedNetworks := make([]string, 0, len(model.AdBlockingConfigurations))
|
||||||
|
for _, config := range model.AdBlockingConfigurations {
|
||||||
|
adBlockedNetworks = append(adBlockedNetworks, config.NetworkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
adBlockedNetworksList, diags := types.ListValueFrom(ctx, types.StringType, adBlockedNetworks)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
d.AdBlockedNetworks = adBlockedNetworksList
|
||||||
|
|
||||||
|
// Handle DNSFilters
|
||||||
|
dnsFilters := make([]DNSFilterModel, 0)
|
||||||
|
|
||||||
|
for _, filter := range model.DNSFilters {
|
||||||
|
dnsFilter := DNSFilterModel{
|
||||||
|
Description: types.StringValue(filter.Description),
|
||||||
|
Filter: types.StringValue(filter.Filter),
|
||||||
|
Name: types.StringValue(filter.Name),
|
||||||
|
NetworkID: types.StringValue(filter.NetworkID),
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedSites, diags := types.ListValueFrom(ctx, types.StringType, filter.AllowedSites)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
dnsFilter.AllowedSites = allowedSites
|
||||||
|
|
||||||
|
blockedSites, diags := types.ListValueFrom(ctx, types.StringType, filter.BlockedSites)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
dnsFilter.BlockedSites = blockedSites
|
||||||
|
|
||||||
|
blockedTlds, diags := types.ListValueFrom(ctx, types.StringType, filter.BlockedTld)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
dnsFilter.BlockedTld = blockedTlds
|
||||||
|
|
||||||
|
dnsFilters = append(dnsFilters, dnsFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsFiltersList, diags := types.ListValueFrom(ctx, types.ObjectType{
|
||||||
|
AttrTypes: (&DNSFilterModel{}).AttributeTypes(),
|
||||||
|
}, dnsFilters)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
d.DNSFilters = dnsFiltersList
|
||||||
|
|
||||||
|
// Handle honeypot
|
||||||
|
honeypotModels := make([]HoneypotModel, 0, len(model.Honeypot))
|
||||||
|
for _, honeypot := range model.Honeypot {
|
||||||
|
honeypotModels = append(honeypotModels, HoneypotModel{
|
||||||
|
IPAddress: types.StringValue(honeypot.IPAddress),
|
||||||
|
NetworkID: types.StringValue(honeypot.NetworkID),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
honeypotList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&HoneypotModel{}).AttributeTypes()}, honeypotModels)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
d.Honeypots = honeypotList
|
||||||
|
|
||||||
|
// Handle suppression
|
||||||
|
suppression := SuppressionModel{}
|
||||||
|
|
||||||
|
// Handle alerts
|
||||||
|
alertModels := make([]AlertsModel, 0)
|
||||||
|
for _, alert := range model.Suppression.Alerts {
|
||||||
|
// Skip alerts with ID 0, because they may come as default values from the API
|
||||||
|
if alert.ID == 0 && alert.Category == "" && alert.Signature == "" && alert.Type == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
alertModel := AlertsModel{
|
||||||
|
Category: types.StringValue(alert.Category),
|
||||||
|
Signature: types.StringValue(alert.Signature),
|
||||||
|
Type: types.StringValue(alert.Type),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tracking
|
||||||
|
trackingModels := make([]TrackingModel, 0)
|
||||||
|
for _, tracking := range alert.Tracking {
|
||||||
|
trackingModels = append(trackingModels, TrackingModel{
|
||||||
|
Direction: types.StringValue(tracking.Direction),
|
||||||
|
Mode: types.StringValue(tracking.Mode),
|
||||||
|
Value: types.StringValue(tracking.Value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
trackings, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&TrackingModel{}).AttributeTypes()}, trackingModels)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
alertModel.Tracking = trackings
|
||||||
|
alertModels = append(alertModels, alertModel)
|
||||||
|
}
|
||||||
|
alerts, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&AlertsModel{}).AttributeTypes()}, alertModels)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
suppression.Alerts = alerts
|
||||||
|
|
||||||
|
// Handle whitelist
|
||||||
|
whitelistModels := make([]WhitelistModel, 0)
|
||||||
|
for _, whitelist := range model.Suppression.Whitelist {
|
||||||
|
whitelistModels = append(whitelistModels, WhitelistModel{
|
||||||
|
Direction: types.StringValue(whitelist.Direction),
|
||||||
|
Mode: types.StringValue(whitelist.Mode),
|
||||||
|
Value: types.StringValue(whitelist.Value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
whitelist, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&WhitelistModel{}).AttributeTypes()}, whitelistModels)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
suppression.Whitelist = whitelist
|
||||||
|
|
||||||
|
suppressionObj, diags := types.ObjectValueFrom(ctx, (&SuppressionModel{}).AttributeTypes(), suppression)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
d.Suppression = suppressionObj
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipsResource struct {
|
||||||
|
*base.GenericResource[*ipsModel]
|
||||||
|
}
|
||||||
|
|
||||||
|
func requiredTogetherIfString(ctx context.Context, config tfsdk.Config, attr, value, reqAttribute string) diag.Diagnostics {
|
||||||
|
v := validators.RequiredTogetherIf(path.MatchRoot(attr), types.StringValue(value), path.MatchRoot(reqAttribute))
|
||||||
|
return v.Validate(ctx, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ipsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
||||||
|
resp.Diagnostics.Append(r.RequireMinVersion("7.4")...)
|
||||||
|
resp.Diagnostics.Append(r.RequireMinVersionForPath("7.5", path.Root("advanced_filtering_preference"), req.Config)...)
|
||||||
|
resp.Diagnostics.Append(r.RequireMinVersionForPath("8.0", path.Root("enabled_networks"), req.Config)...)
|
||||||
|
resp.Diagnostics.Append(r.RequireMinVersionForPath("9.0", path.Root("memory_optimized"), req.Config)...)
|
||||||
|
site, diags := r.GetClient().ResolveSiteFromConfig(ctx, req.Config)
|
||||||
|
if diags.HasError() {
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(r.RequireFeaturesEnabled(ctx, site, features.Ips)...)
|
||||||
|
|
||||||
|
if r.GetClient().Version.GreaterThan(base.ControllerV8) {
|
||||||
|
diags.Append(requiredTogetherIfString(ctx, req.Config, "ips_mode", "ips", "enabled_networks")...)
|
||||||
|
diags.Append(requiredTogetherIfString(ctx, req.Config, "ips_mode", "ids", "enabled_networks")...)
|
||||||
|
diags.Append(requiredTogetherIfString(ctx, req.Config, "ips_mode", "ipsInline", "enabled_networks")...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ipsResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
|
||||||
|
return []resource.ConfigValidator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ipsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "The `unifi_setting_ips` resource allows you to configure the Intrusion Prevention System (IPS) settings for your UniFi network. IPS provides network threat protection by monitoring, detecting, and preventing malicious traffic based on configured rules and policies. Requires controller version 7.4 or later",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": base.ID(),
|
||||||
|
"site": base.SiteAttribute(),
|
||||||
|
"ad_blocked_networks": schema.ListAttribute{
|
||||||
|
MarkdownDescription: "List of network IDs to enable ad blocking for. If any networks are configured, ad blocking will be automatically enabled. Each entry should be a valid network ID from your UniFi configuration. Leave empty to disable ad blocking.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.List{
|
||||||
|
listplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"advanced_filtering_preference": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The advanced filtering preference for IPS. Valid values are:\n" +
|
||||||
|
" * `disabled` - Advanced filtering is disabled\n" +
|
||||||
|
" * `manual` - Advanced filtering is enabled and manually configured",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("disabled", "manual"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dns_filters": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "DNS filters configuration. If any filters are configured, DNS filtering will be automatically enabled. Each filter can be applied to a specific network and provides content filtering capabilities.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.List{
|
||||||
|
listplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"allowed_sites": schema.ListAttribute{
|
||||||
|
MarkdownDescription: "List of allowed sites for this DNS filter. These domains will always be accessible regardless of other filtering rules. Each entry should be a valid domain name (e.g., `example.com`).",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"blocked_sites": schema.ListAttribute{
|
||||||
|
MarkdownDescription: "List of blocked sites for this DNS filter. These domains will be blocked regardless of other filtering rules. Each entry should be a valid domain name (e.g., `example.com`).",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"blocked_tld": schema.ListAttribute{
|
||||||
|
MarkdownDescription: "List of blocked top-level domains (TLDs) for this DNS filter. All domains with these TLDs will be blocked. Each entry should be a valid TLD without the dot prefix (e.g., `xyz`, `info`).",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"description": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Description of the DNS filter. This is used for documentation purposes only and does not affect functionality.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"filter": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Filter type that determines the predefined filtering level. Valid values are:\n" +
|
||||||
|
" * `none` - No predefined filtering\n" +
|
||||||
|
" * `work` - Work-appropriate filtering that blocks adult content\n" +
|
||||||
|
" * `family` - Family-friendly filtering that blocks adult content and other inappropriate sites",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("none", "work", "family"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Name of the DNS filter. This is used to identify the filter in the UniFi interface.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"network_id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Network ID this filter applies to. This should be a valid network ID from your UniFi configuration.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"enabled_categories": schema.ListAttribute{
|
||||||
|
MarkdownDescription: "List of enabled IPS threat categories. Each entry enables detection and prevention for a specific type of threat. The list of valid categories includes common threats like malware, exploits, scanning, and policy violations. See the validator for the complete list of available categories.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.List{
|
||||||
|
listplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
//Default: utils.DefaultEmptyList(types.StringType),
|
||||||
|
Validators: []validator.List{
|
||||||
|
listvalidator.ValueStringsAre(stringvalidator.OneOf("emerging-activex", "emerging-attackresponse", "botcc", "emerging-chat", "ciarmy", "compromised", "emerging-dns", "emerging-dos", "dshield", "emerging-exploit", "emerging-ftp", "emerging-games", "emerging-icmp", "emerging-icmpinfo", "emerging-imap", "emerging-inappropriate", "emerging-info", "emerging-malware", "emerging-misc", "emerging-mobile", "emerging-netbios", "emerging-p2p", "emerging-policy", "emerging-pop3", "emerging-rpc", "emerging-scada", "emerging-scan", "emerging-shellcode", "emerging-smtp", "emerging-snmp", "emerging-sql", "emerging-telnet", "emerging-tftp", "tor", "emerging-useragent", "emerging-voip", "emerging-webapps", "emerging-webclient", "emerging-webserver", "emerging-worm", "exploit-kit", "adware-pup", "botcc-portgrouped", "phishing", "threatview-cs-c2", "3coresec", "chat", "coinminer", "current-events", "drop", "hunting", "icmp-info", "inappropriate", "info", "ja3", "policy", "scada", "dark-web-blocker-list", "malicious-hosts")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"enabled_networks": schema.ListAttribute{
|
||||||
|
MarkdownDescription: "List of network IDs to enable IPS protection for. Each entry should be a valid network ID from your UniFi configuration. IPS will only monitor and protect traffic on these networks.",
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.List{
|
||||||
|
listplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"honeypots": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "Honeypots configuration. Honeypots are decoy systems designed to detect, deflect, or study hacking attempts. They appear as legitimate parts of the network but are isolated and monitored.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.List{
|
||||||
|
listplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"ip_address": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "IP address for the honeypot. This should be an unused IPv4 address within your network range that will be used as a decoy system.",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.Any(validators.IPv4(), validators.IPv6()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"network_id": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Network ID for the honeypot. This should be a valid network ID from your UniFi configuration where the honeypot will be deployed.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ips_mode": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "The IPS operation mode. Valid values are:\n" +
|
||||||
|
" * `ids` - Intrusion Detection System mode (detect and log threats only)\n" +
|
||||||
|
" * `ips` - Intrusion Prevention System mode (detect and block threats)\n" +
|
||||||
|
" * `ipsInline` - Inline Intrusion Prevention System mode (more aggressive blocking)\n" +
|
||||||
|
" * `disabled` - IPS functionality is completely disabled",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("ids", "ips", "ipsInline", "disabled"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"memory_optimized": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether memory optimization is enabled for IPS. When set to `true`, the system will use less memory at the cost of potentially reduced detection capabilities. Useful for devices with limited resources. Defaults to `false`. Requires controller version 9.0 or later.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(false),
|
||||||
|
PlanModifiers: []planmodifier.Bool{
|
||||||
|
boolplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"restrict_torrents": schema.BoolAttribute{
|
||||||
|
MarkdownDescription: "Whether to restrict BitTorrent and other peer-to-peer file sharing traffic. When set to `true`, the system will block P2P traffic across the network. Defaults to `false`.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(false),
|
||||||
|
PlanModifiers: []planmodifier.Bool{
|
||||||
|
boolplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"suppression": schema.SingleNestedAttribute{
|
||||||
|
MarkdownDescription: "Suppression configuration for IPS. This allows you to customize which alerts are suppressed or tracked, and define whitelisted traffic that should never trigger IPS alerts.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.Object{
|
||||||
|
objectplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"alerts": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "Alert suppressions. Each entry defines a specific IPS alert that should be suppressed or tracked differently from the default behavior.",
|
||||||
|
Optional: true,
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"category": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Category of the alert to suppress. This should match one of the categories from the enabled_categories list.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
//"gid": schema.Int64Attribute{
|
||||||
|
// MarkdownDescription: "Group ID of the alert to suppress. This is a numeric identifier for the alert group in the IPS ruleset.",
|
||||||
|
// Required: true,
|
||||||
|
//},
|
||||||
|
//"id": schema.Int64Attribute{
|
||||||
|
// MarkdownDescription: "ID of the alert to suppress. This is a numeric identifier for the specific alert in the IPS ruleset.",
|
||||||
|
// Required: true,
|
||||||
|
//},
|
||||||
|
"signature": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Signature name of the alert to suppress. This is a human-readable identifier for the alert in the IPS ruleset.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"tracking": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "Tracking configuration for the alert. This defines how the system should track occurrences of this alert based on source/destination addresses.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.List{
|
||||||
|
listplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"direction": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Direction for tracking. Valid values are:\n" +
|
||||||
|
" * `src` - Track by source address\n" +
|
||||||
|
" * `dest` - Track by destination address\n" +
|
||||||
|
" * `both` - Track by both source and destination addresses",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("src", "dest", "both"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mode": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Mode for tracking. Valid values are:\n" +
|
||||||
|
" * `ip` - Track by individual IP address\n" +
|
||||||
|
" * `subnet` - Track by subnet\n" +
|
||||||
|
" * `network` - Track by network ID",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("ip", "subnet", "network"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"value": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Value for tracking. The meaning depends on the mode:\n" +
|
||||||
|
" * For `ip` mode: An IP address (e.g., `192.168.1.100`)\n" +
|
||||||
|
" * For `subnet` mode: A CIDR notation subnet (e.g., `192.168.1.0/24`)\n" +
|
||||||
|
" * For `network` mode: A network ID from your UniFi configuration",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Type of suppression. Valid values are:\n" +
|
||||||
|
" * `all` - Suppress all occurrences of this alert\n" +
|
||||||
|
" * `track` - Only track this alert according to the tracking configuration",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("all", "track"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"whitelist": schema.ListNestedAttribute{
|
||||||
|
MarkdownDescription: "Whitelist configuration. Each entry defines traffic that should never trigger IPS alerts, regardless of other rules.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.List{
|
||||||
|
listplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
NestedObject: schema.NestedAttributeObject{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"direction": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Direction for whitelist. Valid values are:\n" +
|
||||||
|
" * `src` - Whitelist by source address\n" +
|
||||||
|
" * `dst` - Whitelist by destination address\n" +
|
||||||
|
" * `both` - Whitelist by both source and destination addresses",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("src", "dst", "both"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mode": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Mode for whitelist. Valid values are:\n" +
|
||||||
|
" * `ip` - Whitelist by individual IP address\n" +
|
||||||
|
" * `subnet` - Whitelist by subnet\n" +
|
||||||
|
" * `network` - Whitelist by network ID",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("ip", "subnet", "network"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"value": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Value for whitelist. The meaning depends on the mode:\n" +
|
||||||
|
" * For `ip` mode: An IP address (e.g., `192.168.1.100`)\n" +
|
||||||
|
" * For `subnet` mode: A CIDR notation subnet (e.g., `192.168.1.0/24`)\n" +
|
||||||
|
" * For `network` mode: A network ID from your UniFi configuration",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIpsResource() resource.Resource {
|
||||||
|
r := &ipsResource{}
|
||||||
|
r.GenericResource = NewSettingResource(
|
||||||
|
"unifi_setting_ips",
|
||||||
|
func() *ipsModel { return &ipsModel{} },
|
||||||
|
func(ctx context.Context, client *base.Client, site string) (interface{}, error) {
|
||||||
|
return client.GetSettingIps(ctx, site)
|
||||||
|
},
|
||||||
|
func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) {
|
||||||
|
return client.UpdateSettingIps(ctx, site, body.(*unifi.SettingIps))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ base.ResourceModel = &ipsModel{}
|
||||||
|
_ resource.Resource = &ipsResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ipsResource{}
|
||||||
|
_ resource.ResourceWithConfigValidators = &ipsResource{}
|
||||||
|
_ resource.ResourceWithModifyPlan = &ipsResource{}
|
||||||
|
)
|
||||||
@@ -195,6 +195,7 @@ type mgmtResource struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mgmtResource) ModifyPlan(_ context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
func (r *mgmtResource) ModifyPlan(_ context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
||||||
|
resp.Diagnostics.Append(r.RequireMinVersionForPath("7.0", path.Root("auto_upgrade_hour"), req.Config)...)
|
||||||
resp.Diagnostics.Append(r.RequireMinVersionForPath("7.3", path.Root("debug_tools_enabled"), req.Config)...)
|
resp.Diagnostics.Append(r.RequireMinVersionForPath("7.3", path.Root("debug_tools_enabled"), req.Config)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package validators
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"github.com/filipowm/terraform-provider-unifi/internal/utils"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,15 +16,15 @@ var _ validator.String = ipv4Validator{}
|
|||||||
|
|
||||||
type ipv4Validator struct{}
|
type ipv4Validator struct{}
|
||||||
|
|
||||||
func (v ipv4Validator) Description(ctx context.Context) string {
|
func (v ipv4Validator) Description(_ context.Context) string {
|
||||||
return "value must be a valid IPv4 address"
|
return "value must be a valid IPv4 address"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v ipv4Validator) MarkdownDescription(ctx context.Context) string {
|
func (v ipv4Validator) MarkdownDescription(_ context.Context) string {
|
||||||
return "value must be a valid IPv4 address"
|
return "value must be a valid IPv4 address"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v ipv4Validator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
func (v ipv4Validator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||||
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
|
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -35,8 +34,7 @@ func (v ipv4Validator) ValidateString(ctx context.Context, req validator.StringR
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(value)
|
if !utils.IsIPv4(value) {
|
||||||
if ip == nil || ip.To4() == nil {
|
|
||||||
resp.Diagnostics.AddAttributeError(
|
resp.Diagnostics.AddAttributeError(
|
||||||
req.Path,
|
req.Path,
|
||||||
"Invalid IPv4 Address",
|
"Invalid IPv4 Address",
|
||||||
|
|||||||
45
internal/provider/validators/ipv6.go
Normal file
45
internal/provider/validators/ipv6.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/utils"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IPv6 returns a validator which ensures that a string value is a valid IPv6 address.
|
||||||
|
func IPv6() validator.String {
|
||||||
|
return ipv6Validator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ validator.String = ipv6Validator{}
|
||||||
|
|
||||||
|
type ipv6Validator struct{}
|
||||||
|
|
||||||
|
func (v ipv6Validator) Description(_ context.Context) string {
|
||||||
|
return "value must be a valid IPv6 address"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ipv6Validator) MarkdownDescription(_ context.Context) string {
|
||||||
|
return "value must be a valid IPv6 address"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ipv6Validator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||||
|
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value := req.ConfigValue.ValueString()
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsIPv6(value) {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
req.Path,
|
||||||
|
"Invalid IPv6 Address",
|
||||||
|
fmt.Sprintf("Value %q is not a valid IPv6 address", value),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
88
internal/provider/validators/ipv6_test.go
Normal file
88
internal/provider/validators/ipv6_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package validators_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIPv6Validator(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
val types.String
|
||||||
|
expectError bool
|
||||||
|
}
|
||||||
|
tests := map[string]testCase{
|
||||||
|
"unknown": {
|
||||||
|
val: types.StringUnknown(),
|
||||||
|
},
|
||||||
|
"null": {
|
||||||
|
val: types.StringNull(),
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
val: types.StringValue(""),
|
||||||
|
},
|
||||||
|
"valid ipv6 full": {
|
||||||
|
val: types.StringValue("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
||||||
|
},
|
||||||
|
"valid ipv6 compressed": {
|
||||||
|
val: types.StringValue("2001:db8:85a3::8a2e:370:7334"),
|
||||||
|
},
|
||||||
|
"valid ipv6 loopback": {
|
||||||
|
val: types.StringValue("::1"),
|
||||||
|
},
|
||||||
|
"valid ipv6 unspecified": {
|
||||||
|
val: types.StringValue("::"),
|
||||||
|
},
|
||||||
|
"valid ipv6 with zone": {
|
||||||
|
val: types.StringValue("fe80::1ff:fe23:4567:890a%eth0"),
|
||||||
|
},
|
||||||
|
"valid ipv6 ipv4-mapped": {
|
||||||
|
val: types.StringValue("::ffff:192.0.2.128"),
|
||||||
|
},
|
||||||
|
"invalid ipv6 - too many segments": {
|
||||||
|
val: types.StringValue("2001:0db8:85a3:0000:0000:8a2e:0370:7334:1111"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid ipv6 - out of range": {
|
||||||
|
val: types.StringValue("2001:0db8:85a3:0000:0000:8a2e:0370:GGGG"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid ipv6 - incomplete": {
|
||||||
|
val: types.StringValue("2001:0db8:85a3"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid ipv6 - ipv4": {
|
||||||
|
val: types.StringValue("192.168.1.1"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid ipv6 - characters": {
|
||||||
|
val: types.StringValue("not-an-ip"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
name, test := name, test
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
req := validator.StringRequest{
|
||||||
|
ConfigValue: test.val,
|
||||||
|
}
|
||||||
|
resp := validator.StringResponse{}
|
||||||
|
validators.IPv6().ValidateString(context.Background(), req, &resp)
|
||||||
|
|
||||||
|
if !test.expectError && resp.Diagnostics.HasError() {
|
||||||
|
t.Fatalf("got unexpected error: %s", resp.Diagnostics.Errors()[0].Detail())
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.expectError && !resp.Diagnostics.HasError() {
|
||||||
|
t.Fatalf("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CidrValidate(raw interface{}, key string) ([]string, []error) {
|
func CidrValidate(raw interface{}, key string) ([]string, []error) {
|
||||||
@@ -53,3 +54,29 @@ func CidrDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
|
|||||||
|
|
||||||
return oldNet.String() == newNet.String()
|
return oldNet.String() == newNet.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsIPv4 checks if the provided address is a valid IPv4 address.
|
||||||
|
// It returns true if the address is a valid IPv4 address, false otherwise.
|
||||||
|
func IsIPv4(address string) bool {
|
||||||
|
ip := net.ParseIP(address)
|
||||||
|
return ip != nil && ip.To4() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIPv6 checks if the provided address is a valid IPv6 address.
|
||||||
|
// It returns true if the address is a valid IPv6 address, false otherwise.
|
||||||
|
func IsIPv6(address string) bool {
|
||||||
|
|
||||||
|
// Handle zone index if present
|
||||||
|
if idx := strings.Index(address, "%"); idx != -1 {
|
||||||
|
address = address[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IPv4-mapped addresses
|
||||||
|
isIPv4Mapped := strings.Contains(address, "::ffff:") && strings.Count(address, ".") == 3
|
||||||
|
|
||||||
|
ip := net.ParseIP(address)
|
||||||
|
if ip == nil || (!isIPv4Mapped && ip.To4() != nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user