Skip to content

Conversation

@h3adex
Copy link
Contributor

@h3adex h3adex commented Oct 10, 2025

Description

This PR adds functionality to list and describe routing tables, as well as perform full CRUD operations on routes within those tables. It does not include support for creating or attaching routing tables, as those operations are currently intended to be managed exclusively through Terraform.

This implementation is primarily aimed at enabling users to inspect and debug routes created via Terraform. Once the routing table feature reaches GA, support for creating routing tables and attaching them to networks will be added to the CLI.

Screenshot 2025-10-10 at 16 22 40

Checklist

  • Issue was linked above
  • Code format was applied: make fmt
  • Examples were added / adjusted (see e.g. here)
  • Docs are up-to-date: make generate-docs (will be checked by CI)
  • Unit tests got implemented or updated
  • Unit tests are passing: make test (will be checked by CI)
  • No linter issues: make lint (will be checked by CI)

@h3adex h3adex requested a review from a team as a code owner October 10, 2025 14:23
@h3adex h3adex force-pushed the feat/add-rt-support branch from 5b9fe56 to 45f6702 Compare October 10, 2025 14:24
@h3adex
Copy link
Contributor Author

h3adex commented Oct 15, 2025

Update waiting for go-sdk iaas api update ^

@h3adex h3adex force-pushed the feat/add-rt-support branch from 45f6702 to c79ff62 Compare October 27, 2025 13:27
@h3adex h3adex force-pushed the feat/add-rt-support branch 20 times, most recently from e8c2521 to 826b579 Compare November 5, 2025 15:20
@h3adex h3adex force-pushed the feat/add-rt-support branch from e2b6a5f to f4fd8d5 Compare November 20, 2025 10:49
@h3adex
Copy link
Contributor Author

h3adex commented Nov 20, 2025

@rubenhoenle I’ve just implemented all of your suggestions and pushed an e2e.py (internal/cmd/routingtable/e2e.py) script to run through the entire command suite. Feel free to use it if you'd like to run the end-to-end tests. I can remove it and squash the commits afterwards. Just let me know what you prefer.

@github-actions
Copy link

This PR was marked as stale after 7 days of inactivity and will be closed after another 7 days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it.

@github-actions github-actions bot added the Stale label Nov 28, 2025
@marceljk marceljk removed the Stale label Nov 28, 2025
@h3adex h3adex force-pushed the feat/add-rt-support branch 2 times, most recently from 424239c to c2cadea Compare December 2, 2025 08:14
@h3adex h3adex requested a review from rubenhoenle December 2, 2025 08:20
} else {
params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err)
}
params.Printer.Outputf("No routing-tables found for organization %q\n", orgLabel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid JSON/YAML output in case -output-format flag is set to JSON/YAML output

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user, I still want to be able to list routing tables for automation purposes, such as listing all routing tables, grepping the IDs, and deleting them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also want users to be able to do that.

In fact running

stackit routing-table list --output-format json currently would output No routing-tables found for organization ... when there are no routing tables present, but as a user I would expect a valid JSON output which would be just [] (an empty list).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #893 for reference please

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a fix and also created automated tests for list both routing-table and route list call

}

