From 76b8e5692d04742aaf3bc58f127fa0c1ce10c15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Mon, 20 May 2024 17:17:39 +0100 Subject: [PATCH] Implement new `stackit_network` resource and datasource (#366) * Copy file from DNS zone resource * Implement resource * Implement datasource * Add examples * Implement acceptance test; Some fixes * Add docs * Fix linter * Fixes after review --- docs/data-sources/network.md | 37 ++ docs/index.md | 3 +- docs/resources/network.md | 42 ++ .../stackit_network/data-source.tf | 4 + .../resources/stackit_network/resource.tf | 6 + go.mod | 1 + go.sum | 2 + stackit/internal/core/core.go | 1 + .../internal/services/iaas/iaas_acc_test.go | 187 +++++++ .../services/iaas/network/datasource.go | 166 +++++++ .../services/iaas/network/resource.go | 461 ++++++++++++++++++ .../services/iaas/network/resource_test.go | 275 +++++++++++ stackit/internal/testutil/testutil.go | 16 + stackit/provider.go | 19 +- 14 files changed, 1216 insertions(+), 4 deletions(-) create mode 100644 docs/data-sources/network.md create mode 100644 docs/resources/network.md create mode 100644 examples/data-sources/stackit_network/data-source.tf create mode 100644 examples/resources/stackit_network/resource.tf create mode 100644 stackit/internal/services/iaas/iaas_acc_test.go create mode 100644 stackit/internal/services/iaas/network/datasource.go create mode 100644 stackit/internal/services/iaas/network/resource.go create mode 100644 stackit/internal/services/iaas/network/resource_test.go diff --git a/docs/data-sources/network.md b/docs/data-sources/network.md new file mode 100644 index 00000000..64adbb0f --- /dev/null +++ b/docs/data-sources/network.md @@ -0,0 +1,37 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_network Data Source - stackit" +subcategory: "" +description: |- + Network resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_network (Data Source) + +Network resource schema. Must have a `region` specified in the provider configuration. + +## Example Usage + +```terraform +data "stackit_network" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `network_id` (String) The network ID. +- `project_id` (String) STACKIT project ID to which the network is associated. + +### Read-Only + +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`network_id`". +- `ipv4_prefix_length` (Number) The IPv4 prefix length of the network. +- `name` (String) The name of the network. +- `nameservers` (List of String) The nameservers of the network. +- `prefixes` (List of String) The prefixes of the network. +- `public_ip` (String) The public IP of the network. diff --git a/docs/index.md b/docs/index.md index 80c2a040..88326178 100644 --- a/docs/index.md +++ b/docs/index.md @@ -143,6 +143,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `argus_custom_endpoint` (String) Custom endpoint for the Argus service - `credentials_path` (String) Path of JSON from where the credentials are read. Takes precedence over the env var `STACKIT_CREDENTIALS_PATH`. Default value is `~/.stackit/credentials.json`. - `dns_custom_endpoint` (String) Custom endpoint for the DNS service +- `iaas_custom_endpoint` (String) Custom endpoint for the IaaS service - `jwks_custom_endpoint` (String, Deprecated) Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when using the key flow - `loadbalancer_custom_endpoint` (String) Custom endpoint for the Load Balancer service - `logme_custom_endpoint` (String) Custom endpoint for the LogMe service @@ -155,7 +156,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `private_key` (String) Private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key. - `private_key_path` (String) Path for the private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key. - `rabbitmq_custom_endpoint` (String) Custom endpoint for the RabbitMQ service -- `redis_custom_endpoint` (String) +- `redis_custom_endpoint` (String) Custom endpoint for the Redis service - `region` (String) Region will be used as the default location for regional services. Not all services require a region, some are global - `resourcemanager_custom_endpoint` (String) Custom endpoint for the Resource Manager service - `secretsmanager_custom_endpoint` (String) Custom endpoint for the Secrets Manager service diff --git a/docs/resources/network.md b/docs/resources/network.md new file mode 100644 index 00000000..9b967776 --- /dev/null +++ b/docs/resources/network.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_network Resource - stackit" +subcategory: "" +description: |- + Network resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_network (Resource) + +Network resource schema. Must have a `region` specified in the provider configuration. + +## Example Usage + +```terraform +resource "stackit_network" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-network" + nameservers = ["1.2.3.4", "5.6.7.8"] + ipv4_prefix_length = 24 +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the network. +- `nameservers` (List of String) The nameservers of the network. +- `project_id` (String) STACKIT project ID to which the network is associated. + +### Optional + +- `ipv4_prefix_length` (Number) The IPv4 prefix length of the network. + +### Read-Only + +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`network_id`". +- `network_id` (String) The network ID. +- `prefixes` (List of String) The prefixes of the network. +- `public_ip` (String) The public IP of the network. diff --git a/examples/data-sources/stackit_network/data-source.tf b/examples/data-sources/stackit_network/data-source.tf new file mode 100644 index 00000000..6a932ba5 --- /dev/null +++ b/examples/data-sources/stackit_network/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_network" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_network/resource.tf b/examples/resources/stackit_network/resource.tf new file mode 100644 index 00000000..10bb9831 --- /dev/null +++ b/examples/resources/stackit_network/resource.tf @@ -0,0 +1,6 @@ +resource "stackit_network" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-network" + nameservers = ["1.2.3.4", "5.6.7.8"] + ipv4_prefix_length = 24 +} diff --git a/go.mod b/go.mod index fba1d61a..1021ca2f 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/core v0.12.0 github.com/stackitcloud/stackit-sdk-go/services/argus v0.10.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.9.1 + github.com/stackitcloud/stackit-sdk-go/services/iaas v0.3.0 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0 github.com/stackitcloud/stackit-sdk-go/services/logme v0.14.0 github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.14.0 diff --git a/go.sum b/go.sum index 0ca2bf7b..5bfbf251 100644 --- a/go.sum +++ b/go.sum @@ -152,6 +152,8 @@ github.com/stackitcloud/stackit-sdk-go/services/argus v0.10.0 h1:FAYOt6UBy/F2jPH github.com/stackitcloud/stackit-sdk-go/services/argus v0.10.0/go.mod h1:nVllQfYODhX1q3bgwVTLO7wHOp+8NMLiKbn3u/Dg5nU= github.com/stackitcloud/stackit-sdk-go/services/dns v0.9.1 h1:pj2nAJvgzFSckA56rCPdi7StXGrr06go8qejI1weNJ8= github.com/stackitcloud/stackit-sdk-go/services/dns v0.9.1/go.mod h1:MdZcRbs19s2NLeJmSLSoqTzm9IPIQhE1ZEMpo9gePq0= +github.com/stackitcloud/stackit-sdk-go/services/iaas v0.3.0 h1:0Nb7CruTyM/HxhZQjntQUHJqwCoKjFUC9KZcBBj+c5c= +github.com/stackitcloud/stackit-sdk-go/services/iaas v0.3.0/go.mod h1:XtJA9FMK/yJ0dj4HtRAogmZPRUsZiFcuwUSfHYNASjo= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0 h1:LAteZO46XmqTsmPw0QV8n8WiGM205pxrcqHqWznNmyY= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0/go.mod h1:wsO3+vXe1XiKLeCIctWAptaHQZ07Un7kmLTQ+drbj7w= github.com/stackitcloud/stackit-sdk-go/services/logme v0.14.0 h1:vvQFCN5sKZA9tdzrbDnAVMsaTijX8lvTYnPaKQHmkoI= diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index e57b41d6..afc043c6 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -19,6 +19,7 @@ type ProviderData struct { Region string ArgusCustomEndpoint string DnsCustomEndpoint string + IaaSCustomEndpoint string LoadBalancerCustomEndpoint string LogMeCustomEndpoint string MariaDBCustomEndpoint string diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go new file mode 100644 index 00000000..25149aac --- /dev/null +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -0,0 +1,187 @@ +package iaas_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "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/testutil" +) + +// 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", +} + +func resourceConfig(name, nameservers string) string { + return fmt.Sprintf(` + %s + + resource "stackit_network" "network" { + project_id = "%s" + name = "%s" + ipv4_prefix_length = "%s" + nameservers = %s + } + `, + testutil.IaaSProviderConfig(), + networkResource["project_id"], + name, + networkResource["ipv4_prefix_length"], + nameservers, + ) +} + +func TestAccIaaS(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckIaaSDestroy, + Steps: []resource.TestStep{ + + // Creation + { + Config: resourceConfig( + networkResource["name"], + fmt.Sprintf( + "[%q]", + networkResource["nameserver0"], + ), + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance + resource.TestCheckResourceAttr("stackit_network.network", "project_id", networkResource["project_id"]), + resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network", "name", networkResource["name"]), + resource.TestCheckResourceAttr("stackit_network.network", "nameservers.#", "1"), + resource.TestCheckResourceAttr("stackit_network.network", "nameservers.0", networkResource["nameserver0"]), + ), + }, + // Data source + { + Config: fmt.Sprintf(` + %s + + data "stackit_network" "network" { + project_id = stackit_network.network.project_id + network_id = stackit_network.network.network_id + }`, + resourceConfig( + networkResource["name"], + fmt.Sprintf( + "[%q]", + networkResource["nameserver0"], + ), + ), + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance + resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", networkResource["project_id"]), + resource.TestCheckResourceAttrPair( + "stackit_network.network", "network_id", + "data.stackit_network.network", "network_id", + ), + resource.TestCheckResourceAttr("data.stackit_network.network", "name", networkResource["name"]), + resource.TestCheckResourceAttr("data.stackit_network.network", "nameservers.0", networkResource["nameserver0"]), + ), + }, + // Import + { + 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") + } + 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, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ipv4_prefix_length"}, // Field is not returned by the API + }, + // Update + { + Config: resourceConfig( + fmt.Sprintf("%s-updated", networkResource["name"]), + fmt.Sprintf( + "[%q, %q]", + networkResource["nameserver0"], + networkResource["nameserver1"], + ), + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance + resource.TestCheckResourceAttr("stackit_network.network", "project_id", networkResource["project_id"]), + resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network", "name", fmt.Sprintf("%s-updated", networkResource["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network", "nameservers.0", networkResource["nameserver0"]), + resource.TestCheckResourceAttr("stackit_network.network", "nameservers.1", networkResource["nameserver1"]), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func testAccCheckIaaSDestroy(s *terraform.State) error { + ctx := context.Background() + var client *iaas.APIClient + var err error + if testutil.IaaSCustomEndpoint == "" { + client, err = iaas.NewAPIClient( + config.WithRegion("eu01"), + ) + } else { + client, err = iaas.NewAPIClient( + config.WithEndpoint(testutil.IaaSCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + networksToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_network" { + continue + } + // network terraform ID: "[project_id],[network_id]" + networkId := strings.Split(rs.Primary.ID, core.Separator)[1] + networksToDestroy = append(networksToDestroy, networkId) + } + + networksResp, err := client.ListNetworksExecute(ctx, testutil.ProjectId) + if err != nil { + return fmt.Errorf("getting networksResp: %w", err) + } + + networks := *networksResp.Items + for i := range networks { + if networks[i].NetworkId == nil { + continue + } + if utils.Contains(networksToDestroy, *networks[i].NetworkId) { + err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, *networks[i].NetworkId) + if err != nil { + return fmt.Errorf("destroying network %s during CheckDestroy: %w", *networks[i].NetworkId, err) + } + } + } + return nil +} diff --git a/stackit/internal/services/iaas/network/datasource.go b/stackit/internal/services/iaas/network/datasource.go new file mode 100644 index 00000000..0890df50 --- /dev/null +++ b/stackit/internal/services/iaas/network/datasource.go @@ -0,0 +1,166 @@ +package network + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "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/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &networkDataSource{} +) + +// NewNetworkDataSource is a helper function to simplify the provider implementation. +func NewNetworkDataSource() datasource.DataSource { + return &networkDataSource{} +} + +// networkDataSource is the data source implementation. +type networkDataSource struct { + client *iaas.APIClient +} + +// Metadata returns the data source type name. +func (d *networkDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_network" +} + +func (d *networkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + var apiClient *iaas.APIClient + var err error + + providerData, ok := req.ProviderData.(core.ProviderData) + if !ok { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) + return + } + + if providerData.IaaSCustomEndpoint != "" { + apiClient, err = iaas.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.IaaSCustomEndpoint), + ) + } else { + apiClient, err = iaas.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithRegion(providerData.Region), + ) + } + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err)) + return + } + + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") +} + +// Schema defines the schema for the data source. +func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Network resource schema. Must have a `region` specified in the provider configuration.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`\".", + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "STACKIT project ID to which the network is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "network_id": schema.StringAttribute{ + Description: "The network ID.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of the network.", + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.LengthAtMost(63), + }, + }, + "nameservers": schema.ListAttribute{ + Description: "The nameservers of the network.", + Computed: true, + ElementType: types.StringType, + }, + "ipv4_prefix_length": schema.Int64Attribute{ + Description: "The IPv4 prefix length of the network.", + Computed: true, + }, + "prefixes": schema.ListAttribute{ + Description: "The prefixes of the network.", + Computed: true, + ElementType: types.StringType, + }, + "public_ip": schema.StringAttribute{ + Description: "The public IP of the network.", + Computed: true, + }, + }, + } +} + +// 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 Model + 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 := d.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 + } + + err = mapFields(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") +} diff --git a/stackit/internal/services/iaas/network/resource.go b/stackit/internal/services/iaas/network/resource.go new file mode 100644 index 00000000..5a12f9c0 --- /dev/null +++ b/stackit/internal/services/iaas/network/resource.go @@ -0,0 +1,461 @@ +package network + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/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/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/runtime" + "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" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &networkResource{} + _ resource.ResourceWithConfigure = &networkResource{} + _ 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"` + IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` + Prefixes types.List `tfsdk:"prefixes"` + PublicIP types.String `tfsdk:"public_ip"` +} + +// NewNetworkResource is a helper function to simplify the provider implementation. +func NewNetworkResource() resource.Resource { + return &networkResource{} +} + +// networkResource is the resource implementation. +type networkResource struct { + client *iaas.APIClient +} + +// Metadata returns the resource type name. +func (r *networkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_network" +} + +// Configure adds the provider configured client to the resource. +func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(core.ProviderData) + if !ok { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) + return + } + + var apiClient *iaas.APIClient + var err error + if providerData.IaaSCustomEndpoint != "" { + ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint) + apiClient, err = iaas.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.IaaSCustomEndpoint), + ) + } else { + apiClient, err = iaas.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithRegion(providerData.Region), + ) + } + + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) + return + } + + r.client = apiClient + tflog.Info(ctx, "IaaS client configured") +} + +// Schema defines the schema for the resource. +func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Network resource schema. Must have a `region` specified in the provider configuration.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: "STACKIT project ID to which the network is associated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "network_id": schema.StringAttribute{ + Description: "The network ID.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of the network.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.LengthAtMost(63), + }, + }, + "nameservers": schema.ListAttribute{ + Description: "The nameservers of the network.", + Required: true, + ElementType: types.StringType, + }, + "ipv4_prefix_length": schema.Int64Attribute{ + Description: "The IPv4 prefix length of the network.", + Optional: true, + }, + "prefixes": schema.ListAttribute{ + Description: "The prefixes of the network.", + Computed: true, + ElementType: types.StringType, + }, + "public_ip": schema.StringAttribute{ + Description: "The public IP of the network.", + Computed: true, + }, + }, + } +} + +// 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 + } + + projectId := model.ProjectId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + + // Generate API request body from model + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Create new network + var httpResp *http.Response + ctxWithHTTPResp := runtime.WithCaptureHTTPResponse(ctx, &httpResp) + err = r.client.CreateNetwork(ctxWithHTTPResp, projectId).CreateNetworkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err)) + return + } + network, err := wait.CreateNetworkWaitHandler(ctx, r.client, projectId, httpResp.Header.Get("x-request-id")).WaitWithContext(context.Background()) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) + return + } + networkId := *network.NetworkId + 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 + } + 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 + } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + + // Generate API request body from model + payload, err := toUpdatePayload(&model) + 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 + } + + 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 + } + + 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") + } + + idParts := []string{ + model.ProjectId.ValueString(), + networkId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + + if networkResp.Nameservers == nil { + model.Nameservers = types.ListNull(types.StringType) + } else { + respNameservers := *networkResp.Nameservers + modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) + if err != nil { + return fmt.Errorf("get current network nameservers from model: %w", err) + } + + reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) + + nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) + if diags.HasError() { + return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) + } + + model.Nameservers = nameserversTF + } + + if networkResp.Prefixes == nil { + model.Prefixes = 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)) + } + + model.Prefixes = prefixesTF + } + + model.NetworkId = types.StringValue(networkId) + model.Name = types.StringPointerValue(networkResp.Name) + model.PublicIP = types.StringPointerValue(networkResp.PublicIp) + + return nil +} + +func toCreatePayload(model *Model) (*iaas.CreateNetworkPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + modelNameservers := []string{} + for _, ns := range model.Nameservers.Elements() { + nameserverString, ok := ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelNameservers = append(modelNameservers, nameserverString.ValueString()) + } + + return &iaas.CreateNetworkPayload{ + Name: conversion.StringValueToPointer(model.Name), + AddressFamily: &iaas.CreateNetworkAddressFamily{ + Ipv4: &iaas.CreateNetworkIPv4{ + PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength), + Nameservers: &modelNameservers, + }, + }, + }, nil +} + +func toUpdatePayload(model *Model) (*iaas.PartialUpdateNetworkPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + modelNameservers := []string{} + for _, ns := range model.Nameservers.Elements() { + nameserverString, ok := ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelNameservers = append(modelNameservers, nameserverString.ValueString()) + } + + return &iaas.PartialUpdateNetworkPayload{ + Name: conversion.StringValueToPointer(model.Name), + AddressFamily: &iaas.UpdateNetworkAddressFamily{ + Ipv4: &iaas.UpdateNetworkIPv4{ + Nameservers: &modelNameservers, + }, + }, + }, nil +} diff --git a/stackit/internal/services/iaas/network/resource_test.go b/stackit/internal/services/iaas/network/resource_test.go new file mode 100644 index 00000000..d4ae17bc --- /dev/null +++ b/stackit/internal/services/iaas/network/resource_test.go @@ -0,0 +1,275 @@ +package network + +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/iaas" +) + +func TestMapFields(t *testing.T) { + tests := []struct { + description string + state Model + input *iaas.Network + expected Model + isValid bool + }{ + { + "id_ok", + Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + }, + Model{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + Prefixes: types.ListNull(types.StringType), + PublicIP: types.StringNull(), + }, + true, + }, + { + "values_ok", + Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + Name: utils.Ptr("name"), + Nameservers: &[]string{ + "ns1", + "ns2", + }, + Prefixes: &[]string{ + "prefix1", + "prefix2", + }, + PublicIp: utils.Ptr("publicIp"), + }, + Model{ + Id: types.StringValue("pid,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"), + }), + IPv4PrefixLength: types.Int64Null(), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix1"), + types.StringValue("prefix2"), + }), + PublicIP: types.StringValue("publicIp"), + }, + true, + }, + { + "nameservers_changed_outisde_tf", + Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + Nameservers: &[]string{ + "ns2", + "ns3", + }, + }, + Model{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Prefixes: types.ListNull(types.StringType), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + }, + true, + }, + { + "prefixes_changed_outisde_tf", + Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix1"), + types.StringValue("prefix2"), + }), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + Prefixes: &[]string{ + "prefix2", + "prefix3", + }, + }, + Model{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix2"), + types.StringValue("prefix3"), + }), + }, + true, + }, + { + "response_nil_fail", + Model{}, + nil, + Model{}, + false, + }, + { + "no_resource_id", + Model{ + ProjectId: types.StringValue("pid"), + }, + &iaas.Network{}, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(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(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 + expected *iaas.CreateNetworkPayload + isValid bool + }{ + { + "default_ok", + &Model{ + Name: types.StringValue("name"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4PrefixLength: types.Int64Value(24), + }, + &iaas.CreateNetworkPayload{ + Name: utils.Ptr("name"), + AddressFamily: &iaas.CreateNetworkAddressFamily{ + Ipv4: &iaas.CreateNetworkIPv4{ + Nameservers: &[]string{ + "ns1", + "ns2", + }, + PrefixLength: utils.Ptr(int64(24)), + }, + }, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toCreatePayload(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) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *iaas.PartialUpdateNetworkPayload + isValid bool + }{ + { + "default_ok", + &Model{ + Name: types.StringValue("name"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + }, + &iaas.PartialUpdateNetworkPayload{ + Name: utils.Ptr("name"), + AddressFamily: &iaas.UpdateNetworkAddressFamily{ + Ipv4: &iaas.UpdateNetworkIPv4{ + Nameservers: &[]string{ + "ns1", + "ns2", + }, + }, + }, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toUpdatePayload(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) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 3f5ac83b..7add8b97 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -39,6 +39,7 @@ var ( ArgusCustomEndpoint = os.Getenv("TF_ACC_ARGUS_CUSTOM_ENDPOINT") DnsCustomEndpoint = os.Getenv("TF_ACC_DNS_CUSTOM_ENDPOINT") + IaaSCustomEndpoint = os.Getenv("TF_ACC_IAAS_CUSTOM_ENDPOINT") LoadBalancerCustomEndpoint = os.Getenv("TF_ACC_LOADBALANCER_CUSTOM_ENDPOINT") LogMeCustomEndpoint = os.Getenv("TF_ACC_LOGME_CUSTOM_ENDPOINT") MariaDBCustomEndpoint = os.Getenv("TF_ACC_MARIADB_CUSTOM_ENDPOINT") @@ -89,6 +90,21 @@ func DnsProviderConfig() string { ) } +func IaaSProviderConfig() string { + if IaaSCustomEndpoint == "" { + return ` + provider "stackit" { + region = "eu01" + }` + } + return fmt.Sprintf(` + provider "stackit" { + iaas_custom_endpoint = "%s" + }`, + IaaSCustomEndpoint, + ) +} + func LoadBalancerProviderConfig() string { if LoadBalancerCustomEndpoint == "" { return ` diff --git a/stackit/provider.go b/stackit/provider.go index 0d1a73fd..b684b4f5 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -14,6 +14,7 @@ import ( argusScrapeConfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/argus/scrapeconfig" dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset" dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone" + iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network" loadBalancerCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/credential" loadBalancer "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/loadbalancer" loadBalancerObservabilityCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/observability-credential" @@ -83,6 +84,7 @@ type providerModel struct { Token types.String `tfsdk:"service_account_token"` Region types.String `tfsdk:"region"` DNSCustomEndpoint types.String `tfsdk:"dns_custom_endpoint"` + IaaSCustomEndpoint types.String `tfsdk:"iaas_custom_endpoint"` PostgreSQLCustomEndpoint types.String `tfsdk:"postgresql_custom_endpoint"` PostgresFlexCustomEndpoint types.String `tfsdk:"postgresflex_custom_endpoint"` MongoDBFlexCustomEndpoint types.String `tfsdk:"mongodbflex_custom_endpoint"` @@ -112,9 +114,9 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "private_key": "Private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key.", "service_account_email": "Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource.", "region": "Region will be used as the default location for regional services. Not all services require a region, some are global", + "argus_custom_endpoint": "Custom endpoint for the Argus service", "dns_custom_endpoint": "Custom endpoint for the DNS service", - "postgresql_custom_endpoint": "Custom endpoint for the PostgreSQL service", - "postgresflex_custom_endpoint": "Custom endpoint for the PostgresFlex service", + "iaas_custom_endpoint": "Custom endpoint for the IaaS service", "mongodbflex_custom_endpoint": "Custom endpoint for the MongoDB Flex service", "loadbalancer_custom_endpoint": "Custom endpoint for the Load Balancer service", "logme_custom_endpoint": "Custom endpoint for the LogMe service", @@ -122,7 +124,9 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "mariadb_custom_endpoint": "Custom endpoint for the MariaDB service", "objectstorage_custom_endpoint": "Custom endpoint for the Object Storage service", "opensearch_custom_endpoint": "Custom endpoint for the OpenSearch service", - "argus_custom_endpoint": "Custom endpoint for the Argus service", + "postgresql_custom_endpoint": "Custom endpoint for the PostgreSQL service", + "postgresflex_custom_endpoint": "Custom endpoint for the PostgresFlex service", + "redis_custom_endpoint": "Custom endpoint for the Redis service", "ske_custom_endpoint": "Custom endpoint for the Kubernetes Engine (SKE) service", "resourcemanager_custom_endpoint": "Custom endpoint for the Resource Manager service", "secretsmanager_custom_endpoint": "Custom endpoint for the Secrets Manager service", @@ -168,6 +172,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["dns_custom_endpoint"], }, + "iaas_custom_endpoint": schema.StringAttribute{ + Optional: true, + Description: descriptions["iaas_custom_endpoint"], + }, "postgresql_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["postgresql_custom_endpoint"], @@ -278,6 +286,9 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, if !(providerConfig.DNSCustomEndpoint.IsUnknown() || providerConfig.DNSCustomEndpoint.IsNull()) { providerData.DnsCustomEndpoint = providerConfig.DNSCustomEndpoint.ValueString() } + if !(providerConfig.IaaSCustomEndpoint.IsUnknown() || providerConfig.IaaSCustomEndpoint.IsNull()) { + providerData.IaaSCustomEndpoint = providerConfig.IaaSCustomEndpoint.ValueString() + } if !(providerConfig.PostgreSQLCustomEndpoint.IsUnknown() || providerConfig.PostgreSQLCustomEndpoint.IsNull()) { providerData.PostgreSQLCustomEndpoint = providerConfig.PostgreSQLCustomEndpoint.ValueString() } @@ -343,6 +354,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource argusScrapeConfig.NewScrapeConfigDataSource, dnsZone.NewZoneDataSource, dnsRecordSet.NewRecordSetDataSource, + iaasNetwork.NewNetworkDataSource, loadBalancer.NewLoadBalancerDataSource, logMeInstance.NewInstanceDataSource, logMeCredential.NewCredentialDataSource, @@ -379,6 +391,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { argusScrapeConfig.NewScrapeConfigResource, dnsZone.NewZoneResource, dnsRecordSet.NewRecordSetResource, + iaasNetwork.NewNetworkResource, loadBalancer.NewLoadBalancerResource, loadBalancerCredential.NewCredentialResource, loadBalancerObservabilityCredential.NewObservabilityCredentialResource,