diff --git a/docs/resources/network.md b/docs/resources/network.md index 089eedaf..c11dad6b 100644 --- a/docs/resources/network.md +++ b/docs/resources/network.md @@ -4,11 +4,17 @@ page_title: "stackit_network Resource - stackit" subcategory: "" description: |- Network resource schema. Must have a region specified in the provider configuration. + ~> 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. + To prevent any nameserver configuration, the ipv4_nameservers attribute should be explicitly set to an empty list []. + In cases where ipv4_nameservers are defined within the resource, the existing behavior will remain unchanged. --- # stackit_network (Resource) Network resource schema. Must have a `region` specified in the provider configuration. +~> 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`. +To prevent any nameserver configuration, the `ipv4_nameservers` attribute should be explicitly set to an empty list `[]`. +In cases where `ipv4_nameservers` are defined within the resource, the existing behavior will remain unchanged. ## Example Usage @@ -68,7 +74,7 @@ import { - `ipv6_prefix` (String) The IPv6 prefix of the network (CIDR). - `ipv6_prefix_length` (Number) The IPv6 prefix length of the network. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container -- `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. +- `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. - `no_ipv4_gateway` (Boolean) If set to `true`, the network doesn't have a gateway. - `no_ipv6_gateway` (Boolean) If set to `true`, the network doesn't have a gateway. - `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. - `ipv4_prefixes` (List of String) The IPv4 prefixes of the network. - `ipv6_prefixes` (List of String) The IPv6 prefixes of the network. - `network_id` (String) The network ID. -- `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. +- `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. - `public_ip` (String) The public IP of the network. diff --git a/stackit/internal/services/iaas/network/resource.go b/stackit/internal/services/iaas/network/resource.go index 365a303e..a1ea9e5d 100644 --- a/stackit/internal/services/iaas/network/resource.go +++ b/stackit/internal/services/iaas/network/resource.go @@ -2,9 +2,11 @@ package network import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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" @@ -37,6 +39,13 @@ var ( _ resource.ResourceWithImportState = &networkResource{} ) +const ( + ipv4BehaviorChangeTitle = "Behavior of not configured `ipv4_nameservers` will change from January 2026" + ipv4BehaviorChangeDescription = "When `ipv4_nameservers` is not set, it will be set to the network area's `default_nameservers`.\n" + + "To prevent any nameserver configuration, the `ipv4_nameservers` attribute should be explicitly set to an empty list `[]`.\n" + + "In cases where `ipv4_nameservers` are defined within the resource, the existing behavior will remain unchanged." +) + // NewNetworkResource is a helper function to simplify the provider implementation. func NewNetworkResource() resource.Resource { return &networkResource{} @@ -88,10 +97,6 @@ func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureR // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - // If the v1 api is used, it's not required to get the fallback region because it isn't used - if !r.isExperimental { - return - } var configModel model.Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { @@ -108,6 +113,15 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla return } + // Warning should only be shown during the plan of the creation. This can be detected by checking if the ID is set. + if utils.IsUndefined(planModel.Id) && utils.IsUndefined(planModel.IPv4Nameservers) { + addIPv4Warning(&resp.Diagnostics) + } + + // If the v1 api is used, it's not required to get the fallback region because it isn't used + if !r.isExperimental { + return + } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) if resp.Diagnostics.HasError() { return @@ -171,8 +185,11 @@ func (r *networkResource) ConfigValidators(_ context.Context) []resource.ConfigV // Schema defines the schema for the resource. func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + description := "Network resource schema. Must have a `region` specified in the provider configuration." + descriptionNote := fmt.Sprintf("~> %s. %s", ipv4BehaviorChangeTitle, ipv4BehaviorChangeDescription) resp.Schema = schema.Schema{ - Description: "Network resource schema. Must have a `region` specified in the provider configuration.", + MarkdownDescription: fmt.Sprintf("%s\n%s", description, descriptionNote), + Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ 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 }, }, "nameservers": schema.ListAttribute{ - Description: "The nameservers of the network. This field is deprecated and will be removed soon, use `ipv4_nameservers` to configure the nameservers for IPv4.", + 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.", DeprecationMessage: "Use `ipv4_nameservers` to configure the nameservers for IPv4.", Optional: true, Computed: true, @@ -259,7 +276,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "prefixes": schema.ListAttribute{ - 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.", + 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.", DeprecationMessage: "Use `ipv4_prefixes` to read the prefixes of the IPv4 networks.", Computed: true, ElementType: types.StringType, @@ -299,6 +316,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re "ipv6_prefix": schema.StringAttribute{ Description: "The IPv6 prefix of the network (CIDR).", Optional: true, + Computed: true, Validators: []validator.String{ validate.CIDR(), }, @@ -309,6 +327,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re "ipv6_prefix_length": schema.Int64Attribute{ Description: "The IPv6 prefix length of the network.", Optional: true, + Computed: true, }, "ipv6_prefixes": schema.ListAttribute{ Description: "The IPv6 prefixes of the network.", @@ -366,6 +385,18 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re // Create creates the resource and sets the initial Terraform state. func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var planModel model.Model + diags := req.Plan.Get(ctx, &planModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // When IPv4Nameserver is not set, print warning that the behavior of ipv4_nameservers will change + if utils.IsUndefined(planModel.IPv4Nameservers) { + addIPv4Warning(&resp.Diagnostics) + } + if !r.isExperimental { v1network.Create(ctx, req, resp, r.client) } else { @@ -409,3 +440,9 @@ func (r *networkResource) ImportState(ctx context.Context, req resource.ImportSt v2network.ImportState(ctx, req, resp) } } + +func addIPv4Warning(diags *diag.Diagnostics) { + diags.AddAttributeWarning(path.Root("ipv4_nameservers"), + ipv4BehaviorChangeTitle, + ipv4BehaviorChangeDescription) +} diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource.go b/stackit/internal/services/iaas/network/utils/v1network/resource.go index ddb91ed6..fa21084d 100644 --- a/stackit/internal/services/iaas/network/utils/v1network/resource.go +++ b/stackit/internal/services/iaas/network/utils/v1network/resource.go @@ -322,8 +322,10 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod model.IPv6Nameservers = ipv6NameserversTF } - if networkResp.PrefixesV6 == nil { + if networkResp.PrefixesV6 == nil || len(*networkResp.PrefixesV6) == 0 { model.IPv6Prefixes = types.ListNull(types.StringType) + model.IPv6Prefix = types.StringNull() + model.IPv6PrefixLength = types.Int64Null() } else { respPrefixesV6 := *networkResp.PrefixesV6 prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) @@ -367,21 +369,32 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.Crea } addressFamily := &iaas.CreateNetworkAddressFamily{} - modelIPv6Nameservers := []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") + var modelIPv6Nameservers []string + // Is true when IPv6Nameservers is not null or unset + if !utils.IsUndefined(model.IPv6Nameservers) { + // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. + // 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 + modelIPv6Nameservers = []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { + ipv6NameserverString, ok := ipv6ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } - if !(model.IPv6Prefix.IsNull() || model.IPv6PrefixLength.IsNull() || model.IPv6Nameservers.IsNull()) { + if !utils.IsUndefined(model.IPv6Prefix) || !utils.IsUndefined(model.IPv6PrefixLength) || (modelIPv6Nameservers != nil) { addressFamily.Ipv6 = &iaas.CreateNetworkIPv6Body{ - Nameservers: &modelIPv6Nameservers, Prefix: conversion.StringValueToPointer(model.IPv6Prefix), PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength), } + // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. + // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, + // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. + if modelIPv6Nameservers != nil { + addressFamily.Ipv6.Nameservers = &modelIPv6Nameservers + } if model.NoIPv6Gateway.ValueBool() { addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil) @@ -445,23 +458,34 @@ func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) } addressFamily := &iaas.UpdateNetworkAddressFamily{} - modelIPv6Nameservers := []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") + var modelIPv6Nameservers []string + // Is true when IPv6Nameservers is not null or unset + if !utils.IsUndefined(model.IPv6Nameservers) { + // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. + // 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 + modelIPv6Nameservers = []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { + ipv6NameserverString, ok := ipv6ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } - if !(model.IPv6Nameservers.IsNull() || model.IPv6Nameservers.IsUnknown()) { - addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{ - Nameservers: &modelIPv6Nameservers, + if !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) || modelIPv6Nameservers != nil { + addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{} + + // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. + // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, + // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. + if modelIPv6Nameservers != nil { + addressFamily.Ipv6.Nameservers = &modelIPv6Nameservers } if model.NoIPv6Gateway.ValueBool() { addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + } else if !utils.IsUndefined(model.IPv6Gateway) { addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) } } diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource_test.go b/stackit/internal/services/iaas/network/utils/v1network/resource_test.go index 21db0a1d..9a1f289a 100644 --- a/stackit/internal/services/iaas/network/utils/v1network/resource_test.go +++ b/stackit/internal/services/iaas/network/utils/v1network/resource_test.go @@ -466,6 +466,66 @@ func TestToCreatePayload(t *testing.T) { }, true, }, + { + "ipv6_nameserver_null", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Value(24), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(false), + IPv6Gateway: types.StringValue("gateway"), + IPv6Prefix: types.StringValue("prefix"), + }, + &iaas.CreateNetworkPayload{ + Name: utils.Ptr("name"), + AddressFamily: &iaas.CreateNetworkAddressFamily{ + Ipv6: &iaas.CreateNetworkIPv6Body{ + Nameservers: nil, + PrefixLength: utils.Ptr(int64(24)), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), + Prefix: utils.Ptr("prefix"), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(false), + }, + true, + }, + { + "ipv6_nameserver_empty_list", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), + IPv6PrefixLength: types.Int64Value(24), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(false), + IPv6Gateway: types.StringValue("gateway"), + IPv6Prefix: types.StringValue("prefix"), + }, + &iaas.CreateNetworkPayload{ + Name: utils.Ptr("name"), + AddressFamily: &iaas.CreateNetworkAddressFamily{ + Ipv6: &iaas.CreateNetworkIPv6Body{ + Nameservers: utils.Ptr([]string{}), + PrefixLength: utils.Ptr(int64(24)), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), + Prefix: utils.Ptr("prefix"), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(false), + }, + true, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { @@ -670,6 +730,66 @@ func TestToUpdatePayload(t *testing.T) { }, true, }, + { + "ipv6_nameserver_null", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListNull(types.StringType), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv6Gateway: types.StringValue("gateway"), + }, + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Labels: types.MapNull(types.StringType), + }, + &iaas.PartialUpdateNetworkPayload{ + Name: utils.Ptr("name"), + AddressFamily: &iaas.UpdateNetworkAddressFamily{ + Ipv6: &iaas.UpdateNetworkIPv6Body{ + Nameservers: nil, + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, + { + "ipv6_nameserver_empty_list", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv6Gateway: types.StringValue("gateway"), + }, + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Labels: types.MapNull(types.StringType), + }, + &iaas.PartialUpdateNetworkPayload{ + Name: utils.Ptr("name"), + AddressFamily: &iaas.UpdateNetworkAddressFamily{ + Ipv6: &iaas.UpdateNetworkIPv6Body{ + Nameservers: &[]string{}, + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource.go b/stackit/internal/services/iaas/network/utils/v2network/resource.go index 2bfbe0d5..dbf31820 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/resource.go +++ b/stackit/internal/services/iaas/network/utils/v2network/resource.go @@ -389,23 +389,34 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha return nil, fmt.Errorf("nil model") } - modelIPv6Nameservers := []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") + var modelIPv6Nameservers []string + // Is true when IPv6Nameservers is not null or unset + if !utils.IsUndefined(model.IPv6Nameservers) { + // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. + // 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 + modelIPv6Nameservers = []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { + ipv6NameserverString, ok := ipv6ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } var ipv6Body *iaasalpha.CreateNetworkIPv6 if !utils.IsUndefined(model.IPv6PrefixLength) { ipv6Body = &iaasalpha.CreateNetworkIPv6{ CreateNetworkIPv6WithPrefixLength: &iaasalpha.CreateNetworkIPv6WithPrefixLength{ - Nameservers: &modelIPv6Nameservers, PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength), }, } + // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. + // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, + // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. + if modelIPv6Nameservers != nil { + ipv6Body.CreateNetworkIPv6WithPrefixLength.Nameservers = &modelIPv6Nameservers + } } else if !utils.IsUndefined(model.IPv6Prefix) { var gateway *iaasalpha.NullableString if model.NoIPv6Gateway.ValueBool() { @@ -416,11 +427,16 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha ipv6Body = &iaasalpha.CreateNetworkIPv6{ CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ - Gateway: gateway, - Nameservers: &modelIPv6Nameservers, - Prefix: conversion.StringValueToPointer(model.IPv6Prefix), + Gateway: gateway, + Prefix: conversion.StringValueToPointer(model.IPv6Prefix), }, } + // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. + // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, + // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. + if modelIPv6Nameservers != nil { + ipv6Body.CreateNetworkIPv6WithPrefix.Nameservers = &modelIPv6Nameservers + } } modelIPv4Nameservers := []string{} @@ -487,19 +503,29 @@ func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) return nil, fmt.Errorf("nil model") } - modelIPv6Nameservers := []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") + var modelIPv6Nameservers []string + // Is true when IPv6Nameservers is not null or unset + if !utils.IsUndefined(model.IPv6Nameservers) { + // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. + // 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 + modelIPv6Nameservers = []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { + ipv6NameserverString, ok := ipv6ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } var ipv6Body *iaasalpha.UpdateNetworkIPv6Body - if !(model.IPv6Nameservers.IsNull() || model.IPv6Nameservers.IsUnknown()) { - ipv6Body = &iaasalpha.UpdateNetworkIPv6Body{ - Nameservers: &modelIPv6Nameservers, + if modelIPv6Nameservers != nil || !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) { + ipv6Body = &iaasalpha.UpdateNetworkIPv6Body{} + // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. + // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, + // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. + if modelIPv6Nameservers != nil { + ipv6Body.Nameservers = &modelIPv6Nameservers } if model.NoIPv6Gateway.ValueBool() { diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go b/stackit/internal/services/iaas/network/utils/v2network/resource_test.go index 93575f07..6f39b9a3 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go +++ b/stackit/internal/services/iaas/network/utils/v2network/resource_test.go @@ -492,6 +492,62 @@ func TestToCreatePayload(t *testing.T) { }, true, }, + { + "ipv6_nameserver_null", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListNull(types.StringType), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(false), + IPv6Gateway: types.StringValue("gateway"), + IPv6Prefix: types.StringValue("prefix"), + }, + &iaasalpha.CreateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv6: &iaasalpha.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ + Nameservers: nil, + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Prefix: utils.Ptr("prefix"), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(false), + }, + true, + }, + { + "ipv6_nameserver_empty_list", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(false), + IPv6Gateway: types.StringValue("gateway"), + IPv6Prefix: types.StringValue("prefix"), + }, + &iaasalpha.CreateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv6: &iaasalpha.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ + Nameservers: utils.Ptr([]string{}), + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Prefix: utils.Ptr("prefix"), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(false), + }, + true, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { @@ -686,6 +742,62 @@ func TestToUpdatePayload(t *testing.T) { }, true, }, + { + "ipv6_nameserver_null", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListNull(types.StringType), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv6Gateway: types.StringValue("gateway"), + }, + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Labels: types.MapNull(types.StringType), + }, + &iaasalpha.PartialUpdateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ + Nameservers: nil, + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, + { + "ipv6_nameserver_empty_list", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv6Gateway: types.StringValue("gateway"), + }, + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Labels: types.MapNull(types.StringType), + }, + &iaasalpha.PartialUpdateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ + Nameservers: utils.Ptr([]string{}), + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) {