terraform-provider-stackitp.../stackit/internal/services/iaas/network/datasource.go
Ruben Hönle 53a3697850
feat(iaas): support for v2 API (#1070)
relates to STACKITTPR-313
2025-12-17 15:40:46 +01:00

402 lines
15 KiB
Go

package network
import (
"context"
"fmt"
"net"
"net/http"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &networkDataSource{}
)
type DataSourceModel struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
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"`
Region types.String `tfsdk:"region"`
RoutingTableID types.String `tfsdk:"routing_table_id"`
}
// NewNetworkDataSource is a helper function to simplify the provider implementation.
func NewNetworkDataSource() datasource.DataSource {
return &networkDataSource{}
}
// networkDataSource is the data source implementation.
type networkDataSource struct {
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
func (d *networkDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_network"
}
func (d *networkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
d.client = apiClient
tflog.Info(ctx, "IaaS client configured")
}
// Schema defines the schema for the data source.
func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Network resource schema. Must have a `region` specified in the provider configuration.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
Description: "STACKIT project ID to which the network is associated.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"network_id": schema.StringAttribute{
Description: "The network ID.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"name": schema.StringAttribute{
Description: "The name of the network.",
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.LengthAtMost(63),
},
},
"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.",
DeprecationMessage: "Use `ipv4_nameservers` to configure the nameservers for IPv4.",
Computed: true,
ElementType: types.StringType,
},
"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.",
Computed: true,
},
"ipv4_nameservers": schema.ListAttribute{
Description: "The IPv4 nameservers of the network.",
Computed: true,
ElementType: types.StringType,
},
"ipv4_prefix": schema.StringAttribute{
Description: "The IPv4 prefix of the network (CIDR).",
DeprecationMessage: "The API supports reading multiple prefixes. So using the attribute 'ipv4_prefixes` should be preferred. This attribute will be populated with the first element from the list",
Computed: true,
},
"ipv4_prefix_length": schema.Int64Attribute{
Description: "The IPv4 prefix length of the network.",
Computed: true,
},
"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.",
DeprecationMessage: "Use `ipv4_prefixes` to read the prefixes of the IPv4 networks.",
Computed: true,
ElementType: types.StringType,
},
"ipv4_prefixes": schema.ListAttribute{
Description: "The IPv4 prefixes of the network.",
Computed: true,
ElementType: types.StringType,
},
"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.",
Computed: true,
},
"ipv6_nameservers": schema.ListAttribute{
Description: "The IPv6 nameservers of the network.",
Computed: true,
ElementType: types.StringType,
},
"ipv6_prefix": schema.StringAttribute{
Description: "The IPv6 prefix of the network (CIDR).",
DeprecationMessage: "The API supports reading multiple prefixes. So using the attribute 'ipv6_prefixes` should be preferred. This attribute will be populated with the first element from the list",
Computed: true,
},
"ipv6_prefix_length": schema.Int64Attribute{
Description: "The IPv6 prefix length of the network.",
Computed: true,
},
"ipv6_prefixes": schema.ListAttribute{
Description: "The IPv6 prefixes of the network.",
Computed: true,
ElementType: types.StringType,
},
"public_ip": schema.StringAttribute{
Description: "The public IP of the network.",
Computed: true,
},
"labels": schema.MapAttribute{
Description: "Labels are key-value string pairs which can be attached to a resource container",
ElementType: types.StringType,
Computed: true,
},
"routed": schema.BoolAttribute{
Description: "Shows if the network is routed and therefore accessible from other networks.",
Computed: true,
},
"region": schema.StringAttribute{
// the region cannot be found, so it has to be passed
Optional: true,
Description: "Can only be used when experimental \"network\" is set. This is likely going to undergo significant changes or be removed in the future.\nThe resource region. If not defined, the provider region is used.",
},
"routing_table_id": schema.StringAttribute{
Description: "Can only be used when experimental \"network\" is set. This is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.\nThe ID of the routing table associated with the network.",
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
},
}
}
// Read refreshes the Terraform state with the latest data.
func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model DataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
networkResp, err := d.client.GetNetwork(ctx, projectId, region, networkId).Execute()
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading network",
fmt.Sprintf("Network with ID %q does not exist in project %q.", networkId, projectId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
},
)
resp.State.RemoveResource(ctx)
return
}
err = mapDataSourceFields(ctx, networkResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network read")
}
func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model *DataSourceModel, region string) error {
if networkResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var networkId string
if model.NetworkId.ValueString() != "" {
networkId = model.NetworkId.ValueString()
} else if networkResp.Id != nil {
networkId = *networkResp.Id
} else {
return fmt.Errorf("network id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId)
labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels)
if err != nil {
return err
}
// IPv4
if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil {
model.Nameservers = types.ListNull(types.StringType)
model.IPv4Nameservers = types.ListNull(types.StringType)
} else {
respNameservers := *networkResp.Ipv4.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.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil {
model.Prefixes = types.ListNull(types.StringType)
model.IPv4Prefixes = types.ListNull(types.StringType)
} else {
respPrefixes := *networkResp.Ipv4.Prefixes
prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes)
if diags.HasError() {
return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixes) > 0 {
model.IPv4Prefix = types.StringValue(respPrefixes[0])
_, netmask, err := net.ParseCIDR(respPrefixes[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv4PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv4PrefixLength = types.Int64Value(int64(ones))
}
}
model.Prefixes = prefixesTF
model.IPv4Prefixes = prefixesTF
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil {
model.IPv4Gateway = types.StringNull()
} else {
model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway())
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil {
model.PublicIP = types.StringNull()
} else {
model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp)
}
// IPv6
if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil {
model.IPv6Nameservers = types.ListNull(types.StringType)
} else {
respIPv6Nameservers := *networkResp.Ipv6.Nameservers
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.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil {
model.IPv6Prefixes = types.ListNull(types.StringType)
} else {
respPrefixesV6 := *networkResp.Ipv6.Prefixes
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
if diags.HasError() {
return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixesV6) > 0 {
model.IPv6Prefix = types.StringValue(respPrefixesV6[0])
_, netmask, err := net.ParseCIDR(respPrefixesV6[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv6PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv6PrefixLength = types.Int64Value(int64(ones))
}
}
model.IPv6Prefixes = prefixesV6TF
}
if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil {
model.IPv6Gateway = types.StringNull()
} else {
model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway())
}
model.RoutingTableID = types.StringNull()
if networkResp.RoutingTableId != nil {
model.RoutingTableID = types.StringValue(*networkResp.RoutingTableId)
}
model.NetworkId = types.StringValue(networkId)
model.Name = types.StringPointerValue(networkResp.Name)
model.Labels = labels
model.Routed = types.BoolPointerValue(networkResp.Routed)
model.Region = types.StringValue(region)
return nil
}