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
@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

Copy link
Member

Choose a reason for hiding this comment

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

This will still create invalid JSON / YAML output

@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
@h3adex h3adex force-pushed the feat/add-rt-support branch from 75cddf2 to 208163f Compare December 6, 2025 14:35
networkAreaIdFlag = "network-area-id"
nonDynamicRoutesFlag = "non-dynamic-routes"
organizationIdFlag = "organization-id"
routingTableIdArg = "ROUTE_TABLE_ID"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
routingTableIdArg = "ROUTE_TABLE_ID"
routingTableIdArg = "ROUTING_TABLE_ID"

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