Skip to content

Commit 4552b14

Browse files
authored
feat(iaas): add warning that behavior of network resource will change (#1031)
relates to STACKITTPR-366 * feat(iaas): add warning that behavior of network resource will change * fix: changed payload for ipv6_nameservers * if is unset / null: ipv6_nameservers will not be sent * if set list / empty list: ipv6_nameserver will be sent with the set list / empty list
1 parent 10eced4 commit 4552b14

File tree

6 files changed

+372
-47
lines changed

6 files changed

+372
-47
lines changed

docs/resources/network.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@ page_title: "stackit_network Resource - stackit"
44
subcategory: ""
55
description: |-
66
Network resource schema. Must have a region specified in the provider configuration.
7+
~> Behavior of not configured ipv4_nameservers will change from January 2026. When ipv4_nameservers is not set, it will be set to the network area's default_nameservers.
8+
To prevent any nameserver configuration, the ipv4_nameservers attribute should be explicitly set to an empty list [].
9+
In cases where ipv4_nameservers are defined within the resource, the existing behavior will remain unchanged.
710
---
811

912
# stackit_network (Resource)
1013

1114
Network resource schema. Must have a `region` specified in the provider configuration.
15+
~> Behavior of not configured `ipv4_nameservers` will change from January 2026. When `ipv4_nameservers` is not set, it will be set to the network area's `default_nameservers`.
16+
To prevent any nameserver configuration, the `ipv4_nameservers` attribute should be explicitly set to an empty list `[]`.
17+
In cases where `ipv4_nameservers` are defined within the resource, the existing behavior will remain unchanged.
1218

1319
## Example Usage
1420

@@ -68,7 +74,7 @@ import {
6874
- `ipv6_prefix` (String) The IPv6 prefix of the network (CIDR).
6975
- `ipv6_prefix_length` (Number) The IPv6 prefix length of the network.
7076
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
71-
- `nameservers` (List of String, Deprecated) The nameservers of the network. This field is deprecated and will be removed soon, use `ipv4_nameservers` to configure the nameservers for IPv4.
77+
- `nameservers` (List of String, Deprecated) The nameservers of the network. This field is deprecated and will be removed in January 2026, use `ipv4_nameservers` to configure the nameservers for IPv4.
7278
- `no_ipv4_gateway` (Boolean) If set to `true`, the network doesn't have a gateway.
7379
- `no_ipv6_gateway` (Boolean) If set to `true`, the network doesn't have a gateway.
7480
- `region` (String) Can only be used when experimental "network" is set.
@@ -83,5 +89,5 @@ The ID of the routing table associated with the network.
8389
- `ipv4_prefixes` (List of String) The IPv4 prefixes of the network.
8490
- `ipv6_prefixes` (List of String) The IPv6 prefixes of the network.
8591
- `network_id` (String) The network ID.
86-
- `prefixes` (List of String, Deprecated) The prefixes of the network. This field is deprecated and will be removed soon, use `ipv4_prefixes` to read the prefixes of the IPv4 networks.
92+
- `prefixes` (List of String, Deprecated) The prefixes of the network. This field is deprecated and will be removed in January 2026, use `ipv4_prefixes` to read the prefixes of the IPv4 networks.
8793
- `public_ip` (String) The public IP of the network.

stackit/internal/services/iaas/network/resource.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package network
22

33
import (
44
"context"
5+
"fmt"
56

67
"github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
78
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
810
"github.com/hashicorp/terraform-plugin-framework/path"
911
"github.com/hashicorp/terraform-plugin-framework/resource"
1012
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -37,6 +39,13 @@ var (
3739
_ resource.ResourceWithImportState = &networkResource{}
3840
)
3941

42+
const (
43+
ipv4BehaviorChangeTitle = "Behavior of not configured `ipv4_nameservers` will change from January 2026"
44+
ipv4BehaviorChangeDescription = "When `ipv4_nameservers` is not set, it will be set to the network area's `default_nameservers`.\n" +
45+
"To prevent any nameserver configuration, the `ipv4_nameservers` attribute should be explicitly set to an empty list `[]`.\n" +
46+
"In cases where `ipv4_nameservers` are defined within the resource, the existing behavior will remain unchanged."
47+
)
48+
4049
// NewNetworkResource is a helper function to simplify the provider implementation.
4150
func NewNetworkResource() resource.Resource {
4251
return &networkResource{}
@@ -88,10 +97,6 @@ func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureR
8897
// ModifyPlan implements resource.ResourceWithModifyPlan.
8998
// Use the modifier to set the effective region in the current plan.
9099
func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
91-
// If the v1 api is used, it's not required to get the fallback region because it isn't used
92-
if !r.isExperimental {
93-
return
94-
}
95100
var configModel model.Model
96101
// skip initial empty configuration to avoid follow-up errors
97102
if req.Config.Raw.IsNull() {
@@ -108,6 +113,15 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla
108113
return
109114
}
110115

116+
// Warning should only be shown during the plan of the creation. This can be detected by checking if the ID is set.
117+
if utils.IsUndefined(planModel.Id) && utils.IsUndefined(planModel.IPv4Nameservers) {
118+
addIPv4Warning(&resp.Diagnostics)
119+
}
120+
121+
// If the v1 api is used, it's not required to get the fallback region because it isn't used
122+
if !r.isExperimental {
123+
return
124+
}
111125
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
112126
if resp.Diagnostics.HasError() {
113127
return
@@ -171,8 +185,11 @@ func (r *networkResource) ConfigValidators(_ context.Context) []resource.ConfigV
171185

172186
// Schema defines the schema for the resource.
173187
func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
188+
description := "Network resource schema. Must have a `region` specified in the provider configuration."
189+
descriptionNote := fmt.Sprintf("~> %s. %s", ipv4BehaviorChangeTitle, ipv4BehaviorChangeDescription)
174190
resp.Schema = schema.Schema{
175-
Description: "Network resource schema. Must have a `region` specified in the provider configuration.",
191+
MarkdownDescription: fmt.Sprintf("%s\n%s", description, descriptionNote),
192+
Description: description,
176193
Attributes: map[string]schema.Attribute{
177194
"id": schema.StringAttribute{
178195
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`\".",
@@ -212,7 +229,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
212229
},
213230
},
214231
"nameservers": schema.ListAttribute{
215-
Description: "The nameservers of the network. This field is deprecated and will be removed soon, use `ipv4_nameservers` to configure the nameservers for IPv4.",
232+
Description: "The nameservers of the network. This field is deprecated and will be removed in January 2026, use `ipv4_nameservers` to configure the nameservers for IPv4.",
216233
DeprecationMessage: "Use `ipv4_nameservers` to configure the nameservers for IPv4.",
217234
Optional: true,
218235
Computed: true,
@@ -259,7 +276,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
259276
},
260277
},
261278
"prefixes": schema.ListAttribute{
262-
Description: "The prefixes of the network. This field is deprecated and will be removed soon, use `ipv4_prefixes` to read the prefixes of the IPv4 networks.",
279+
Description: "The prefixes of the network. This field is deprecated and will be removed in January 2026, use `ipv4_prefixes` to read the prefixes of the IPv4 networks.",
263280
DeprecationMessage: "Use `ipv4_prefixes` to read the prefixes of the IPv4 networks.",
264281
Computed: true,
265282
ElementType: types.StringType,
@@ -299,6 +316,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
299316
"ipv6_prefix": schema.StringAttribute{
300317
Description: "The IPv6 prefix of the network (CIDR).",
301318
Optional: true,
319+
Computed: true,
302320
Validators: []validator.String{
303321
validate.CIDR(),
304322
},
@@ -309,6 +327,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
309327
"ipv6_prefix_length": schema.Int64Attribute{
310328
Description: "The IPv6 prefix length of the network.",
311329
Optional: true,
330+
Computed: true,
312331
},
313332
"ipv6_prefixes": schema.ListAttribute{
314333
Description: "The IPv6 prefixes of the network.",
@@ -366,6 +385,18 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
366385

367386
// Create creates the resource and sets the initial Terraform state.
368387
func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
388+
// Retrieve values from plan
389+
var planModel model.Model
390+
diags := req.Plan.Get(ctx, &planModel)
391+
resp.Diagnostics.Append(diags...)
392+
if resp.Diagnostics.HasError() {
393+
return
394+
}
395+
// When IPv4Nameserver is not set, print warning that the behavior of ipv4_nameservers will change
396+
if utils.IsUndefined(planModel.IPv4Nameservers) {
397+
addIPv4Warning(&resp.Diagnostics)
398+
}
399+
369400
if !r.isExperimental {
370401
v1network.Create(ctx, req, resp, r.client)
371402
} else {
@@ -409,3 +440,9 @@ func (r *networkResource) ImportState(ctx context.Context, req resource.ImportSt
409440
v2network.ImportState(ctx, req, resp)
410441
}
411442
}
443+
444+
func addIPv4Warning(diags *diag.Diagnostics) {
445+
diags.AddAttributeWarning(path.Root("ipv4_nameservers"),
446+
ipv4BehaviorChangeTitle,
447+
ipv4BehaviorChangeDescription)
448+
}

stackit/internal/services/iaas/network/utils/v1network/resource.go

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,10 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod
322322
model.IPv6Nameservers = ipv6NameserversTF
323323
}
324324

325-
if networkResp.PrefixesV6 == nil {
325+
if networkResp.PrefixesV6 == nil || len(*networkResp.PrefixesV6) == 0 {
326326
model.IPv6Prefixes = types.ListNull(types.StringType)
327+
model.IPv6Prefix = types.StringNull()
328+
model.IPv6PrefixLength = types.Int64Null()
327329
} else {
328330
respPrefixesV6 := *networkResp.PrefixesV6
329331
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
@@ -367,21 +369,32 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.Crea
367369
}
368370
addressFamily := &iaas.CreateNetworkAddressFamily{}
369371

370-
modelIPv6Nameservers := []string{}
371-
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
372-
ipv6NameserverString, ok := ipv6ns.(types.String)
373-
if !ok {
374-
return nil, fmt.Errorf("type assertion failed")
372+
var modelIPv6Nameservers []string
373+
// Is true when IPv6Nameservers is not null or unset
374+
if !utils.IsUndefined(model.IPv6Nameservers) {
375+
// If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice.
376+
// empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set
377+
modelIPv6Nameservers = []string{}
378+
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
379+
ipv6NameserverString, ok := ipv6ns.(types.String)
380+
if !ok {
381+
return nil, fmt.Errorf("type assertion failed")
382+
}
383+
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
375384
}
376-
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
377385
}
378386

379-
if !(model.IPv6Prefix.IsNull() || model.IPv6PrefixLength.IsNull() || model.IPv6Nameservers.IsNull()) {
387+
if !utils.IsUndefined(model.IPv6Prefix) || !utils.IsUndefined(model.IPv6PrefixLength) || (modelIPv6Nameservers != nil) {
380388
addressFamily.Ipv6 = &iaas.CreateNetworkIPv6Body{
381-
Nameservers: &modelIPv6Nameservers,
382389
Prefix: conversion.StringValueToPointer(model.IPv6Prefix),
383390
PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength),
384391
}
392+
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
393+
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
394+
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
395+
if modelIPv6Nameservers != nil {
396+
addressFamily.Ipv6.Nameservers = &modelIPv6Nameservers
397+
}
385398

386399
if model.NoIPv6Gateway.ValueBool() {
387400
addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil)
@@ -445,23 +458,34 @@ func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model)
445458
}
446459
addressFamily := &iaas.UpdateNetworkAddressFamily{}
447460

448-
modelIPv6Nameservers := []string{}
449-
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
450-
ipv6NameserverString, ok := ipv6ns.(types.String)
451-
if !ok {
452-
return nil, fmt.Errorf("type assertion failed")
461+
var modelIPv6Nameservers []string
462+
// Is true when IPv6Nameservers is not null or unset
463+
if !utils.IsUndefined(model.IPv6Nameservers) {
464+
// If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice.
465+
// empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set
466+
modelIPv6Nameservers = []string{}
467+
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
468+
ipv6NameserverString, ok := ipv6ns.(types.String)
469+
if !ok {
470+
return nil, fmt.Errorf("type assertion failed")
471+
}
472+
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
453473
}
454-
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
455474
}
456475

457-
if !(model.IPv6Nameservers.IsNull() || model.IPv6Nameservers.IsUnknown()) {
458-
addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{
459-
Nameservers: &modelIPv6Nameservers,
476+
if !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) || modelIPv6Nameservers != nil {
477+
addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{}
478+
479+
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
480+
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
481+
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
482+
if modelIPv6Nameservers != nil {
483+
addressFamily.Ipv6.Nameservers = &modelIPv6Nameservers
460484
}
461485

462486
if model.NoIPv6Gateway.ValueBool() {
463487
addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil)
464-
} else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) {
488+
} else if !utils.IsUndefined(model.IPv6Gateway) {
465489
addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
466490
}
467491
}

stackit/internal/services/iaas/network/utils/v1network/resource_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,66 @@ func TestToCreatePayload(t *testing.T) {
466466
},
467467
true,
468468
},
469+
{
470+
"ipv6_nameserver_null",
471+
&model.Model{
472+
Name: types.StringValue("name"),
473+
IPv6Nameservers: types.ListNull(types.StringType),
474+
IPv6PrefixLength: types.Int64Value(24),
475+
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
476+
"key": types.StringValue("value"),
477+
}),
478+
Routed: types.BoolValue(false),
479+
IPv6Gateway: types.StringValue("gateway"),
480+
IPv6Prefix: types.StringValue("prefix"),
481+
},
482+
&iaas.CreateNetworkPayload{
483+
Name: utils.Ptr("name"),
484+
AddressFamily: &iaas.CreateNetworkAddressFamily{
485+
Ipv6: &iaas.CreateNetworkIPv6Body{
486+
Nameservers: nil,
487+
PrefixLength: utils.Ptr(int64(24)),
488+
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
489+
Prefix: utils.Ptr("prefix"),
490+
},
491+
},
492+
Labels: &map[string]interface{}{
493+
"key": "value",
494+
},
495+
Routed: utils.Ptr(false),
496+
},
497+
true,
498+
},
499+
{
500+
"ipv6_nameserver_empty_list",
501+
&model.Model{
502+
Name: types.StringValue("name"),
503+
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}),
504+
IPv6PrefixLength: types.Int64Value(24),
505+
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
506+
"key": types.StringValue("value"),
507+
}),
508+
Routed: types.BoolValue(false),
509+
IPv6Gateway: types.StringValue("gateway"),
510+
IPv6Prefix: types.StringValue("prefix"),
511+
},
512+
&iaas.CreateNetworkPayload{
513+
Name: utils.Ptr("name"),
514+
AddressFamily: &iaas.CreateNetworkAddressFamily{
515+
Ipv6: &iaas.CreateNetworkIPv6Body{
516+
Nameservers: utils.Ptr([]string{}),
517+
PrefixLength: utils.Ptr(int64(24)),
518+
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
519+
Prefix: utils.Ptr("prefix"),
520+
},
521+
},
522+
Labels: &map[string]interface{}{
523+
"key": "value",
524+
},
525+
Routed: utils.Ptr(false),
526+
},
527+
true,
528+
},
469529
}
470530
for _, tt := range tests {
471531
t.Run(tt.description, func(t *testing.T) {
@@ -670,6 +730,66 @@ func TestToUpdatePayload(t *testing.T) {
670730
},
671731
true,
672732
},
733+
{
734+
"ipv6_nameserver_null",
735+
&model.Model{
736+
Name: types.StringValue("name"),
737+
IPv6Nameservers: types.ListNull(types.StringType),
738+
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
739+
"key": types.StringValue("value"),
740+
}),
741+
Routed: types.BoolValue(true),
742+
IPv6Gateway: types.StringValue("gateway"),
743+
},
744+
model.Model{
745+
ProjectId: types.StringValue("pid"),
746+
NetworkId: types.StringValue("nid"),
747+
Labels: types.MapNull(types.StringType),
748+
},
749+
&iaas.PartialUpdateNetworkPayload{
750+
Name: utils.Ptr("name"),
751+
AddressFamily: &iaas.UpdateNetworkAddressFamily{
752+
Ipv6: &iaas.UpdateNetworkIPv6Body{
753+
Nameservers: nil,
754+
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
755+
},
756+
},
757+
Labels: &map[string]interface{}{
758+
"key": "value",
759+
},
760+
},
761+
true,
762+
},
763+
{
764+
"ipv6_nameserver_empty_list",
765+
&model.Model{
766+
Name: types.StringValue("name"),
767+
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}),
768+
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
769+
"key": types.StringValue("value"),
770+
}),
771+
Routed: types.BoolValue(true),
772+
IPv6Gateway: types.StringValue("gateway"),
773+
},
774+
model.Model{
775+
ProjectId: types.StringValue("pid"),
776+
NetworkId: types.StringValue("nid"),
777+
Labels: types.MapNull(types.StringType),
778+
},
779+
&iaas.PartialUpdateNetworkPayload{
780+
Name: utils.Ptr("name"),
781+
AddressFamily: &iaas.UpdateNetworkAddressFamily{
782+
Ipv6: &iaas.UpdateNetworkIPv6Body{
783+
Nameservers: &[]string{},
784+
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
785+
},
786+
},
787+
Labels: &map[string]interface{}{
788+
"key": "value",
789+
},
790+
},
791+
true,
792+
},
673793
}
674794
for _, tt := range tests {
675795
t.Run(tt.description, func(t *testing.T) {

0 commit comments

Comments
 (0)