diff --git a/README.md b/README.md index eeef283e..ea961c3b 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ To enable experiments set the experiments field in the provider definition: ```hcl provider "stackit" { default_region = "eu01" - experiments = ["iam", "routing-tables"] + experiments = ["iam", "routing-tables", "network"] } ``` @@ -197,6 +197,12 @@ Enables IAM management features in the Terraform provider. The underlying IAM AP This feature enables experimental routing table capabilities in the Terraform Provider, available only to designated SNAs at this time. +#### `network` + +The `stackit_network` provides the fields `region` and `routing_table_id` when the experiment flag `network` is set. +The underlying API is not stable yet and could change in the future. +If you don't need these fields, don't set the experiment flag `network`, to use the stable api. + ## Acceptance Tests Terraform acceptance tests are run using the command `make test-acceptance-tf`. For all services, diff --git a/docs/data-sources/network.md b/docs/data-sources/network.md index 69845786..dba50f60 100644 --- a/docs/data-sources/network.md +++ b/docs/data-sources/network.md @@ -27,6 +27,11 @@ data "stackit_network" "example" { - `network_id` (String) The network ID. - `project_id` (String) STACKIT project ID to which the network is associated. +### Optional + +- `region` (String) Can only be used when experimental "network" is set. This is likely going to undergo significant changes or be removed in the future. +The resource region. If not defined, the provider region is used. + ### Read-Only - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`network_id`". @@ -46,3 +51,5 @@ data "stackit_network" "example" { - `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. - `public_ip` (String) The public IP of the network. - `routed` (Boolean) Shows if the network is routed and therefore accessible from other networks. +- `routing_table_id` (String) 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. +The ID of the routing table associated with the network. diff --git a/docs/index.md b/docs/index.md index d2eb290e..b58bf66c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -157,7 +157,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `default_region` (String) Region will be used as the default location for regional services. Not all services require a region, some are global - `dns_custom_endpoint` (String) Custom endpoint for the DNS service - `enable_beta_resources` (Boolean) Enable beta resources. Default is false. -- `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: [iam routing-tables] +- `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: iam, routing-tables, network - `git_custom_endpoint` (String) Custom endpoint for the Git service - `iaas_custom_endpoint` (String) Custom endpoint for the IaaS service - `loadbalancer_custom_endpoint` (String) Custom endpoint for the Load Balancer service diff --git a/docs/resources/network.md b/docs/resources/network.md index 9f5083ca..a149519b 100644 --- a/docs/resources/network.md +++ b/docs/resources/network.md @@ -63,7 +63,11 @@ resource "stackit_network" "example_non_routed_network" { - `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. - `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. +The resource region. If not defined, the provider region is used. - `routed` (Boolean) If set to `true`, the network is routed and therefore accessible from other networks. +- `routing_table_id` (String) Can only be used when experimental "network" is set. +The ID of the routing table associated with the network. ### Read-Only diff --git a/go.mod b/go.mod index 4f01ddac..922c8b83 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.0 github.com/stackitcloud/stackit-sdk-go/services/git v0.6.0 github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0 - github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.19-alpha + github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.0 github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.0 diff --git a/go.sum b/go.sum index c2bcef40..d5c09a58 100644 --- a/go.sum +++ b/go.sum @@ -162,8 +162,8 @@ github.com/stackitcloud/stackit-sdk-go/services/git v0.6.0 h1:C+8z3MdvnTngcH9L72 github.com/stackitcloud/stackit-sdk-go/services/git v0.6.0/go.mod h1:agI7SONeLR/IZL3TOgn1tDzfS63O2rWKQE8+huRjEzU= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0 h1:7qm/Tft79wFlHomPdgjUJ9uJU8kEk+k9ficMGRoHtf0= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0/go.mod h1:lUGkcbyMkd4nRBDFmKohIwlgtOZqQo4Ek5S5ajw90Xg= -github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.19-alpha h1:HnQyJSXbtYzN9IhTO02zxLrcSxyauIbeJD+GTf23A50= -github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.19-alpha/go.mod h1:Wt77ucOwpe9g/84LijU+YhWbn3vLcpkAoRy2i+FobNQ= +github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha h1:m1jq6a8dbUe+suFuUNdHmM/cSehpGLUtDbK1CqLqydg= +github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha/go.mod h1:Nu1b5Phsv8plgZ51+fkxPVsU91ZJ5Ayz+cthilxdmQ8= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0 h1:Ef4SyTBjIkfwaws4mssa6AoK+OokHFtr7ZIflUpoXVE= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0/go.mod h1:FiVhDlw9+yuTiUmnyGLn2qpsLW26w9OC4TS1y78czvg= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.0 h1:QKOfaB7EcuJmBCxpFXN2K7g2ih0gQM6cyZ1VhTmtQfI= diff --git a/stackit/internal/features/experiments.go b/stackit/internal/features/experiments.go index a2d2f622..35193048 100644 --- a/stackit/internal/features/experiments.go +++ b/stackit/internal/features/experiments.go @@ -13,9 +13,11 @@ import ( const ( RoutingTablesExperiment = "routing-tables" + NetworkExperiment = "network" + IamExperiment = "iam" ) -var AvailableExperiments = []string{"iam", RoutingTablesExperiment} +var AvailableExperiments = []string{IamExperiment, RoutingTablesExperiment, NetworkExperiment} // Check if an experiment is valid. func ValidExperiment(experiment string, diags *diag.Diagnostics) bool { @@ -31,11 +33,21 @@ func ValidExperiment(experiment string, diags *diag.Diagnostics) bool { // Check if an experiment is enabled. func CheckExperimentEnabled(ctx context.Context, data *core.ProviderData, experiment, resourceName string, resourceType core.ResourceType, diags *diag.Diagnostics) { + if CheckExperimentEnabledWithoutError(ctx, data, experiment, resourceName, resourceType, diags) { + return + } + errTitle := fmt.Sprintf("%s is part of the %s experiment, which is currently disabled by default", resourceName, experiment) + errContent := fmt.Sprintf(`Enable the %s experiment by adding it into your provider block.`, experiment) + tflog.Error(ctx, fmt.Sprintf("%s | %s", errTitle, errContent)) + diags.AddError(errTitle, errContent) +} + +func CheckExperimentEnabledWithoutError(ctx context.Context, data *core.ProviderData, experiment, resourceName string, resourceType core.ResourceType, diags *diag.Diagnostics) bool { if !ValidExperiment(experiment, diags) { errTitle := fmt.Sprintf("The experiment %s does not exist.", experiment) errContent := "This is a bug in the STACKIT Terraform Provider. Please open an issue here: https://github.com/stackitcloud/terraform-provider-stackit/issues" diags.AddError(errTitle, errContent) - return + return false } experimentActive := slices.ContainsFunc(data.Experiments, func(e string) bool { return strings.EqualFold(e, experiment) @@ -46,12 +58,9 @@ func CheckExperimentEnabled(ctx context.Context, data *core.ProviderData, experi warnContent := fmt.Sprintf("This %s is part of the %s experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.", resourceType, experiment) tflog.Warn(ctx, fmt.Sprintf("%s | %s", warnTitle, warnContent)) diags.AddWarning(warnTitle, warnContent) - return + return true } - errTitle := fmt.Sprintf("%s is part of the %s experiment, which is currently disabled by default", resourceName, experiment) - errContent := fmt.Sprintf(`Enable the %s experiment by adding it into your provider block.`, experiment) - tflog.Error(ctx, fmt.Sprintf("%s | %s", errTitle, errContent)) - diags.AddError(errTitle, errContent) + return false } func AddExperimentDescription(description, experiment string, resourceType core.ResourceType) string { diff --git a/stackit/internal/features/experiments_test.go b/stackit/internal/features/experiments_test.go index 392469ae..f8692597 100644 --- a/stackit/internal/features/experiments_test.go +++ b/stackit/internal/features/experiments_test.go @@ -21,7 +21,7 @@ func TestValidExperiment(t *testing.T) { { name: "valid", args: args{ - experiment: "iam", + experiment: IamExperiment, diags: &diag.Diagnostics{}, }, want: true, @@ -64,9 +64,9 @@ func TestCheckExperimentEnabled(t *testing.T) { args: args{ ctx: context.Background(), data: &core.ProviderData{ - Experiments: []string{"iam"}, + Experiments: []string{IamExperiment}, }, - experiment: "iam", + experiment: IamExperiment, resourceType: core.Resource, diags: &diag.Diagnostics{}, }, @@ -80,7 +80,7 @@ func TestCheckExperimentEnabled(t *testing.T) { data: &core.ProviderData{ Experiments: []string{}, }, - experiment: "iam", + experiment: IamExperiment, resourceType: core.Resource, diags: &diag.Diagnostics{}, }, @@ -92,7 +92,7 @@ func TestCheckExperimentEnabled(t *testing.T) { args: args{ ctx: context.Background(), data: &core.ProviderData{ - Experiments: []string{"iam"}, + Experiments: []string{IamExperiment}, }, experiment: "foobar", resourceType: core.Resource, @@ -101,6 +101,34 @@ func TestCheckExperimentEnabled(t *testing.T) { wantDiagsErr: true, wantDiagsWarning: false, }, + { + name: "enabled multiple experiment", + args: args{ + ctx: context.Background(), + data: &core.ProviderData{ + Experiments: []string{IamExperiment, NetworkExperiment, RoutingTablesExperiment}, + }, + experiment: NetworkExperiment, + resourceType: core.Resource, + diags: &diag.Diagnostics{}, + }, + wantDiagsErr: false, + wantDiagsWarning: true, + }, + { + name: "enabled multiple experiment - without the required experiment", + args: args{ + ctx: context.Background(), + data: &core.ProviderData{ + Experiments: []string{IamExperiment, RoutingTablesExperiment}, + }, + experiment: NetworkExperiment, + resourceType: core.Resource, + diags: &diag.Diagnostics{}, + }, + wantDiagsErr: true, + wantDiagsWarning: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -114,3 +142,111 @@ func TestCheckExperimentEnabled(t *testing.T) { }) } } + +func TestCheckExperimentEnabledWithoutError(t *testing.T) { + type args struct { + ctx context.Context + data *core.ProviderData + experiment string + resourceName string + resourceType core.ResourceType + diags *diag.Diagnostics + } + tests := []struct { + name string + args args + wantEnabled bool + wantDiagsErr bool + wantDiagsWarning bool + }{ + + { + name: "enabled", + args: args{ + ctx: context.Background(), + data: &core.ProviderData{ + Experiments: []string{IamExperiment}, + }, + experiment: IamExperiment, + resourceType: core.Resource, + diags: &diag.Diagnostics{}, + }, + wantEnabled: true, + wantDiagsErr: false, + wantDiagsWarning: true, + }, + { + name: "disabled - no error", + args: args{ + ctx: context.Background(), + data: &core.ProviderData{ + Experiments: []string{}, + }, + experiment: NetworkExperiment, + resourceType: core.Resource, + diags: &diag.Diagnostics{}, + }, + wantEnabled: false, + wantDiagsErr: false, + wantDiagsWarning: false, + }, + { + name: "invalid experiment", + args: args{ + ctx: context.Background(), + data: &core.ProviderData{ + Experiments: []string{IamExperiment}, + }, + experiment: "foobar", + resourceType: core.Resource, + diags: &diag.Diagnostics{}, + }, + wantEnabled: false, + wantDiagsErr: true, + wantDiagsWarning: false, + }, + { + name: "enabled multiple experiment", + args: args{ + ctx: context.Background(), + data: &core.ProviderData{ + Experiments: []string{IamExperiment, NetworkExperiment, RoutingTablesExperiment}, + }, + experiment: NetworkExperiment, + resourceType: core.Resource, + diags: &diag.Diagnostics{}, + }, + wantEnabled: true, + wantDiagsErr: false, + wantDiagsWarning: true, + }, + { + name: "enabled multiple experiment - without the required experiment", + args: args{ + ctx: context.Background(), + data: &core.ProviderData{ + Experiments: []string{IamExperiment, RoutingTablesExperiment}, + }, + experiment: NetworkExperiment, + resourceType: core.Resource, + diags: &diag.Diagnostics{}, + }, + wantEnabled: false, + wantDiagsErr: false, + wantDiagsWarning: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CheckExperimentEnabledWithoutError(tt.args.ctx, tt.args.data, tt.args.experiment, tt.args.resourceName, tt.args.resourceType, tt.args.diags); got != tt.wantEnabled { + t.Errorf("CheckExperimentEnabledWithoutError() = %v, want %v", got, tt.wantEnabled) + } + if got := tt.args.diags.HasError(); got != tt.wantDiagsErr { + t.Errorf("CheckExperimentEnabled() diags.HasError() = %v, want %v", got, tt.wantDiagsErr) + } + if got := tt.args.diags.WarningsCount() > 0; got != tt.wantDiagsWarning { + t.Errorf("CheckExperimentEnabled() diags.WarningsCount() > 0 = %v, want %v", got, tt.wantDiagsErr) + } + }) + } +} diff --git a/stackit/internal/services/authorization/roleassignments/resource.go b/stackit/internal/services/authorization/roleassignments/resource.go index 7b106bff..32d5909d 100644 --- a/stackit/internal/services/authorization/roleassignments/resource.go +++ b/stackit/internal/services/authorization/roleassignments/resource.go @@ -32,9 +32,6 @@ var roleTargets = []string{ "organization", } -// This resource is part of the "iam" experiment -var experiment = "iam" - // Ensure the implementation satisfies the expected interfaces. var ( _ resource.Resource = &roleAssignmentResource{} @@ -84,7 +81,7 @@ func (r *roleAssignmentResource) Configure(ctx context.Context, req resource.Con return } - features.CheckExperimentEnabled(ctx, &providerData, experiment, fmt.Sprintf("stackit_authorization_%s_role_assignment", r.apiName), core.Resource, &resp.Diagnostics) + features.CheckExperimentEnabled(ctx, &providerData, features.IamExperiment, fmt.Sprintf("stackit_authorization_%s_role_assignment", r.apiName), core.Resource, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -100,7 +97,7 @@ func (r *roleAssignmentResource) Configure(ctx context.Context, req resource.Con // Schema defines the schema for the resource. func (r *roleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ - "main": features.AddExperimentDescription(fmt.Sprintf("%s Role Assignment resource schema.", r.apiName), experiment, core.Resource), + "main": features.AddExperimentDescription(fmt.Sprintf("%s Role Assignment resource schema.", r.apiName), features.IamExperiment, core.Resource), "id": "Terraform's internal resource identifier. It is structured as \"[resource_id],[role],[subject]\".", "resource_id": fmt.Sprintf("%s Resource to assign the role to.", r.apiName), "role": "Role to be assigned", diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go index 574c6e82..38b04894 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -5,6 +5,7 @@ import ( _ "embed" "errors" "fmt" + "maps" "net/http" "os" "path/filepath" @@ -21,6 +22,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + waitAlpha "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -50,11 +53,17 @@ var ( //go:embed testdata/resource-network-area-max.tf resourceNetworkAreaMaxConfig string - //go:embed testdata/resource-network-min.tf - resourceNetworkMinConfig string + //go:embed testdata/resource-network-v1-min.tf + resourceNetworkV1MinConfig string - //go:embed testdata/resource-network-max.tf - resourceNetworkMaxConfig string + //go:embed testdata/resource-network-v1-max.tf + resourceNetworkV1MaxConfig string + + //go:embed testdata/resource-network-v2-min.tf + resourceNetworkV2MinConfig string + + //go:embed testdata/resource-network-v2-max.tf + resourceNetworkV2MaxConfig string //go:embed testdata/resource-network-interface-min.tf resourceNetworkInterfaceMinConfig string @@ -83,21 +92,10 @@ var ( const ( keypairPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDsPd27M449akqCtdFg2+AmRVJz6eWio0oMP9dVg7XZ" + // TODO: create network area using terraform resource instead once it's out of experimental stage and GA + testNetworkAreaId = "25bbf23a-8134-4439-9f5e-1641caf8354e" ) -// Network resource data -var networkResource = map[string]string{ - "project_id": testutil.ProjectId, - "name": fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)), - "ipv4_prefix_length": "24", - "nameserver0": "1.2.3.4", - "nameserver1": "5.6.7.8", - "ipv4_gateway": "10.2.2.1", - "ipv4_prefix": "10.2.2.0/24", - "routed": "false", - "name_updated": fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)), -} - var testConfigServerVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), @@ -225,31 +223,67 @@ var testConfigVolumeVarsMaxUpdated = func() config.Variables { return updatedConfig }() -var testConfigNetworkVarsMin = config.Variables{ +var testConfigNetworkV1VarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), } -var testConfigNetworkVarsMax = config.Variables{ +var testConfigNetworkV1VarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "ipv4_gateway": config.StringVariable("10.2.2.1"), - "ipv4_nameservers": config.StringVariable("10.2.2.2"), + "ipv4_nameserver_0": config.StringVariable("10.2.2.2"), + "ipv4_nameserver_1": config.StringVariable("10.2.2.3"), "ipv4_prefix": config.StringVariable("10.2.2.0/24"), "ipv4_prefix_length": config.IntegerVariable(24), "routed": config.BoolVariable(false), "label": config.StringVariable("label"), } -var testConfigNetworkVarsMaxUpdated = func() config.Variables { +var testConfigNetworkV1VarsMaxUpdated = func() config.Variables { updatedConfig := config.Variables{} - for k, v := range testConfigNetworkVarsMax { + for k, v := range testConfigNetworkV1VarsMax { updatedConfig[k] = v } + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) updatedConfig["ipv4_gateway"] = config.StringVariable("") - updatedConfig["ipv4_nameservers"] = config.StringVariable("10.2.2.3") - updatedConfig["ipv4_prefix"] = config.StringVariable("10.2.2.0/25") - updatedConfig["ipv4_prefix_length"] = config.IntegerVariable(25) + updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") + updatedConfig["label"] = config.StringVariable("updated") + return updatedConfig +}() + +var testConfigNetworkV2VarsMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), +} + +var testConfigNetworkV2VarsMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigNetworkV2VarsMin) + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) + return updatedConfig +}() + +var testConfigNetworkV2VarsMax = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), + "ipv4_gateway": config.StringVariable("10.2.2.1"), + "ipv4_nameserver_0": config.StringVariable("10.2.2.2"), + "ipv4_nameserver_1": config.StringVariable("10.2.2.3"), + "ipv4_prefix": config.StringVariable("10.2.2.0/24"), + "ipv4_prefix_length": config.IntegerVariable(24), + "routed": config.BoolVariable(true), + "label": config.StringVariable("label"), + "organization_id": config.StringVariable(testutil.OrganizationId), + "network_area_id": config.StringVariable(testNetworkAreaId), +} + +var testConfigNetworkV2VarsMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigNetworkV2VarsMax) + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) + updatedConfig["ipv4_gateway"] = config.StringVariable("") + updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") updatedConfig["label"] = config.StringVariable("updated") return updatedConfig }() @@ -456,20 +490,20 @@ var testConfigKeyPairMaxUpdated = func() config.Variables { // if no local file is provided the test should create a default file and work with this instead of failing var localFileForIaasImage os.File -func TestAccNetworkMin(t *testing.T) { - t.Logf("TestAccNetworkMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])) +func TestAccNetworkV1Min(t *testing.T) { + t.Logf("TestAccNetworkV1Min name: %s", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])) resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ // Creation { - ConfigVariables: testConfigNetworkVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkMinConfig), + ConfigVariables: testConfigNetworkV1VarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MinConfig), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), @@ -477,7 +511,7 @@ func TestAccNetworkMin(t *testing.T) { }, // Data source { - ConfigVariables: testConfigNetworkVarsMin, + ConfigVariables: testConfigNetworkV1VarsMin, Config: fmt.Sprintf(` %s %s @@ -487,12 +521,12 @@ func TestAccNetworkMin(t *testing.T) { network_id = stackit_network.network.network_id } `, - testutil.IaaSProviderConfig(), resourceNetworkMinConfig, + testutil.IaaSProviderConfig(), resourceNetworkV1MinConfig, ), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), @@ -501,7 +535,7 @@ func TestAccNetworkMin(t *testing.T) { // Import { - ConfigVariables: testConfigNetworkVarsMin, + ConfigVariables: testConfigNetworkV1VarsMin, ResourceName: "stackit_network.network", ImportStateIdFunc: func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources["stackit_network.network"] @@ -517,8 +551,8 @@ func TestAccNetworkMin(t *testing.T) { ImportState: true, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), @@ -530,70 +564,106 @@ func TestAccNetworkMin(t *testing.T) { }) } -func TestAccNetworkMax(t *testing.T) { - t.Logf("TestAccNetworkMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])) +func TestAccNetworkV1Max(t *testing.T) { + t.Logf("TestAccNetworkV1Max name: %s", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])) resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ - // Creation { - ConfigVariables: testConfigNetworkVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkMaxConfig), + ConfigVariables: testConfigNetworkV1VarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), - resource.TestCheckNoResourceAttr("stackit_network.network", "no_ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_nameservers.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameservers"])), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network", "public_ip")), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), + + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "public_ip"), + ), }, // Data source { - ConfigVariables: testConfigNetworkVarsMax, + ConfigVariables: testConfigNetworkV1VarsMax, Config: fmt.Sprintf(` %s %s - data "stackit_network" "network" { - project_id = stackit_network.network.project_id - network_id = stackit_network.network.network_id + data "stackit_network" "network_prefix" { + project_id = stackit_network.network_prefix.project_id + network_id = stackit_network.network_prefix.network_id + } + + data "stackit_network" "network_prefix_length" { + project_id = stackit_network.network_prefix_length.project_id + network_id = stackit_network.network_prefix_length.network_id } `, - testutil.IaaSProviderConfig(), resourceNetworkMaxConfig, + testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig, ), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_nameservers.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameservers"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), + + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "network_id"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), + resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv4_prefix"), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), ), }, // Import { - ConfigVariables: testConfigNetworkVarsMax, - ResourceName: "stackit_network.network", + ConfigVariables: testConfigNetworkV1VarsMax, + ResourceName: "stackit_network.network_prefix", ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network"] + r, ok := s.RootModule().Resources["stackit_network.network_prefix"] if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network") + return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") } networkId, ok := r.Primary.Attributes["network_id"] if !ok { @@ -603,39 +673,449 @@ func TestAccNetworkMax(t *testing.T) { }, ImportState: true, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", networkResource["name"]), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_gateway", networkResource["ipv4_gateway"]), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network", "nameservers.*", networkResource["nameserver0"]), - resource.TestCheckTypeSetElemAttr("stackit_network.network", "nameservers.*", networkResource["nameserver1"]), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_prefix", networkResource["ipv4_prefix"]), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_prefix_length", networkResource["ipv4_prefix_length"]), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network.network", "ipv4_prefixes.0", networkResource["ipv4_prefix"]), - resource.TestCheckResourceAttr("data.stackit_network.network", "routed", networkResource["routed"]), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), + ), + }, + { + ConfigVariables: testConfigNetworkV1VarsMax, + ResourceName: "stackit_network.network_prefix_length", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_network.network_prefix_length"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix_length") + } + networkId, ok := r.Primary.Attributes["network_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_id") + } + return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + }, + ImportState: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), + // nameservers may be returned in a randomized order, so we have to check them with a helper function + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), ), }, // Update { - ConfigVariables: testConfigNetworkVarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkMaxConfig), + ConfigVariables: testConfigNetworkV1VarsMaxUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["name"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway", "true"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["label"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), + + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["name"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix_length"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["label"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "public_ip"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccNetworkV2Min(t *testing.T) { + t.Logf("TestAccNetworkV2Min name: %s", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])) + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckNetworkV2Destroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigNetworkV2VarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), - resource.TestCheckNoResourceAttr("stackit_network.network", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_nameservers.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameservers"])), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("stackit_network.network", "region"), + resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), + ), + }, + // Data source + { + ConfigVariables: testConfigNetworkV2VarsMin, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_network" "network" { + project_id = stackit_network.network.project_id + network_id = stackit_network.network.network_id + } + `, + testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), + resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), + resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), + resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("data.stackit_network.network", "region"), + resource.TestCheckResourceAttrSet("data.stackit_network.network", "routing_table_id"), + ), + }, + + // Import + { + ConfigVariables: testConfigNetworkV2VarsMin, + ResourceName: "stackit_network.network", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_network.network"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network.network") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + networkId, ok := r.Primary.Attributes["network_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_id") + } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, networkId), nil + }, + ImportState: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), + resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("stackit_network.network", "region"), + resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), + ), + }, + // Update + { + ConfigVariables: testConfigNetworkV2VarsMinUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMinUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMinUpdated["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), + resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("stackit_network.network", "region"), + resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccNetworkV2Max(t *testing.T) { + t.Logf("TestAccNetworkV2Max name: %s", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])) + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckNetworkV2Destroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigNetworkV2VarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // TODO: enable test cases for prefix option, when the API works again + // Network with prefix + // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), + // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), + + // Network with prefix_length + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + // resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), + + resource.TestCheckResourceAttrPair( + "stackit_network.network_prefix_length", "routing_table_id", + "stackit_routing_table.routing_table", "routing_table_id", + ), + + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), + resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), + ), + }, + // Data source + { + ConfigVariables: testConfigNetworkV2VarsMax, + Config: fmt.Sprintf(` + %s + %s + + //data "stackit_network" "network_prefix" { + // project_id = stackit_network.network_prefix.project_id + // network_id = stackit_network.network_prefix.network_id + //} + + data "stackit_network" "network_prefix_length" { + project_id = stackit_network.network_prefix_length.project_id + network_id = stackit_network.network_prefix_length.network_id + } + + data "stackit_routing_table" "routing_table" { + organization_id = stackit_routing_table.routing_table.organization_id + network_area_id = stackit_routing_table.routing_table.network_area_id + routing_table_id = stackit_routing_table.routing_table.routing_table_id + } + `, + testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // TODO: enable test cases for prefix option, when the API works again + // Network with prefix + // resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + // resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), + // resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + // resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), + // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + + // Network with prefix_length + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "network_id"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + // resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv4_prefix"), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "region", testutil.Region), + + resource.TestCheckResourceAttrPair( + "data.stackit_network.network_prefix_length", "routing_table_id", + "data.stackit_routing_table.routing_table", "routing_table_id", + ), + + // Routing table + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["network_area_id"])), + resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.%", "0"), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "region", testutil.Region), + resource.TestCheckNoResourceAttr("data.stackit_routing_table.routing_table", "description"), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "system_routes", "true"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "updated_at"), + ), + }, + // Import + // TODO: enable test cases for prefix option, when the API works again + //{ + // ConfigVariables: testConfigNetworkV2VarsMax, + // ResourceName: "stackit_network.network_prefix", + // ImportStateIdFunc: func(s *terraform.State) (string, error) { + // r, ok := s.RootModule().Resources["stackit_network.network_prefix"] + // if !ok { + // return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") + // } + // networkId, ok := r.Primary.Attributes["network_id"] + // if !ok { + // return "", fmt.Errorf("couldn't find attribute network_id") + // } + // return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + // }, + // ImportState: true, + // Check: resource.ComposeAggregateTestCheckFunc( + // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + // // nameservers may be returned in a randomized order, so we have to check them with a helper function + // resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), + // resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), + // ), + // }, + { + ConfigVariables: testConfigNetworkV2VarsMax, + ResourceName: "stackit_network.network_prefix_length", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_network.network_prefix_length"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix_length") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + networkId, ok := r.Primary.Attributes["network_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_id") + } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, networkId), nil + }, + ImportState: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), + // resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), + // nameservers may be returned in a randomized order, so we have to check them with a helper function + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), + ), + }, + // Update + { + ConfigVariables: testConfigNetworkV2VarsMaxUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // TODO: enable test cases for prefix option, when the API works again + // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["project_id"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), + // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "ipv4_gateway"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway", "true"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_0"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_1"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix_length"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["routed"])), + // resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["label"])), + // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), + + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), + // resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix_length"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["label"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), + + resource.TestCheckResourceAttrPair( + "stackit_network.network_prefix_length", "routing_table_id", + "stackit_routing_table.routing_table", "routing_table_id", + ), + + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), + resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), ), }, // Deletion is done by the framework implicitly @@ -3544,7 +4024,7 @@ func TestAccImageMax(t *testing.T) { func testAccCheckDestroy(s *terraform.State) error { checkFunctions := []func(s *terraform.State) error{ - testAccCheckNetworkDestroy, + testAccCheckNetworkV1Destroy, testAccCheckNetworkInterfaceDestroy, testAccCheckNetworkAreaDestroy, testAccCheckIaaSVolumeDestroy, @@ -3573,7 +4053,7 @@ func testAccCheckDestroy(s *terraform.State) error { return errors.Join(errs...) } -func testAccCheckNetworkDestroy(s *terraform.State) error { +func testAccCheckNetworkV1Destroy(s *terraform.State) error { ctx := context.Background() var client *iaas.APIClient var err error @@ -3616,6 +4096,48 @@ func testAccCheckNetworkDestroy(s *terraform.State) error { return errors.Join(errs...) } +func testAccCheckNetworkV2Destroy(s *terraform.State) error { + ctx := context.Background() + var client *iaasalpha.APIClient + var err error + if testutil.IaaSCustomEndpoint == "" { + client, err = iaasalpha.NewAPIClient() + } else { + client, err = iaasalpha.NewAPIClient( + stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var errs []error + // networks + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_network" { + continue + } + region := strings.Split(rs.Primary.ID, core.Separator)[1] + networkId := strings.Split(rs.Primary.ID, core.Separator)[2] + err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, region, networkId) + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + continue + } + } + errs = append(errs, fmt.Errorf("cannot trigger network deletion %q: %w", networkId, err)) + } + _, err = waitAlpha.DeleteNetworkWaitHandler(ctx, client, testutil.ProjectId, region, networkId).WaitWithContext(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("cannot delete network %q: %w", networkId, err)) + } + } + + return errors.Join(errs...) +} + func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { ctx := context.Background() var client *iaas.APIClient diff --git a/stackit/internal/services/iaas/network/datasource.go b/stackit/internal/services/iaas/network/datasource.go index 19dbd640..a78d11b9 100644 --- a/stackit/internal/services/iaas/network/datasource.go +++ b/stackit/internal/services/iaas/network/datasource.go @@ -2,12 +2,14 @@ package network import ( "context" - "fmt" - "net" - "net/http" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -17,7 +19,6 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -26,28 +27,6 @@ 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"` -} - // NewNetworkDataSource is a helper function to simplify the provider implementation. func NewNetworkDataSource() datasource.DataSource { return &networkDataSource{} @@ -56,6 +35,10 @@ func NewNetworkDataSource() datasource.DataSource { // networkDataSource is the data source implementation. type networkDataSource struct { client *iaas.APIClient + // alphaClient will be used in case the experimental flag "network" is set + alphaClient *iaasalpha.APIClient + isExperimental bool + providerData core.ProviderData } // Metadata returns the data source type name. @@ -64,16 +47,30 @@ func (d *networkDataSource) Metadata(_ context.Context, req datasource.MetadataR } func (d *networkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + d.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &d.providerData, features.NetworkExperiment, "stackit_network", core.Datasource, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - d.client = apiClient + + if d.isExperimental { + alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.alphaClient = alphaApiClient + } else { + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + } tflog.Info(ctx, "IaaS client configured") } @@ -181,193 +178,28 @@ func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest 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 + if !d.isExperimental { + v1network.DatasourceRead(ctx, req, resp, d.client) + } else { + v2network.DatasourceRead(ctx, req, resp, d.alphaClient, d.providerData) } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - networkResp, err := d.client.GetNetwork(ctx, projectId, 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) - 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) 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.NetworkId != nil { - networkId = *networkResp.NetworkId - } else { - return fmt.Errorf("network id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), networkId) - - labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) - if err != nil { - return err - } - - // 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) - 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.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)) - } - 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.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 } diff --git a/stackit/internal/services/iaas/network/resource.go b/stackit/internal/services/iaas/network/resource.go index 09fd09e5..365a303e 100644 --- a/stackit/internal/services/iaas/network/resource.go +++ b/stackit/internal/services/iaas/network/resource.go @@ -2,31 +2,30 @@ package network import ( "context" - "fmt" - "net" - "net/http" - "strings" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "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/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/int64planmodifier" "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" "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/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -38,30 +37,6 @@ var ( _ resource.ResourceWithImportState = &networkResource{} ) -type Model 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"` - NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"` - NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"` -} - // NewNetworkResource is a helper function to simplify the provider implementation. func NewNetworkResource() resource.Resource { return &networkResource{} @@ -70,6 +45,10 @@ func NewNetworkResource() resource.Resource { // networkResource is the resource implementation. type networkResource struct { client *iaas.APIClient + // alphaClient will be used in case the experimental flag "network" is set + alphaClient *iaasalpha.APIClient + isExperimental bool + providerData core.ProviderData } // Metadata returns the resource type name. @@ -79,29 +58,85 @@ func (r *networkResource) Metadata(_ context.Context, req resource.MetadataReque // Configure adds the provider configured client to the resource. func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + r.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &r.providerData, features.NetworkExperiment, "stackit_network", core.Resource, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - r.client = apiClient + + if r.isExperimental { + alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.alphaClient = alphaApiClient + } else { + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + } 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)...) +// 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() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) if resp.Diagnostics.HasError() { return } - if !model.Nameservers.IsUnknown() && !model.IPv4Nameservers.IsUnknown() && !model.Nameservers.IsNull() && !model.IPv4Nameservers.IsNull() { + var planModel model.Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *networkResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var resourceModel model.Model + resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) + if resp.Diagnostics.HasError() { + return + } + + if !resourceModel.Nameservers.IsUnknown() && !resourceModel.IPv4Nameservers.IsUnknown() && !resourceModel.Nameservers.IsNull() && !resourceModel.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.") } + if !r.isExperimental { + if !utils.IsUndefined(resourceModel.Region) { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the `region` is not supported yet. This can only be configured when the experiments `network` is set.") + } + if !utils.IsUndefined(resourceModel.RoutingTableID) { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the field `routing_table_id` is not supported yet. This can only be configured when the experiments `network` is set.") + } + } } // ConfigValidators validates the resource configuration @@ -115,6 +150,22 @@ func (r *networkResource) ConfigValidators(_ context.Context) []resource.ConfigV path.MatchRoot("no_ipv6_gateway"), path.MatchRoot("ipv6_gateway"), ), + resourcevalidator.Conflicting( + path.MatchRoot("ipv4_prefix"), + path.MatchRoot("ipv4_prefix_length"), + ), + resourcevalidator.Conflicting( + path.MatchRoot("ipv6_prefix"), + path.MatchRoot("ipv6_prefix_length"), + ), + resourcevalidator.Conflicting( + path.MatchRoot("ipv4_prefix_length"), + path.MatchRoot("ipv4_gateway"), + ), + resourcevalidator.Conflicting( + path.MatchRoot("ipv6_prefix_length"), + path.MatchRoot("ipv6_gateway"), + ), } } @@ -196,13 +247,16 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re validate.CIDR(), }, PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), + stringplanmodifier.RequiresReplaceIfConfigured(), }, }, "ipv4_prefix_length": schema.Int64Attribute{ Description: "The IPv4 prefix length of the network.", Computed: true, Optional: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplaceIfConfigured(), + }, }, "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.", @@ -285,498 +339,73 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re boolplanmodifier.RequiresReplace(), }, }, + "routing_table_id": schema.StringAttribute{ + Description: "Can only be used when experimental \"network\" is set.\nThe ID of the routing table associated with the network.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: "Can only be used when experimental \"network\" is set.\nThe resource region. If not defined, the provider region is used.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, }, } } // 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 model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + if !r.isExperimental { + v1network.Create(ctx, req, resp, r.client) + } else { + v2network.Create(ctx, req, resp, r.alphaClient) } - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new network - - network, err := r.client.CreateNetwork(ctx, projectId).CreateNetworkPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err)) - return - } - - networkId := *network.NetworkId - network, err = wait.CreateNetworkWaitHandler(ctx, r.client, projectId, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) - return - } - - ctx = tflog.SetField(ctx, "network_id", networkId) - - // Map response body to schema - err = mapFields(ctx, network, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network created") } // Read refreshes the Terraform state with the latest data. func (r *networkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + if !r.isExperimental { + v1network.Read(ctx, req, resp, r.client) + } else { + v2network.Read(ctx, req, resp, r.alphaClient, r.providerData) } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - networkResp, err := r.client.GetNetwork(ctx, projectId, networkId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, networkResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network read") } // Update updates the resource and sets the updated Terraform state on success. func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + if !r.isExperimental { + v1network.Update(ctx, req, resp, r.client) + } else { + v2network.Update(ctx, req, resp, r.alphaClient) } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - 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 - } - // Update existing network - err = r.client.PartialUpdateNetwork(ctx, projectId, networkId).PartialUpdateNetworkPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) - return - } - waitResp, err := wait.UpdateNetworkWaitHandler(ctx, r.client, projectId, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) - return - } - - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating 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 updated") } // Delete deletes the resource and removes the Terraform state on success. func (r *networkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + if !r.isExperimental { + v1network.Delete(ctx, req, resp, r.client) + } else { + v2network.Delete(ctx, req, resp, r.alphaClient) } - - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - // Delete existing network - err := r.client.DeleteNetwork(ctx, projectId, networkId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) - return - } - _, err = wait.DeleteNetworkWaitHandler(ctx, r.client, projectId, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Network deleted") } // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,network_id func (r *networkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network", - fmt.Sprintf("Expected import identifier with format: [project_id],[network_id] Got: %q", req.ID), - ) - return + if !r.isExperimental { + v1network.ImportState(ctx, req, resp) + } else { + v2network.ImportState(ctx, req, resp) } - - projectId := idParts[0] - networkId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...) - tflog.Info(ctx, "Network state imported") -} - -func mapFields(ctx context.Context, networkResp *iaas.Network, model *Model) 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.NetworkId != nil { - networkId = *networkResp.NetworkId - } else { - return fmt.Errorf("network id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), networkId) - - labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) - if err != nil { - return err - } - - // 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) - 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.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)) - } - 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.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 -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - 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") - } - 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) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - payload := iaas.CreateNetworkPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - Routed: conversion.BoolValueToPointer(model.Routed), - } - - if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil { - payload.AddressFamily = addressFamily - } - - return &payload, nil -} - -func toUpdatePayload(ctx context.Context, model, stateModel *Model) (*iaas.PartialUpdateNetworkPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil 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") - } - 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) - } - - payload := iaas.PartialUpdateNetworkPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - } - - if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil { - payload.AddressFamily = addressFamily - } - - return &payload, nil } diff --git a/stackit/internal/services/iaas/network/utils/model/model.go b/stackit/internal/services/iaas/network/utils/model/model.go new file mode 100644 index 00000000..73f994ec --- /dev/null +++ b/stackit/internal/services/iaas/network/utils/model/model.go @@ -0,0 +1,53 @@ +package model + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type Model 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"` + NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"` + NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"` + Region types.String `tfsdk:"region"` + RoutingTableID types.String `tfsdk:"routing_table_id"` +} + +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"` +} diff --git a/stackit/internal/services/iaas/network/utils/v1network/datasource.go b/stackit/internal/services/iaas/network/utils/v1network/datasource.go new file mode 100644 index 00000000..08f8da5b --- /dev/null +++ b/stackit/internal/services/iaas/network/utils/v1network/datasource.go @@ -0,0 +1,203 @@ +package v1network + +import ( + "context" + "fmt" + "net" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "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/core" + networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform + var model networkModel.DataSourceModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + + networkResp, err := client.GetNetwork(ctx, projectId, 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) + 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 *networkModel.DataSourceModel) 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.NetworkId != nil { + networkId = *networkResp.NetworkId + } else { + return fmt.Errorf("network id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), networkId) + + labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) + if err != nil { + return err + } + + // 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) + 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 + } + + model.IPv4Gateway = types.StringNull() + if networkResp.Gateway != nil { + model.IPv4Gateway = types.StringPointerValue(networkResp.GetGateway()) + } + + // 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)) + } + 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 + } + + model.IPv6Gateway = types.StringNull() + if networkResp.Gatewayv6 != nil { + model.IPv6Gateway = types.StringPointerValue(networkResp.GetGatewayv6()) + } + + 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) + model.RoutingTableID = types.StringNull() + model.Region = types.StringNull() + + return nil +} diff --git a/stackit/internal/services/iaas/network/datasource_test.go b/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go similarity index 92% rename from stackit/internal/services/iaas/network/datasource_test.go rename to stackit/internal/services/iaas/network/utils/v1network/datasource_test.go index 21034d5c..2ce9b5c9 100644 --- a/stackit/internal/services/iaas/network/datasource_test.go +++ b/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go @@ -1,4 +1,4 @@ -package network +package v1network import ( "context" @@ -9,19 +9,20 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" ) func TestMapDataSourceFields(t *testing.T) { tests := []struct { description string - state DataSourceModel + state networkModel.DataSourceModel input *iaas.Network - expected DataSourceModel + expected networkModel.DataSourceModel isValid bool }{ { "id_ok", - DataSourceModel{ + networkModel.DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, @@ -29,7 +30,7 @@ func TestMapDataSourceFields(t *testing.T) { NetworkId: utils.Ptr("nid"), Gateway: iaas.NewNullableString(nil), }, - DataSourceModel{ + networkModel.DataSourceModel{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -54,7 +55,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "values_ok", - DataSourceModel{ + networkModel.DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, @@ -85,7 +86,7 @@ func TestMapDataSourceFields(t *testing.T) { Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")), }, - DataSourceModel{ + networkModel.DataSourceModel{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -130,7 +131,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_nameservers_changed_outside_tf", - DataSourceModel{ + networkModel.DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -149,7 +150,7 @@ func TestMapDataSourceFields(t *testing.T) { "ns3", }, }, - DataSourceModel{ + networkModel.DataSourceModel{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -172,7 +173,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv6_nameservers_changed_outside_tf", - DataSourceModel{ + networkModel.DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -187,7 +188,7 @@ func TestMapDataSourceFields(t *testing.T) { "ns3", }, }, - DataSourceModel{ + networkModel.DataSourceModel{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -207,7 +208,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_prefixes_changed_outside_tf", - DataSourceModel{ + networkModel.DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -222,7 +223,7 @@ func TestMapDataSourceFields(t *testing.T) { "10.100.10.0/16", }, }, - DataSourceModel{ + networkModel.DataSourceModel{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -248,7 +249,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv6_prefixes_changed_outside_tf", - DataSourceModel{ + networkModel.DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -263,7 +264,7 @@ func TestMapDataSourceFields(t *testing.T) { "fd12:3456:789a:4::/64", }, }, - DataSourceModel{ + networkModel.DataSourceModel{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -286,14 +287,14 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_ipv6_gateway_nil", - DataSourceModel{ + networkModel.DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, &iaas.Network{ NetworkId: utils.Ptr("nid"), }, - DataSourceModel{ + networkModel.DataSourceModel{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -316,18 +317,18 @@ func TestMapDataSourceFields(t *testing.T) { }, { "response_nil_fail", - DataSourceModel{}, + networkModel.DataSourceModel{}, nil, - DataSourceModel{}, + networkModel.DataSourceModel{}, false, }, { "no_resource_id", - DataSourceModel{ + networkModel.DataSourceModel{ ProjectId: types.StringValue("pid"), }, &iaas.Network{}, - DataSourceModel{}, + networkModel.DataSourceModel{}, false, }, } diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource.go b/stackit/internal/services/iaas/network/utils/v1network/resource.go new file mode 100644 index 00000000..ddb91ed6 --- /dev/null +++ b/stackit/internal/services/iaas/network/utils/v1network/resource.go @@ -0,0 +1,512 @@ +package v1network + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model networkModel.Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + + // Generate API request body from model + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Create new network + + network, err := client.CreateNetwork(ctx, projectId).CreateNetworkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err)) + return + } + + networkId := *network.NetworkId + network, err = wait.CreateNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) + return + } + + ctx = tflog.SetField(ctx, "network_id", networkId) + + // Map response body to schema + err = mapFields(ctx, network, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set state to fully populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network created") +} + +func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform + var model networkModel.Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + + networkResp, err := client.GetNetwork(ctx, projectId, networkId).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, networkResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network read") +} + +func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model networkModel.Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + + // Retrieve values from state + var stateModel networkModel.Model + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from model + 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 + } + // Update existing network + err = client.PartialUpdateNetwork(ctx, projectId, networkId).PartialUpdateNetworkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) + return + } + waitResp, err := wait.UpdateNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) + return + } + + err = mapFields(ctx, waitResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating 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 updated") +} + +func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state + var model networkModel.Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + + // Delete existing network + err := client.DeleteNetwork(ctx, projectId, networkId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) + return + } + _, err = wait.DeleteNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Network deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: project_id,network_id +func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing network", + fmt.Sprintf("Expected import identifier with format: [project_id],[network_id] Got: %q", req.ID), + ) + return + } + + projectId := idParts[0] + networkId := idParts[1] + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...) + tflog.Info(ctx, "Network state imported") +} + +func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkModel.Model) 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.NetworkId != nil { + networkId = *networkResp.NetworkId + } else { + return fmt.Errorf("network id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), networkId) + + labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) + if err != nil { + return err + } + + // 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) + 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.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)) + } + 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.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) + model.Region = types.StringNull() + model.RoutingTableID = types.StringNull() + + return nil +} + +func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.CreateNetworkPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + 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") + } + 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) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + payload := iaas.CreateNetworkPayload{ + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + Routed: conversion.BoolValueToPointer(model.Routed), + } + + if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil { + payload.AddressFamily = addressFamily + } + + return &payload, nil +} + +func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) (*iaas.PartialUpdateNetworkPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil 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") + } + 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) + } + + payload := iaas.PartialUpdateNetworkPayload{ + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + } + + if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil { + payload.AddressFamily = addressFamily + } + + return &payload, nil +} diff --git a/stackit/internal/services/iaas/network/resource_test.go b/stackit/internal/services/iaas/network/utils/v1network/resource_test.go similarity index 96% rename from stackit/internal/services/iaas/network/resource_test.go rename to stackit/internal/services/iaas/network/utils/v1network/resource_test.go index cc5cbb4c..21db0a1d 100644 --- a/stackit/internal/services/iaas/network/resource_test.go +++ b/stackit/internal/services/iaas/network/utils/v1network/resource_test.go @@ -1,4 +1,4 @@ -package network +package v1network import ( "context" @@ -9,19 +9,20 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" ) func TestMapFields(t *testing.T) { tests := []struct { description string - state Model + state model.Model input *iaas.Network - expected Model + expected model.Model isValid bool }{ { "id_ok", - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, @@ -29,7 +30,7 @@ func TestMapFields(t *testing.T) { NetworkId: utils.Ptr("nid"), Gateway: iaas.NewNullableString(nil), }, - Model{ + model.Model{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -54,7 +55,7 @@ func TestMapFields(t *testing.T) { }, { "values_ok", - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, @@ -85,7 +86,7 @@ func TestMapFields(t *testing.T) { Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")), }, - Model{ + model.Model{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -130,7 +131,7 @@ func TestMapFields(t *testing.T) { }, { "ipv4_nameservers_changed_outside_tf", - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -149,7 +150,7 @@ func TestMapFields(t *testing.T) { "ns3", }, }, - Model{ + model.Model{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -172,7 +173,7 @@ func TestMapFields(t *testing.T) { }, { "ipv6_nameservers_changed_outside_tf", - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -187,7 +188,7 @@ func TestMapFields(t *testing.T) { "ns3", }, }, - Model{ + model.Model{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -207,7 +208,7 @@ func TestMapFields(t *testing.T) { }, { "ipv4_prefixes_changed_outside_tf", - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -222,7 +223,7 @@ func TestMapFields(t *testing.T) { "192.168.55.0/24", }, }, - Model{ + model.Model{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -248,7 +249,7 @@ func TestMapFields(t *testing.T) { }, { "ipv6_prefixes_changed_outside_tf", - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -263,7 +264,7 @@ func TestMapFields(t *testing.T) { "fd12:3456:789a:2::/64", }, }, - Model{ + model.Model{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -286,14 +287,14 @@ func TestMapFields(t *testing.T) { }, { "ipv4_ipv6_gateway_nil", - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, &iaas.Network{ NetworkId: utils.Ptr("nid"), }, - Model{ + model.Model{ Id: types.StringValue("pid,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -316,18 +317,18 @@ func TestMapFields(t *testing.T) { }, { "response_nil_fail", - Model{}, + model.Model{}, nil, - Model{}, + model.Model{}, false, }, { "no_resource_id", - Model{ + model.Model{ ProjectId: types.StringValue("pid"), }, &iaas.Network{}, - Model{}, + model.Model{}, false, }, } @@ -353,13 +354,13 @@ func TestMapFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { tests := []struct { description string - input *Model + input *model.Model expected *iaas.CreateNetworkPayload isValid bool }{ { "default_ok", - &Model{ + &model.Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -395,7 +396,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv4_nameservers_okay", - &Model{ + &model.Model{ Name: types.StringValue("name"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -431,7 +432,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv6_default_ok", - &Model{ + &model.Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -488,14 +489,14 @@ func TestToCreatePayload(t *testing.T) { func TestToUpdatePayload(t *testing.T) { tests := []struct { description string - input *Model - state Model + input *model.Model + state model.Model expected *iaas.PartialUpdateNetworkPayload isValid bool }{ { "default_ok", - &Model{ + &model.Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -507,7 +508,7 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv4Gateway: types.StringValue("gateway"), }, - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), @@ -531,7 +532,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv4_nameservers_okay", - &Model{ + &model.Model{ Name: types.StringValue("name"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -543,7 +544,7 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv4Gateway: types.StringValue("gateway"), }, - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), @@ -567,7 +568,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv4_gateway_nil", - &Model{ + &model.Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -578,7 +579,7 @@ func TestToUpdatePayload(t *testing.T) { }), Routed: types.BoolValue(true), }, - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), @@ -601,7 +602,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_default_ok", - &Model{ + &model.Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -613,7 +614,7 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv6Gateway: types.StringValue("gateway"), }, - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), @@ -637,7 +638,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_gateway_nil", - &Model{ + &model.Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -648,7 +649,7 @@ func TestToUpdatePayload(t *testing.T) { }), Routed: types.BoolValue(true), }, - Model{ + model.Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), diff --git a/stackit/internal/services/iaas/network/utils/v2network/datasource.go b/stackit/internal/services/iaas/network/utils/v2network/datasource.go new file mode 100644 index 00000000..bc447b82 --- /dev/null +++ b/stackit/internal/services/iaas/network/utils/v2network/datasource.go @@ -0,0 +1,215 @@ +package v2network + +import ( + "context" + "fmt" + "net" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { // nolint:gocritic // function signature required by Terraform + var model networkModel.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 := providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + + networkResp, err := 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 *iaasalpha.Network, model *networkModel.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 +} diff --git a/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go b/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go new file mode 100644 index 00000000..eba9d411 --- /dev/null +++ b/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go @@ -0,0 +1,387 @@ +package v2network + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" +) + +const ( + testRegion = "region" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + state networkModel.DataSourceModel + input *iaasalpha.Network + region string + expected networkModel.DataSourceModel + isValid bool + }{ + { + "id_ok", + networkModel.DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv4: &iaasalpha.NetworkIPv4{ + Gateway: iaasalpha.NewNullableString(nil), + }, + }, + testRegion, + networkModel.DataSourceModel{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + IPv4Gateway: types.StringNull(), + IPv4Prefix: types.StringNull(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Gateway: types.StringNull(), + IPv6Prefix: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + PublicIP: types.StringNull(), + Labels: types.MapNull(types.StringType), + Routed: types.BoolNull(), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "values_ok", + networkModel.DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Name: utils.Ptr("name"), + Ipv4: &iaasalpha.NetworkIPv4{ + Nameservers: &[]string{ + "ns1", + "ns2", + }, + Prefixes: &[]string{ + "192.168.42.0/24", + "10.100.10.0/16", + }, + PublicIp: utils.Ptr("publicIp"), + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + }, + Ipv6: &iaasalpha.NetworkIPv6{ + Nameservers: &[]string{ + "ns1", + "ns2", + }, + Prefixes: &[]string{ + "fd12:3456:789a:1::/64", + "fd12:3456:789a:2::/64", + }, + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(true), + }, + testRegion, + networkModel.DataSourceModel{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringValue("name"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4PrefixLength: types.Int64Value(24), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.42.0/24"), + types.StringValue("10.100.10.0/16"), + }), + IPv4Prefix: types.StringValue("192.168.42.0/24"), + IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.42.0/24"), + types.StringValue("10.100.10.0/16"), + }), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv6PrefixLength: types.Int64Value(64), + IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("fd12:3456:789a:1::/64"), + types.StringValue("fd12:3456:789a:2::/64"), + }), + PublicIP: types.StringValue("publicIp"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv4Gateway: types.StringValue("gateway"), + IPv6Gateway: types.StringValue("gateway"), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv4_nameservers_changed_outside_tf", + networkModel.DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv4: &iaasalpha.NetworkIPv4{ + Nameservers: &[]string{ + "ns2", + "ns3", + }, + }, + }, + testRegion, + networkModel.DataSourceModel{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + Labels: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv6_nameservers_changed_outside_tf", + networkModel.DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv6: &iaasalpha.NetworkIPv6{ + Nameservers: &[]string{ + "ns2", + "ns3", + }, + }, + }, + testRegion, + networkModel.DataSourceModel{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + Labels: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv4_prefixes_changed_outside_tf", + networkModel.DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.42.0/24"), + types.StringValue("10.100.10.0/16"), + }), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv4: &iaasalpha.NetworkIPv4{ + Prefixes: &[]string{ + "10.100.20.0/16", + "10.100.10.0/16", + }, + }, + }, + testRegion, + networkModel.DataSourceModel{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Prefixes: types.ListNull(types.StringType), + Labels: types.MapNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Value(16), + IPv4Prefix: types.StringValue("10.100.20.0/16"), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("10.100.20.0/16"), + types.StringValue("10.100.10.0/16"), + }), + IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("10.100.20.0/16"), + types.StringValue("10.100.10.0/16"), + }), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv6_prefixes_changed_outside_tf", + networkModel.DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("fd12:3456:789a:1::/64"), + types.StringValue("fd12:3456:789a:2::/64"), + }), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv6: &iaasalpha.NetworkIPv6{ + Prefixes: &[]string{ + "fd12:3456:789a:3::/64", + "fd12:3456:789a:4::/64", + }, + }, + }, + testRegion, + networkModel.DataSourceModel{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Labels: types.MapNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Value(64), + IPv6Prefix: types.StringValue("fd12:3456:789a:3::/64"), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("fd12:3456:789a:3::/64"), + types.StringValue("fd12:3456:789a:4::/64"), + }), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv4_ipv6_gateway_nil", + networkModel.DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + }, + testRegion, + networkModel.DataSourceModel{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + IPv4Gateway: types.StringNull(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Gateway: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + PublicIP: types.StringNull(), + Labels: types.MapNull(types.StringType), + Routed: types.BoolNull(), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "response_nil_fail", + networkModel.DataSourceModel{}, + nil, + testRegion, + networkModel.DataSourceModel{}, + false, + }, + { + "no_resource_id", + networkModel.DataSourceModel{ + ProjectId: types.StringValue("pid"), + }, + &iaasalpha.Network{}, + testRegion, + networkModel.DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceFields(context.Background(), tt.input, &tt.state, tt.region) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource.go b/stackit/internal/services/iaas/network/utils/v2network/resource.go new file mode 100644 index 00000000..2bfbe0d5 --- /dev/null +++ b/stackit/internal/services/iaas/network/utils/v2network/resource.go @@ -0,0 +1,555 @@ +package v2network + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model networkModel.Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + // Generate API request body from model + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Create new network + + network, err := client.CreateNetwork(ctx, projectId, region).CreateNetworkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err)) + return + } + + networkId := *network.Id + network, err = wait.CreateNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) + return + } + + ctx = tflog.SetField(ctx, "network_id", networkId) + + // Map response body to schema + err = mapFields(ctx, network, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set state to fully populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network created") +} + +func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { // nolint:gocritic // function signature required by Terraform + var model networkModel.Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + region := providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + ctx = tflog.SetField(ctx, "region", region) + + networkResp, err := client.GetNetwork(ctx, projectId, region, networkId).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, networkResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network read") +} + +func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model networkModel.Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + ctx = tflog.SetField(ctx, "region", region) + + // Retrieve values from state + var stateModel networkModel.Model + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from model + 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 + } + // Update existing network + err = client.PartialUpdateNetwork(ctx, projectId, region, networkId).PartialUpdateNetworkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) + return + } + waitResp, err := wait.UpdateNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) + return + } + + err = mapFields(ctx, waitResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating 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 updated") +} + +func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state + var model networkModel.Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + ctx = tflog.SetField(ctx, "region", region) + + // Delete existing network + err := client.DeleteNetwork(ctx, projectId, region, networkId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) + return + } + _, err = wait.DeleteNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Network deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: project_id,region,network_id +func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing network", + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id] Got: %q", req.ID), + ) + return + } + + projectId := idParts[0] + region := idParts[1] + networkId := idParts[2] + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "network_id", networkId) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...) + tflog.Info(ctx, "Network state imported") +} + +func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *networkModel.Model, 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 { + tflog.Error(ctx, fmt.Sprintf("ipv4_prefix_length: %+v", err)) + // 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()) + } + + if networkResp.RoutingTableId != nil { + model.RoutingTableID = types.StringPointerValue(networkResp.RoutingTableId) + } else { + model.RoutingTableID = types.StringNull() + } + + 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 +} + +func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha.CreateNetworkPayload, error) { + if model == nil { + 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") + } + 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), + }, + } + } else if !utils.IsUndefined(model.IPv6Prefix) { + var gateway *iaasalpha.NullableString + if model.NoIPv6Gateway.ValueBool() { + gateway = iaasalpha.NewNullableString(nil) + } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) + } + + ipv6Body = &iaasalpha.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ + Gateway: gateway, + Nameservers: &modelIPv6Nameservers, + Prefix: conversion.StringValueToPointer(model.IPv6Prefix), + }, + } + } + + 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()) + } + + var ipv4Body *iaasalpha.CreateNetworkIPv4 + if !utils.IsUndefined(model.IPv4PrefixLength) { + ipv4Body = &iaasalpha.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefixLength: &iaasalpha.CreateNetworkIPv4WithPrefixLength{ + Nameservers: &modelIPv4Nameservers, + PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength), + }, + } + } else if !utils.IsUndefined(model.IPv4Prefix) { + var gateway *iaasalpha.NullableString + if model.NoIPv4Gateway.ValueBool() { + gateway = iaasalpha.NewNullableString(nil) + } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { + gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) + } + + ipv4Body = &iaasalpha.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{ + Nameservers: &modelIPv4Nameservers, + Prefix: conversion.StringValueToPointer(model.IPv4Prefix), + Gateway: gateway, + }, + } + } + + labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + payload := iaasalpha.CreateNetworkPayload{ + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + Routed: conversion.BoolValueToPointer(model.Routed), + Ipv4: ipv4Body, + Ipv6: ipv6Body, + RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID), + } + + return &payload, nil +} + +func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) (*iaasalpha.PartialUpdateNetworkPayload, error) { + if model == nil { + 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") + } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) + } + + var ipv6Body *iaasalpha.UpdateNetworkIPv6Body + if !(model.IPv6Nameservers.IsNull() || model.IPv6Nameservers.IsUnknown()) { + ipv6Body = &iaasalpha.UpdateNetworkIPv6Body{ + Nameservers: &modelIPv6Nameservers, + } + + if model.NoIPv6Gateway.ValueBool() { + ipv6Body.Gateway = iaasalpha.NewNullableString(nil) + } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + ipv6Body.Gateway = iaasalpha.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()) + } + + var ipv4Body *iaasalpha.UpdateNetworkIPv4Body + if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() { + ipv4Body = &iaasalpha.UpdateNetworkIPv4Body{ + Nameservers: &modelIPv4Nameservers, + } + + if model.NoIPv4Gateway.ValueBool() { + ipv4Body.Gateway = iaasalpha.NewNullableString(nil) + } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { + ipv4Body.Gateway = iaasalpha.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) + } + + payload := iaasalpha.PartialUpdateNetworkPayload{ + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + Ipv4: ipv4Body, + Ipv6: ipv6Body, + RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID), + } + + return &payload, nil +} diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go b/stackit/internal/services/iaas/network/utils/v2network/resource_test.go new file mode 100644 index 00000000..93575f07 --- /dev/null +++ b/stackit/internal/services/iaas/network/utils/v2network/resource_test.go @@ -0,0 +1,707 @@ +package v2network + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" +) + +func TestMapFields(t *testing.T) { + const testRegion = "region" + tests := []struct { + description string + state model.Model + input *iaasalpha.Network + region string + expected model.Model + isValid bool + }{ + { + "id_ok", + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv4: &iaasalpha.NetworkIPv4{ + Gateway: iaasalpha.NewNullableString(nil), + }, + }, + testRegion, + model.Model{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + IPv4Gateway: types.StringNull(), + IPv4Prefix: types.StringNull(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Gateway: types.StringNull(), + IPv6Prefix: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + PublicIP: types.StringNull(), + Labels: types.MapNull(types.StringType), + Routed: types.BoolNull(), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "values_ok", + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Name: utils.Ptr("name"), + Ipv4: &iaasalpha.NetworkIPv4{ + Nameservers: utils.Ptr([]string{"ns1", "ns2"}), + Prefixes: utils.Ptr( + []string{ + "192.168.42.0/24", + "10.100.10.0/16", + }, + ), + PublicIp: utils.Ptr("publicIp"), + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + }, + Ipv6: &iaasalpha.NetworkIPv6{ + Nameservers: utils.Ptr([]string{"ns1", "ns2"}), + Prefixes: utils.Ptr([]string{ + "fd12:3456:789a:1::/64", + "fd12:3456:789b:1::/64", + }), + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(true), + }, + testRegion, + model.Model{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringValue("name"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4PrefixLength: types.Int64Value(24), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.42.0/24"), + types.StringValue("10.100.10.0/16"), + }), + IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.42.0/24"), + types.StringValue("10.100.10.0/16"), + }), + IPv4Prefix: types.StringValue("192.168.42.0/24"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv6PrefixLength: types.Int64Value(64), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("fd12:3456:789a:1::/64"), + types.StringValue("fd12:3456:789b:1::/64"), + }), + IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), + PublicIP: types.StringValue("publicIp"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv4Gateway: types.StringValue("gateway"), + IPv6Gateway: types.StringValue("gateway"), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv4_nameservers_changed_outside_tf", + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv4: &iaasalpha.NetworkIPv4{ + Nameservers: utils.Ptr([]string{ + "ns2", + "ns3", + }), + }, + }, + testRegion, + model.Model{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + Labels: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv6_nameservers_changed_outside_tf", + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv6: &iaasalpha.NetworkIPv6{ + Nameservers: utils.Ptr([]string{ + "ns2", + "ns3", + }), + }, + }, + testRegion, + model.Model{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + Labels: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv4_prefixes_changed_outside_tf", + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.42.0/24"), + types.StringValue("10.100.10.0/24"), + }), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv4: &iaasalpha.NetworkIPv4{ + Prefixes: utils.Ptr( + []string{ + "192.168.54.0/24", + "192.168.55.0/24", + }, + ), + }, + }, + testRegion, + model.Model{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Prefixes: types.ListNull(types.StringType), + Labels: types.MapNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Value(24), + IPv4Prefix: types.StringValue("192.168.54.0/24"), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.54.0/24"), + types.StringValue("192.168.55.0/24"), + }), + IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.54.0/24"), + types.StringValue("192.168.55.0/24"), + }), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv6_prefixes_changed_outside_tf", + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("fd12:3456:789a:1::/64"), + types.StringValue("fd12:3456:789a:2::/64"), + }), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Ipv6: &iaasalpha.NetworkIPv6{ + Prefixes: utils.Ptr( + []string{ + "fd12:3456:789a:1::/64", + "fd12:3456:789a:2::/64", + }, + ), + }, + }, + testRegion, + model.Model{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Labels: types.MapNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Value(64), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("fd12:3456:789a:1::/64"), + types.StringValue("fd12:3456:789a:2::/64"), + }), + IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "ipv4_ipv6_gateway_nil", + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaasalpha.Network{ + Id: utils.Ptr("nid"), + }, + testRegion, + model.Model{ + Id: types.StringValue("pid,region,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + IPv4Gateway: types.StringNull(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Gateway: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + PublicIP: types.StringNull(), + Labels: types.MapNull(types.StringType), + Routed: types.BoolNull(), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "response_nil_fail", + model.Model{}, + nil, + testRegion, + model.Model{}, + false, + }, + { + "no_resource_id", + model.Model{ + ProjectId: types.StringValue("pid"), + }, + &iaasalpha.Network{}, + testRegion, + model.Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(context.Background(), tt.input, &tt.state, tt.region) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *model.Model + expected *iaasalpha.CreateNetworkPayload + isValid bool + }{ + { + "default_ok", + &model.Model{ + Name: types.StringValue("name"), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(false), + IPv4Gateway: types.StringValue("gateway"), + IPv4Prefix: types.StringValue("prefix"), + }, + &iaasalpha.CreateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv4: &iaasalpha.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{ + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Nameservers: utils.Ptr([]string{ + "ns1", + "ns2", + }), + Prefix: utils.Ptr("prefix"), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(false), + }, + true, + }, + { + "ipv4_nameservers_okay", + &model.Model{ + Name: types.StringValue("name"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(false), + IPv4Gateway: types.StringValue("gateway"), + IPv4Prefix: types.StringValue("prefix"), + }, + &iaasalpha.CreateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv4: &iaasalpha.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{ + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Nameservers: utils.Ptr([]string{ + "ns1", + "ns2", + }), + Prefix: utils.Ptr("prefix"), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(false), + }, + true, + }, + { + "ipv6_default_ok", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + 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{ + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Nameservers: utils.Ptr([]string{ + "ns1", + "ns2", + }), + 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) { + output, err := toCreatePayload(context.Background(), tt.input) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *model.Model + state model.Model + expected *iaasalpha.PartialUpdateNetworkPayload + isValid bool + }{ + { + "default_ok", + &model.Model{ + Name: types.StringValue("name"), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv4Gateway: types.StringValue("gateway"), + }, + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Labels: types.MapNull(types.StringType), + }, + &iaasalpha.PartialUpdateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Nameservers: utils.Ptr([]string{ + "ns1", + "ns2", + }), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, + { + "ipv4_nameservers_okay", + &model.Model{ + Name: types.StringValue("name"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv4Gateway: types.StringValue("gateway"), + }, + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Labels: types.MapNull(types.StringType), + }, + &iaasalpha.PartialUpdateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Nameservers: utils.Ptr([]string{ + "ns1", + "ns2", + }), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, + { + "ipv4_gateway_nil", + &model.Model{ + Name: types.StringValue("name"), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + }, + model.Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Labels: types.MapNull(types.StringType), + }, + &iaasalpha.PartialUpdateNetworkPayload{ + Name: utils.Ptr("name"), + Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ + Nameservers: utils.Ptr([]string{ + "ns1", + "ns2", + }), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, + { + "ipv6_default_ok", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + 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{ + Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Nameservers: utils.Ptr([]string{ + "ns1", + "ns2", + }), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, + { + "ipv6_gateway_nil", + &model.Model{ + Name: types.StringValue("name"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + }, + 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{ + "ns1", + "ns2", + }), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toUpdatePayload(context.Background(), tt.input, &tt.state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/iaas/testdata/resource-network-max.tf b/stackit/internal/services/iaas/testdata/resource-network-v1-max.tf similarity index 52% rename from stackit/internal/services/iaas/testdata/resource-network-max.tf rename to stackit/internal/services/iaas/testdata/resource-network-v1-max.tf index ac4d6a12..33e9c32f 100644 --- a/stackit/internal/services/iaas/testdata/resource-network-max.tf +++ b/stackit/internal/services/iaas/testdata/resource-network-v1-max.tf @@ -1,19 +1,32 @@ variable "project_id" {} variable "name" {} variable "ipv4_gateway" {} -variable "ipv4_nameservers" {} +variable "ipv4_nameserver_0" {} +variable "ipv4_nameserver_1" {} variable "ipv4_prefix" {} variable "ipv4_prefix_length" {} variable "routed" {} variable "label" {} -resource "stackit_network" "network" { +resource "stackit_network" "network_prefix" { project_id = var.project_id name = var.name ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null no_ipv4_gateway = var.ipv4_gateway != "" ? null : true - ipv4_nameservers = [var.ipv4_nameservers] + ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] ipv4_prefix = var.ipv4_prefix + routed = var.routed + labels = { + "acc-test" : var.label + } +} + + +resource "stackit_network" "network_prefix_length" { + project_id = var.project_id + name = var.name + no_ipv4_gateway = true + ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] ipv4_prefix_length = var.ipv4_prefix_length routed = var.routed labels = { diff --git a/stackit/internal/services/iaas/testdata/resource-network-min.tf b/stackit/internal/services/iaas/testdata/resource-network-v1-min.tf similarity index 100% rename from stackit/internal/services/iaas/testdata/resource-network-min.tf rename to stackit/internal/services/iaas/testdata/resource-network-v1-min.tf diff --git a/stackit/internal/services/iaas/testdata/resource-network-v2-max.tf b/stackit/internal/services/iaas/testdata/resource-network-v2-max.tf new file mode 100644 index 00000000..63a0fcf0 --- /dev/null +++ b/stackit/internal/services/iaas/testdata/resource-network-v2-max.tf @@ -0,0 +1,43 @@ +variable "project_id" {} +variable "name" {} +variable "ipv4_gateway" {} +variable "ipv4_nameserver_0" {} +variable "ipv4_nameserver_1" {} +variable "ipv4_prefix" {} +variable "ipv4_prefix_length" {} +variable "routed" {} +variable "label" {} +variable "organization_id" {} +variable "network_area_id" {} + +# resource "stackit_network" "network_prefix" { +# project_id = var.project_id +# name = var.name +# # ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null +# # no_ipv4_gateway = var.ipv4_gateway != "" ? null : true +# ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] +# ipv4_prefix = var.ipv4_prefix +# routed = var.routed +# labels = { +# "acc-test" : var.label +# } +# } + +resource "stackit_network" "network_prefix_length" { + project_id = var.project_id + name = var.name + # no_ipv4_gateway = true + ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] + ipv4_prefix_length = var.ipv4_prefix_length + routed = var.routed + labels = { + "acc-test" : var.label + } + routing_table_id = stackit_routing_table.routing_table.routing_table_id +} + +resource "stackit_routing_table" "routing_table" { + organization_id = var.organization_id + network_area_id = var.network_area_id + name = var.name +} diff --git a/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf b/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf new file mode 100644 index 00000000..e2748bdd --- /dev/null +++ b/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf @@ -0,0 +1,7 @@ +variable "project_id" {} +variable "name" {} + +resource "stackit_network" "network" { + project_id = var.project_id + name = var.name +} \ No newline at end of file diff --git a/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go b/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go index 2522a7fb..dd0e0654 100644 --- a/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go +++ b/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go @@ -5,6 +5,7 @@ import ( _ "embed" "errors" "fmt" + "maps" "net/http" "strings" "sync" @@ -19,8 +20,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "maps" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -123,7 +122,7 @@ func TestAccRoutingTable(t *testing.T) { // Creation { ConfigVariables: testConfigRoutingTableMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableMinConfig), + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMinConfig), Check: resource.ComposeAggregateTestCheckFunc( // Routing table resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["organization_id"])), @@ -158,7 +157,7 @@ func TestAccRoutingTable(t *testing.T) { network_area_id = stackit_routing_table.routing_table.network_area_id } `, - testutil.IaaSProviderConfig(), resourceRoutingTableMinConfig, + testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMinConfig, ), Check: resource.ComposeAggregateTestCheckFunc( // Routing table @@ -232,7 +231,7 @@ func TestAccRoutingTable(t *testing.T) { // Update { ConfigVariables: testConfigRoutingTableMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableMinConfig), + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMinConfig), Check: resource.ComposeAggregateTestCheckFunc( // Routing table resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMinUpdated["organization_id"])), @@ -261,7 +260,7 @@ func TestAccRoutingTable(t *testing.T) { // Creation { ConfigVariables: testConfigRoutingTableMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableMaxConfig), + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( // Routing table resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["organization_id"])), @@ -297,7 +296,7 @@ func TestAccRoutingTable(t *testing.T) { network_area_id = stackit_routing_table.routing_table.network_area_id } `, - testutil.IaaSProviderConfig(), resourceRoutingTableMaxConfig, + testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMaxConfig, ), Check: resource.ComposeAggregateTestCheckFunc( // Routing table @@ -373,7 +372,7 @@ func TestAccRoutingTable(t *testing.T) { // Update { ConfigVariables: testConfigRoutingTableMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableMaxConfig), + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( // Routing table resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["organization_id"])), @@ -403,7 +402,7 @@ func TestAccRoutingTable(t *testing.T) { // Creation { ConfigVariables: testConfigRoutingTableRouteMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableRouteMinConfig), + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMinConfig), Check: resource.ComposeAggregateTestCheckFunc( // Routing table resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), @@ -451,7 +450,7 @@ func TestAccRoutingTable(t *testing.T) { routing_table_id = stackit_routing_table_route.route.routing_table_id } `, - testutil.IaaSProviderConfig(), resourceRoutingTableRouteMinConfig, + testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMinConfig, ), Check: resource.ComposeAggregateTestCheckFunc( // Routing table route @@ -529,7 +528,7 @@ func TestAccRoutingTable(t *testing.T) { // Update { ConfigVariables: testConfigRoutingTableRouteMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableRouteMinConfig), + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMinConfig), Check: resource.ComposeAggregateTestCheckFunc( // Routing table resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["organization_id"])), @@ -569,7 +568,7 @@ func TestAccRoutingTable(t *testing.T) { // Creation { ConfigVariables: testConfigRoutingTableRouteMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableRouteMaxConfig), + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( // Routing table resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), @@ -618,7 +617,7 @@ func TestAccRoutingTable(t *testing.T) { routing_table_id = stackit_routing_table_route.route.routing_table_id } `, - testutil.IaaSProviderConfig(), resourceRoutingTableRouteMaxConfig, + testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMaxConfig, ), Check: resource.ComposeAggregateTestCheckFunc( // Routing table route @@ -698,7 +697,7 @@ func TestAccRoutingTable(t *testing.T) { // Update { ConfigVariables: testConfigRoutingTableRouteMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableRouteMaxConfig), + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( // Routing table resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["organization_id"])), diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 8f48353b..f763d5b6 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -122,13 +122,28 @@ func IaaSProviderConfig() string { return ` provider "stackit" { default_region = "eu01" - experiments = ["routing-tables"] }` } return fmt.Sprintf(` provider "stackit" { iaas_custom_endpoint = "%s" - experiments = ["routing-tables"] + }`, + IaaSCustomEndpoint, + ) +} + +func IaaSProviderConfigWithExperiments() string { + if IaaSCustomEndpoint == "" { + return ` + provider "stackit" { + default_region = "eu01" + experiments = [ "routing-tables", "network" ] + }` + } + return fmt.Sprintf(` + provider "stackit" { + iaas_custom_endpoint = "%s" + experiments = [ "routing-tables", "network" ] }`, IaaSCustomEndpoint, ) diff --git a/stackit/provider.go b/stackit/provider.go index 4ef40c3c..55041043 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -3,6 +3,7 @@ package stackit import ( "context" "fmt" + "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -186,7 +187,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "service_enablement_custom_endpoint": "Custom endpoint for the Service Enablement API", "token_custom_endpoint": "Custom endpoint for the token API, which is used to request access tokens when using the key flow", "enable_beta_resources": "Enable beta resources. Default is false.", - "experiments": fmt.Sprintf("Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: %v", features.AvailableExperiments), + "experiments": fmt.Sprintf("Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: %v", strings.Join(features.AvailableExperiments, ", ")), } resp.Schema = schema.Schema{