Skip to content

Commit 83fa531

Browse files
authored
Fix dhcp.dhcp required fields
As it turns out, there's actually a case where these "required" fields don't exist from the get-go. For WAN interfaces, they already have a DHCP pool setup that is ignored. There's no `leasetime`, `limit`, or `start` option associated with them. So you end up in this weird state of questioning whether you should add these three fields to satiate this Terraform provider, not import the WAN DHCP pool, delete the DHCP pool from UCI all up, or something equally obtuse. None of these things should be a thing someone has to think about. So we change the attributes to not be required all the time, but only when the `ignore` attribute is not `true`. We do this by adding a new validator that checks the other attribute for its value and acts accordingly. We update the tests to validate this behavior. This is yet another example of how not having sum types causes so much extra complexity. Ignoring the fact that OpenWrt shouldn't ship with an ignored DHCP pool on the WAN interface, this situation is trivial (in implementation) to represent with a sum type. But trying to represent it with nothing by product and exponent types is incredibly complex. Ah well, maybe one day things will get better… Branch: joneshf/fix-dhcp-dhcp-required-fields Pull-Request: #130
1 parent 45fb78f commit 83fa531

File tree

5 files changed

+244
-20
lines changed

5 files changed

+244
-20
lines changed

docs/data-sources/dhcp_dhcp.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ data "openwrt_dhcp_dhcp" "testing" {
3131
- `dhcpv6` (String) The mode of the DHCPv6 server. Must be one of: "disabled", "relay", "server".
3232
- `force` (Boolean) Forces DHCP serving on the specified interface even if another DHCP server is detected on the same network segment.
3333
- `ignore` (Boolean) Specifies whether dnsmasq should ignore this pool.
34-
- `interface` (String) The interface associated with this DHCP address pool. This name is what the interface is known as in UCI, or the `id` field in Terraform.
35-
- `leasetime` (String) The lease time of addresses handed out to clients. E.g. `12h`, or `30m`.
36-
- `limit` (Number) Specifies the size of the address pool. E.g. With start = 100, and limit = 150, the maximum address will be 249.
34+
- `interface` (String) The interface associated with this DHCP address pool. This name is what the interface is known as in UCI, or the `id` field in Terraform. Required if `ignore` is not `true`.
35+
- `leasetime` (String) The lease time of addresses handed out to clients. E.g. `12h`, or `30m`. Required if `ignore` is not `true`.
36+
- `limit` (Number) Specifies the size of the address pool. E.g. With start = 100, and limit = 150, the maximum address will be 249. Required if `ignore` is not `true`.
3737
- `ra` (String) The mode of Router Advertisements. Must be one of: "disabled", "relay", "server".
3838
- `ra_flags` (Set of String) Router Advertisement flags to include in messages. Must be one of: "home-agent", "managed-config", "none", "other-config".
39-
- `start` (Number) Specifies the offset from the network address of the underlying interface to calculate the minimum address that may be leased to clients. It may be greater than 255 to span subnets.
39+
- `start` (Number) Specifies the offset from the network address of the underlying interface to calculate the minimum address that may be leased to clients. It may be greater than 255 to span subnets. Required if `ignore` is not `true`.
4040

4141

docs/resources/dhcp_dhcp.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,19 @@ resource "openwrt_dhcp_dhcp" "testing" {
5757
### Required
5858

5959
- `id` (String) Name of the section. This name is only used when interacting with UCI directly.
60-
- `leasetime` (String) The lease time of addresses handed out to clients. E.g. `12h`, or `30m`.
61-
- `limit` (Number) Specifies the size of the address pool. E.g. With start = 100, and limit = 150, the maximum address will be 249.
62-
- `start` (Number) Specifies the offset from the network address of the underlying interface to calculate the minimum address that may be leased to clients. It may be greater than 255 to span subnets.
6360

6461
### Optional
6562

6663
- `dhcpv4` (String) The mode of the DHCPv4 server. Must be one of: "disabled", "server".
6764
- `dhcpv6` (String) The mode of the DHCPv6 server. Must be one of: "disabled", "relay", "server".
6865
- `force` (Boolean) Forces DHCP serving on the specified interface even if another DHCP server is detected on the same network segment.
6966
- `ignore` (Boolean) Specifies whether dnsmasq should ignore this pool.
70-
- `interface` (String) The interface associated with this DHCP address pool. This name is what the interface is known as in UCI, or the `id` field in Terraform.
67+
- `interface` (String) The interface associated with this DHCP address pool. This name is what the interface is known as in UCI, or the `id` field in Terraform. Required if `ignore` is not `true`.
68+
- `leasetime` (String) The lease time of addresses handed out to clients. E.g. `12h`, or `30m`. Required if `ignore` is not `true`.
69+
- `limit` (Number) Specifies the size of the address pool. E.g. With start = 100, and limit = 150, the maximum address will be 249. Required if `ignore` is not `true`.
7170
- `ra` (String) The mode of Router Advertisements. Must be one of: "disabled", "relay", "server".
7271
- `ra_flags` (Set of String) Router Advertisement flags to include in messages. Must be one of: "home-agent", "managed-config", "none", "other-config".
72+
- `start` (Number) Specifies the offset from the network address of the underlying interface to calculate the minimum address that may be leased to clients. It may be greater than 255 to span subnets. Required if `ignore` is not `true`.
7373

7474
## Import
7575

openwrt/dhcp/dhcp/dhcp.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
55
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
66
"github.com/hashicorp/terraform-plugin-framework/datasource"
7+
"github.com/hashicorp/terraform-plugin-framework/path"
78
"github.com/hashicorp/terraform-plugin-framework/resource"
89
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
910
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -34,15 +35,15 @@ const (
3435
ignoreUCIOption = "ignore"
3536

3637
interfaceAttribute = "interface"
37-
interfaceAttributeDescription = "The interface associated with this DHCP address pool. This name is what the interface is known as in UCI, or the `id` field in Terraform."
38+
interfaceAttributeDescription = "The interface associated with this DHCP address pool. This name is what the interface is known as in UCI, or the `id` field in Terraform. Required if `ignore` is not `true`."
3839
interfaceUCIOption = "interface"
3940

4041
leaseTimeAttribute = "leasetime"
41-
leaseTimeAttributeDescription = "The lease time of addresses handed out to clients. E.g. `12h`, or `30m`."
42+
leaseTimeAttributeDescription = "The lease time of addresses handed out to clients. E.g. `12h`, or `30m`. Required if `ignore` is not `true`."
4243
leaseTimeUCIOption = "leasetime"
4344

4445
limitAttribute = "limit"
45-
limitAttributeDescription = "Specifies the size of the address pool. E.g. With start = 100, and limit = 150, the maximum address will be 249."
46+
limitAttributeDescription = "Specifies the size of the address pool. E.g. With start = 100, and limit = 150, the maximum address will be 249. Required if `ignore` is not `true`."
4647
limitUCIOption = "limit"
4748

4849
routerAdvertisementFlagsAttribute = "ra_flags"
@@ -63,7 +64,7 @@ const (
6364
schemaDescription = "Per interface lease pools and settings for serving DHCP requests."
6465

6566
startAttribute = "start"
66-
startAttributeDescription = "Specifies the offset from the network address of the underlying interface to calculate the minimum address that may be leased to clients. It may be greater than 255 to span subnets."
67+
startAttributeDescription = "Specifies the offset from the network address of the underlying interface to calculate the minimum address that may be leased to clients. It may be greater than 255 to span subnets. Required if `ignore` is not `true`."
6768
startUCIOption = "start"
6869

6970
uciConfig = "dhcp"
@@ -117,20 +118,29 @@ var (
117118
ReadResponse: lucirpcglue.ReadResponseOptionString(modelSetInterface, interfaceAttribute, interfaceUCIOption),
118119
ResourceExistence: lucirpcglue.NoValidation,
119120
UpsertRequest: lucirpcglue.UpsertRequestOptionString(modelGetInterface, interfaceAttribute, interfaceUCIOption),
121+
Validators: []validator.String{
122+
lucirpcglue.RequiredIfAttributeNotEqualBool(path.MatchRoot(ignoreAttribute), true),
123+
},
120124
}
121125

122126
leaseTimeSchemaAttribute = lucirpcglue.StringSchemaAttribute[model, lucirpc.Options, lucirpc.Options]{
123127
Description: leaseTimeAttributeDescription,
124128
ReadResponse: lucirpcglue.ReadResponseOptionString(modelSetLeaseTime, leaseTimeAttribute, leaseTimeUCIOption),
125-
ResourceExistence: lucirpcglue.Required,
129+
ResourceExistence: lucirpcglue.NoValidation,
126130
UpsertRequest: lucirpcglue.UpsertRequestOptionString(modelGetLeaseTime, leaseTimeAttribute, leaseTimeUCIOption),
131+
Validators: []validator.String{
132+
lucirpcglue.RequiredIfAttributeNotEqualBool(path.MatchRoot(ignoreAttribute), true),
133+
},
127134
}
128135

129136
limitSchemaAttribute = lucirpcglue.Int64SchemaAttribute[model, lucirpc.Options, lucirpc.Options]{
130137
Description: limitAttributeDescription,
131138
ReadResponse: lucirpcglue.ReadResponseOptionInt64(modelSetLimit, limitAttribute, limitUCIOption),
132-
ResourceExistence: lucirpcglue.Required,
139+
ResourceExistence: lucirpcglue.NoValidation,
133140
UpsertRequest: lucirpcglue.UpsertRequestOptionInt64(modelGetLimit, limitAttribute, limitUCIOption),
141+
Validators: []validator.Int64{
142+
lucirpcglue.RequiredIfAttributeNotEqualBool(path.MatchRoot(ignoreAttribute), true),
143+
},
134144
}
135145

136146
routerAdvertisementFlagsSchemaAttribute = lucirpcglue.SetStringSchemaAttribute[model, lucirpc.Options, lucirpc.Options]{
@@ -181,8 +191,11 @@ var (
181191
startSchemaAttribute = lucirpcglue.Int64SchemaAttribute[model, lucirpc.Options, lucirpc.Options]{
182192
Description: startAttributeDescription,
183193
ReadResponse: lucirpcglue.ReadResponseOptionInt64(modelSetStart, startAttribute, startUCIOption),
184-
ResourceExistence: lucirpcglue.Required,
194+
ResourceExistence: lucirpcglue.NoValidation,
185195
UpsertRequest: lucirpcglue.UpsertRequestOptionInt64(modelGetStart, startAttribute, startUCIOption),
196+
Validators: []validator.Int64{
197+
lucirpcglue.RequiredIfAttributeNotEqualBool(path.MatchRoot(ignoreAttribute), true),
198+
},
186199
}
187200
)
188201

openwrt/dhcp/dhcp/dhcp_acceptance_test.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,27 +75,62 @@ func TestResourceAcceptance(t *testing.T) {
7575
7676
resource "openwrt_dhcp_dhcp" "testing" {
7777
id = "testing"
78+
ignore = true
79+
}
80+
`,
81+
providerBlock,
82+
),
83+
Check: resource.ComposeAggregateTestCheckFunc(
84+
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "id", "testing"),
85+
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "ignore", "true"),
86+
resource.TestCheckNoResourceAttr("openwrt_dhcp_dhcp.testing", "dhcpv4"),
87+
resource.TestCheckNoResourceAttr("openwrt_dhcp_dhcp.testing", "dhcpv6"),
88+
resource.TestCheckNoResourceAttr("openwrt_dhcp_dhcp.testing", "interface"),
89+
resource.TestCheckNoResourceAttr("openwrt_dhcp_dhcp.testing", "leasetime"),
90+
resource.TestCheckNoResourceAttr("openwrt_dhcp_dhcp.testing", "limit"),
91+
resource.TestCheckNoResourceAttr("openwrt_dhcp_dhcp.testing", "ra_flags"),
92+
resource.TestCheckNoResourceAttr("openwrt_dhcp_dhcp.testing", "start"),
93+
),
94+
}
95+
importValidation := resource.TestStep{
96+
ImportState: true,
97+
ImportStateVerify: true,
98+
ResourceName: "openwrt_dhcp_dhcp.testing",
99+
}
100+
ignoreWithOtherAttributes := resource.TestStep{
101+
Config: fmt.Sprintf(`
102+
%s
103+
104+
resource "openwrt_dhcp_dhcp" "testing" {
105+
dhcpv4 = "server"
106+
dhcpv6 = "disabled"
107+
id = "testing"
108+
ignore = true
78109
interface = "testing"
79110
leasetime = "12h"
80111
limit = 150
112+
ra_flags = [
113+
"managed-config",
114+
"other-config",
115+
]
81116
start = 100
82117
}
83118
`,
84119
providerBlock,
85120
),
86121
Check: resource.ComposeAggregateTestCheckFunc(
87122
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "id", "testing"),
123+
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "dhcpv4", "server"),
124+
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "dhcpv6", "disabled"),
125+
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "ignore", "true"),
88126
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "interface", "testing"),
89127
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "leasetime", "12h"),
90128
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "limit", "150"),
129+
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "ra_flags.0", "managed-config"),
130+
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "ra_flags.1", "other-config"),
91131
resource.TestCheckResourceAttr("openwrt_dhcp_dhcp.testing", "start", "100"),
92132
),
93133
}
94-
importValidation := resource.TestStep{
95-
ImportState: true,
96-
ImportStateVerify: true,
97-
ResourceName: "openwrt_dhcp_dhcp.testing",
98-
}
99134
updateAndReadResource := resource.TestStep{
100135
Config: fmt.Sprintf(`
101136
%s
@@ -133,6 +168,7 @@ resource "openwrt_dhcp_dhcp" "testing" {
133168
t,
134169
createAndReadResource,
135170
importValidation,
171+
ignoreWithOtherAttributes,
136172
updateAndReadResource,
137173
)
138174
}

openwrt/internal/lucirpcglue/attribute.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ const (
3333
var (
3434
_ validator.Bool = anyValidatorBool{}
3535

36+
_ validator.Bool = requiredIfAttributeNot[any]{}
37+
_ validator.Int64 = requiredIfAttributeNot[any]{}
38+
_ validator.List = requiredIfAttributeNot[any]{}
39+
_ validator.Set = requiredIfAttributeNot[any]{}
40+
_ validator.String = requiredIfAttributeNot[any]{}
41+
3642
_ validator.Bool = requiresAttribute[any]{}
3743
_ validator.Int64 = requiresAttribute[any]{}
3844
_ validator.List = requiresAttribute[any]{}
@@ -413,6 +419,18 @@ func ReadResponseOptionString[Model any](
413419
return ctx, model, diagnostics
414420
}
415421
}
422+
423+
func RequiredIfAttributeNotEqualBool(
424+
expression path.Expression,
425+
expected bool,
426+
) requiredIfAttributeNot[bool] {
427+
return requiredIfAttributeNotEqual(
428+
types.BoolType,
429+
expression,
430+
expected,
431+
)
432+
}
433+
416434
func RequiresAttributeEqualBool(
417435
expression path.Expression,
418436
expected bool,
@@ -705,6 +723,163 @@ func hasValue(
705723
return !attribute.IsNull() && !attribute.IsUnknown()
706724
}
707725

726+
type requiredIfAttributeNot[Value any] struct {
727+
attrType attr.Type
728+
expected Value
729+
expression path.Expression
730+
}
731+
732+
func (a requiredIfAttributeNot[Value]) Description(ctx context.Context) string {
733+
return a.MarkdownDescription(ctx)
734+
}
735+
736+
func (a requiredIfAttributeNot[Value]) MarkdownDescription(ctx context.Context) string {
737+
return fmt.Sprintf("Ensures that an attribute is set, if %q is also set to %v", a.expression, a.expected)
738+
}
739+
740+
func (a requiredIfAttributeNot[Value]) ValidateBool(
741+
ctx context.Context,
742+
req validator.BoolRequest,
743+
res *validator.BoolResponse,
744+
) {
745+
diagnostics := a.validate(ctx, req.Config, req.Path, req.ConfigValue)
746+
res.Diagnostics.Append(diagnostics...)
747+
if res.Diagnostics.HasError() {
748+
return
749+
}
750+
}
751+
752+
func (a requiredIfAttributeNot[Value]) ValidateInt64(
753+
ctx context.Context,
754+
req validator.Int64Request,
755+
res *validator.Int64Response,
756+
) {
757+
diagnostics := a.validate(ctx, req.Config, req.Path, req.ConfigValue)
758+
res.Diagnostics.Append(diagnostics...)
759+
if res.Diagnostics.HasError() {
760+
return
761+
}
762+
}
763+
764+
func (a requiredIfAttributeNot[Value]) ValidateList(
765+
ctx context.Context,
766+
req validator.ListRequest,
767+
res *validator.ListResponse,
768+
) {
769+
diagnostics := a.validate(ctx, req.Config, req.Path, req.ConfigValue)
770+
res.Diagnostics.Append(diagnostics...)
771+
if res.Diagnostics.HasError() {
772+
return
773+
}
774+
}
775+
776+
func (a requiredIfAttributeNot[Value]) ValidateSet(
777+
ctx context.Context,
778+
req validator.SetRequest,
779+
res *validator.SetResponse,
780+
) {
781+
diagnostics := a.validate(ctx, req.Config, req.Path, req.ConfigValue)
782+
res.Diagnostics.Append(diagnostics...)
783+
if res.Diagnostics.HasError() {
784+
return
785+
}
786+
}
787+
788+
func (a requiredIfAttributeNot[Value]) ValidateString(
789+
ctx context.Context,
790+
req validator.StringRequest,
791+
res *validator.StringResponse,
792+
) {
793+
diagnostics := a.validate(ctx, req.Config, req.Path, req.ConfigValue)
794+
res.Diagnostics.Append(diagnostics...)
795+
if res.Diagnostics.HasError() {
796+
return
797+
}
798+
}
799+
800+
func (a requiredIfAttributeNot[Value]) validate(
801+
ctx context.Context,
802+
config tfsdk.Config,
803+
requestPath path.Path,
804+
configValue interface{ IsNull() bool },
805+
) (allDiagnostics diag.Diagnostics) {
806+
if !configValue.IsNull() {
807+
return
808+
}
809+
810+
matchedPaths, diagnostics := config.PathMatches(ctx, a.expression)
811+
allDiagnostics.Append(diagnostics...)
812+
if allDiagnostics.HasError() {
813+
return
814+
}
815+
816+
for _, matchedPath := range matchedPaths {
817+
if matchedPath.Equal(requestPath) {
818+
allDiagnostics.Append(
819+
validatordiag.BugInProviderDiagnostic(
820+
fmt.Sprintf("Attribute %q cannot require itself to have a specific value", requestPath),
821+
),
822+
)
823+
continue
824+
}
825+
826+
var actual attr.Value
827+
diagnostics = config.GetAttribute(ctx, matchedPath, &actual)
828+
allDiagnostics.Append(diagnostics...)
829+
if allDiagnostics.HasError() {
830+
continue
831+
}
832+
833+
if actual.IsUnknown() {
834+
// Ignore this value until it is known.
835+
continue
836+
}
837+
838+
if actual.IsNull() {
839+
// If the value is null,
840+
// it cannot be what we expect.
841+
// We ignore the value.
842+
continue
843+
}
844+
845+
var expected attr.Value
846+
diagnostics = tfsdk.ValueFrom(ctx, a.expected, a.attrType, &expected)
847+
allDiagnostics.Append(diagnostics...)
848+
if allDiagnostics.HasError() {
849+
continue
850+
}
851+
852+
if !actual.Equal(expected) {
853+
allDiagnostics.Append(
854+
diag.NewAttributeErrorDiagnostic(
855+
requestPath,
856+
"Missing required argument",
857+
fmt.Sprintf("Attribute %q is required when %q is not %v", requestPath, matchedPath, expected),
858+
),
859+
)
860+
continue
861+
}
862+
}
863+
864+
if allDiagnostics.HasError() {
865+
return
866+
}
867+
868+
return
869+
}
870+
871+
func requiredIfAttributeNotEqual[Value any](
872+
attrType attr.Type,
873+
expression path.Expression,
874+
expected Value,
875+
) requiredIfAttributeNot[Value] {
876+
return requiredIfAttributeNot[Value]{
877+
attrType: attrType,
878+
expected: expected,
879+
expression: expression,
880+
}
881+
}
882+
708883
type requiresAttribute[Value any] struct {
709884
attrType attr.Type
710885
expected Value

0 commit comments

Comments
 (0)