Extend network resource fields (#576)

* deprecate nameservers filed

* onboard routed field

* onboard new ipv4 fields

* onboard new ipv6 fields

* update examples

* update examples

* update examples

* remove nameserver(deprecated) mapping

* make fields computed

* Revert "remove nameserver(deprecated) mapping"

This reverts commit e4bf1dc184289f3bddc10c5d3b2320966b529649.

* remove routed from update payload (not yet supported)

* Update docs/resources/network.md

Co-authored-by: João Palet <joao.palet@outlook.com>

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

Co-authored-by: João Palet <joao.palet@outlook.com>

* fix the field descriptions

* remove ipv6 from examples

* use nameservers as rollback value to support deprecated field

* extend acceptance tests

* add condition that nameserver and ipv4_nameserver cannot be provided at the same time

* extend acc test

* improve conditions in create payload

* adapt modify plan to support update and delete operations

* fix acceptance test

* deprecate prefixes and create ipv4_prefixes field

* fix unit tests

* fix update issues

* fix linter issues

* extend modifyPlan condition

* add validateConfig function

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

Co-authored-by: João Palet <joao.palet@outlook.com>

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

Co-authored-by: João Palet <joao.palet@outlook.com>

* update descriptions

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

Co-authored-by: João Palet <joao.palet@outlook.com>

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

Co-authored-by: João Palet <joao.palet@outlook.com>

* implement no_gateway field and condition check

* implement no_ipv6_gateway field and condition check

* update examples

* update examples and descriptions

* fix linter issues

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

Co-authored-by: João Palet <joao.palet@outlook.com>

* adapt descriptions

* apply acceptance comments

* adapt conditions in create and update

* add plan modifiers

* add requiresReplace

---------

Co-authored-by: João Palet <joao.palet@outlook.com>
This commit is contained in:
GokceGK 2024-11-19 15:25:56 +01:00 committed by GitHub
parent 3ac1d50253
commit b1fb9ab9b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 857 additions and 60 deletions

View file

@ -6,11 +6,14 @@ import (
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
"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/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
@ -40,10 +43,22 @@ type Model struct {
NetworkId types.String `tfsdk:"network_id"`
Name types.String `tfsdk:"name"`
Nameservers types.List `tfsdk:"nameservers"`
IPv4Gateway types.String `tfsdk:"ipv4_gateway"`
IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"`
IPv4Prefix types.String `tfsdk:"ipv4_prefix"`
IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"`
Prefixes types.List `tfsdk:"prefixes"`
IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"`
IPv6Gateway types.String `tfsdk:"ipv6_gateway"`
IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"`
IPv6Prefix types.String `tfsdk:"ipv6_prefix"`
IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"`
IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"`
PublicIP types.String `tfsdk:"public_ip"`
Labels types.Map `tfsdk:"labels"`
Routed types.Bool `tfsdk:"routed"`
NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"`
NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"`
}
// NewNetworkResource is a helper function to simplify the provider implementation.
@ -98,6 +113,32 @@ func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureR
tflog.Info(ctx, "IaaS client configured")
}
func (r networkResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var model Model
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
if !model.Nameservers.IsUnknown() && !model.IPv4Nameservers.IsUnknown() && !model.Nameservers.IsNull() && !model.IPv4Nameservers.IsNull() {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "You cannot provide both the `nameservers` and `ipv4_nameservers` fields simultaneously. Please remove the deprecated `nameservers` field, and use `ipv4_nameservers` to configure nameservers for IPv4.")
}
}
// ConfigValidators validates the resource configuration
func (r *networkResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
return []resource.ConfigValidator{
resourcevalidator.Conflicting(
path.MatchRoot("no_ipv4_gateway"),
path.MatchRoot("ipv4_gateway"),
),
resourcevalidator.Conflicting(
path.MatchRoot("no_ipv6_gateway"),
path.MatchRoot("ipv6_gateway"),
),
}
}
// Schema defines the schema for the resource.
func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
@ -141,32 +182,128 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
},
"nameservers": schema.ListAttribute{
Description: "The nameservers of the network.",
Description: "The nameservers of the network. This field is deprecated and will be removed soon, use `ipv4_nameservers` to configure the nameservers for IPv4.",
DeprecationMessage: "Use `ipv4_nameservers` to configure the nameservers for IPv4.",
Optional: true,
Computed: true,
ElementType: types.StringType,
},
"no_ipv4_gateway": schema.BoolAttribute{
Description: "If set to `true`, the network doesn't have a gateway.",
Optional: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"ipv4_gateway": schema.StringAttribute{
Description: "The IPv4 gateway of a network. If not specified, the first IP of the network will be assigned as the gateway.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
Validators: []validator.String{
validate.IP(),
},
},
"ipv4_nameservers": schema.ListAttribute{
Description: "The IPv4 nameservers of the network.",
Optional: true,
Computed: true,
ElementType: types.StringType,
},
"ipv4_prefix": schema.StringAttribute{
Description: "The IPv4 prefix of the network (CIDR).",
Optional: true,
Validators: []validator.String{
validate.CIDR(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"ipv4_prefix_length": schema.Int64Attribute{
Description: "The IPv4 prefix length of the network.",
Optional: true,
},
"prefixes": schema.ListAttribute{
Description: "The prefixes of the network.",
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.",
DeprecationMessage: "Use `ipv4_prefixes` to read the prefixes of the IPv4 networks.",
Computed: true,
ElementType: types.StringType,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
},
"ipv4_prefixes": schema.ListAttribute{
Description: "The IPv4 prefixes of the network.",
Computed: true,
ElementType: types.StringType,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
},
"no_ipv6_gateway": schema.BoolAttribute{
Description: "If set to `true`, the network doesn't have a gateway.",
Optional: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"ipv6_gateway": schema.StringAttribute{
Description: "The IPv6 gateway of a network. If not specified, the first IP of the network will be assigned as the gateway.",
Optional: true,
Computed: true,
Validators: []validator.String{
validate.IP(),
},
},
"ipv6_nameservers": schema.ListAttribute{
Description: "The IPv6 nameservers of the network.",
Optional: true,
Computed: true,
ElementType: types.StringType,
},
"ipv6_prefix": schema.StringAttribute{
Description: "The IPv6 prefix of the network (CIDR).",
Optional: true,
Validators: []validator.String{
validate.CIDR(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"ipv6_prefix_length": schema.Int64Attribute{
Description: "The IPv6 prefix length of the network.",
Optional: true,
},
"ipv6_prefixes": schema.ListAttribute{
Description: "The IPv6 prefixes of the network.",
Computed: true,
ElementType: types.StringType,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
},
"public_ip": schema.StringAttribute{
Description: "The public IP of the network.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"labels": schema.MapAttribute{
Description: "Labels are key-value string pairs which can be attached to a resource container",
ElementType: types.StringType,
Optional: true,
},
"routed": schema.BoolAttribute{
Description: "If set to `true`, the network is routed and therefore accessible from other networks.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
boolplanmodifier.RequiresReplace(),
},
},
},
}
}
@ -285,7 +422,7 @@ func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest
}
// Generate API request body from model
payload, err := toUpdatePayload(ctx, &model, stateModel.Labels)
payload, err := toUpdatePayload(ctx, &model, &stateModel)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err))
return
@ -407,27 +544,41 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *Model) err
labels = types.MapNull(types.StringType)
}
// IPv4
if networkResp.Nameservers == nil {
model.Nameservers = types.ListNull(types.StringType)
model.IPv4Nameservers = types.ListNull(types.StringType)
} else {
respNameservers := *networkResp.Nameservers
modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers)
modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers)
if err != nil {
return fmt.Errorf("get current network nameservers from model: %w", err)
}
if errIpv4 != nil {
return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4)
}
reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers)
reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers)
nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers)
ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers)
if diags.HasError() {
return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags))
}
if ipv4Diags.HasError() {
return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags))
}
model.Nameservers = nameserversTF
model.IPv4Nameservers = ipv4NameserversTF
}
if networkResp.Prefixes == nil {
model.Prefixes = types.ListNull(types.StringType)
model.IPv4Prefixes = types.ListNull(types.StringType)
} else {
respPrefixes := *networkResp.Prefixes
prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes)
@ -436,12 +587,59 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *Model) err
}
model.Prefixes = prefixesTF
model.IPv4Prefixes = prefixesTF
}
if networkResp.Gateway != nil {
model.IPv4Gateway = types.StringPointerValue(networkResp.GetGateway())
} else {
model.IPv4Gateway = types.StringNull()
}
// IPv6
if networkResp.NameserversV6 == nil {
model.IPv6Nameservers = types.ListNull(types.StringType)
} else {
respIPv6Nameservers := *networkResp.NameserversV6
modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers)
if errIpv6 != nil {
return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6)
}
reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers)
ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers)
if ipv6Diags.HasError() {
return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags))
}
model.IPv6Nameservers = ipv6NameserversTF
}
if networkResp.PrefixesV6 == nil {
model.IPv6Prefixes = types.ListNull(types.StringType)
} else {
respPrefixesV6 := *networkResp.PrefixesV6
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
if diags.HasError() {
return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags))
}
model.IPv6Prefixes = prefixesV6TF
}
if networkResp.Gatewayv6 != nil {
model.IPv6Gateway = types.StringPointerValue(networkResp.GetGatewayv6())
} else {
model.IPv6Gateway = types.StringNull()
}
model.NetworkId = types.StringValue(networkId)
model.Name = types.StringPointerValue(networkResp.Name)
model.PublicIP = types.StringPointerValue(networkResp.PublicIp)
model.Labels = labels
model.Routed = types.BoolPointerValue(networkResp.Routed)
return nil
}
@ -450,14 +648,60 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkPayl
if model == nil {
return nil, fmt.Errorf("nil model")
}
addressFamily := &iaas.CreateNetworkAddressFamily{}
modelNameservers := []string{}
for _, ns := range model.Nameservers.Elements() {
nameserverString, ok := ns.(types.String)
modelIPv6Nameservers := []string{}
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
ipv6NameserverString, ok := ipv6ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelNameservers = append(modelNameservers, nameserverString.ValueString())
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
}
if !(model.IPv6Prefix.IsNull() || model.IPv6PrefixLength.IsNull() || model.IPv6Nameservers.IsNull()) {
addressFamily.Ipv6 = &iaas.CreateNetworkIPv6Body{
Nameservers: &modelIPv6Nameservers,
Prefix: conversion.StringValueToPointer(model.IPv6Prefix),
PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength),
}
if model.NoIPv6Gateway.ValueBool() {
addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) {
addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
}
}
modelIPv4Nameservers := []string{}
var modelIPv4List []attr.Value
if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) {
modelIPv4List = model.IPv4Nameservers.Elements()
} else {
modelIPv4List = model.Nameservers.Elements()
}
for _, ipv4ns := range modelIPv4List {
ipv4NameserverString, ok := ipv4ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString())
}
if !model.IPv4Prefix.IsNull() || !model.IPv4PrefixLength.IsNull() || !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() {
addressFamily.Ipv4 = &iaas.CreateNetworkIPv4Body{
Nameservers: &modelIPv4Nameservers,
Prefix: conversion.StringValueToPointer(model.IPv4Prefix),
PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength),
}
if model.NoIPv4Gateway.ValueBool() {
addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) {
addressFamily.Ipv4.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway))
}
}
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
@ -465,44 +709,88 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkPayl
return nil, fmt.Errorf("converting to Go map: %w", err)
}
return &iaas.CreateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
AddressFamily: &iaas.CreateNetworkAddressFamily{
Ipv4: &iaas.CreateNetworkIPv4Body{
PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength),
Nameservers: &modelNameservers,
},
},
payload := iaas.CreateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
}, nil
Routed: conversion.BoolValueToPointer(model.Routed),
}
if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil {
payload.AddressFamily = addressFamily
}
return &payload, nil
}
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.PartialUpdateNetworkPayload, error) {
func toUpdatePayload(ctx context.Context, model, stateModel *Model) (*iaas.PartialUpdateNetworkPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
addressFamily := &iaas.UpdateNetworkAddressFamily{}
modelNameservers := []string{}
for _, ns := range model.Nameservers.Elements() {
nameserverString, ok := ns.(types.String)
modelIPv6Nameservers := []string{}
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
ipv6NameserverString, ok := ipv6ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelNameservers = append(modelNameservers, nameserverString.ValueString())
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
}
if !(model.IPv6Nameservers.IsNull() || model.IPv6Nameservers.IsUnknown()) {
addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{
Nameservers: &modelIPv6Nameservers,
}
if model.NoIPv6Gateway.ValueBool() {
addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) {
addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
}
}
modelIPv4Nameservers := []string{}
var modelIPv4List []attr.Value
if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) {
modelIPv4List = model.IPv4Nameservers.Elements()
} else {
modelIPv4List = model.Nameservers.Elements()
}
for _, ipv4ns := range modelIPv4List {
ipv4NameserverString, ok := ipv4ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString())
}
if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() {
addressFamily.Ipv4 = &iaas.UpdateNetworkIPv4Body{
Nameservers: &modelIPv4Nameservers,
}
if model.NoIPv4Gateway.ValueBool() {
addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) {
addressFamily.Ipv4.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway))
}
}
currentLabels := stateModel.Labels
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
return &iaas.PartialUpdateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv4: &iaas.UpdateNetworkIPv4Body{
Nameservers: &modelNameservers,
},
},
payload := iaas.PartialUpdateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
}, nil
}
if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil {
payload.AddressFamily = addressFamily
}
return &payload, nil
}