if items := response.Items; items == nil || len(*items) == 0 {
params.Printer.Outputf("No routes found for routing-table %q\n", model.RoutingTableId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid JSON/YAML output in case -output-format flag is set to JSON/YAML output

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're even handling that case in your outputResult func

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user, I still want to be able to list routes for automation purposes, such as listing all routes, grepping the IDs, and deleting them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see other conversation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a fix and also created automated tests for list both routing-table and route list call

outputFormat string
route iaas.Route
wantErr bool
}{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test your table output...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are kind of useless in their current state.

You must test edge cases, not only the happy paths for unit tests to be useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. I admit I forgot to implement them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added more cases for all TestOutputResult functions

@rubenhoenle rubenhoenle self-assigned this Dec 4, 2025
@h3adex h3adex marked this pull request as draft December 4, 2025 17:02
@h3adex
Copy link
Contributor Author

h3adex commented Dec 5, 2025

Test script:

import subprocess
import sys
import yaml
from datetime import datetime

# Static variables
PROJECT_ID = "f28453cc-9c37-4948-b2c5-36c0bae0c47a"
NETWORK_AREA_ID = "f1ffef6c-078e-4580-8282-93b8ade6cb49"
ORG_ID = "03a34540-3c1a-4794-b2c6-7111ecf824ef"

# Dynamic variables initialized during test flow
NETWORK_ID = ""
ROUTING_TABLE_ID = ""
ROUTING_TABLE_ID_2 = ""
ROUTE_ID = ""

def log(msg: str):
    print(f"[{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] {msg}", file=sys.stdout)

def run_command(description: str, _expected: str, *args):
    log(f"{description}")
    result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

    if result.returncode == 0:
        log(f"Command succeeded: {description}")
        if result.stdout.strip():
            print("STDOUT:")
            print(result.stdout.strip())
    else:
        log(f"Command failed: {description}")
        if result.stderr.strip():
            print("STDERR:")
            print(result.stderr.strip())
        elif result.stdout.strip():
            # Some errors may go to stdout
            print("STDOUT (unexpected):")
            print(result.stdout.strip())

def extract_id(description: str, yq_path: str, *args) -> str:
    full_args = list(args) + ["-o", "yaml"]
    try:
        result = subprocess.run(full_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, text=True)
        parsed_yaml = yaml.safe_load(result.stdout)

        if isinstance(parsed_yaml, list):
            first_item = parsed_yaml[0] if parsed_yaml else None
            id_val = first_item.get("id") if first_item else None
        elif isinstance(parsed_yaml, dict):
            if yq_path.startswith(".items"):
                items = parsed_yaml.get("items", [])
                id_val = items[0].get("id") if items else None
            elif yq_path.startswith("."):
                id_val = parsed_yaml.get(yq_path.lstrip("."))
            else:
                id_val = parsed_yaml.get(yq_path)
        else:
            id_val = None

        if not id_val:
            raise ValueError("ID not found")

        log(f"{description} ID: {id_val}")
        return id_val

    except Exception as e:
        log(f"{description} Failed to extract ID: {e} {" ".join(full_args)}")
        sys.exit(1)

def run():
    global ROUTING_TABLE_ID, ROUTING_TABLE_ID_2, NETWORK_ID, ROUTE_ID

    run_command("Set project ID", "success", "./bin/stackit", "config", "set", "--project-id", PROJECT_ID)

    ROUTING_TABLE_ID = extract_id("Create routing-table rt_test", ".id",
                                  "./bin/stackit", "routing-table", "create", "--network-area-id", NETWORK_AREA_ID,
                                  "--organization-id", ORG_ID, "--name", "rt_test", "-y")

    NETWORK_ID = extract_id("Create network with RT ID", ".id",
                            "./bin/stackit", "network", "create", "--name", "network-rt", "--routing-table-id", ROUTING_TABLE_ID, "-y")

    run_command("List networks (check RT ID shown)", "success", "./bin/stackit", "network", "list", "-o", "pretty")
    run_command("Describe network", "success", "./bin/stackit", "network", "describe", NETWORK_ID)

    ROUTING_TABLE_ID_2 = extract_id("Create routing-table rt_test_2", ".id",
                                    "./bin/stackit", "routing-table", "create", "--network-area-id", NETWORK_AREA_ID,
                                    "--organization-id", ORG_ID, "--name", "rt_test_2", "-y")

    run_command("Update network with RT 2 ID", "success",
                "./bin/stackit", "network", "update", NETWORK_ID, "--routing-table-id", ROUTING_TABLE_ID_2, "-y")

    run_command("Describe routing-table 1", "success",
                "./bin/stackit", "routing-table", "describe", ROUTING_TABLE_ID,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-o", "pretty")

    run_command("Describe routing-table 2", "success",
                "./bin/stackit", "routing-table", "describe", ROUTING_TABLE_ID_2,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-o", "pretty")

    # not working due to missing id
    run_command("Describe routing-table 2", "fail",
                "./bin/stackit", "routing-table", "describe", "",
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-o", "pretty")

    run_command("List routing-tables", "success",
                "./bin/stackit", "routing-table", "list", "--network-area-id", NETWORK_AREA_ID,
                "--organization-id", ORG_ID, "-o", "pretty")

    run_command("Delete second routing-table", "success",
                "./bin/stackit", "routing-table", "delete", ROUTING_TABLE_ID_2,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-y")

    run_command("Update RT: disable dynamic-routes", "success",
                "./bin/stackit", "routing-table", "update", ROUTING_TABLE_ID, "--network-area-id", NETWORK_AREA_ID,
                "--organization-id", ORG_ID, "--description", "Test desc", "--non-dynamic-routes", "-y")

    run_command("Update RT: re-enable dynamic-routes", "success",
                "./bin/stackit", "routing-table", "update", ROUTING_TABLE_ID, "--network-area-id", NETWORK_AREA_ID,
                "--organization-id", ORG_ID, "--description", "Test desc", "-y")

    run_command("Update RT: name", "success",
                "./bin/stackit", "routing-table", "update", ROUTING_TABLE_ID, "--network-area-id", NETWORK_AREA_ID,
                "--organization-id", ORG_ID, "--name", "rt_test", "-y")

    run_command("Update RT: labels + name", "success",
                "./bin/stackit", "routing-table", "update", ROUTING_TABLE_ID, "--network-area-id", NETWORK_AREA_ID,
                "--organization-id", ORG_ID, "--labels", "xxx=yyy,zzz=bbb", "--name", "rt_test", "-y")

    ROUTE_ID = extract_id("Create route with next-hop IPv4", ".items.0.id",
                          "./bin/stackit", "routing-table", "route", "create", "--routing-table-id", ROUTING_TABLE_ID,
                          "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-y",
                          "--destination-type", "cidrv4", "--destination-value", "0.0.0.0/0",
                          "--nexthop-type", "ipv4", "--nexthop-value", "10.1.1.0")

    run_command("Create route with next-hop blackhole", "success",
                "./bin/stackit", "routing-table", "route", "create", "--routing-table-id", ROUTING_TABLE_ID,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-y",
                "--destination-type", "cidrv4", "--destination-value", "0.0.0.0/0", "--nexthop-type", "blackhole")

    run_command("Create route with next-hop internet", "success",
                "./bin/stackit", "routing-table", "route", "create", "--routing-table-id", ROUTING_TABLE_ID,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-y",
                "--destination-type", "cidrv4", "--destination-value", "0.0.0.0/0", "--nexthop-type", "internet")

    run_command("Negative test: invalid next-hop", "fail",
                "./bin/stackit", "routing-table", "route", "create", "--routing-table-id", ROUTING_TABLE_ID,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID,
                "--destination-type", "cidrv4", "--destination-value", "0.0.0.0/0", "--nexthop-type", "error")

    run_command("Negative test: invalid destination-type", "fail",
                "./bin/stackit", "routing-table", "route", "create", "--routing-table-id", ROUTING_TABLE_ID,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID,
                "--destination-type", "error", "--destination-value", "0.0.0.0/0", "--nexthop-type", "internet")

    run_command("List all routing-table routes", "success",
                "./bin/stackit", "routing-table", "route", "list", "--routing-table-id", ROUTING_TABLE_ID,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-o", "pretty")

    run_command("Describe route", "success",
                "./bin/stackit", "routing-table", "route", "describe", ROUTE_ID,
                "--routing-table-id", ROUTING_TABLE_ID, "--network-area-id", NETWORK_AREA_ID,
                "--organization-id", ORG_ID, "-o", "pretty")

    # not working due to missing id
    run_command("Describe route", "fail",
                "./bin/stackit", "routing-table", "route", "describe", "",
                "--routing-table-id", ROUTING_TABLE_ID, "--network-area-id", NETWORK_AREA_ID,
                "--organization-id", ORG_ID, "-o", "pretty")

    run_command("Update route labels", "success",
                "./bin/stackit", "routing-table", "route", "update", ROUTE_ID, "--routing-table-id", ROUTING_TABLE_ID,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID,
                "--labels", "key=value,foo=bar", "-y")

    run_command("Delete route", "success",
                "./bin/stackit", "routing-table", "route", "delete", ROUTE_ID, "--routing-table-id", ROUTING_TABLE_ID,
                "--network-area-id", NETWORK_AREA_ID, "--organization-id", ORG_ID, "-y")

    log("Cleanup: Removing all routing-tables named rt_test or rt_test_2.")
    cleanup_entities("routing-table", ["rt_test", "rt_test_2"],
                     ["--organization-id", ORG_ID, "--network-area-id", NETWORK_AREA_ID])

    log("Cleanup: Removing all networks named network-rt.")
    cleanup_entities("network", ["network-rt"], [])

    log("All tests finished successfully.")

def cleanup_entities(entity_type, name_list, extra_args):
    result = subprocess.run(["./bin/stackit", entity_type, "list", "-o", "yaml"] + extra_args,
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    items = yaml.safe_load(result.stdout)
    for item in items:
        if item.get("name") in name_list:
            entity_id = item.get("id")
            cmd = ["./bin/stackit", entity_type, "delete", entity_id] + extra_args + ["-y"]
            run_command(f"Cleanup delete {entity_type} {item['name']}", "success", *cmd)

if __name__ == "__main__":
    run()

@h3adex h3adex force-pushed the feat/add-rt-support branch 8 times, most recently from fd94802 to 4df31e5 Compare December 5, 2025 15:40
@h3adex h3adex marked this pull request as ready for review December 5, 2025 15:44
@h3adex h3adex requested a review from rubenhoenle December 5, 2025 15:44
@h3adex h3adex force-pushed the feat/add-rt-support branch 2 times, most recently from 25a8f19 to 75cddf2 Compare December 6, 2025 14:26

table := tables.NewTable()
table.SetHeader("ID", "NAME", "DESCRIPTION", "CREATED_AT", "UPDATED_AT", "DEFAULT", "LABELS", "SYSTEM_ROUTES", "DYNAMIC_ROUTES")
table.SetHeader("ID", "NAME", "DESCRIPTION", "DEFAULT", "LABELS", "SYSTEM ROUTES", "DYNAMIC ROUTES", "CREATED AT", "UPDATED AT")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve reordered the table header. I thought it would be more readable to move created_at and updated_at to the end.

@h3adex h3adex force-pushed the feat/add-rt-support branch from 75cddf2 to 208163f Compare December 6, 2025 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants