feat: support Guest Access settings with resource_setting_guest_access (#61)
* feat: support Guest Access settings with `resource_setting_guest_access` * feat: add support for redirect after authentication in guest access settings * feat: add support for Facebook authentication in guest access settings * feat: add support for Google authentication in guest access settings * feat: add support for RADIUS authentication in guest access settings * feat: add support for Wechat authentication in guest access settings * feat: add support for Facebook Wifi authentication in guest access settings * feat: add support for restricted DNS servers * feat: add support for guest portal UI customization * feat: add support for restricted subnet in guest portal * feat: retry client action on HTTP 401, but first attempt relogging in * require controllr version 7.4 for several portal customization attributes * enable acceptance tests workflow concurrency
This commit is contained in:
committed by
GitHub
parent
28d28f17f6
commit
ca21f79083
5
.github/workflows/acctest.yml
vendored
5
.github/workflows/acctest.yml
vendored
@@ -24,6 +24,11 @@ on:
|
||||
- "Makefile"
|
||||
schedule:
|
||||
- cron: "0 13 * * *"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Matrix Test
|
||||
|
||||
4
go.mod
4
go.mod
@@ -2,7 +2,7 @@ module github.com/filipowm/terraform-provider-unifi
|
||||
|
||||
go 1.23.5
|
||||
|
||||
//replace github.com/filipowm/go-unifi v1.5.3 => ../go-unifi
|
||||
//replace github.com/filipowm/go-unifi v1.6.0 => ../go-unifi
|
||||
// replace github.com/hashicorp/terraform-plugin-docs => ../../hashicorp/terraform-plugin-docs
|
||||
// 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/biter777/countries v1.7.5
|
||||
github.com/deckarep/golang-set/v2 v2.7.0
|
||||
github.com/filipowm/go-unifi v1.6.0
|
||||
github.com/filipowm/go-unifi v1.6.1
|
||||
github.com/golangci/golangci-lint v1.64.7
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/hashicorp/terraform-plugin-docs v0.21.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -284,6 +284,8 @@ github.com/filipowm/go-unifi v1.5.4 h1:kc1jWx0Rht+tiEyFsDjPAYJ6h2EV+Atq7eIoTA6fH
|
||||
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/filipowm/go-unifi v1.6.1 h1:zkxLUkbUWO3d62cUGr97knIzSJd+/id9RVc32iPojqo=
|
||||
github.com/filipowm/go-unifi v1.6.1/go.mod h1:hB5XyhjtnnU9GC6lYPYxuNmYq4J/SyjmElRVazCKT0U=
|
||||
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
|
||||
1129
internal/provider/acctest/resource_setting_guest_access_test.go
Normal file
1129
internal/provider/acctest/resource_setting_guest_access_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,9 @@ package base
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
@@ -42,6 +44,20 @@ type DatasourceModel interface {
|
||||
Merge(context.Context, interface{}) diag.Diagnostics
|
||||
}
|
||||
|
||||
type NestedObject interface {
|
||||
AttributeTypes() map[string]attr.Type
|
||||
}
|
||||
|
||||
func ObjectNull(obj interface{}) (basetypes.ObjectValue, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
if nested, ok := obj.(NestedObject); ok {
|
||||
obj := types.ObjectNull(nested.AttributeTypes())
|
||||
return obj, diags
|
||||
}
|
||||
diags.AddError("Invalid object type", fmt.Sprintf("Expected NestedObject, got: %T", obj))
|
||||
return types.ObjectNull(map[string]attr.Type{}), diags
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
Site types.String `tfsdk:"site"`
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -54,7 +55,7 @@ func NewClient(cfg *ClientConfig) (*Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{
|
||||
Client: unifiClient,
|
||||
Client: NewRetryableUnifiClient(unifiClient),
|
||||
Site: cfg.Site,
|
||||
Version: version.Must(version.NewVersion(unifiClient.Version())),
|
||||
}
|
||||
@@ -64,6 +65,41 @@ func NewClient(cfg *ClientConfig) (*Client, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewRetryableUnifiClient(client unifi.Client) unifi.Client {
|
||||
return &RetryableUnifiClient{
|
||||
Client: client,
|
||||
loginMutex: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
type RetryableUnifiClient struct {
|
||||
unifi.Client
|
||||
loginMutex sync.Mutex
|
||||
}
|
||||
|
||||
func (c *RetryableUnifiClient) relogin(err error) error {
|
||||
c.loginMutex.Lock()
|
||||
defer c.loginMutex.Unlock()
|
||||
loginErr := c.Client.Login()
|
||||
if loginErr != nil {
|
||||
return fmt.Errorf("Tried relogging in after %w, but failed: %w.", err, loginErr)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RetryableUnifiClient) Do(ctx context.Context, method string, apiPath string, reqBody interface{}, respBody interface{}) error {
|
||||
err := c.Client.Do(ctx, method, apiPath, reqBody, respBody)
|
||||
if err != nil && IsServerErrorStatusCode(err, 401) {
|
||||
err := c.relogin(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Client.Do(ctx, method, apiPath, reqBody, respBody)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
unifi.Client
|
||||
Site string
|
||||
|
||||
@@ -14,6 +14,17 @@ func ErrorInvalidModelMergeTarget(expectedType, actualType interface{}) diag.Dia
|
||||
return diag.NewErrorDiagnostic("Invalid model merge target", "Expected target type to be the same a receiver: "+e+". Was : "+a)
|
||||
}
|
||||
|
||||
func IsServerErrorStatusCode(err error, statusCode int) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var se *unifi.ServerError
|
||||
if errors.As(err, &se) {
|
||||
return se.StatusCode == statusCode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsServerErrorContains(err error, messageContains string) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
||||
@@ -148,6 +148,13 @@ func TestFeaturesIsUnavailable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newTestClient(mock *MockUnifiClient) *Client {
|
||||
return &Client{
|
||||
Client: mock,
|
||||
Site: "default",
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewFeatureValidator tests the NewFeatureValidator function
|
||||
func TestNewFeatureValidator(t *testing.T) {
|
||||
mockUnifiClient := &MockUnifiClient{
|
||||
@@ -156,10 +163,7 @@ func TestNewFeatureValidator(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Client: mockUnifiClient,
|
||||
Site: "default",
|
||||
}
|
||||
client := newTestClient(mockUnifiClient)
|
||||
|
||||
validator := NewFeatureValidator(client)
|
||||
|
||||
@@ -225,10 +229,7 @@ func TestGetFeatures(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockUnifiClient := tt.setup()
|
||||
client := &Client{
|
||||
Client: mockUnifiClient,
|
||||
Site: "default",
|
||||
}
|
||||
client := newTestClient(mockUnifiClient)
|
||||
|
||||
validator := &featureEnabledValidator{
|
||||
client: client,
|
||||
@@ -267,10 +268,7 @@ func TestGetFeaturesConcurrent(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Client: mockUnifiClient,
|
||||
Site: "default",
|
||||
}
|
||||
client := newTestClient(mockUnifiClient)
|
||||
|
||||
validator := &featureEnabledValidator{
|
||||
client: client,
|
||||
@@ -367,10 +365,7 @@ func TestRequireFeatures(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockUnifiClient := &MockUnifiClient{}
|
||||
client := &Client{
|
||||
Client: mockUnifiClient,
|
||||
Site: "default",
|
||||
}
|
||||
client := newTestClient(mockUnifiClient)
|
||||
|
||||
validator := &featureEnabledValidator{
|
||||
client: client,
|
||||
@@ -465,10 +460,7 @@ func TestRequireFeaturesEnabledForPath(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockUnifiClient := tt.setupClient()
|
||||
client := &Client{
|
||||
Client: mockUnifiClient,
|
||||
Site: "default",
|
||||
}
|
||||
client := newTestClient(mockUnifiClient)
|
||||
|
||||
// Create a wrapper FeatureValidator that provides a minimal implementation
|
||||
// of RequireFeaturesEnabledForPath without needing a real tfsdk.Config
|
||||
@@ -538,10 +530,7 @@ func TestRequireFeaturesEnabled(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockUnifiClient := tt.setupClient()
|
||||
client := &Client{
|
||||
Client: mockUnifiClient,
|
||||
Site: "default",
|
||||
}
|
||||
client := newTestClient(mockUnifiClient)
|
||||
validator := NewFeatureValidator(client)
|
||||
diags := validator.RequireFeaturesEnabled(context.Background(), "site1", tt.requiredFeatures...)
|
||||
assert.Equal(t, tt.expectedHasErrors, diags.HasError())
|
||||
@@ -605,10 +594,7 @@ func TestFeatureValidatorCache(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Client: mockUnifiClient,
|
||||
Site: "default",
|
||||
}
|
||||
client := newTestClient(mockUnifiClient)
|
||||
|
||||
validator := NewFeatureValidator(client)
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource
|
||||
settings.NewAutoSpeedtestResource,
|
||||
settings.NewCountryResource,
|
||||
settings.NewDpiResource,
|
||||
settings.NewGuestAccessResource,
|
||||
settings.NewIpsResource,
|
||||
settings.NewLcmResource,
|
||||
settings.NewLocaleResource,
|
||||
|
||||
1618
internal/provider/settings/resource_setting_guest_access.go
Normal file
1618
internal/provider/settings/resource_setting_guest_access.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
"net/http"
|
||||
@@ -266,12 +267,14 @@ func (te *TestEnvironment) newTestClient() (unifi.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unifi.NewClient(&unifi.ClientConfig{
|
||||
client, err := unifi.NewClient(&unifi.ClientConfig{
|
||||
URL: te.Endpoint,
|
||||
User: user,
|
||||
Password: password,
|
||||
VerifySSL: false,
|
||||
RememberMe: true,
|
||||
ValidationMode: unifi.DisableValidation,
|
||||
Logger: unifi.NewDefaultLogger(unifi.WarnLevel),
|
||||
})
|
||||
return base.NewRetryableUnifiClient(client), err
|
||||
}
|
||||
|
||||
48
internal/provider/validators/email.go
Normal file
48
internal/provider/validators/email.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
)
|
||||
|
||||
// A common regex pattern for validating email addresses
|
||||
// This is a simplified version and may not catch all edge cases
|
||||
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
||||
|
||||
// Email returns a validator which ensures that the string value is a valid email address.
|
||||
func Email() validator.String {
|
||||
return emailValidator{}
|
||||
}
|
||||
|
||||
type emailValidator struct{}
|
||||
|
||||
func (v emailValidator) Description(_ context.Context) string {
|
||||
return "must be a valid email address"
|
||||
}
|
||||
|
||||
func (v emailValidator) MarkdownDescription(ctx context.Context) string {
|
||||
return v.Description(ctx)
|
||||
}
|
||||
|
||||
func (v emailValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||
value := req.ConfigValue
|
||||
if !base.IsDefined(value) {
|
||||
return
|
||||
}
|
||||
|
||||
val := value.ValueString()
|
||||
if !emailRegex.MatchString(val) {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q is not a valid email address", val),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
82
internal/provider/validators/email_test.go
Normal file
82
internal/provider/validators/email_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package validators_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
func TestEmailValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
val types.String
|
||||
expectError bool
|
||||
}
|
||||
tests := map[string]testCase{
|
||||
"unknown": {
|
||||
val: types.StringUnknown(),
|
||||
},
|
||||
"null": {
|
||||
val: types.StringNull(),
|
||||
},
|
||||
"valid-simple": {
|
||||
val: types.StringValue("test@example.com"),
|
||||
},
|
||||
"valid-with-dots": {
|
||||
val: types.StringValue("john.doe@example.com"),
|
||||
},
|
||||
"valid-with-plus": {
|
||||
val: types.StringValue("john+test@example.com"),
|
||||
},
|
||||
"valid-with-subdomain": {
|
||||
val: types.StringValue("john@sub.example.com"),
|
||||
},
|
||||
"valid-with-numbers": {
|
||||
val: types.StringValue("user123@example.com"),
|
||||
},
|
||||
"invalid-no-at": {
|
||||
val: types.StringValue("testexample.com"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-no-domain": {
|
||||
val: types.StringValue("test@"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-no-tld": {
|
||||
val: types.StringValue("test@example"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-space": {
|
||||
val: types.StringValue("test user@example.com"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-special-chars": {
|
||||
val: types.StringValue("test*user@example.com"),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
name, test := name, test
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
request := validator.StringRequest{
|
||||
ConfigValue: test.val,
|
||||
}
|
||||
response := validator.StringResponse{}
|
||||
validators.Email().ValidateString(context.Background(), request, &response)
|
||||
|
||||
if !response.Diagnostics.HasError() && test.expectError {
|
||||
t.Fatal("expected error, got no error")
|
||||
}
|
||||
|
||||
if response.Diagnostics.HasError() && !test.expectError {
|
||||
t.Fatalf("got unexpected error: %s", response.Diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
47
internal/provider/validators/hex_color.go
Normal file
47
internal/provider/validators/hex_color.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
)
|
||||
|
||||
var hexColorRegex = regexp.MustCompile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
|
||||
|
||||
// HexColor returns a validator which ensures that the string value is a valid hex color code.
|
||||
// Valid formats are: #RGB or #RRGGBB
|
||||
func HexColor() validator.String {
|
||||
return hexColorValidator{}
|
||||
}
|
||||
|
||||
type hexColorValidator struct{}
|
||||
|
||||
func (v hexColorValidator) Description(_ context.Context) string {
|
||||
return "must be a valid hex color code (e.g., #FFF or #FFFFFF)"
|
||||
}
|
||||
|
||||
func (v hexColorValidator) MarkdownDescription(ctx context.Context) string {
|
||||
return v.Description(ctx)
|
||||
}
|
||||
|
||||
func (v hexColorValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||
value := req.ConfigValue
|
||||
if !base.IsDefined(value) {
|
||||
return
|
||||
}
|
||||
|
||||
val := value.ValueString()
|
||||
if !hexColorRegex.MatchString(val) {
|
||||
resp.Diagnostics.Append(
|
||||
validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
v.Description(ctx),
|
||||
fmt.Sprintf("%q is not a valid hex color code", val),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
75
internal/provider/validators/hex_color_test.go
Normal file
75
internal/provider/validators/hex_color_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package validators_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
func TestHexColorValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
val types.String
|
||||
expectError bool
|
||||
}
|
||||
tests := map[string]testCase{
|
||||
"unknown": {
|
||||
val: types.StringUnknown(),
|
||||
},
|
||||
"null": {
|
||||
val: types.StringNull(),
|
||||
},
|
||||
"valid-6-digits": {
|
||||
val: types.StringValue("#123456"),
|
||||
},
|
||||
"valid-3-digits": {
|
||||
val: types.StringValue("#123"),
|
||||
},
|
||||
"valid-uppercase": {
|
||||
val: types.StringValue("#ABCDEF"),
|
||||
},
|
||||
"valid-mixed-case": {
|
||||
val: types.StringValue("#aBcDeF"),
|
||||
},
|
||||
"invalid-missing-hash": {
|
||||
val: types.StringValue("123456"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-too-short": {
|
||||
val: types.StringValue("#12"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-too-long": {
|
||||
val: types.StringValue("#1234567"),
|
||||
expectError: true,
|
||||
},
|
||||
"invalid-wrong-chars": {
|
||||
val: types.StringValue("#12345G"),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
name, test := name, test
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
request := validator.StringRequest{
|
||||
ConfigValue: test.val,
|
||||
}
|
||||
response := validator.StringResponse{}
|
||||
validators.HexColor().ValidateString(context.Background(), request, &response)
|
||||
|
||||
if !response.Diagnostics.HasError() && test.expectError {
|
||||
t.Fatal("expected error, got no error")
|
||||
}
|
||||
|
||||
if response.Diagnostics.HasError() && !test.expectError {
|
||||
t.Fatalf("got unexpected error: %s", response.Diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,17 @@ The UniFi provider enables infrastructure-as-code management of [Ubiquiti's UniF
|
||||
|
||||
## Supported Features
|
||||
|
||||
The provider supports management of:
|
||||
|
||||
* Networks and VLANs
|
||||
* Wireless Networks (WLANs)
|
||||
* Firewall Rules and Groups
|
||||
* Port Forwarding
|
||||
* DNS Records
|
||||
* User Management
|
||||
* Device Configuration
|
||||
* And more...
|
||||
- Manage UniFi network resources using Infrastructure as Code
|
||||
- Support for UniFi Controller version 6.x and later
|
||||
- Compatible with UDM, UDM-Pro, UCG, and standard controller deployments
|
||||
- Comprehensive resource management including:
|
||||
- Network/WLAN configuration
|
||||
- Firewall rules and groups
|
||||
- Port forwarding
|
||||
- DNS records
|
||||
- User management
|
||||
- Device management
|
||||
- And more...
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
|
||||
Reference in New Issue
Block a user