Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/resources/routing_table.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ subcategory: ""
description: |-
Routing table resource schema. Must have a region specified in the provider configuration.
This resource is for SNA, not VPC, based networks.
The platform deletes routing tables and their routes together with the network area region (stackit_network_area_region) they belong to. When the network area or its region no longer exists, the provider treats the routing table as deleted: it is removed from the Terraform state on refresh, and destroying it succeeds.
~> This resource is part of the experimental feature routing-tables and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.
---

Expand All @@ -14,6 +15,8 @@ Routing table resource schema. Must have a `region` specified in the provider co

This resource is for SNA, not VPC, based networks.

The platform deletes routing tables and their routes together with the network area region (`stackit_network_area_region`) they belong to. When the network area or its region no longer exists, the provider treats the routing table as deleted: it is removed from the Terraform state on refresh, and destroying it succeeds.

~> This resource is part of the experimental feature routing-tables and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.

## Example Usage
Expand Down
3 changes: 3 additions & 0 deletions docs/resources/routing_table_route.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ subcategory: ""
description: |-
Routing table route resource schema. Must have a region specified in the provider configuration.
This resource is for SNA, not VPC, based networks.
The platform deletes routing tables and their routes together with the network area region (stackit_network_area_region) they belong to. When the network area or its region no longer exists, the provider treats the route as deleted: it is removed from the Terraform state on refresh, and destroying it succeeds.
~> This resource is part of the experimental feature routing-tables and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.
---

Expand All @@ -14,6 +15,8 @@ Routing table route resource schema. Must have a `region` specified in the provi

This resource is for SNA, not VPC, based networks.

The platform deletes routing tables and their routes together with the network area region (`stackit_network_area_region`) they belong to. When the network area or its region no longer exists, the provider treats the route as deleted: it is removed from the Terraform state on refresh, and destroying it succeeds.

~> This resource is part of the experimental feature routing-tables and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.

## Example Usage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ func (r *routeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR
// Schema defines the schema for the resource.
func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Routing table route resource schema. Must have a `region` specified in the provider configuration.\n\n" +
"This resource is for SNA, not VPC, based networks."
"This resource is for SNA, not VPC, based networks.\n\n" +
"The platform deletes routing tables and their routes together with the network area region (`stackit_network_area_region`) they belong to. " +
"When the network area or its region no longer exists, the provider treats the route as deleted: " +
"it is removed from the Terraform state on refresh, and destroying it succeeds."
resp.Schema = schema.Schema{
Description: description,
MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Resource),
Expand Down
22 changes: 10 additions & 12 deletions stackit/internal/services/iaas/routingtable/table/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ func (r *routingTableResource) ModifyPlan(ctx context.Context, req resource.Modi

func (r *routingTableResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Routing table resource schema. Must have a `region` specified in the provider configuration.\n\n" +
"This resource is for SNA, not VPC, based networks."
"This resource is for SNA, not VPC, based networks.\n\n" +
"The platform deletes routing tables and their routes together with the network area region (`stackit_network_area_region`) they belong to. " +
"When the network area or its region no longer exists, the provider treats the routing table as deleted: " +
"it is removed from the Terraform state on refresh, and destroying it succeeds."
resp.Schema = schema.Schema{
Description: description,
MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Resource),
Expand Down Expand Up @@ -302,17 +305,12 @@ func (r *routingTableResource) Read(ctx context.Context, req resource.ReadReques

routingTableResp, err := r.client.DefaultAPI.GetRoutingTableOfArea(ctx, organizationId, networkAreaId, region, routingTableId).Execute()
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading routing table",
fmt.Sprintf("routing table with ID %q does not exist in organization %q.", routingTableId, organizationId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId),
},
)
resp.State.RemoveResource(ctx)
var oapiErr *oapierror.GenericOpenAPIError
if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Calling API: %v", err))
return
}

Expand Down
144 changes: 144 additions & 0 deletions stackit/internal/services/iaas/routingtable/table/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ package table
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/gorilla/mux"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/config"
iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api"

"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)

func TestMapFields(t *testing.T) {
Expand Down Expand Up @@ -215,3 +224,138 @@ func TestToUpdatePayload(t *testing.T) {
})
}
}

