stackit-cli icon indicating copy to clipboard operation
stackit-cli copied to clipboard

feat(routing-table): provide rt and routes functionality

Open h3adex opened this issue 3 months ago • 4 comments

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

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

h3adex avatar Oct 10 '25 14:10 h3adex

Update waiting for go-sdk iaas api update ^

h3adex avatar Oct 15 '25 06:10 h3adex

I've rebased this PR and updated it to use the official Iaas V2 API. I've also updated the network command to allow setting and updating the routing-table ID. I manually updated the routing table help text to note that this API is currently available only to a select group of customers. IPv6 isn't working on our platform yet, but the Iaas API supports it, so I integrated the relevant route create commands into the CLI. It's unlikely many will use them now, but this will ensure that the CLI is ready once IPv6 goes live. Here are the commands I manually tested:

#!/bin/zsh

PROJECT_ID="xxx"
NETWORK_AREA_ID="xxx"
ORG_ID="xxx"
ROUTING_TABLE_ID_2="xxx"
NETWORK_ID="xxx"
ROUTING_TABLE_ID="xxx"
ROUTE_ID="xxx"


# set project id
bin/stackit config set --project-id $PROJECT_ID

# create routing-table
bin/stackit routing-table create --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID --name rt_test


bin/stackit network create --name network-rt --routing-table-id $ROUTING_TABLE_ID

# check if routing table id is shown
bin/stackit network list -o pretty

# check if routing table id is shown
bin/stackit network describe $NETWORK_ID

# create another rt
bin/stackit routing-table create --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID --name rt_test_2

# check if network update with rt works
bin/stackit network update $NETWORK_ID --routing-table-id $ROUTING_TABLE_ID_2

# describe rt
bin/stackit routing-table describe $ROUTING_TABLE_ID --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID -o pretty
bin/stackit routing-table describe $ROUTING_TABLE_ID_2 --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID -o pretty

# list rt
bin/stackit routing-table list --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID -o pretty

# delete rt
bin/stackit routing-table delete $ROUTING_TABLE_ID_2 --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID -o pretty

# update rt

# disable dynamic-routes
bin/stackit routing-table update $ROUTING_TABLE_ID --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID --description "xxx yyy zzz" --non-dynamic-routes
# enable again
bin/stackit routing-table update $ROUTING_TABLE_ID --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID --description "xxx yyy zzz"
# update name
bin/stackit routing-table update $ROUTING_TABLE_ID --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID --description "xxx yyy zzz" --name rt_test_123
# update labels
bin/stackit routing-table update $ROUTING_TABLE_ID --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID --description "xxx yyy zzz" --labels xxx=yyy,zzz=bbb --name rt_test_12344

# test routes in rt
bin/stackit routing-table update --routing-table-id $ROUTING_TABLE_ID --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID --description "xxx yyy zzz"

# create ipv4 route
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 ipv4 --nexthop-value 10.1.1.0

# create ipv4 route next hop internet
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 blackhole

# create ipv4 route next hop blackhole
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 internet

# error
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

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

# ipv6 can not be tested since it is not supported right now cidrv6 destination-type next-hop-type ipv6
# cli it would ready once it is released
bin/stackit routing-table route create --routing-table-id $ROUTING_TABLE_ID \
  --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID \
  --destination-type cidrv6 --destination-value 2001:db8::/32 \
  --nexthop-type ipv6 --nexthop-value ::1
# Error: create route request failed: 400 Bad Request, status code 400, Body: {"code":400,"msg":"unsupported route destination type: cidrv6"}

# list all routes
bin/stackit routing-table route list --routing-table-id $ROUTING_TABLE_ID \
  --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID -o pretty

# describe single route
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

# describe single route
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

# update single route
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

# delete single route
bin/stackit routing-table route delete $ROUTE_ID --routing-table-id $ROUTING_TABLE_ID \
  --network-area-id $NETWORK_AREA_ID --organization-id $ORG_ID
Screenshot 2025-11-05 at 16 21 20

h3adex avatar Nov 05 '25 15:11 h3adex

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

h3adex avatar Nov 20 '25 10:11 h3adex

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[bot] avatar Nov 28 '25 02:11 github-actions[bot]

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 avatar Dec 05 '25 13:12 h3adex

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[bot] avatar Dec 16 '25 03:12 github-actions[bot]