func TestRead(t *testing.T) {
const (
organizationId = "0f0f0f0f-0f0f-0f0f-0f0f-0f0f0f0f0f0f"
networkAreaId = "1e1e1e1e-1e1e-1e1e-1e1e-1e1e1e1e1e1e"
routingTableId = "2d2d2d2d-2d2d-2d2d-2d2d-2d2d2d2d2d2d"
region = "eu01"
)
priorState := Model{
Id: utils.BuildInternalTerraformId(organizationId, region, networkAreaId, routingTableId),
OrganizationId: types.StringValue(organizationId),
RoutingTableId: types.StringValue(routingTableId),
NetworkAreaId: types.StringValue(networkAreaId),
Name: types.StringValue("example"),
Region: types.StringValue(region),
Labels: types.MapNull(types.StringType),
SystemRoutes: types.BoolValue(true),
DynamicRoutes: types.BoolValue(true),
}

tests := []struct {
name string
statusCode int
body string
closeServer bool // close the mocked server before Read to provoke a transport error
wantErr bool
wantRemoved bool
wantState *Model // expected state after Read; nil keeps the prior state
}{
{
name: "routing table exists",
statusCode: http.StatusOK,
body: fmt.Sprintf(`{"id": %q, "name": "renamed", "description": "d", "systemRoutes": false, "dynamicRoutes": true, "labels": {"k": "v"}}`, routingTableId),
wantState: &Model{
Id: utils.BuildInternalTerraformId(organizationId, region, networkAreaId, routingTableId),
OrganizationId: types.StringValue(organizationId),
RoutingTableId: types.StringValue(routingTableId),
NetworkAreaId: types.StringValue(networkAreaId),
Name: types.StringValue("renamed"),
Description: types.StringValue("d"),
Region: types.StringValue(region),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"k": types.StringValue("v")}),
SystemRoutes: types.BoolValue(false),
DynamicRoutes: types.BoolValue(true),
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
},
},
{
// The IaaS API answers 404 for routing tables of a deleted network area or network area region
name: "404 removes the routing table from state without an error",
statusCode: http.StatusNotFound,
body: `{"code": 404, "msg": "resource not found: area"}`,
wantRemoved: true,
},
{
name: "403 reports an error and keeps the state",
statusCode: http.StatusForbidden,
body: `{"code": 403, "msg": "forbidden"}`,
wantErr: true,
},
{
name: "500 reports an error and keeps the state",
statusCode: http.StatusInternalServerError,
body: `{"code": 500, "msg": "internal error"}`,
wantErr: true,
},
{
name: "transport error reports an error and keeps the state",
closeServer: true,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router := mux.NewRouter()
router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/routing-tables/{routingTableId}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
if r.Method != http.MethodGet || vars["organizationId"] != organizationId || vars["areaId"] != networkAreaId || vars["region"] != region || vars["routingTableId"] != routingTableId {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(tt.statusCode)
if _, err := w.Write([]byte(tt.body)); err != nil {
t.Errorf("Get routing table handler: failed to write response: %v", err)
}
})
mockedServer := httptest.NewServer(router)
defer mockedServer.Close()
client, err := iaas.NewAPIClient(
config.WithEndpoint(mockedServer.URL),
config.WithoutAuthentication(),
)
if err != nil {
t.Fatalf("Failed to initialize client: %v", err)
}
if tt.closeServer {
mockedServer.Close()
}
r := &routingTableResource{client: client, providerData: core.ProviderData{DefaultRegion: region}}

ctx := context.Background()
var schemaResp resource.SchemaResponse
r.Schema(ctx, resource.SchemaRequest{}, &schemaResp)
state := tfsdk.State{Schema: schemaResp.Schema}
if diags := state.Set(ctx, priorState); diags.HasError() {
t.Fatalf("Failed to build state: %v", diags)
}

resp := resource.ReadResponse{State: state}
r.Read(ctx, resource.ReadRequest{State: state}, &resp)

if resp.Diagnostics.HasError() != tt.wantErr {
t.Errorf("Read() error diagnostics = %v, wantErr %v: %v", resp.Diagnostics.HasError(), tt.wantErr, resp.Diagnostics)
}
if resp.State.Raw.IsNull() != tt.wantRemoved {
t.Errorf("Read() removed resource from state = %v, want %v", resp.State.Raw.IsNull(), tt.wantRemoved)
}
if tt.wantRemoved {
return
}
var got Model
if diags := resp.State.Get(ctx, &got); diags.HasError() {
t.Fatalf("Failed to read state back: %v", diags)
}
want := priorState
if tt.wantState != nil {
want = *tt.wantState
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Read() state mismatch (-want +got):\n%s", diff)
}
})
}
}