From 1d5e649dbdb96a7efcf33bf90de8d6a113b077f3 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Tue, 23 Sep 2025 17:23:04 +0200 Subject: [PATCH 1/2] feat(routing-table): provide rt and routes functionality --- docs/stackit_beta.md | 1 + docs/stackit_beta_routing-table.md | 45 ++ docs/stackit_beta_routing-table_describe.md | 42 ++ docs/stackit_beta_routing-table_list.md | 50 ++ docs/stackit_beta_routing-table_route.md | 38 + ...stackit_beta_routing-table_route_create.md | 63 ++ ...stackit_beta_routing-table_route_delete.md | 43 ++ ...ackit_beta_routing-table_route_describe.md | 43 ++ docs/stackit_beta_routing-table_route_list.md | 51 ++ ...stackit_beta_routing-table_route_update.md | 44 ++ internal/cmd/beta/beta.go | 2 + .../beta/routingtable/describe/describe.go | 154 ++++ .../routingtable/describe/describe_test.go | 190 +++++ internal/cmd/beta/routingtable/list/list.go | 201 ++++++ .../cmd/beta/routingtable/list/list_test.go | 215 ++++++ .../beta/routingtable/route/create/create.go | 290 ++++++++ .../routingtable/route/create/create_test.go | 674 ++++++++++++++++++ .../beta/routingtable/route/delete/delete.go | 115 +++ .../routingtable/route/delete/delete_test.go | 141 ++++ .../routingtable/route/describe/describe.go | 197 +++++ .../route/describe/describe_test.go | 225 ++++++ .../cmd/beta/routingtable/route/list/list.go | 183 +++++ .../beta/routingtable/route/list/list_test.go | 240 +++++++ internal/cmd/beta/routingtable/route/route.go | 33 + .../beta/routingtable/route/update/update.go | 150 ++++ .../routingtable/route/update/update_test.go | 236 ++++++ .../cmd/beta/routingtable/routingtable.go | 40 ++ .../pkg/services/iaas/client/alphaclient.go | 44 ++ .../pkg/services/routing-table/utils/utils.go | 40 ++ .../routing-table/utils/utils_test.go | 135 ++++ 30 files changed, 3925 insertions(+) create mode 100644 docs/stackit_beta_routing-table.md create mode 100644 docs/stackit_beta_routing-table_describe.md create mode 100644 docs/stackit_beta_routing-table_list.md create mode 100644 docs/stackit_beta_routing-table_route.md create mode 100644 docs/stackit_beta_routing-table_route_create.md create mode 100644 docs/stackit_beta_routing-table_route_delete.md create mode 100644 docs/stackit_beta_routing-table_route_describe.md create mode 100644 docs/stackit_beta_routing-table_route_list.md create mode 100644 docs/stackit_beta_routing-table_route_update.md create mode 100644 internal/cmd/beta/routingtable/describe/describe.go create mode 100644 internal/cmd/beta/routingtable/describe/describe_test.go create mode 100644 internal/cmd/beta/routingtable/list/list.go create mode 100644 internal/cmd/beta/routingtable/list/list_test.go create mode 100644 internal/cmd/beta/routingtable/route/create/create.go create mode 100644 internal/cmd/beta/routingtable/route/create/create_test.go create mode 100644 internal/cmd/beta/routingtable/route/delete/delete.go create mode 100644 internal/cmd/beta/routingtable/route/delete/delete_test.go create mode 100644 internal/cmd/beta/routingtable/route/describe/describe.go create mode 100644 internal/cmd/beta/routingtable/route/describe/describe_test.go create mode 100644 internal/cmd/beta/routingtable/route/list/list.go create mode 100644 internal/cmd/beta/routingtable/route/list/list_test.go create mode 100644 internal/cmd/beta/routingtable/route/route.go create mode 100644 internal/cmd/beta/routingtable/route/update/update.go create mode 100644 internal/cmd/beta/routingtable/route/update/update_test.go create mode 100644 internal/cmd/beta/routingtable/routingtable.go create mode 100644 internal/pkg/services/iaas/client/alphaclient.go create mode 100644 internal/pkg/services/routing-table/utils/utils.go create mode 100644 internal/pkg/services/routing-table/utils/utils_test.go diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md index 23097f8ca..b0cac67b6 100644 --- a/docs/stackit_beta.md +++ b/docs/stackit_beta.md @@ -43,5 +43,6 @@ stackit beta [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS +* [stackit beta routing-table](./stackit_beta_routing-table.md) - Manage routing-tables and its according routes * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex diff --git a/docs/stackit_beta_routing-table.md b/docs/stackit_beta_routing-table.md new file mode 100644 index 000000000..c90f7d29e --- /dev/null +++ b/docs/stackit_beta_routing-table.md @@ -0,0 +1,45 @@ +## stackit beta routing-table + +Manage routing-tables and its according routes + +### Synopsis + +Manage routing tables and their associated routes. + +This functionality is currently in BETA. At this stage, only listing and describing +routing-tables, as well as full CRUD operations for routes, are supported. +This feature is primarily intended for debugging routes created through Terraform. + +Once the feature reaches General Availability, we plan to introduce support +for creating routing tables and attaching them to networks directly via the +CLI. Until then, we recommend users continue managing routing tables and +attachments through the Terraform provider. + +``` +stackit beta routing-table [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta routing-table" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta routing-table describe](./stackit_beta_routing-table_describe.md) - Describe a routing-table +* [stackit beta routing-table list](./stackit_beta_routing-table_list.md) - List all routing-tables +* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_beta_routing-table_describe.md b/docs/stackit_beta_routing-table_describe.md new file mode 100644 index 000000000..d2db08f5f --- /dev/null +++ b/docs/stackit_beta_routing-table_describe.md @@ -0,0 +1,42 @@ +## stackit beta routing-table describe + +Describe a routing-table + +### Synopsis + +Describe a routing-table + +``` +stackit beta routing-table describe ROUTING_TABLE_ID_ARG [flags] +``` + +### Examples + +``` + Describe a routing-table + $ stackit beta routing-table describe xxxx-xxxx-xxxx-xxxx --organization-id xxx --network-area-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta routing-table describe" + --network-area-id string Network-Area ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta routing-table](./stackit_beta_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_beta_routing-table_list.md b/docs/stackit_beta_routing-table_list.md new file mode 100644 index 000000000..79f01b581 --- /dev/null +++ b/docs/stackit_beta_routing-table_list.md @@ -0,0 +1,50 @@ +## stackit beta routing-table list + +List all routing-tables + +### Synopsis + +List all routing-tables + +``` +stackit beta routing-table list [flags] +``` + +### Examples + +``` + List all routing-tables + $ stackit beta routing-table list --organization-id xxx --network-area-id yyy + + List all routing-tables with labels + $ stackit beta routing-table list --label-selector env=dev,env=rc --organization-id xxx --network-area-id yyy + + List all routing-tables with labels and set limit to 10 + $ stackit beta routing-table list --label-selector env=dev,env=rc --limit 10 --organization-id xxx --network-area-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta routing-table list" + --label-selector string Filter by label + --limit int Maximum number of entries to list + --network-area-id string Network-Area ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta routing-table](./stackit_beta_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_beta_routing-table_route.md b/docs/stackit_beta_routing-table_route.md new file mode 100644 index 000000000..3add3abcd --- /dev/null +++ b/docs/stackit_beta_routing-table_route.md @@ -0,0 +1,38 @@ +## stackit beta routing-table route + +Manage routes of a routing-table + +### Synopsis + +Manage routes of a routing-table + +``` +stackit beta routing-table route [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta routing-table route" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta routing-table](./stackit_beta_routing-table.md) - Manage routing-tables and its according routes +* [stackit beta routing-table route create](./stackit_beta_routing-table_route_create.md) - Creates a route in a routing-table +* [stackit beta routing-table route delete](./stackit_beta_routing-table_route_delete.md) - Deletes a route within a routing-table +* [stackit beta routing-table route describe](./stackit_beta_routing-table_route_describe.md) - Describe a route within a routing-table +* [stackit beta routing-table route list](./stackit_beta_routing-table_route_list.md) - list all routes within a routing-table +* [stackit beta routing-table route update](./stackit_beta_routing-table_route_update.md) - Updates a route in a routing-table + diff --git a/docs/stackit_beta_routing-table_route_create.md b/docs/stackit_beta_routing-table_route_create.md new file mode 100644 index 000000000..7cb5b7823 --- /dev/null +++ b/docs/stackit_beta_routing-table_route_create.md @@ -0,0 +1,63 @@ +## stackit beta routing-table route create + +Creates a route in a routing-table + +### Synopsis + +Creates a route in a routing-table. + +``` +stackit beta routing-table route create [flags] +``` + +### Examples + +``` + Create a route with CIDRv4 destination and IPv4 nexthop + stackit beta routing-tables route create \ +--routing-table-id xxx --organization-id yyy --network-area-id zzz \ +--destination-type cidrv4 --destination-value \ +--nexthop-type ipv4 --nexthop-value + + Create a route with CIDRv6 destination and IPv6 nexthop + stackit beta routing-tables route create \ +--routing-table-id xxx --organization-id yyy --network-area-id zzz \ +--destination-type cidrv6 --destination-value \ +--nexthop-type ipv6 --nexthop-value + + Create a route with CIDRv6 destination and Nexthop Internet + stackit beta routing-tables route create \ +--routing-table-id xxx --organization-id yyy --network-area-id zzz \ +--destination-type cidrv6 --destination-value \ +--nexthop-type internet +``` + +### Options + +``` + --destination-type string Destination type + --destination-value string Destination value + -h, --help Help for "stackit beta routing-table route create" + --labels stringToString Key=value labels (default []) + --network-area-id string Network-Area ID + --nexthop-type string Next hop type + --nexthop-value string NextHop value + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_beta_routing-table_route_delete.md b/docs/stackit_beta_routing-table_route_delete.md new file mode 100644 index 000000000..ba10a41ef --- /dev/null +++ b/docs/stackit_beta_routing-table_route_delete.md @@ -0,0 +1,43 @@ +## stackit beta routing-table route delete + +Deletes a route within a routing-table + +### Synopsis + +Deletes a route within a routing-table + +``` +stackit beta routing-table route delete routing-table-id [flags] +``` + +### Examples + +``` + Deletes a route within a routing-table + $ stackit beta routing-table route delete xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz +``` + +### Options + +``` + -h, --help Help for "stackit beta routing-table route delete" + --network-area-id string Network-Area ID + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_beta_routing-table_route_describe.md b/docs/stackit_beta_routing-table_route_describe.md new file mode 100644 index 000000000..586bf88d6 --- /dev/null +++ b/docs/stackit_beta_routing-table_route_describe.md @@ -0,0 +1,43 @@ +## stackit beta routing-table route describe + +Describe a route within a routing-table + +### Synopsis + +Describe a route within a routing-table + +``` +stackit beta routing-table route describe ROUTE_ID_ARG [flags] +``` + +### Examples + +``` + Describe a route within a routing-table + $ stackit beta routing-table route describe xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz +``` + +### Options + +``` + -h, --help Help for "stackit beta routing-table route describe" + --network-area-id string Network-Area ID + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_beta_routing-table_route_list.md b/docs/stackit_beta_routing-table_route_list.md new file mode 100644 index 000000000..0dfd13f46 --- /dev/null +++ b/docs/stackit_beta_routing-table_route_list.md @@ -0,0 +1,51 @@ +## stackit beta routing-table route list + +list all routes within a routing-table + +### Synopsis + +list all routes within a routing-table + +``` +stackit beta routing-table route list [flags] +``` + +### Examples + +``` + List all routes within a routing-table + $ stackit beta routing-table route list --routing-table-id xxx --organization-id yyy --network-area-id zzz + + List all routes within a routing-table with labels + $ stackit beta routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc + + List all routes within a routing-tables with labels and limit to 10 + $ stackit beta routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta routing-table route list" + --label-selector string Filter by label + --limit int Maximum number of entries to list + --network-area-id string Network-Area ID + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_beta_routing-table_route_update.md b/docs/stackit_beta_routing-table_route_update.md new file mode 100644 index 000000000..044e3b165 --- /dev/null +++ b/docs/stackit_beta_routing-table_route_update.md @@ -0,0 +1,44 @@ +## stackit beta routing-table route update + +Updates a route in a routing-table + +### Synopsis + +Updates a route in a routing-table. + +``` +stackit beta routing-table route update ROUTE_ID_ARG [flags] +``` + +### Examples + +``` + Updates the label(s) of a route with ID "xxx" in a routing-table ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit beta routing-table route update xxx --labels key=value,foo=bar --routing-table-id xxx --organization-id yyy --network-area-id zzz +``` + +### Options + +``` + -h, --help Help for "stackit beta routing-table route update" + --labels stringToString Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels (default []) + --network-area-id string Network-Area ID + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table + diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index b026da770..2f83eaed1 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -5,6 +5,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -40,4 +41,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(sqlserverflex.NewCmd(params)) cmd.AddCommand(alb.NewCmd(params)) cmd.AddCommand(kms.NewCmd(params)) + cmd.AddCommand(routingtable.NewCmd(params)) } diff --git a/internal/cmd/beta/routingtable/describe/describe.go b/internal/cmd/beta/routingtable/describe/describe.go new file mode 100644 index 000000000..66eb9a6e0 --- /dev/null +++ b/internal/cmd/beta/routingtable/describe/describe.go @@ -0,0 +1,154 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + organizationIdFlag = "organization-id" + networkAreaIdFlag = "network-area-id" + routingTableArg = "ROUTING_TABLE_ID_ARG" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId *string + NetworkAreaId *string + RoutingTableId *string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", routingTableArg), + Short: "Describe a routing-table", + Long: "Describe a routing-table", + Args: args.SingleArg(routingTableArg, nil), + Example: examples.Build( + examples.NewExample( + `Describe a routing-table`, + `$ stackit beta routing-table describe xxxx-xxxx-xxxx-xxxx --organization-id xxx --network-area-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + request := apiClient.GetRoutingTableOfArea( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + *model.RoutingTableId, + ) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("describe routing-tables: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, response) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if len(args) == 0 { + return nil, fmt.Errorf("at least one argument is required") + } + routingTableId := args[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + RoutingTableId: &routingTableId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, routingTable *iaasalpha.RoutingTable) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(routingTable, "", " ") + if err != nil { + return fmt.Errorf("marshal routing-table describe: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(routingTable, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal routing-table describe: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + var labels []string + for key, value := range *routingTable.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "DESCRIPTION", "CREATED_AT", "UPDATED_AT", "DEFAULT", "LABELS", "SYSTEM_ROUTES") + table.AddRow( + utils.PtrString(routingTable.Id), + utils.PtrString(routingTable.Name), + utils.PtrString(routingTable.Description), + routingTable.CreatedAt.String(), + routingTable.UpdatedAt.String(), + utils.PtrString(routingTable.Default), + strings.Join(labels, "\n"), + utils.PtrString(routingTable.SystemRoutes), + ) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/routingtable/describe/describe_test.go b/internal/cmd/beta/routingtable/describe/describe_test.go new file mode 100644 index 000000000..5a57d9a32 --- /dev/null +++ b/internal/cmd/beta/routingtable/describe/describe_test.go @@ -0,0 +1,190 @@ +package describe + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +var testRegion = "eu01" +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +var testLabels = &map[string]string{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + RoutingTableId: utils.Ptr(testRoutingTableId), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRoutingTableId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "network-area-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRouteTable := iaasalpha.RoutingTable{ + CreatedAt: utils.Ptr(time.Now()), + Default: nil, + Description: utils.Ptr("description"), + Id: utils.Ptr("route-foo"), + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + Name: utils.Ptr("route-foo"), + SystemRoutes: utils.Ptr(true), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routingTable iaasalpha.RoutingTable + wantErr bool + }{ + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + routingTable: dummyRouteTable, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + routingTable: dummyRouteTable, + wantErr: false, + }, + } + + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.outputFormat, &tt.routingTable); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/routingtable/list/list.go b/internal/cmd/beta/routingtable/list/list.go new file mode 100644 index 000000000..d290e705f --- /dev/null +++ b/internal/cmd/beta/routingtable/list/list.go @@ -0,0 +1,201 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" + rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + organizationIdFlag = "organization-id" + networkAreaIdFlag = "network-area-id" + labelSelectorFlag = "label-selector" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId *string + NetworkAreaId *string + LabelSelector *string + Limit *int64 +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all routing-tables", + Long: "List all routing-tables", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all routing-tables`, + `$ stackit beta routing-table list --organization-id xxx --network-area-id yyy`, + ), + examples.NewExample( + `List all routing-tables with labels`, + `$ stackit beta routing-table list --label-selector env=dev,env=rc --organization-id xxx --network-area-id yyy`, + ), + examples.NewExample( + `List all routing-tables with labels and set limit to 10`, + `$ stackit beta routing-table list --label-selector env=dev,env=rc --limit 10 --organization-id xxx --network-area-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + request := buildRequest(ctx, model, apiClient) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("list routing-tables: %w", err) + } + + if items := response.Items; items == nil || len(*items) == 0 { + var orgLabel string + rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) + if err == nil { + orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) + orgLabel = *model.OrganizationId + } else if orgLabel == "" { + orgLabel = *model.OrganizationId + } + } else { + params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) + } + params.Printer.Info("No routing-tables found for organization %q\n", orgLabel) + return nil + } + + // Truncate output + items := *response.Items + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, items) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().String(labelSelectorFlag, "", "Filter by label") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaasalpha.APIClient) iaasalpha.ApiListRoutingTablesOfAreaRequest { + request := apiClient.ListRoutingTablesOfArea(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region) + if model.LabelSelector != nil { + request.LabelSelector(*model.LabelSelector) + } + + return request +} +func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.RoutingTable) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(items, "", " ") + if err != nil { + return fmt.Errorf("marshal routing-table list: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal routing-table list: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID", "NAME", "DESCRIPTION", "CREATED_AT", "UPDATED_AT", "DEFAULT", "LABELS", "SYSTEM_ROUTES") + + for _, item := range items { + var labels []string + for key, value := range *item.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + + table.AddRow( + utils.PtrString(item.Id), + utils.PtrString(item.Name), + utils.PtrString(item.Description), + item.CreatedAt.String(), + item.UpdatedAt.String(), + utils.PtrString(item.Default), + strings.Join(labels, "\n"), + utils.PtrString(item.SystemRoutes), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/routingtable/list/list_test.go b/internal/cmd/beta/routingtable/list/list_test.go new file mode 100644 index 000000000..02976bd2e --- /dev/null +++ b/internal/cmd/beta/routingtable/list/list_test.go @@ -0,0 +1,215 @@ +package list + +import ( + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +var testRegion = "eu01" +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() + +var testLabelSelectorFlag = "key1=value1,key2=value2" +var testLabels = &map[string]string{ + "key1": "value1", + "key2": "value2", +} + +var testLimitFlag = int64(10) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + labelSelectorFlag: testLabelSelectorFlag, + limitFlag: strconv.Itoa(int(testLimitFlag)), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + LabelSelector: utils.Ptr(testLabelSelectorFlag), + Limit: utils.Ptr(testLimitFlag), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "network-area-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "labels missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelSelectorFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.LabelSelector = nil + }), + }, + { + description: "limit missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, limitFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Limit = nil + }), + }, + { + description: "invalid limit flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "negative limit flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-10" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRouteTable := iaasalpha.RoutingTable{ + CreatedAt: utils.Ptr(time.Now()), + Default: nil, + Description: utils.Ptr("description"), + Id: utils.Ptr("route-foo"), + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + Name: utils.Ptr("route-foo"), + SystemRoutes: utils.Ptr(true), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routingTable []iaasalpha.RoutingTable + wantErr bool + }{ + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + routingTable: []iaasalpha.RoutingTable{dummyRouteTable}, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + routingTable: []iaasalpha.RoutingTable{dummyRouteTable}, + wantErr: false, + }, + } + + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.outputFormat, tt.routingTable); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/routingtable/route/create/create.go b/internal/cmd/beta/routingtable/route/create/create.go new file mode 100644 index 000000000..f67a49fc5 --- /dev/null +++ b/internal/cmd/beta/routingtable/route/create/create.go @@ -0,0 +1,290 @@ +package create + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + routeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/routing-table/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + organizationIdFlag = "organization-id" + networkAreaIdFlag = "network-area-id" + routingTableIdFlag = "routing-table-id" + destinationTypeFlag = "destination-type" + destinationValueFlag = "destination-value" + nextHopTypeFlag = "nexthop-type" + nextHopValueFlag = "nexthop-value" + labelFlag = "labels" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId *string + NetworkAreaId *string + RoutingTableId *string + DestinationType *string + DestinationValue *string + NextHopType *string + NextHopValue *string + Labels *map[string]string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a route in a routing-table", + Long: "Creates a route in a routing-table.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample("Create a route with CIDRv4 destination and IPv4 nexthop", + `stackit beta routing-tables route create \ +--routing-table-id xxx --organization-id yyy --network-area-id zzz \ +--destination-type cidrv4 --destination-value \ +--nexthop-type ipv4 --nexthop-value `), + + examples.NewExample("Create a route with CIDRv6 destination and IPv6 nexthop", + `stackit beta routing-tables route create \ +--routing-table-id xxx --organization-id yyy --network-area-id zzz \ +--destination-type cidrv6 --destination-value \ +--nexthop-type ipv6 --nexthop-value `), + + examples.NewExample("Create a route with CIDRv6 destination and Nexthop Internet", + `stackit beta routing-tables route create \ +--routing-table-id xxx --organization-id yyy --network-area-id zzz \ +--destination-type cidrv6 --destination-value \ +--nexthop-type internet`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create a route for routing-table with id %q?", *model.RoutingTableId) + if err := params.Printer.PromptForConfirmation(prompt); err != nil { + return err + } + } + + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create route request failed: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, *resp.Items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + cmd.Flags().Var(flags.CIDRFlag(), destinationValueFlag, "Destination value") + cmd.Flags().String(nextHopValueFlag, "", "NextHop value") + + cmd.Flags().Var( + flags.EnumFlag(true, "", "cidrv4", "cidrv6"), + destinationTypeFlag, + "Destination type") + + cmd.Flags().Var( + flags.EnumFlag(true, "", "ipv4", "ipv6", "internet", "blackhole"), + nextHopTypeFlag, + "Next hop type") + + cmd.Flags().StringToString(labelFlag, nil, "Key=value labels") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag, destinationTypeFlag, destinationValueFlag, nextHopTypeFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := &inputModel{ + GlobalFlagModel: globalFlags, + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), + DestinationType: flags.FlagToStringPointer(p, cmd, destinationTypeFlag), + DestinationValue: flags.FlagToStringPointer(p, cmd, destinationValueFlag), + NextHopType: flags.FlagToStringPointer(p, cmd, nextHopTypeFlag), + NextHopValue: flags.FlagToStringPointer(p, cmd, nextHopValueFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + } + + // Next Hop validation logic + switch strings.ToLower(*model.NextHopType) { + case "internet", "blackhole": + if model.NextHopValue != nil && *model.NextHopValue != "" { + return nil, errors.New("--nexthop-value is not allowed when --nexthop-type is 'internet' or 'blackhole'") + } + case "ipv4", "ipv6": + if model.NextHopValue == nil || *model.NextHopValue == "" { + return nil, errors.New("--nexthop-value is required when --nexthop-type is 'ipv4' or 'ipv6'") + } + default: + return nil, fmt.Errorf("invalid nexthop-type: %q", *model.NextHopType) + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaasalpha.APIClient) (iaasalpha.ApiAddRoutesToRoutingTableRequest, error) { + destination := buildDestination(model) + nextHop := buildNextHop(model) + + if destination != nil && nextHop != nil { + payload := iaasalpha.AddRoutesToRoutingTablePayload{ + Items: &[]iaasalpha.Route{ + { + Destination: destination, + Nexthop: nextHop, + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + }, + }, + } + + return apiClient.AddRoutesToRoutingTable( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + *model.RoutingTableId, + ).AddRoutesToRoutingTablePayload(payload), nil + } + + return nil, fmt.Errorf("invalid input") +} + +func buildDestination(model *inputModel) *iaasalpha.RouteDestination { + if model.DestinationValue == nil { + return nil + } + + destinationType := strings.ToLower(*model.DestinationType) + switch destinationType { + case "cidrv4": + return &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: model.DestinationType, + Value: model.DestinationValue, + }, + } + case "cidrv6": + return &iaasalpha.RouteDestination{ + DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + Type: model.DestinationType, + Value: model.DestinationValue, + }, + } + default: + return nil + } +} + +func buildNextHop(model *inputModel) *iaasalpha.RouteNexthop { + nextHopType := strings.ToLower(*model.NextHopType) + switch nextHopType { + case "ipv4": + return &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: model.NextHopType, + Value: model.NextHopValue, + }, + } + case "ipv6": + return &iaasalpha.RouteNexthop{ + NexthopIPv6: &iaasalpha.NexthopIPv6{ + Type: model.NextHopType, + Value: model.NextHopValue, + }, + } + case "internet": + return &iaasalpha.RouteNexthop{ + NexthopInternet: &iaasalpha.NexthopInternet{ + Type: model.NextHopType, + }, + } + case "blackhole": + return &iaasalpha.RouteNexthop{ + NexthopBlackhole: &iaasalpha.NexthopBlackhole{ + Type: model.NextHopType, + }, + } + default: + return nil + } +} + +func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.Route) error { + if len(items) == 0 { + return fmt.Errorf("create routes response is empty") + } + + switch outputFormat { + case print.JSONOutputFormat: + data, err := json.MarshalIndent(items, "", " ") + if err != nil { + return fmt.Errorf("marshal routes: %w", err) + } + p.Outputln(string(data)) + case print.YAMLOutputFormat: + data, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal routes: %w", err) + } + p.Outputln(string(data)) + default: + table := tables.NewTable() + table.SetHeader("ID", "DEST. TYPE", "DEST. VALUE", "NEXTHOP TYPE", "NEXTHOP VALUE", "LABELS", "CREATED", "UPDATED") + for _, item := range items { + destType, destValue, hopType, hopValue, labels := routeUtils.ExtractRouteDetails(item) + + table.AddRow( + utils.PtrString(item.Id), + destType, + destValue, + hopType, + hopValue, + labels, + item.CreatedAt.String(), + item.UpdatedAt.String(), + ) + } + return table.Display(p) + } + return nil +} diff --git a/internal/cmd/beta/routingtable/route/create/create_test.go b/internal/cmd/beta/routingtable/route/create/create_test.go new file mode 100644 index 000000000..89af7afdc --- /dev/null +++ b/internal/cmd/beta/routingtable/route/create/create_test.go @@ -0,0 +1,674 @@ +package create + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaasalpha.APIClient{} + +var testRegion = "eu01" +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +var testDestinationTypeFlag = "cidrv4" +var testDestinationValueFlag = "1.1.1.0/24" +var testNextHopTypeFlag = "ipv4" +var testNextHopValueFlag = "1.1.1.1" +var testLabelSelectorFlag = "key1=value1,key2=value2" +var testLabels = &map[string]string{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + labelFlag: testLabelSelectorFlag, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + destinationTypeFlag: testDestinationTypeFlag, + destinationValueFlag: testDestinationValueFlag, + nextHopTypeFlag: testNextHopTypeFlag, + nextHopValueFlag: testNextHopValueFlag, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + RoutingTableId: utils.Ptr(testRoutingTableId), + DestinationType: utils.Ptr(testDestinationTypeFlag), + DestinationValue: utils.Ptr(testDestinationValueFlag), + NextHopType: utils.Ptr(testNextHopTypeFlag), + NextHopValue: utils.Ptr(testNextHopValueFlag), + Labels: testLabels, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest)) iaasalpha.ApiAddRoutesToRoutingTableRequest { + request := testClient.AddRoutesToRoutingTable(testCtx, testOrgId, testNetworkAreaId, testRegion, testRoutingTableId) + request = request.AddRoutesToRoutingTablePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaasalpha.AddRoutesToRoutingTablePayload)) iaasalpha.AddRoutesToRoutingTablePayload { + payload := iaasalpha.AddRoutesToRoutingTablePayload{ + Items: &[]iaasalpha.Route{ + { + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: utils.Ptr(testDestinationTypeFlag), + Value: utils.Ptr(testDestinationValueFlag), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: utils.Ptr(testNextHopTypeFlag), + Value: utils.Ptr(testNextHopValueFlag), + }, + }, + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + }, + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "routing-table-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "destination-value missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, destinationValueFlag) + }), + isValid: false, + }, + { + description: "destination-type missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, destinationTypeFlag) + }), + isValid: false, + }, + { + description: "nexthop-type missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nextHopTypeFlag) + }), + isValid: false, + }, + { + description: "nexthop-value missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nextHopValueFlag) + }), + isValid: false, + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "network area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "network area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "network area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "invalid destination type enum", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[destinationTypeFlag] = "ipv4" + }), + isValid: false, + }, + { + description: "destination value not ipv4 cidr", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[destinationValueFlag] = "0.0.0.0" + }), + isValid: false, + }, + { + description: "destination value not ipv6 cidr", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[destinationTypeFlag] = "cidrv6" + flagValues[destinationValueFlag] = "2001:db8::" + }), + isValid: false, + }, + { + description: "destination value is ipv6 cidr", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[destinationTypeFlag] = "cidrv6" + flagValues[destinationValueFlag] = "2001:db8::/32" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.DestinationType = utils.Ptr("cidrv6") + model.DestinationValue = utils.Ptr("2001:db8::/32") + }), + }, + { + description: "invalid next hop type enum", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag] = "cidrv4" + }), + isValid: false, + }, + { + description: "nexthop-type is internet and nexthop-value is provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag] = "internet" + flagValues[nextHopValueFlag] = "1.1.1.1" // should not be allowed + }), + isValid: false, + }, + { + description: "nexthop-type is blackhole and nexthop-value is provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag] = "blackhole" + flagValues[nextHopValueFlag] = "1.1.1.1" + }), + isValid: false, + }, + { + description: "nexthop-type is internet and nexthop-value is not provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag] = "internet" + delete(flagValues, nextHopValueFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NextHopType = utils.Ptr("internet") + model.NextHopValue = nil + }), + isValid: true, + }, + { + description: "nexthop-type is blackhole and nexthop-value is not provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag] = "blackhole" + delete(flagValues, nextHopValueFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NextHopType = utils.Ptr("blackhole") + model.NextHopValue = nil + }), + isValid: true, + }, + { + description: "nexthop-type is ipv4 and nexthop-value is missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag] = "ipv4" + delete(flagValues, nextHopValueFlag) + }), + isValid: false, + }, + { + description: "nexthop-type is ipv6 and nexthop-value is missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag] = "ipv6" + delete(flagValues, nextHopValueFlag) + }), + isValid: false, + }, + { + description: "invalid nexthop-type provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag] = "invalid-type" + }), + isValid: false, + }, + { + description: "optional labels is provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[labelFlag] = "key=value" + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Labels = utils.Ptr(map[string]string{"key": "value"}) + }), + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildNextHop(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected *iaasalpha.RouteNexthop + }{ + { + description: "IPv4 next hop", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = utils.Ptr("ipv4") + m.NextHopValue = utils.Ptr("1.1.1.1") + }), + expected: &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("1.1.1.1"), + }, + }, + }, + { + description: "IPv6 next hop", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = utils.Ptr("ipv6") + m.NextHopValue = utils.Ptr("::1") + }), + expected: &iaasalpha.RouteNexthop{ + NexthopIPv6: &iaasalpha.NexthopIPv6{ + Type: utils.Ptr("ipv6"), + Value: utils.Ptr("::1"), + }, + }, + }, + { + description: "Internet next hop", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = utils.Ptr("internet") + m.NextHopValue = nil + }), + expected: &iaasalpha.RouteNexthop{ + NexthopInternet: &iaasalpha.NexthopInternet{ + Type: utils.Ptr("internet"), + }, + }, + }, + { + description: "Blackhole next hop", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = utils.Ptr("blackhole") + m.NextHopValue = nil + }), + expected: &iaasalpha.RouteNexthop{ + NexthopBlackhole: &iaasalpha.NexthopBlackhole{ + Type: utils.Ptr("blackhole"), + }, + }, + }, + { + description: "Unsupported next hop type", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = utils.Ptr("unsupported") + }), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := buildNextHop(tt.model) + if diff := cmp.Diff(tt.expected, got); diff != "" { + t.Errorf("buildNextHop() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestBuildDestination(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected *iaasalpha.RouteDestination + }{ + { + description: "CIDRv4 destination", + model: fixtureInputModel(func(m *inputModel) { + m.DestinationType = utils.Ptr("cidrv4") + m.DestinationValue = utils.Ptr("192.168.1.0/24") + }), + expected: &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("192.168.1.0/24"), + }, + }, + }, + { + description: "CIDRv6 destination", + model: fixtureInputModel(func(m *inputModel) { + m.DestinationType = utils.Ptr("cidrv6") + m.DestinationValue = utils.Ptr("2001:db8::/32") + }), + expected: &iaasalpha.RouteDestination{ + DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + Type: utils.Ptr("cidrv6"), + Value: utils.Ptr("2001:db8::/32"), + }, + }, + }, + { + description: "unsupported destination type", + model: fixtureInputModel(func(m *inputModel) { + m.DestinationType = utils.Ptr("other") + m.DestinationValue = utils.Ptr("1.1.1.1") + }), + expected: nil, + }, + { + description: "nil destination value", + model: fixtureInputModel(func(m *inputModel) { + m.DestinationValue = nil + }), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := buildDestination(tt.model) + if diff := cmp.Diff(tt.expected, got); diff != "" { + t.Errorf("buildDestination() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaasalpha.ApiAddRoutesToRoutingTableRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "optional labels provided", + model: fixtureInputModel(func(model *inputModel) { + model.Labels = utils.Ptr(map[string]string{"key": "value"}) + }), + expectedRequest: fixtureRequest(func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest) { + *request = (*request).AddRoutesToRoutingTablePayload(fixturePayload(func(payload *iaasalpha.AddRoutesToRoutingTablePayload) { + (*payload.Items)[0].Labels = utils.ConvertStringMapToInterfaceMap(utils.Ptr(map[string]string{"key": "value"})) + })) + }), + }, + { + description: "destination is cidrv6 and nexthop is ipv6", + model: fixtureInputModel(func(model *inputModel) { + model.DestinationType = utils.Ptr("cidrv6") + model.DestinationValue = utils.Ptr("2001:db8::/32") + model.NextHopType = utils.Ptr("ipv6") + model.NextHopValue = utils.Ptr("2001:db8::1") + }), + expectedRequest: fixtureRequest(func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest) { + *request = (*request).AddRoutesToRoutingTablePayload(iaasalpha.AddRoutesToRoutingTablePayload{ + Items: &[]iaasalpha.Route{ + { + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + Type: utils.Ptr("cidrv6"), + Value: utils.Ptr("2001:db8::/32"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopIPv6: &iaasalpha.NexthopIPv6{ + Type: utils.Ptr("ipv6"), + Value: utils.Ptr("2001:db8::1"), + }, + }, + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + }, + }, + }) + }), + }, + { + description: "nexthop type is internet (no value)", + model: fixtureInputModel(func(model *inputModel) { + model.NextHopType = utils.Ptr("internet") + model.NextHopValue = nil + }), + expectedRequest: fixtureRequest(func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaasalpha.AddRoutesToRoutingTablePayload) { + (*payload.Items)[0].Nexthop = &iaasalpha.RouteNexthop{ + NexthopInternet: &iaasalpha.NexthopInternet{ + Type: utils.Ptr("internet"), + }, + } + }) + *request = (*request).AddRoutesToRoutingTablePayload(payload) + }), + }, + { + description: "nexthop type is blackhole (no value)", + model: fixtureInputModel(func(model *inputModel) { + model.NextHopType = utils.Ptr("blackhole") + model.NextHopValue = nil + }), + expectedRequest: fixtureRequest(func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaasalpha.AddRoutesToRoutingTablePayload) { + (*payload.Items)[0].Nexthop = &iaasalpha.RouteNexthop{ + NexthopBlackhole: &iaasalpha.NexthopBlackhole{ + Type: utils.Ptr("blackhole"), + }, + } + }) + *request = (*request).AddRoutesToRoutingTablePayload(payload) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("buildRequest returned error: %v", err) + } + + if diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx)); diff != "" { + t.Errorf("buildRequest() mismatch (-got +want):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoute := iaasalpha.Route{ + Id: utils.Ptr("route-foo"), + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("10.0.0.0/24"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("10.0.0.1"), + }, + }, + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + items []iaasalpha.Route + wantErr bool + }{ + { + name: "nil items should return error", + outputFormat: "", + items: nil, + wantErr: true, + }, + { + name: "empty items list", + outputFormat: "", + items: []iaasalpha.Route{}, + wantErr: true, + }, + { + name: "table output with one route", + outputFormat: "", + items: []iaasalpha.Route{dummyRoute}, + wantErr: false, + }, + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + items: []iaasalpha.Route{dummyRoute}, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + items: []iaasalpha.Route{dummyRoute}, + wantErr: false, + }, + } + + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.outputFormat, tt.items); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/routingtable/route/delete/delete.go b/internal/cmd/beta/routingtable/route/delete/delete.go new file mode 100644 index 000000000..cf329ca6a --- /dev/null +++ b/internal/cmd/beta/routingtable/route/delete/delete.go @@ -0,0 +1,115 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" +) + +const ( + organizationIdFlag = "organization-id" + networkAreaIdFlag = "network-area-id" + routingTableIdFlag = "routing-table-id" + routeIdArg = "ROUTE_ID_ARG" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId *string + NetworkAreaId *string + RoutingTableId *string + RouteID *string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", routingTableIdFlag), + Short: "Deletes a route within a routing-table", + Long: "Deletes a route within a routing-table", + Args: args.SingleArg(routeIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Deletes a route within a routing-table`, + `$ stackit beta routing-table route delete xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete the route %q in routing-table %q for network-area-id %q?", *model.RouteID, *model.RoutingTableId, *model.OrganizationId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := apiClient.DeleteRouteFromRoutingTable( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + *model.RoutingTableId, + *model.RouteID, + ) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete route from routing-table: %w", err) + } + + params.Printer.Outputf("Route %q from routing-table %q deleted.", *model.RouteID, *model.RoutingTableId) + return nil + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if len(inputArgs) == 0 { + return nil, fmt.Errorf("at least one argument is required") + } + routeId := inputArgs[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), + RouteID: &routeId, + } + + p.DebugInputModel(model) + return &model, nil +} diff --git a/internal/cmd/beta/routingtable/route/delete/delete_test.go b/internal/cmd/beta/routingtable/route/delete/delete_test.go new file mode 100644 index 000000000..ab842a7fb --- /dev/null +++ b/internal/cmd/beta/routingtable/route/delete/delete_test.go @@ -0,0 +1,141 @@ +package delete + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +var ( + testOrgId = uuid.NewString() + testNetworkAreaId = uuid.NewString() + testRoutingTableId = uuid.NewString() + testRouteId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { + flagValues := map[string]string{ + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(*inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.InfoVerbosity, + }, + OrganizationId: &testOrgId, + NetworkAreaId: &testNetworkAreaId, + RoutingTableId: &testRoutingTableId, + RouteID: &testRouteId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + args []string + flagValues map[string]string + isValid bool + expectedRoute *inputModel + }{ + { + description: "valid input", + args: []string{testRouteId}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedRoute: fixtureInputModel(func(m *inputModel) { + m.RouteID = &testRouteId + }), + }, + { + description: "missing route id arg", + args: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing organization-id flag", + args: []string{testRouteId}, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, "organization-id") + }), + isValid: false, + }, + { + description: "missing network-area-id flag", + args: []string{testRouteId}, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, "network-area-id") + }), + isValid: false, + }, + { + description: "missing routing-table-id flag", + args: []string{testRouteId}, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, "routing-table-id") + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.args) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedRoute) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/routingtable/route/describe/describe.go b/internal/cmd/beta/routingtable/route/describe/describe.go new file mode 100644 index 000000000..4a3a1c7d9 --- /dev/null +++ b/internal/cmd/beta/routingtable/route/describe/describe.go @@ -0,0 +1,197 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + organizationIdFlag = "organization-id" + networkAreaIdFlag = "network-area-id" + routingTableIdFlag = "routing-table-id" + routeIdArg = "ROUTE_ID_ARG" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId *string + NetworkAreaId *string + RoutingTableId *string + RouteID *string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", routeIdArg), + Short: "Describe a route within a routing-table", + Long: "Describe a route within a routing-table", + Args: args.SingleArg(routeIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Describe a route within a routing-table`, + `$ stackit beta routing-table route describe xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + request := apiClient.GetRouteOfRoutingTable( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + *model.RoutingTableId, + *model.RouteID, + ) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("describe route: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, response) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if len(args) == 0 { + return nil, fmt.Errorf("at least one argument is required") + } + routeId := args[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), + RouteID: &routeId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, routingTable *iaasalpha.Route) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(routingTable, "", " ") + if err != nil { + return fmt.Errorf("marshal route describe: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(routingTable, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal route describe: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + var labels []string + for key, value := range *routingTable.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + + destinationType := "" + destinationValue := "" + if dest := routingTable.Destination.DestinationCIDRv4; dest != nil { + if dest.Type != nil { + destinationType = *dest.Type + } + if dest.Value != nil { + destinationValue = *dest.Value + } + } + if dest := routingTable.Destination.DestinationCIDRv6; dest != nil { + if dest.Type != nil { + destinationType = *dest.Type + } + if dest.Value != nil { + destinationValue = *dest.Value + } + } + + nextHopType := "" + nextHopValue := "" + if nextHop := routingTable.Destination.DestinationCIDRv4; nextHop != nil { + if nextHop.Type != nil { + nextHopType = *nextHop.Type + } + if nextHop.Value != nil { + nextHopValue = *nextHop.Value + } + } + if nextHop := routingTable.Destination.DestinationCIDRv6; nextHop != nil { + if nextHop.Type != nil { + nextHopType = *nextHop.Type + } + if nextHop.Value != nil { + nextHopValue = *nextHop.Value + } + } + + table := tables.NewTable() + table.SetHeader("ID", "CREATED_AT", "UPDATED_AT", "DESTINATION TYPE", "DESTINATION VALUE", "NEXTHOP TYPE", "NEXTHOP VALUE", "LABELS") + table.AddRow( + utils.PtrString(routingTable.Id), + routingTable.CreatedAt.String(), + routingTable.UpdatedAt.String(), + destinationType, + destinationValue, + nextHopType, + nextHopValue, + strings.Join(labels, "\n"), + ) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/routingtable/route/describe/describe_test.go b/internal/cmd/beta/routingtable/route/describe/describe_test.go new file mode 100644 index 000000000..63706b8d3 --- /dev/null +++ b/internal/cmd/beta/routingtable/route/describe/describe_test.go @@ -0,0 +1,225 @@ +package describe + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +var testRegion = "eu01" +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() +var testRouteId = uuid.NewString() + +var testLabels = &map[string]string{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + RoutingTableId: utils.Ptr(testRoutingTableId), + RouteID: utils.Ptr(testRouteId), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRouteId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "routing-table-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "network-area-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "routing-table-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "routing-id missing", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routeIdArg) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoute := iaasalpha.Route{ + Id: utils.Ptr("route-foo"), + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("10.0.0.0/24"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("10.0.0.1"), + }, + }, + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + route iaasalpha.Route + wantErr bool + }{ + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + route: dummyRoute, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + route: dummyRoute, + wantErr: false, + }, + } + + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.outputFormat, &tt.route); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/routingtable/route/list/list.go b/internal/cmd/beta/routingtable/route/list/list.go new file mode 100644 index 000000000..86bc78f18 --- /dev/null +++ b/internal/cmd/beta/routingtable/route/list/list.go @@ -0,0 +1,183 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + routeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/routing-table/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + organizationIdFlag = "organization-id" + networkAreaIdFlag = "network-area-id" + routingTableIdFlag = "routing-table-id" + labelSelectorFlag = "label-selector" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId *string + NetworkAreaId *string + RoutingTableId *string + LabelSelector *string + Limit *int64 +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list all routes within a routing-table", + Long: "list all routes within a routing-table", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all routes within a routing-table`, + `$ stackit beta routing-table route list --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + ), + examples.NewExample( + `List all routes within a routing-table with labels`, + `$ stackit beta routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc`, + ), + examples.NewExample( + `List all routes within a routing-tables with labels and limit to 10`, + `$ stackit beta routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc --limit 10`, + ), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + request := apiClient.ListRoutesOfRoutingTable( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + *model.RoutingTableId, + ) + + if model.LabelSelector != nil { + request.LabelSelector(*model.LabelSelector) + } + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("list routes: %w", err) + } + + if items := response.Items; items == nil || len(*items) == 0 { + params.Printer.Info("No routes found for routing-table %q\n", *model.RoutingTableId) + return nil + } + + // Truncate output + items := *response.Items + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, items) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + cmd.Flags().String(labelSelectorFlag, "", "Filter by label") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), + LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.Route) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(items, "", " ") + if err != nil { + return fmt.Errorf("marshal routes list: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal routes list: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID", "DEST. TYPE", "DEST. VALUE", "NEXTHOP TYPE", "NEXTHOP VALUE", "LABELS", "CREATED", "UPDATED") + for _, item := range items { + destType, destValue, hopType, hopValue, labels := routeUtils.ExtractRouteDetails(item) + + table.AddRow( + utils.PtrString(item.Id), + destType, + destValue, + hopType, + hopValue, + labels, + item.CreatedAt.String(), + item.UpdatedAt.String(), + ) + } + return table.Display(p) + } +} diff --git a/internal/cmd/beta/routingtable/route/list/list_test.go b/internal/cmd/beta/routingtable/route/list/list_test.go new file mode 100644 index 000000000..3a9b07763 --- /dev/null +++ b/internal/cmd/beta/routingtable/route/list/list_test.go @@ -0,0 +1,240 @@ +package list + +import ( + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +var testRegion = "eu01" +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +var testLabelSelectorFlag = "key1=value1,key2=value2" +var testLabels = &map[string]string{ + "key1": "value1", + "key2": "value2", +} + +var testLimitFlag = int64(10) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + labelSelectorFlag: testLabelSelectorFlag, + limitFlag: strconv.Itoa(int(testLimitFlag)), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + RoutingTableId: utils.Ptr(testRoutingTableId), + LabelSelector: utils.Ptr(testLabelSelectorFlag), + Limit: utils.Ptr(testLimitFlag), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "routing-table-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "network-area-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "routing-table-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "labels missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelSelectorFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.LabelSelector = nil + }), + }, + { + description: "limit missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, limitFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Limit = nil + }), + }, + { + description: "invalid limit flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "negative limit flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-10" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoute := iaasalpha.Route{ + Id: utils.Ptr("route-foo"), + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("10.0.0.0/24"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("10.0.0.1"), + }, + }, + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routes []iaasalpha.Route + wantErr bool + }{ + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + routes: []iaasalpha.Route{dummyRoute}, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + routes: []iaasalpha.Route{dummyRoute}, + wantErr: false, + }, + } + + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.outputFormat, tt.routes); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/routingtable/route/route.go b/internal/cmd/beta/routingtable/route/route.go new file mode 100644 index 000000000..cfca28e1a --- /dev/null +++ b/internal/cmd/beta/routingtable/route/route.go @@ -0,0 +1,33 @@ +package route + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/update" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "route", + Short: "Manage routes of a routing-table", + Long: "Manage routes of a routing-table", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) +} diff --git a/internal/cmd/beta/routingtable/route/update/update.go b/internal/cmd/beta/routingtable/route/update/update.go new file mode 100644 index 000000000..d3847848a --- /dev/null +++ b/internal/cmd/beta/routingtable/route/update/update.go @@ -0,0 +1,150 @@ +package update + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + organizationIdFlag = "organization-id" + networkAreaIdFlag = "network-area-id" + routingTableIdFlag = "routing-table-id" + labelFlag = "labels" + routeIdArg = "ROUTE_ID_ARG" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId *string + NetworkAreaId *string + RoutingTableId *string + RouteId string + Labels *map[string]string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", routeIdArg), + Short: "Updates a route in a routing-table", + Long: "Updates a route in a routing-table.", + Args: args.SingleArg(routeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Updates the label(s) of a route with ID "xxx" in a routing-table ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit beta routing-table route update xxx --labels key=value,foo=bar --routing-table-id xxx --organization-id yyy --network-area-id zzz", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := apiClient.UpdateRouteOfRoutingTable( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + *model.RoutingTableId, + model.RouteId, + ) + + payload := iaasalpha.UpdateRouteOfRoutingTablePayload{ + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + } + req = req.UpdateRouteOfRoutingTablePayload(payload) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update route %q of routing-table %q : %w", model.RouteId, *model.RoutingTableId, err) + } + + return outputResult(params.Printer, model.OutputFormat, *model.RoutingTableId, *model.NetworkAreaId, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels") + + err := flags.MarkFlagsRequired(cmd, labelFlag, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if len(inputArgs) == 0 { + return nil, fmt.Errorf("at least one argument is required") + } + routeId := inputArgs[0] + + labels := flags.FlagToStringToStringPointer(p, cmd, labelFlag) + + if labels == nil { + return nil, &cliErr.EmptyUpdateError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), + RouteId: routeId, + Labels: labels, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat, routingTableId, networkAreaId string, route iaasalpha.Route) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(route, "", " ") + if err != nil { + return fmt.Errorf("marshal route: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(route, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal route: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + p.Outputf("Updated route %q for routing-table %q in network-area %q.", *route.Id, routingTableId, networkAreaId) + return nil + } +} diff --git a/internal/cmd/beta/routingtable/route/update/update_test.go b/internal/cmd/beta/routingtable/route/update/update_test.go new file mode 100644 index 000000000..2a0ed75ff --- /dev/null +++ b/internal/cmd/beta/routingtable/route/update/update_test.go @@ -0,0 +1,236 @@ +package update + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +var testRegion = "eu01" +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() +var testRouteId = uuid.NewString() + +var testLabelSelectorFlag = "key1=value1,key2=value2" +var testLabels = &map[string]string{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + labelFlag: testLabelSelectorFlag, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + RoutingTableId: utils.Ptr(testRoutingTableId), + RouteId: testRouteId, + Labels: testLabels, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRouteId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "routing-table-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "network-area-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "routing-table-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "routing-id missing", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routeIdArg) + }), + isValid: false, + }, + { + description: "labels are missing", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoute := iaasalpha.Route{ + Id: utils.Ptr("route-foo"), + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("10.0.0.0/24"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("10.0.0.1"), + }, + }, + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + route iaasalpha.Route + wantErr bool + }{ + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + route: dummyRoute, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + route: dummyRoute, + wantErr: false, + }, + } + + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.outputFormat, "", "", tt.route); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/routingtable/routingtable.go b/internal/cmd/beta/routingtable/routingtable.go new file mode 100644 index 000000000..7eafa684a --- /dev/null +++ b/internal/cmd/beta/routingtable/routingtable.go @@ -0,0 +1,40 @@ +package routingtable + +import ( + "github.com/spf13/cobra" + rtDescribe "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/describe" + rtList "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/list" + route "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "routing-table", + Short: "Manage routing-tables and its according routes", + Long: `Manage routing tables and their associated routes. + +This functionality is currently in BETA. At this stage, only listing and describing +routing-tables, as well as full CRUD operations for routes, are supported. +This feature is primarily intended for debugging routes created through Terraform. + +Once the feature reaches General Availability, we plan to introduce support +for creating routing tables and attaching them to networks directly via the +CLI. Until then, we recommend users continue managing routing tables and +attachments through the Terraform provider.`, + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + cmd.AddCommand( + rtList.NewCmd(params), + rtDescribe.NewCmd(params), + route.NewCmd(params), + ) +} diff --git a/internal/pkg/services/iaas/client/alphaclient.go b/internal/pkg/services/iaas/client/alphaclient.go new file mode 100644 index 000000000..0f9db99f9 --- /dev/null +++ b/internal/pkg/services/iaas/client/alphaclient.go @@ -0,0 +1,44 @@ +package client + +import ( + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + + "github.com/spf13/viper" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" +) + +func ConfigureAlphaClient(p *print.Printer, cliVersion string) (*iaasalpha.APIClient, error) { + authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) + if err != nil { + p.Debug(print.ErrorLevel, "configure authentication: %v", err) + return nil, &errors.AuthError{} + } + cfgOptions := []sdkConfig.ConfigurationOption{ + utils.UserAgentConfigOption(cliVersion), + authCfgOption, + } + + customEndpoint := viper.GetString(config.IaaSCustomEndpointKey) + if customEndpoint != "" { + cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) + } + + if p.IsVerbosityDebug() { + cfgOptions = append(cfgOptions, + sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), + ) + } + + apiClient, err := iaasalpha.NewAPIClient(cfgOptions...) + if err != nil { + p.Debug(print.ErrorLevel, "create new API client: %v", err) + return nil, &errors.AuthError{} + } + + return apiClient, nil +} diff --git a/internal/pkg/services/routing-table/utils/utils.go b/internal/pkg/services/routing-table/utils/utils.go new file mode 100644 index 000000000..02afac663 --- /dev/null +++ b/internal/pkg/services/routing-table/utils/utils.go @@ -0,0 +1,40 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +func ExtractRouteDetails(item iaasalpha.Route) (destType, destValue, hopType, hopValue, labels string) { + if item.Destination.DestinationCIDRv4 != nil { + destType = utils.PtrString(item.Destination.DestinationCIDRv4.Type) + destValue = utils.PtrString(item.Destination.DestinationCIDRv4.Value) + } else if item.Destination.DestinationCIDRv6 != nil { + destType = utils.PtrString(item.Destination.DestinationCIDRv6.Type) + destValue = utils.PtrString(item.Destination.DestinationCIDRv6.Value) + } + + if item.Nexthop.NexthopIPv4 != nil { + hopType = utils.PtrString(item.Nexthop.NexthopIPv4.Type) + hopValue = utils.PtrString(item.Nexthop.NexthopIPv4.Value) + } else if item.Nexthop.NexthopIPv6 != nil { + hopType = utils.PtrString(item.Nexthop.NexthopIPv6.Type) + hopValue = utils.PtrString(item.Nexthop.NexthopIPv6.Value) + } else if item.Nexthop.NexthopInternet != nil { + hopType = utils.PtrString(item.Nexthop.NexthopInternet.Type) + } else if item.Nexthop.NexthopBlackhole != nil { + hopType = utils.PtrString(item.Nexthop.NexthopBlackhole.Type) + } + + var sortedLabels []string + if item.Labels != nil && len(*item.Labels) > 0 { + for key, value := range *item.Labels { + sortedLabels = append(sortedLabels, fmt.Sprintf("%s: %s", key, value)) + } + } + + return destType, destValue, hopType, hopValue, strings.Join(sortedLabels, ",") +} diff --git a/internal/pkg/services/routing-table/utils/utils_test.go b/internal/pkg/services/routing-table/utils/utils_test.go new file mode 100644 index 000000000..eda0626c8 --- /dev/null +++ b/internal/pkg/services/routing-table/utils/utils_test.go @@ -0,0 +1,135 @@ +package utils + +import ( + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +func TestExtractRouteDetails(t *testing.T) { + tests := []struct { + description string + input *iaasalpha.Route + wantDestType string + wantDestValue string + wantHopType string + wantHopValue string + wantLabels string + }{ + { + description: "CIDRv4 destination, IPv4 nexthop, with labels", + input: &iaasalpha.Route{ + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: utils.Ptr("CIDRv4"), + Value: utils.Ptr("10.0.0.0/24"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: utils.Ptr("IPv4"), + Value: utils.Ptr("10.0.0.1"), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + wantDestType: "CIDRv4", + wantDestValue: "10.0.0.0/24", + wantHopType: "IPv4", + wantHopValue: "10.0.0.1", + wantLabels: "key=value", + }, + { + description: "CIDRv6 destination, IPv6 nexthop, with no labels", + input: &iaasalpha.Route{ + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + Type: utils.Ptr("CIDRv6"), + Value: utils.Ptr("2001:db8::/32"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopIPv4: &iaasalpha.NexthopIPv4{ + Type: utils.Ptr("IPv6"), + Value: utils.Ptr("2001:db8::1"), + }, + }, + Labels: nil, + }, + wantDestType: "CIDRv6", + wantDestValue: "2001:db8::/32", + wantHopType: "IPv6", + wantHopValue: "2001:db8::1", + wantLabels: "", + }, + { + description: "Internet nexthop without value", + input: &iaasalpha.Route{ + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Type: utils.Ptr("CIDRv4"), + Value: utils.Ptr("0.0.0.0/0"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopInternet: &iaasalpha.NexthopInternet{ + Type: utils.Ptr("Internet"), + }, + }, + Labels: nil, + }, + wantDestType: "CIDRv4", + wantDestValue: "0.0.0.0/0", + wantHopType: "Internet", + wantHopValue: "", + wantLabels: "", + }, + { + description: "Blackhole nexthop without value and nil labels map", + input: &iaasalpha.Route{ + Destination: &iaasalpha.RouteDestination{ + DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + Type: utils.Ptr("CIDRv6"), + Value: utils.Ptr("::/0"), + }, + }, + Nexthop: &iaasalpha.RouteNexthop{ + NexthopBlackhole: &iaasalpha.NexthopBlackhole{ + Type: utils.Ptr("Blackhole"), + }, + }, + Labels: nil, + }, + wantDestType: "CIDRv6", + wantDestValue: "::/0", + wantHopType: "Blackhole", + wantHopValue: "", + wantLabels: "", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + destType, destValue, hopType, hopValue, labels := ExtractRouteDetails(*tt.input) + + if destType != tt.wantDestType { + t.Errorf("destType = %v, want %v", destType, tt.wantDestType) + } + if destValue != tt.wantDestValue { + t.Errorf("destValue = %v, want %v", destValue, tt.wantDestValue) + } + if hopType != tt.wantHopType { + t.Errorf("hopType = %v, want %v", hopType, tt.wantHopType) + } + if hopValue != tt.wantHopValue { + t.Errorf("hopValue = %v, want %v", hopValue, tt.wantHopValue) + } + if (tt.wantLabels != "" && labels == "") || (tt.wantLabels == "" && labels != "") { + t.Errorf("labels mismatch: got %q, want %q", labels, tt.wantLabels) + } + }) + } +} From 826b579e09d47af64804824c7d558ea782af4324 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Mon, 27 Oct 2025 14:27:29 +0100 Subject: [PATCH 2/2] review changes --- docs/stackit.md | 1 + docs/stackit_beta.md | 1 - docs/stackit_beta_routing-table.md | 45 --- docs/stackit_beta_routing-table_route.md | 38 -- docs/stackit_network_create.md | 4 + docs/stackit_network_update.md | 4 + docs/stackit_routing-table.md | 42 +++ docs/stackit_routing-table_create.md | 56 +++ docs/stackit_routing-table_delete.md | 42 +++ ...e.md => stackit_routing-table_describe.md} | 14 +- ..._list.md => stackit_routing-table_list.md} | 18 +- docs/stackit_routing-table_route.md | 38 ++ ... => stackit_routing-table_route_create.md} | 14 +- ... => stackit_routing-table_route_delete.md} | 10 +- ...> stackit_routing-table_route_describe.md} | 14 +- ...md => stackit_routing-table_route_list.md} | 18 +- ... => stackit_routing-table_route_update.md} | 10 +- docs/stackit_routing-table_update.md | 55 +++ internal/cmd/beta/beta.go | 2 - .../cmd/beta/routingtable/routingtable.go | 40 -- internal/cmd/network/create/create.go | 19 +- internal/cmd/network/create/create_test.go | 41 +- internal/cmd/network/describe/describe.go | 5 + internal/cmd/network/list/list.go | 3 +- internal/cmd/network/update/update.go | 17 +- internal/cmd/network/update/update_test.go | 13 + internal/cmd/root.go | 28 +- internal/cmd/routingtable/create/create.go | 208 +++++++++++ .../cmd/routingtable/create/create_test.go | 350 ++++++++++++++++++ internal/cmd/routingtable/delete/delete.go | 111 ++++++ .../cmd/routingtable/delete/delete_test.go | 145 ++++++++ .../routingtable/describe/describe.go | 68 ++-- .../routingtable/describe/describe_test.go | 116 +++--- .../cmd/{beta => }/routingtable/list/list.go | 79 ++-- .../{beta => }/routingtable/list/list_test.go | 70 +--- .../routingtable/route/create/create.go | 121 +++--- .../routingtable/route/create/create_test.go | 257 +++++++------ .../routingtable/route/delete/delete.go | 19 +- .../routingtable/route/delete/delete_test.go | 93 +++-- .../routingtable/route/describe/describe.go | 72 ++-- .../route/describe/describe_test.go | 92 ++--- .../routingtable/route/list/list.go | 84 ++--- .../routingtable/route/list/list_test.go | 38 +- .../{beta => }/routingtable/route/route.go | 14 +- .../routingtable/route/update/update.go | 70 ++-- .../routingtable/route/update/update_test.go | 91 ++--- internal/cmd/routingtable/routingtable.go | 40 ++ internal/cmd/routingtable/update/update.go | 167 +++++++++ .../cmd/routingtable/update/update_test.go | 221 +++++++++++ .../pkg/services/iaas/client/alphaclient.go | 44 --- .../pkg/services/routing-table/utils/utils.go | 4 +- .../routing-table/utils/utils_test.go | 44 +-- 52 files changed, 2261 insertions(+), 949 deletions(-) delete mode 100644 docs/stackit_beta_routing-table.md delete mode 100644 docs/stackit_beta_routing-table_route.md create mode 100644 docs/stackit_routing-table.md create mode 100644 docs/stackit_routing-table_create.md create mode 100644 docs/stackit_routing-table_delete.md rename docs/{stackit_beta_routing-table_describe.md => stackit_routing-table_describe.md} (61%) rename docs/{stackit_beta_routing-table_list.md => stackit_routing-table_list.md} (59%) create mode 100644 docs/stackit_routing-table_route.md rename docs/{stackit_beta_routing-table_route_create.md => stackit_routing-table_route_create.md} (80%) rename docs/{stackit_beta_routing-table_route_delete.md => stackit_routing-table_route_delete.md} (66%) rename docs/{stackit_beta_routing-table_route_describe.md => stackit_routing-table_route_describe.md} (60%) rename docs/{stackit_beta_routing-table_route_list.md => stackit_routing-table_route_list.md} (57%) rename docs/{stackit_beta_routing-table_route_update.md => stackit_routing-table_route_update.md} (72%) create mode 100644 docs/stackit_routing-table_update.md delete mode 100644 internal/cmd/beta/routingtable/routingtable.go create mode 100644 internal/cmd/routingtable/create/create.go create mode 100644 internal/cmd/routingtable/create/create_test.go create mode 100644 internal/cmd/routingtable/delete/delete.go create mode 100644 internal/cmd/routingtable/delete/delete_test.go rename internal/cmd/{beta => }/routingtable/describe/describe.go (70%) rename internal/cmd/{beta => }/routingtable/describe/describe_test.go (61%) rename internal/cmd/{beta => }/routingtable/list/list.go (77%) rename internal/cmd/{beta => }/routingtable/list/list_test.go (73%) rename internal/cmd/{beta => }/routingtable/route/create/create.go (79%) rename internal/cmd/{beta => }/routingtable/route/create/create_test.go (70%) rename internal/cmd/{beta => }/routingtable/route/delete/delete.go (90%) rename internal/cmd/{beta => }/routingtable/route/delete/delete_test.go (58%) rename internal/cmd/{beta => }/routingtable/route/describe/describe.go (78%) rename internal/cmd/{beta => }/routingtable/route/describe/describe_test.go (74%) rename internal/cmd/{beta => }/routingtable/route/list/list.go (77%) rename internal/cmd/{beta => }/routingtable/route/list/list_test.go (90%) rename internal/cmd/{beta => }/routingtable/route/route.go (57%) rename internal/cmd/{beta => }/routingtable/route/update/update.go (77%) rename internal/cmd/{beta => }/routingtable/route/update/update_test.go (72%) create mode 100644 internal/cmd/routingtable/routingtable.go create mode 100644 internal/cmd/routingtable/update/update.go create mode 100644 internal/cmd/routingtable/update/update_test.go delete mode 100644 internal/pkg/services/iaas/client/alphaclient.go diff --git a/docs/stackit.md b/docs/stackit.md index d0ddc4554..afa5a0c96 100644 --- a/docs/stackit.md +++ b/docs/stackit.md @@ -52,6 +52,7 @@ stackit [flags] * [stackit quota](./stackit_quota.md) - Manage server quotas * [stackit rabbitmq](./stackit_rabbitmq.md) - Provides functionality for RabbitMQ * [stackit redis](./stackit_redis.md) - Provides functionality for Redis +* [stackit routing-table](./stackit_routing-table.md) - Manage routing-tables and its according routes * [stackit secrets-manager](./stackit_secrets-manager.md) - Provides functionality for Secrets Manager * [stackit security-group](./stackit_security-group.md) - Manage security groups * [stackit server](./stackit_server.md) - Provides functionality for servers diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md index b0cac67b6..23097f8ca 100644 --- a/docs/stackit_beta.md +++ b/docs/stackit_beta.md @@ -43,6 +43,5 @@ stackit beta [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS -* [stackit beta routing-table](./stackit_beta_routing-table.md) - Manage routing-tables and its according routes * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex diff --git a/docs/stackit_beta_routing-table.md b/docs/stackit_beta_routing-table.md deleted file mode 100644 index c90f7d29e..000000000 --- a/docs/stackit_beta_routing-table.md +++ /dev/null @@ -1,45 +0,0 @@ -## stackit beta routing-table - -Manage routing-tables and its according routes - -### Synopsis - -Manage routing tables and their associated routes. - -This functionality is currently in BETA. At this stage, only listing and describing -routing-tables, as well as full CRUD operations for routes, are supported. -This feature is primarily intended for debugging routes created through Terraform. - -Once the feature reaches General Availability, we plan to introduce support -for creating routing tables and attaching them to networks directly via the -CLI. Until then, we recommend users continue managing routing tables and -attachments through the Terraform provider. - -``` -stackit beta routing-table [flags] -``` - -### Options - -``` - -h, --help Help for "stackit beta routing-table" -``` - -### Options inherited from parent commands - -``` - -y, --assume-yes If set, skips all confirmation prompts - --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] - -p, --project-id string Project ID - --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") -``` - -### SEE ALSO - -* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands -* [stackit beta routing-table describe](./stackit_beta_routing-table_describe.md) - Describe a routing-table -* [stackit beta routing-table list](./stackit_beta_routing-table_list.md) - List all routing-tables -* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table - diff --git a/docs/stackit_beta_routing-table_route.md b/docs/stackit_beta_routing-table_route.md deleted file mode 100644 index 3add3abcd..000000000 --- a/docs/stackit_beta_routing-table_route.md +++ /dev/null @@ -1,38 +0,0 @@ -## stackit beta routing-table route - -Manage routes of a routing-table - -### Synopsis - -Manage routes of a routing-table - -``` -stackit beta routing-table route [flags] -``` - -### Options - -``` - -h, --help Help for "stackit beta routing-table route" -``` - -### Options inherited from parent commands - -``` - -y, --assume-yes If set, skips all confirmation prompts - --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] - -p, --project-id string Project ID - --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") -``` - -### SEE ALSO - -* [stackit beta routing-table](./stackit_beta_routing-table.md) - Manage routing-tables and its according routes -* [stackit beta routing-table route create](./stackit_beta_routing-table_route_create.md) - Creates a route in a routing-table -* [stackit beta routing-table route delete](./stackit_beta_routing-table_route_delete.md) - Deletes a route within a routing-table -* [stackit beta routing-table route describe](./stackit_beta_routing-table_route_describe.md) - Describe a route within a routing-table -* [stackit beta routing-table route list](./stackit_beta_routing-table_route_list.md) - list all routes within a routing-table -* [stackit beta routing-table route update](./stackit_beta_routing-table_route_update.md) - Updates a route in a routing-table - diff --git a/docs/stackit_network_create.md b/docs/stackit_network_create.md index 146264977..44934bee3 100644 --- a/docs/stackit_network_create.md +++ b/docs/stackit_network_create.md @@ -30,6 +30,9 @@ stackit network create [flags] Create an IPv6 network with name "network-1" with DNS name servers, a prefix and a gateway $ stackit network create --name network-1 --ipv6-dns-name-servers "2001:4860:4860::8888,2001:4860:4860::8844" --ipv6-prefix "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888" + + Create a network with name "network-1" and attach routing-table "xxx" + $ stackit network create --name network-1 --routing-table-id xxx ``` ### Options @@ -49,6 +52,7 @@ stackit network create [flags] --no-ipv4-gateway If set to true, the network doesn't have an IPv4 gateway --no-ipv6-gateway If set to true, the network doesn't have an IPv6 gateway --non-routed If set to true, the network is not routed and therefore not accessible from other networks + --routing-table-id string The ID of the routing-table for the network ``` ### Options inherited from parent commands diff --git a/docs/stackit_network_update.md b/docs/stackit_network_update.md index 313ce68fa..7069b26d2 100644 --- a/docs/stackit_network_update.md +++ b/docs/stackit_network_update.md @@ -24,6 +24,9 @@ stackit network update NETWORK_ID [flags] Update IPv6 network with ID "xxx" with new name "network-1-new", new gateway and new DNS name servers $ stackit network update xxx --name network-1-new --ipv6-dns-name-servers "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888" + + Update network with ID "xxx" with new routing-table id "xxx" + $ stackit network update xxx --routing-table-id xxx ``` ### Options @@ -38,6 +41,7 @@ stackit network update NETWORK_ID [flags] -n, --name string Network name --no-ipv4-gateway If set to true, the network doesn't have an IPv4 gateway --no-ipv6-gateway If set to true, the network doesn't have an IPv6 gateway + --routing-table-id string The ID of the routing-table for the network ``` ### Options inherited from parent commands diff --git a/docs/stackit_routing-table.md b/docs/stackit_routing-table.md new file mode 100644 index 000000000..accc36f68 --- /dev/null +++ b/docs/stackit_routing-table.md @@ -0,0 +1,42 @@ +## stackit routing-table + +Manage routing-tables and its according routes + +### Synopsis + +Manage routing-tables and their associated routes. + +This API is currently available only to selected customers. +To request access, please contact your account manager or submit a support ticket. + +``` +stackit routing-table [flags] +``` + +### Options + +``` + -h, --help Help for "stackit routing-table" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit routing-table create](./stackit_routing-table_create.md) - Creates a routing-table +* [stackit routing-table delete](./stackit_routing-table_delete.md) - Deletes a routing-table +* [stackit routing-table describe](./stackit_routing-table_describe.md) - Describes a routing-table +* [stackit routing-table list](./stackit_routing-table_list.md) - Lists all routing-tables +* [stackit routing-table route](./stackit_routing-table_route.md) - Manages routes of a routing-table +* [stackit routing-table update](./stackit_routing-table_update.md) - Updates a routing-table + diff --git a/docs/stackit_routing-table_create.md b/docs/stackit_routing-table_create.md new file mode 100644 index 000000000..d04fefa64 --- /dev/null +++ b/docs/stackit_routing-table_create.md @@ -0,0 +1,56 @@ +## stackit routing-table create + +Creates a routing-table + +### Synopsis + +Creates a routing-table. + +``` +stackit routing-table create [flags] +``` + +### Examples + +``` + Create a routing-table with name `rt` + stackit routing-table create --organization-id xxx --network-area-id yyy --name "rt" + + Create a routing-table with name `rt` and description `some description` + stackit routing-table create --organization-id xxx --network-area-id yyy --name "rt" --description "some description" + + Create a routing-table with name `rt` with system_routes disabled + stackit routing-table create --organization-id xxx --network-area-id yyy --name "rt" --non-system-routes + + Create a routing-table with name `rt` with dynamic_routes disabled + stackit routing-table create --organization-id xxx --network-area-id yyy --name "rt" --non-dynamic-routes +``` + +### Options + +``` + --description string Description of the routing-table + -h, --help Help for "stackit routing-table create" + --labels stringToString Key=value labels (default []) + --name string Name of the routing-table + --network-area-id string Network-Area ID + --non-dynamic-routes If true, preventing dynamic routes from propagating to the routing-table. + --non-system-routes If true, automatically disables routes for project-to-project communication. + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit routing-table](./stackit_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_routing-table_delete.md b/docs/stackit_routing-table_delete.md new file mode 100644 index 000000000..d40dab675 --- /dev/null +++ b/docs/stackit_routing-table_delete.md @@ -0,0 +1,42 @@ +## stackit routing-table delete + +Deletes a routing-table + +### Synopsis + +Deletes a routing-table + +``` +stackit routing-table delete ROUTING_TABLE_ARG [flags] +``` + +### Examples + +``` + Deletes a a routing-table + $ stackit routing-table delete xxxx-xxxx-xxxx-xxxx --organization-id yyy --network-area-id zzz +``` + +### Options + +``` + -h, --help Help for "stackit routing-table delete" + --network-area-id string Network-Area ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit routing-table](./stackit_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_beta_routing-table_describe.md b/docs/stackit_routing-table_describe.md similarity index 61% rename from docs/stackit_beta_routing-table_describe.md rename to docs/stackit_routing-table_describe.md index d2db08f5f..5d7c8fd09 100644 --- a/docs/stackit_beta_routing-table_describe.md +++ b/docs/stackit_routing-table_describe.md @@ -1,26 +1,26 @@ -## stackit beta routing-table describe +## stackit routing-table describe -Describe a routing-table +Describes a routing-table ### Synopsis -Describe a routing-table +Describes a routing-table ``` -stackit beta routing-table describe ROUTING_TABLE_ID_ARG [flags] +stackit routing-table describe ROUTING_TABLE_ID_ARG [flags] ``` ### Examples ``` Describe a routing-table - $ stackit beta routing-table describe xxxx-xxxx-xxxx-xxxx --organization-id xxx --network-area-id yyy + $ stackit routing-table describe xxxx-xxxx-xxxx-xxxx --organization-id xxx --network-area-id yyy ``` ### Options ``` - -h, --help Help for "stackit beta routing-table describe" + -h, --help Help for "stackit routing-table describe" --network-area-id string Network-Area ID --organization-id string Organization ID ``` @@ -38,5 +38,5 @@ stackit beta routing-table describe ROUTING_TABLE_ID_ARG [flags] ### SEE ALSO -* [stackit beta routing-table](./stackit_beta_routing-table.md) - Manage routing-tables and its according routes +* [stackit routing-table](./stackit_routing-table.md) - Manage routing-tables and its according routes diff --git a/docs/stackit_beta_routing-table_list.md b/docs/stackit_routing-table_list.md similarity index 59% rename from docs/stackit_beta_routing-table_list.md rename to docs/stackit_routing-table_list.md index 79f01b581..d45350821 100644 --- a/docs/stackit_beta_routing-table_list.md +++ b/docs/stackit_routing-table_list.md @@ -1,32 +1,32 @@ -## stackit beta routing-table list +## stackit routing-table list -List all routing-tables +Lists all routing-tables ### Synopsis -List all routing-tables +Lists all routing-tables ``` -stackit beta routing-table list [flags] +stackit routing-table list [flags] ``` ### Examples ``` List all routing-tables - $ stackit beta routing-table list --organization-id xxx --network-area-id yyy + $ stackit routing-table list --organization-id xxx --network-area-id yyy List all routing-tables with labels - $ stackit beta routing-table list --label-selector env=dev,env=rc --organization-id xxx --network-area-id yyy + $ stackit routing-table list --label-selector env=dev,env=rc --organization-id xxx --network-area-id yyy List all routing-tables with labels and set limit to 10 - $ stackit beta routing-table list --label-selector env=dev,env=rc --limit 10 --organization-id xxx --network-area-id yyy + $ stackit routing-table list --label-selector env=dev,env=rc --limit 10 --organization-id xxx --network-area-id yyy ``` ### Options ``` - -h, --help Help for "stackit beta routing-table list" + -h, --help Help for "stackit routing-table list" --label-selector string Filter by label --limit int Maximum number of entries to list --network-area-id string Network-Area ID @@ -46,5 +46,5 @@ stackit beta routing-table list [flags] ### SEE ALSO -* [stackit beta routing-table](./stackit_beta_routing-table.md) - Manage routing-tables and its according routes +* [stackit routing-table](./stackit_routing-table.md) - Manage routing-tables and its according routes diff --git a/docs/stackit_routing-table_route.md b/docs/stackit_routing-table_route.md new file mode 100644 index 000000000..aa28d570d --- /dev/null +++ b/docs/stackit_routing-table_route.md @@ -0,0 +1,38 @@ +## stackit routing-table route + +Manages routes of a routing-table + +### Synopsis + +Manages routes of a routing-table + +``` +stackit routing-table route [flags] +``` + +### Options + +``` + -h, --help Help for "stackit routing-table route" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit routing-table](./stackit_routing-table.md) - Manage routing-tables and its according routes +* [stackit routing-table route create](./stackit_routing-table_route_create.md) - Creates a route in a routing-table +* [stackit routing-table route delete](./stackit_routing-table_route_delete.md) - Deletes a route within a routing-table +* [stackit routing-table route describe](./stackit_routing-table_route_describe.md) - Describes a route within a routing-table +* [stackit routing-table route list](./stackit_routing-table_route_list.md) - Lists all routes within a routing-table +* [stackit routing-table route update](./stackit_routing-table_route_update.md) - Updates a route in a routing-table + diff --git a/docs/stackit_beta_routing-table_route_create.md b/docs/stackit_routing-table_route_create.md similarity index 80% rename from docs/stackit_beta_routing-table_route_create.md rename to docs/stackit_routing-table_route_create.md index 7cb5b7823..4dff90c8b 100644 --- a/docs/stackit_beta_routing-table_route_create.md +++ b/docs/stackit_routing-table_route_create.md @@ -1,4 +1,4 @@ -## stackit beta routing-table route create +## stackit routing-table route create Creates a route in a routing-table @@ -7,26 +7,26 @@ Creates a route in a routing-table Creates a route in a routing-table. ``` -stackit beta routing-table route create [flags] +stackit routing-table route create [flags] ``` ### Examples ``` Create a route with CIDRv4 destination and IPv4 nexthop - stackit beta routing-tables route create \ + stackit routing-table route create \ --routing-table-id xxx --organization-id yyy --network-area-id zzz \ --destination-type cidrv4 --destination-value \ --nexthop-type ipv4 --nexthop-value Create a route with CIDRv6 destination and IPv6 nexthop - stackit beta routing-tables route create \ + stackit routing-table route create \ --routing-table-id xxx --organization-id yyy --network-area-id zzz \ --destination-type cidrv6 --destination-value \ --nexthop-type ipv6 --nexthop-value Create a route with CIDRv6 destination and Nexthop Internet - stackit beta routing-tables route create \ + stackit routing-table route create \ --routing-table-id xxx --organization-id yyy --network-area-id zzz \ --destination-type cidrv6 --destination-value \ --nexthop-type internet @@ -37,7 +37,7 @@ stackit beta routing-table route create [flags] ``` --destination-type string Destination type --destination-value string Destination value - -h, --help Help for "stackit beta routing-table route create" + -h, --help Help for "stackit routing-table route create" --labels stringToString Key=value labels (default []) --network-area-id string Network-Area ID --nexthop-type string Next hop type @@ -59,5 +59,5 @@ stackit beta routing-table route create [flags] ### SEE ALSO -* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table +* [stackit routing-table route](./stackit_routing-table_route.md) - Manages routes of a routing-table diff --git a/docs/stackit_beta_routing-table_route_delete.md b/docs/stackit_routing-table_route_delete.md similarity index 66% rename from docs/stackit_beta_routing-table_route_delete.md rename to docs/stackit_routing-table_route_delete.md index ba10a41ef..bda6b9311 100644 --- a/docs/stackit_beta_routing-table_route_delete.md +++ b/docs/stackit_routing-table_route_delete.md @@ -1,4 +1,4 @@ -## stackit beta routing-table route delete +## stackit routing-table route delete Deletes a route within a routing-table @@ -7,20 +7,20 @@ Deletes a route within a routing-table Deletes a route within a routing-table ``` -stackit beta routing-table route delete routing-table-id [flags] +stackit routing-table route delete routing-table-id [flags] ``` ### Examples ``` Deletes a route within a routing-table - $ stackit beta routing-table route delete xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz + $ stackit routing-table route delete xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz ``` ### Options ``` - -h, --help Help for "stackit beta routing-table route delete" + -h, --help Help for "stackit routing-table route delete" --network-area-id string Network-Area ID --organization-id string Organization ID --routing-table-id string Routing-Table ID @@ -39,5 +39,5 @@ stackit beta routing-table route delete routing-table-id [flags] ### SEE ALSO -* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table +* [stackit routing-table route](./stackit_routing-table_route.md) - Manages routes of a routing-table diff --git a/docs/stackit_beta_routing-table_route_describe.md b/docs/stackit_routing-table_route_describe.md similarity index 60% rename from docs/stackit_beta_routing-table_route_describe.md rename to docs/stackit_routing-table_route_describe.md index 586bf88d6..c9cb51f2b 100644 --- a/docs/stackit_beta_routing-table_route_describe.md +++ b/docs/stackit_routing-table_route_describe.md @@ -1,26 +1,26 @@ -## stackit beta routing-table route describe +## stackit routing-table route describe -Describe a route within a routing-table +Describes a route within a routing-table ### Synopsis -Describe a route within a routing-table +Describes a route within a routing-table ``` -stackit beta routing-table route describe ROUTE_ID_ARG [flags] +stackit routing-table route describe ROUTE_ID_ARG [flags] ``` ### Examples ``` Describe a route within a routing-table - $ stackit beta routing-table route describe xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz + $ stackit routing-table route describe xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz ``` ### Options ``` - -h, --help Help for "stackit beta routing-table route describe" + -h, --help Help for "stackit routing-table route describe" --network-area-id string Network-Area ID --organization-id string Organization ID --routing-table-id string Routing-Table ID @@ -39,5 +39,5 @@ stackit beta routing-table route describe ROUTE_ID_ARG [flags] ### SEE ALSO -* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table +* [stackit routing-table route](./stackit_routing-table_route.md) - Manages routes of a routing-table diff --git a/docs/stackit_beta_routing-table_route_list.md b/docs/stackit_routing-table_route_list.md similarity index 57% rename from docs/stackit_beta_routing-table_route_list.md rename to docs/stackit_routing-table_route_list.md index 0dfd13f46..597be772c 100644 --- a/docs/stackit_beta_routing-table_route_list.md +++ b/docs/stackit_routing-table_route_list.md @@ -1,32 +1,32 @@ -## stackit beta routing-table route list +## stackit routing-table route list -list all routes within a routing-table +Lists all routes within a routing-table ### Synopsis -list all routes within a routing-table +Lists all routes within a routing-table ``` -stackit beta routing-table route list [flags] +stackit routing-table route list [flags] ``` ### Examples ``` List all routes within a routing-table - $ stackit beta routing-table route list --routing-table-id xxx --organization-id yyy --network-area-id zzz + $ stackit routing-table route list --routing-table-id xxx --organization-id yyy --network-area-id zzz List all routes within a routing-table with labels - $ stackit beta routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc + $ stackit routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc List all routes within a routing-tables with labels and limit to 10 - $ stackit beta routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc --limit 10 + $ stackit routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc --limit 10 ``` ### Options ``` - -h, --help Help for "stackit beta routing-table route list" + -h, --help Help for "stackit routing-table route list" --label-selector string Filter by label --limit int Maximum number of entries to list --network-area-id string Network-Area ID @@ -47,5 +47,5 @@ stackit beta routing-table route list [flags] ### SEE ALSO -* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table +* [stackit routing-table route](./stackit_routing-table_route.md) - Manages routes of a routing-table diff --git a/docs/stackit_beta_routing-table_route_update.md b/docs/stackit_routing-table_route_update.md similarity index 72% rename from docs/stackit_beta_routing-table_route_update.md rename to docs/stackit_routing-table_route_update.md index 044e3b165..447512218 100644 --- a/docs/stackit_beta_routing-table_route_update.md +++ b/docs/stackit_routing-table_route_update.md @@ -1,4 +1,4 @@ -## stackit beta routing-table route update +## stackit routing-table route update Updates a route in a routing-table @@ -7,20 +7,20 @@ Updates a route in a routing-table Updates a route in a routing-table. ``` -stackit beta routing-table route update ROUTE_ID_ARG [flags] +stackit routing-table route update ROUTE_ID_ARG [flags] ``` ### Examples ``` Updates the label(s) of a route with ID "xxx" in a routing-table ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" - $ stackit beta routing-table route update xxx --labels key=value,foo=bar --routing-table-id xxx --organization-id yyy --network-area-id zzz + $ stackit routing-table route update xxx --labels key=value,foo=bar --routing-table-id xxx --organization-id yyy --network-area-id zzz ``` ### Options ``` - -h, --help Help for "stackit beta routing-table route update" + -h, --help Help for "stackit routing-table route update" --labels stringToString Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels (default []) --network-area-id string Network-Area ID --organization-id string Organization ID @@ -40,5 +40,5 @@ stackit beta routing-table route update ROUTE_ID_ARG [flags] ### SEE ALSO -* [stackit beta routing-table route](./stackit_beta_routing-table_route.md) - Manage routes of a routing-table +* [stackit routing-table route](./stackit_routing-table_route.md) - Manages routes of a routing-table diff --git a/docs/stackit_routing-table_update.md b/docs/stackit_routing-table_update.md new file mode 100644 index 000000000..7afd5d16b --- /dev/null +++ b/docs/stackit_routing-table_update.md @@ -0,0 +1,55 @@ +## stackit routing-table update + +Updates a routing-table + +### Synopsis + +Updates a routing-table. + +``` +stackit routing-table update ROUTE_TABLE_ID_ARG [flags] +``` + +### Examples + +``` + Updates the label(s) of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit routing-table update xxx --labels key=value,foo=bar --organization-id yyy --network-area-id zzz + + Updates the name of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit routing-table update xxx --name foo --organization-id yyy --network-area-id zzz + + Updates the description of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit routing-table update xxx --description foo --organization-id yyy --network-area-id zzz + + Disables the dynamic_routes of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit routing-table update xxx --organization-id yyy --network-area-id zzz --non-dynamic-routes +``` + +### Options + +``` + --description string Description of the routing-table + -h, --help Help for "stackit routing-table update" + --labels stringToString Key=value labels (default []) + --name string Name of the routing-table + --network-area-id string Network-Area ID + --non-dynamic-routes If true, preventing dynamic routes from propagating to the routing-table. + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit routing-table](./stackit_routing-table.md) - Manage routing-tables and its according routes + diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index 2f83eaed1..b026da770 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -5,7 +5,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -41,5 +40,4 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(sqlserverflex.NewCmd(params)) cmd.AddCommand(alb.NewCmd(params)) cmd.AddCommand(kms.NewCmd(params)) - cmd.AddCommand(routingtable.NewCmd(params)) } diff --git a/internal/cmd/beta/routingtable/routingtable.go b/internal/cmd/beta/routingtable/routingtable.go deleted file mode 100644 index 7eafa684a..000000000 --- a/internal/cmd/beta/routingtable/routingtable.go +++ /dev/null @@ -1,40 +0,0 @@ -package routingtable - -import ( - "github.com/spf13/cobra" - rtDescribe "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/describe" - rtList "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/list" - route "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/args" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" -) - -func NewCmd(params *params.CmdParams) *cobra.Command { - cmd := &cobra.Command{ - Use: "routing-table", - Short: "Manage routing-tables and its according routes", - Long: `Manage routing tables and their associated routes. - -This functionality is currently in BETA. At this stage, only listing and describing -routing-tables, as well as full CRUD operations for routes, are supported. -This feature is primarily intended for debugging routes created through Terraform. - -Once the feature reaches General Availability, we plan to introduce support -for creating routing tables and attaching them to networks directly via the -CLI. Until then, we recommend users continue managing routing tables and -attachments through the Terraform provider.`, - Args: args.NoArgs, - Run: utils.CmdHelp, - } - addSubcommands(cmd, params) - return cmd -} - -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { - cmd.AddCommand( - rtList.NewCmd(params), - rtDescribe.NewCmd(params), - route.NewCmd(params), - ) -} diff --git a/internal/cmd/network/create/create.go b/internal/cmd/network/create/create.go index 9877e4477..a44b9c822 100644 --- a/internal/cmd/network/create/create.go +++ b/internal/cmd/network/create/create.go @@ -34,6 +34,7 @@ const ( nonRoutedFlag = "non-routed" noIpv4GatewayFlag = "no-ipv4-gateway" noIpv6GatewayFlag = "no-ipv6-gateway" + routingTableIdFlag = "routing-table-id" labelFlag = "labels" ) @@ -51,6 +52,7 @@ type inputModel struct { NonRouted bool NoIPv4Gateway bool NoIPv6Gateway bool + RoutingTableID *string Labels *map[string]string } @@ -85,6 +87,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create an IPv6 network with name "network-1" with DNS name servers, a prefix and a gateway`, `$ stackit network create --name network-1 --ipv6-dns-name-servers "2001:4860:4860::8888,2001:4860:4860::8844" --ipv6-prefix "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888"`, ), + examples.NewExample( + `Create a network with name "network-1" and attach routing-table "xxx"`, + `$ stackit network create --name network-1 --routing-table-id xxx`, + ), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -158,6 +164,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(nonRoutedFlag, false, "If set to true, the network is not routed and therefore not accessible from other networks") cmd.Flags().Bool(noIpv4GatewayFlag, false, "If set to true, the network doesn't have an IPv4 gateway") cmd.Flags().Bool(noIpv6GatewayFlag, false, "If set to true, the network doesn't have an IPv6 gateway") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "The ID of the routing-table for the network") cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network. E.g. '--labels key1=value1,key2=value2,...'") // IPv4 checks @@ -196,6 +203,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, NonRouted: flags.FlagToBoolValue(p, cmd, nonRoutedFlag), NoIPv4Gateway: flags.FlagToBoolValue(p, cmd, noIpv4GatewayFlag), NoIPv6Gateway: flags.FlagToBoolValue(p, cmd, noIpv6GatewayFlag), + RoutingTableID: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), } @@ -294,11 +302,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } payload := iaas.CreateNetworkPayload{ - Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), - Routed: &routed, - Ipv4: ipv4Network, - Ipv6: ipv6Network, + Name: model.Name, + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Routed: &routed, + Ipv4: ipv4Network, + Ipv6: ipv6Network, + RoutingTableId: model.RoutingTableID, } return req.CreateNetworkPayload(payload) diff --git a/internal/cmd/network/create/create_test.go b/internal/cmd/network/create/create_test.go index a73f7d07a..0b82ad56a 100644 --- a/internal/cmd/network/create/create_test.go +++ b/internal/cmd/network/create/create_test.go @@ -39,9 +39,10 @@ var ( type testCtxKey struct{} var ( - testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} - testProjectId = uuid.NewString() + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() + testRoutingTableId = uuid.NewString() ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -49,9 +50,10 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, - nameFlag: testNetworkName, - nonRoutedFlag: strconv.FormatBool(testNonRouted), - labelFlag: "key=value", + nameFlag: testNetworkName, + nonRoutedFlag: strconv.FormatBool(testNonRouted), + labelFlag: "key=value", + routingTableIdFlag: testRoutingTableId, } for _, mod := range mods { mod(flagValues) @@ -101,6 +103,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Labels: utils.Ptr(map[string]string{ "key": "value", }), + RoutingTableID: utils.Ptr(testRoutingTableId), } for _, mod := range mods { mod(model) @@ -168,6 +171,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateNetworkPayload)) iaas.Creat Labels: utils.Ptr(map[string]interface{}{ "key": "value", }), + RoutingTableId: utils.Ptr(testRoutingTableId), } for _, mod := range mods { mod(&payload) @@ -468,6 +472,14 @@ func TestParseInput(t *testing.T) { }), isValid: true, }, + { + description: "routing-table id invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[routingTableIdFlag] = "invalid-uuid" + }), + expectedModel: nil, + isValid: false, + }, } for _, tt := range tests { @@ -530,6 +542,23 @@ func TestBuildRequest(t *testing.T) { Routed: utils.Ptr(false), }), }, + { + description: "network with routing-table id attached", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Name: utils.Ptr(testNetworkName), + RoutingTableID: utils.Ptr(testRoutingTableId), + }, + expectedRequest: testClient.CreateNetwork(testCtx, testProjectId, testRegion).CreateNetworkPayload(iaas.CreateNetworkPayload{ + Name: utils.Ptr(testNetworkName), + RoutingTableId: utils.Ptr(testRoutingTableId), + Routed: utils.Ptr(true), + }), + }, { description: "use ipv4 dns servers and prefix length", model: &inputModel{ diff --git a/internal/cmd/network/describe/describe.go b/internal/cmd/network/describe/describe.go index c7f0d08bc..9f85dead0 100644 --- a/internal/cmd/network/describe/describe.go +++ b/internal/cmd/network/describe/describe.go @@ -150,6 +150,11 @@ func outputResult(p *print.Printer, outputFormat string, network *iaas.Network) table.AddRow("ROUTED", routed) table.AddSeparator() + if network.RoutingTableId != nil { + table.AddRow("ROUTING-TABLE ID", utils.PtrString(network.RoutingTableId)) + table.AddSeparator() + } + if ipv4Gateway != nil { table.AddRow("IPv4 GATEWAY", *ipv4Gateway) table.AddSeparator() diff --git a/internal/cmd/network/list/list.go b/internal/cmd/network/list/list.go index e92ab31cc..01d5bd32f 100644 --- a/internal/cmd/network/list/list.go +++ b/internal/cmd/network/list/list.go @@ -140,7 +140,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli func outputResult(p *print.Printer, outputFormat string, networks []iaas.Network) error { return p.OutputResult(outputFormat, networks, func() error { table := tables.NewTable() - table.SetHeader("ID", "NAME", "STATUS", "PUBLIC IP", "PREFIXES", "ROUTED") + table.SetHeader("ID", "NAME", "STATUS", "PUBLIC IP", "PREFIXES", "ROUTED", "ROUTING TABLE ID") for _, network := range networks { var publicIp, prefixes string @@ -161,6 +161,7 @@ func outputResult(p *print.Printer, outputFormat string, networks []iaas.Network publicIp, prefixes, routed, + utils.PtrString(network.RoutingTableId), ) table.AddSeparator() } diff --git a/internal/cmd/network/update/update.go b/internal/cmd/network/update/update.go index b1891fd34..919421752 100644 --- a/internal/cmd/network/update/update.go +++ b/internal/cmd/network/update/update.go @@ -31,6 +31,7 @@ const ( ipv6GatewayFlag = "ipv6-gateway" noIpv4GatewayFlag = "no-ipv4-gateway" noIpv6GatewayFlag = "no-ipv6-gateway" + routingTableIdFlag = "routing-table-id" labelFlag = "labels" ) @@ -44,6 +45,7 @@ type inputModel struct { IPv6Gateway *string NoIPv4Gateway bool NoIPv6Gateway bool + RoutingTableId *string Labels *map[string]string } @@ -70,6 +72,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Update IPv6 network with ID "xxx" with new name "network-1-new", new gateway and new DNS name servers`, `$ stackit network update xxx --name network-1-new --ipv6-dns-name-servers "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888"`, ), + examples.NewExample( + `Update network with ID "xxx" with new routing-table id "xxx"`, + `$ stackit network update xxx --routing-table-id xxx`, + ), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -139,6 +145,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(ipv6GatewayFlag, "", "The IPv6 gateway of a network. If not specified, the first IP of the network will be assigned as the gateway") cmd.Flags().Bool(noIpv4GatewayFlag, false, "If set to true, the network doesn't have an IPv4 gateway") cmd.Flags().Bool(noIpv6GatewayFlag, false, "If set to true, the network doesn't have an IPv6 gateway") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "The ID of the routing-table for the network") cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network. E.g. '--labels key1=value1,key2=value2,...'") } @@ -160,6 +167,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu IPv6Gateway: flags.FlagToStringPointer(p, cmd, ipv6GatewayFlag), NoIPv4Gateway: flags.FlagToBoolValue(p, cmd, noIpv4GatewayFlag), NoIPv6Gateway: flags.FlagToBoolValue(p, cmd, noIpv6GatewayFlag), + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), } @@ -197,10 +205,11 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } payload := iaas.PartialUpdateNetworkPayload{ - Name: model.Name, - Ipv4: payloadIPv4, - Ipv6: payloadIPv6, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Name: model.Name, + Ipv4: payloadIPv4, + Ipv6: payloadIPv6, + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + RoutingTableId: model.RoutingTableId, } return req.PartialUpdateNetworkPayload(payload) diff --git a/internal/cmd/network/update/update_test.go b/internal/cmd/network/update/update_test.go index 236fbcd8b..88c34e4a8 100644 --- a/internal/cmd/network/update/update_test.go +++ b/internal/cmd/network/update/update_test.go @@ -26,6 +26,7 @@ var testClient = &iaas.APIClient{} var testProjectId = uuid.NewString() var testNetworkId = uuid.NewString() +var testRoutingTableId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -48,6 +49,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st ipv6DnsNameServersFlag: "2001:4860:4860::8888,2001:4860:4860::8844", ipv6GatewayFlag: "2001:4860:4860::8888", labelFlag: "key=value", + routingTableIdFlag: testRoutingTableId, } for _, mod := range mods { mod(flagValues) @@ -71,6 +73,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Labels: utils.Ptr(map[string]string{ "key": "value", }), + RoutingTableId: utils.Ptr(testRoutingTableId), } for _, mod := range mods { mod(model) @@ -101,6 +104,7 @@ func fixturePayload(mods ...func(payload *iaas.PartialUpdateNetworkPayload)) iaa Nameservers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}), Gateway: iaas.NewNullableString(utils.Ptr("2001:4860:4860::8888")), }, + RoutingTableId: utils.Ptr(testRoutingTableId), } for _, mod := range mods { mod(&payload) @@ -240,6 +244,15 @@ func TestParseInput(t *testing.T) { }), isValid: true, }, + { + description: "route-table id wrong format", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[routingTableIdFlag] = "wrong-format" + }), + expectedModel: nil, + isValid: false, + }, } for _, tt := range tests { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 4e0ac6ea9..d81e73dc6 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -33,6 +33,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/quota" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq" "github.com/stackitcloud/stackit-cli/internal/cmd/redis" + "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable" secretsmanager "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager" securitygroup "github.com/stackitcloud/stackit-cli/internal/cmd/security-group" "github.com/stackitcloud/stackit-cli/internal/cmd/server" @@ -160,38 +161,39 @@ func configureFlags(cmd *cobra.Command) error { } func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + cmd.AddCommand(affinityGroups.NewCmd(params)) cmd.AddCommand(auth.NewCmd(params)) - cmd.AddCommand(configCmd.NewCmd(params)) cmd.AddCommand(beta.NewCmd(params)) + cmd.AddCommand(configCmd.NewCmd(params)) cmd.AddCommand(curl.NewCmd(params)) cmd.AddCommand(dns.NewCmd(params)) + cmd.AddCommand(git.NewCmd(params)) + cmd.AddCommand(image.NewCmd(params)) + cmd.AddCommand(keypair.NewCmd(params)) cmd.AddCommand(loadbalancer.NewCmd(params)) cmd.AddCommand(logme.NewCmd(params)) cmd.AddCommand(mariadb.NewCmd(params)) cmd.AddCommand(mongodbflex.NewCmd(params)) - cmd.AddCommand(objectstorage.NewCmd(params)) + cmd.AddCommand(network.NewCmd(params)) + cmd.AddCommand(networkArea.NewCmd(params)) + cmd.AddCommand(networkinterface.NewCmd(params)) cmd.AddCommand(observability.NewCmd(params)) + cmd.AddCommand(objectstorage.NewCmd(params)) cmd.AddCommand(opensearch.NewCmd(params)) cmd.AddCommand(organization.NewCmd(params)) cmd.AddCommand(postgresflex.NewCmd(params)) cmd.AddCommand(project.NewCmd(params)) + cmd.AddCommand(publicip.NewCmd(params)) + cmd.AddCommand(quota.NewCmd(params)) cmd.AddCommand(rabbitmq.NewCmd(params)) cmd.AddCommand(redis.NewCmd(params)) + cmd.AddCommand(routingtable.NewCmd(params)) + cmd.AddCommand(securitygroup.NewCmd(params)) cmd.AddCommand(secretsmanager.NewCmd(params)) + cmd.AddCommand(server.NewCmd(params)) cmd.AddCommand(serviceaccount.NewCmd(params)) cmd.AddCommand(ske.NewCmd(params)) - cmd.AddCommand(server.NewCmd(params)) - cmd.AddCommand(networkArea.NewCmd(params)) - cmd.AddCommand(network.NewCmd(params)) cmd.AddCommand(volume.NewCmd(params)) - cmd.AddCommand(networkinterface.NewCmd(params)) - cmd.AddCommand(publicip.NewCmd(params)) - cmd.AddCommand(securitygroup.NewCmd(params)) - cmd.AddCommand(keypair.NewCmd(params)) - cmd.AddCommand(image.NewCmd(params)) - cmd.AddCommand(quota.NewCmd(params)) - cmd.AddCommand(affinityGroups.NewCmd(params)) - cmd.AddCommand(git.NewCmd(params)) } // traverseCommands calls f for c and all of its children. diff --git a/internal/cmd/routingtable/create/create.go b/internal/cmd/routingtable/create/create.go new file mode 100644 index 000000000..c8c196a5a --- /dev/null +++ b/internal/cmd/routingtable/create/create.go @@ -0,0 +1,208 @@ +package create + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + descriptionFlag = "description" + labelFlag = "labels" + nameFlag = "name" + networkAreaIdFlag = "network-area-id" + nonDynamicRoutesFlag = "non-dynamic-routes" + nonSystemRoutesFlag = "non-system-routes" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Description *string + Labels *map[string]string + Name *string + NetworkAreaId *string + NonSystemRoutes bool + NonDynamicRoutes bool + OrganizationId *string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a routing-table", + Long: "Creates a routing-table.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + "Create a routing-table with name `rt`", + `stackit routing-table create --organization-id xxx --network-area-id yyy --name "rt"`, + ), + examples.NewExample( + "Create a routing-table with name `rt` and description `some description`", + `stackit routing-table create --organization-id xxx --network-area-id yyy --name "rt" --description "some description"`, + ), + examples.NewExample( + "Create a routing-table with name `rt` with system_routes disabled", + `stackit routing-table create --organization-id xxx --network-area-id yyy --name "rt" --non-system-routes`, + ), + examples.NewExample( + "Create a routing-table with name `rt` with dynamic_routes disabled", + `stackit routing-table create --organization-id xxx --network-area-id yyy --name "rt" --non-dynamic-routes`, + ), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, nil) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := "Are you sure you want to create a routing-table?" + if err := params.Printer.PromptForConfirmation(prompt); err != nil { + return err + } + } + + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + + routingTableResp, err := req.Execute() + if err != nil { + return fmt.Errorf("create routing-table request failed: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, routingTableResp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(descriptionFlag, "", "Description of the routing-table") + cmd.Flags().StringToString(labelFlag, nil, "Key=value labels") + cmd.Flags().String(nameFlag, "", "Name of the routing-table") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Bool(nonDynamicRoutesFlag, false, "If true, preventing dynamic routes from propagating to the routing-table.") + cmd.Flags().Bool(nonSystemRoutesFlag, false, "If true, automatically disables routes for project-to-project communication.") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, nameFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := &inputModel{ + GlobalFlagModel: globalFlags, + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + NonDynamicRoutes: flags.FlagToBoolValue(p, cmd, nonDynamicRoutesFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + Name: flags.FlagToStringPointer(p, cmd, nameFlag), + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + NonSystemRoutes: flags.FlagToBoolValue(p, cmd, nonSystemRoutesFlag), + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) (iaas.ApiAddRoutingTableToAreaRequest, error) { + systemRoutes := true + if model.NonSystemRoutes { + systemRoutes = false + } + + dynamicRoutes := true + if model.NonDynamicRoutes { + dynamicRoutes = false + } + + payload := iaas.AddRoutingTableToAreaPayload{ + Description: model.Description, + Name: model.Name, + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + SystemRoutes: utils.Ptr(systemRoutes), + DynamicRoutes: utils.Ptr(dynamicRoutes), + } + + return apiClient.AddRoutingTableToArea( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + ).AddRoutingTableToAreaPayload(payload), nil +} + +func outputResult(p *print.Printer, outputFormat string, routingTable *iaas.RoutingTable) error { + if routingTable == nil { + return fmt.Errorf("create routing-table response is empty") + } + + if routingTable.Id == nil { + return fmt.Errorf("routing-table Id is empty") + } + + return p.OutputResult(outputFormat, routingTable, func() error { + var labels []string + if routingTable.Labels != nil && len(*routingTable.Labels) > 0 { + for key, value := range *routingTable.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + } + + createdAt := "" + if routingTable.CreatedAt != nil { + createdAt = routingTable.CreatedAt.Format(time.RFC3339) + } + + updatedAt := "" + if routingTable.UpdatedAt != nil { + updatedAt = routingTable.UpdatedAt.Format(time.RFC3339) + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "DESCRIPTION", "CREATED_AT", "UPDATED_AT", "DEFAULT", "LABELS", "SYSTEM_ROUTES", "DYNAMIC_ROUTES") + table.AddRow( + utils.PtrString(routingTable.Id), + utils.PtrString(routingTable.Name), + utils.PtrString(routingTable.Description), + createdAt, + updatedAt, + utils.PtrString(routingTable.Default), + strings.Join(labels, "\n"), + utils.PtrString(routingTable.SystemRoutes), + utils.PtrString(routingTable.DynamicRoutes), + ) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/routingtable/create/create_test.go b/internal/cmd/routingtable/create/create_test.go new file mode 100644 index 000000000..7d79c34fb --- /dev/null +++ b/internal/cmd/routingtable/create/create_test.go @@ -0,0 +1,350 @@ +package create + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + pprint "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{} + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() + +const testRoutingTableName = "test" +const testRoutingTableDescription = "test" +const systemRoutesDisabled = true +const dynamicRoutesDisabled = true +const testLabelSelectorFlag = "key1=value1,key2=value2" + +var testLabels = &map[string]string{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + descriptionFlag: testRoutingTableDescription, + nameFlag: testRoutingTableName, + nonSystemRoutesFlag: strconv.FormatBool(systemRoutesDisabled), + nonDynamicRoutesFlag: strconv.FormatBool(dynamicRoutesDisabled), + labelFlag: testLabelSelectorFlag, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + Name: utils.Ptr(testRoutingTableName), + Description: utils.Ptr(testRoutingTableDescription), + NonSystemRoutes: systemRoutesDisabled, + NonDynamicRoutes: dynamicRoutesDisabled, + Labels: utils.Ptr(*testLabels), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiAddRoutingTableToAreaRequest)) iaas.ApiAddRoutingTableToAreaRequest { + request := testClient.AddRoutingTableToArea(testCtx, testOrgId, testNetworkAreaId, testRegion) + request = request.AddRoutingTableToAreaPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.AddRoutingTableToAreaPayload)) iaas.AddRoutingTableToAreaPayload { + systemRoutes := true + if dynamicRoutesDisabled { + systemRoutes = false + } + + dynamicRoutes := true + if systemRoutesDisabled { + dynamicRoutes = false + } + + payload := iaas.AddRoutingTableToAreaPayload{ + Description: utils.Ptr(testRoutingTableDescription), + Name: utils.Ptr(testRoutingTableName), + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + SystemRoutes: utils.Ptr(systemRoutes), + DynamicRoutes: utils.Ptr(dynamicRoutes), + } + + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "dynamic_routes disabled", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nonDynamicRoutesFlag] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NonDynamicRoutes = true + }), + }, + { + description: "system_routes disabled", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nonSystemRoutesFlag] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NonSystemRoutes = true + }), + }, + { + description: "missing organization ID", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "invalid organization ID - empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid organization ID - format", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing network area ID", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "invalid network area ID - empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid network area ID - format", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing name", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + { + description: "missing labels", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Labels = nil + }), + }, + { + description: "missing description", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + }), + }, + { + description: "no flags provided", + flagValues: map[string]string{}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiAddRoutingTableToAreaRequest + }{ + { + description: "valid input", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "labels missing", + model: fixtureInputModel(func(model *inputModel) { + model.Labels = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutingTableToAreaRequest) { + *request = (*request).AddRoutingTableToAreaPayload(fixturePayload(func(payload *iaas.AddRoutingTableToAreaPayload) { + payload.Labels = nil + })) + }), + }, + { + description: "system routes disabled", + model: fixtureInputModel(func(model *inputModel) { + model.NonSystemRoutes = true + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutingTableToAreaRequest) { + *request = (*request).AddRoutingTableToAreaPayload(fixturePayload(func(payload *iaas.AddRoutingTableToAreaPayload) { + payload.SystemRoutes = utils.Ptr(false) + })) + }), + }, + { + description: "dynamic routes disabled", + model: fixtureInputModel(func(model *inputModel) { + model.NonDynamicRoutes = true + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutingTableToAreaRequest) { + *request = (*request).AddRoutingTableToAreaPayload(fixturePayload(func(payload *iaas.AddRoutingTableToAreaPayload) { + payload.DynamicRoutes = utils.Ptr(false) + })) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("buildRequest returned error: %v", err) + } + + if diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx)); diff != "" { + t.Errorf("buildRequest() mismatch (-got +want):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoutingTable := iaas.RoutingTable{ + Id: utils.Ptr("id-foo"), + Name: utils.Ptr("route-table-foo"), + Description: utils.Ptr("description-foo"), + SystemRoutes: utils.Ptr(true), + DynamicRoutes: utils.Ptr(true), + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routingTable *iaas.RoutingTable + wantErr bool + }{ + { + name: "nil routing-table should return error", + outputFormat: "", + routingTable: nil, + wantErr: true, + }, + { + name: "empty routing-table", + outputFormat: "", + routingTable: &iaas.RoutingTable{}, + wantErr: true, + }, + { + name: "table output routing-table", + outputFormat: "", + routingTable: &dummyRoutingTable, + wantErr: false, + }, + { + name: "json output routing-table", + outputFormat: pprint.JSONOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + { + name: "yaml output routing-table", + outputFormat: pprint.YAMLOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + } + + p := pprint.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.outputFormat, tt.routingTable); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/routingtable/delete/delete.go b/internal/cmd/routingtable/delete/delete.go new file mode 100644 index 000000000..9141f8f6f --- /dev/null +++ b/internal/cmd/routingtable/delete/delete.go @@ -0,0 +1,111 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + routingTableIdArg = "ROUTING_TABLE_ARG" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + NetworkAreaId *string + OrganizationId *string + RoutingTableId *string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", routingTableIdArg), + Short: "Deletes a routing-table", + Long: "Deletes a routing-table", + Args: args.SingleArg(routingTableIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Deletes a a routing-table`, + `$ stackit routing-table delete xxxx-xxxx-xxxx-xxxx --organization-id yyy --network-area-id zzz`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete the routing-table %q for network-area-id %q?", *model.RoutingTableId, *model.OrganizationId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := apiClient.DeleteRoutingTableFromArea( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + *model.RoutingTableId, + ) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete routing-table: %w", err) + } + + params.Printer.Outputf("Routing-table %q deleted.", *model.RoutingTableId) + return nil + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if len(inputArgs) == 0 { + return nil, fmt.Errorf("at least one argument is required") + } + routingTableId := inputArgs[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + RoutingTableId: &routingTableId, + } + + p.DebugInputModel(model) + return &model, nil +} diff --git a/internal/cmd/routingtable/delete/delete_test.go b/internal/cmd/routingtable/delete/delete_test.go new file mode 100644 index 000000000..4fad17554 --- /dev/null +++ b/internal/cmd/routingtable/delete/delete_test.go @@ -0,0 +1,145 @@ +package delete + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const testRegion = "eu01" + +var ( + testOrgId = uuid.NewString() + testNetworkAreaId = uuid.NewString() + testRoutingTableId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + RoutingTableId: utils.Ptr(testRoutingTableId), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(func(m *inputModel) { + m.RoutingTableId = &testRoutingTableId + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "missing organization ID", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "invalid organization ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid organization ID - format", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing network area ID", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "invalid network area ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid network area ID - format", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing routing-table ID", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid routing-table ID - format", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid routing-table ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[routingTableIdArg] = "" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} diff --git a/internal/cmd/beta/routingtable/describe/describe.go b/internal/cmd/routingtable/describe/describe.go similarity index 70% rename from internal/cmd/beta/routingtable/describe/describe.go rename to internal/cmd/routingtable/describe/describe.go index 66eb9a6e0..573e0e100 100644 --- a/internal/cmd/beta/routingtable/describe/describe.go +++ b/internal/cmd/routingtable/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" + "time" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -17,32 +16,32 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( - organizationIdFlag = "organization-id" networkAreaIdFlag = "network-area-id" - routingTableArg = "ROUTING_TABLE_ID_ARG" + organizationIdFlag = "organization-id" + routingTableIdArg = "ROUTING_TABLE_ID_ARG" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string NetworkAreaId *string + OrganizationId *string RoutingTableId *string } func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: fmt.Sprintf("describe %s", routingTableArg), - Short: "Describe a routing-table", - Long: "Describe a routing-table", - Args: args.SingleArg(routingTableArg, nil), + Use: fmt.Sprintf("describe %s", routingTableIdArg), + Short: "Describes a routing-table", + Long: "Describes a routing-table", + Args: args.SingleArg(routingTableIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Describe a routing-table`, - `$ stackit beta routing-table describe xxxx-xxxx-xxxx-xxxx --organization-id xxx --network-area-id yyy`, + `$ stackit routing-table describe xxxx-xxxx-xxxx-xxxx --organization-id xxx --network-area-id yyy`, ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -53,7 +52,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Configure API client - apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -107,48 +106,47 @@ func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputMode return &model, nil } -func outputResult(p *print.Printer, outputFormat string, routingTable *iaasalpha.RoutingTable) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(routingTable, "", " ") - if err != nil { - return fmt.Errorf("marshal routing-table describe: %w", err) +func outputResult(p *print.Printer, outputFormat string, routingTable *iaas.RoutingTable) error { + if routingTable == nil { + return fmt.Errorf("describe routingtable response is empty") + } + + return p.OutputResult(outputFormat, routingTable, func() error { + var labels []string + if routingTable.Labels != nil && len(*routingTable.Labels) > 0 { + for key, value := range *routingTable.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(routingTable, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal routing-table describe: %w", err) + createdAt := "" + if routingTable.CreatedAt != nil { + createdAt = routingTable.CreatedAt.Format(time.RFC3339) } - p.Outputln(string(details)) - return nil - default: - var labels []string - for key, value := range *routingTable.Labels { - labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + updatedAt := "" + if routingTable.UpdatedAt != nil { + updatedAt = routingTable.UpdatedAt.Format(time.RFC3339) } table := tables.NewTable() - table.SetHeader("ID", "NAME", "DESCRIPTION", "CREATED_AT", "UPDATED_AT", "DEFAULT", "LABELS", "SYSTEM_ROUTES") + table.SetHeader("ID", "NAME", "DESCRIPTION", "CREATED_AT", "UPDATED_AT", "DEFAULT", "LABELS", "SYSTEM_ROUTES", "DYNAMIC_ROUTES") table.AddRow( utils.PtrString(routingTable.Id), utils.PtrString(routingTable.Name), utils.PtrString(routingTable.Description), - routingTable.CreatedAt.String(), - routingTable.UpdatedAt.String(), + createdAt, + updatedAt, utils.PtrString(routingTable.Default), strings.Join(labels, "\n"), utils.PtrString(routingTable.SystemRoutes), + utils.PtrString(routingTable.DynamicRoutes), ) err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } - return nil - } + }) } diff --git a/internal/cmd/beta/routingtable/describe/describe_test.go b/internal/cmd/routingtable/describe/describe_test.go similarity index 61% rename from internal/cmd/beta/routingtable/describe/describe_test.go rename to internal/cmd/routingtable/describe/describe_test.go index 5a57d9a32..c598bc333 100644 --- a/internal/cmd/beta/routingtable/describe/describe_test.go +++ b/internal/cmd/routingtable/describe/describe_test.go @@ -4,16 +4,17 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var testRegion = "eu01" +const testRegion = "eu01" + var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() var testRoutingTableId = uuid.NewString() @@ -70,7 +71,7 @@ func TestParseInput(t *testing.T) { expectedModel *inputModel }{ { - description: "base", + description: "valid input", flagValues: fixtureFlagValues(), argValues: fixtureArgValues(), isValid: true, @@ -83,18 +84,70 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "network-area-id missing", - argValues: fixtureArgValues(), + description: "missing organization ID", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "invalid organization ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid organization ID - format", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing network area ID", + argValues: []string{testRoutingTableId}, flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, networkAreaIdFlag) }), isValid: false, }, { - description: "org-id missing", - argValues: fixtureArgValues(), + description: "invalid network area ID - empty", + argValues: []string{testRoutingTableId}, flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, organizationIdFlag) + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid network area ID - format", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing routing-table ID", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid routing-table ID - format", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid routing-table ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[routingTableIdArg] = "" }), isValid: false, }, @@ -102,52 +155,13 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } func TestOutputResult(t *testing.T) { - dummyRouteTable := iaasalpha.RoutingTable{ + dummyRouteTable := iaas.RoutingTable{ CreatedAt: utils.Ptr(time.Now()), Default: nil, Description: utils.Ptr("description"), @@ -161,7 +175,7 @@ func TestOutputResult(t *testing.T) { tests := []struct { name string outputFormat string - routingTable iaasalpha.RoutingTable + routingTable iaas.RoutingTable wantErr bool }{ { diff --git a/internal/cmd/beta/routingtable/list/list.go b/internal/cmd/routingtable/list/list.go similarity index 77% rename from internal/cmd/beta/routingtable/list/list.go rename to internal/cmd/routingtable/list/list.go index d290e705f..f5f857131 100644 --- a/internal/cmd/beta/routingtable/list/list.go +++ b/internal/cmd/routingtable/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" "strings" + "time" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -20,53 +19,53 @@ import ( rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( - organizationIdFlag = "organization-id" - networkAreaIdFlag = "network-area-id" labelSelectorFlag = "label-selector" limitFlag = "limit" + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string - NetworkAreaId *string LabelSelector *string Limit *int64 + NetworkAreaId *string + OrganizationId *string } func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", - Short: "List all routing-tables", - Long: "List all routing-tables", + Short: "Lists all routing-tables", + Long: "Lists all routing-tables", Args: args.NoArgs, Example: examples.Build( examples.NewExample( `List all routing-tables`, - `$ stackit beta routing-table list --organization-id xxx --network-area-id yyy`, + `$ stackit routing-table list --organization-id xxx --network-area-id yyy`, ), examples.NewExample( `List all routing-tables with labels`, - `$ stackit beta routing-table list --label-selector env=dev,env=rc --organization-id xxx --network-area-id yyy`, + `$ stackit routing-table list --label-selector env=dev,env=rc --organization-id xxx --network-area-id yyy`, ), examples.NewExample( `List all routing-tables with labels and set limit to 10`, - `$ stackit beta routing-table list --label-selector env=dev,env=rc --limit 10 --organization-id xxx --network-area-id yyy`, + `$ stackit routing-table list --label-selector env=dev,env=rc --limit 10 --organization-id xxx --network-area-id yyy`, ), ), RunE: func(cmd *cobra.Command, _ []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, nil) if err != nil { return err } // Configure API client - apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -113,15 +112,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") - cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") - cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") cmd.Flags().String(labelSelectorFlag, "", "Filter by label") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) @@ -134,17 +133,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, + LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), Limit: limit, NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaasalpha.APIClient) iaasalpha.ApiListRoutingTablesOfAreaRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListRoutingTablesOfAreaRequest { request := apiClient.ListRoutingTablesOfArea(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region) if model.LabelSelector != nil { request.LabelSelector(*model.LabelSelector) @@ -152,27 +151,14 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaasalpha.A return request } -func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.RoutingTable) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal routing-table list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal routing-table list: %w", err) - } - p.Outputln(string(details)) +func outputResult(p *print.Printer, outputFormat string, items []iaas.RoutingTable) error { + if len(items) == 0 { + return fmt.Errorf("list routingtable response is empty") + } - return nil - default: + return p.OutputResult(outputFormat, items, func() error { table := tables.NewTable() - table.SetHeader("ID", "NAME", "DESCRIPTION", "CREATED_AT", "UPDATED_AT", "DEFAULT", "LABELS", "SYSTEM_ROUTES") + table.SetHeader("ID", "NAME", "DESCRIPTION", "CREATED_AT", "UPDATED_AT", "DEFAULT", "LABELS", "SYSTEM_ROUTES", "DYNAMIC_ROUTES") for _, item := range items { var labels []string @@ -180,15 +166,26 @@ func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.Routi labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } + createdAt := "" + if item.CreatedAt != nil { + createdAt = item.CreatedAt.Format(time.RFC3339) + } + + updatedAt := "" + if item.UpdatedAt != nil { + updatedAt = item.UpdatedAt.Format(time.RFC3339) + } + table.AddRow( utils.PtrString(item.Id), utils.PtrString(item.Name), utils.PtrString(item.Description), - item.CreatedAt.String(), - item.UpdatedAt.String(), + createdAt, + updatedAt, utils.PtrString(item.Default), strings.Join(labels, "\n"), utils.PtrString(item.SystemRoutes), + utils.PtrString(item.DynamicRoutes), ) } err := table.Display(p) @@ -197,5 +194,5 @@ func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.Routi } return nil - } + }) } diff --git a/internal/cmd/beta/routingtable/list/list_test.go b/internal/cmd/routingtable/list/list_test.go similarity index 73% rename from internal/cmd/beta/routingtable/list/list_test.go rename to internal/cmd/routingtable/list/list_test.go index 02976bd2e..b3a4f3134 100644 --- a/internal/cmd/beta/routingtable/list/list_test.go +++ b/internal/cmd/routingtable/list/list_test.go @@ -5,20 +5,22 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var testRegion = "eu01" +const testRegion = "eu01" + var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() -var testLabelSelectorFlag = "key1=value1,key2=value2" +const testLabelSelectorFlag = "key1=value1,key2=value2" + var testLabels = &map[string]string{ "key1": "value1", "key2": "value2", @@ -60,12 +62,13 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel }{ { - description: "base", + description: "valid input", flagValues: fixtureFlagValues(), isValid: true, expectedModel: fixtureInputModel(), @@ -76,21 +79,21 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "network-area-id missing", + description: "missing network area ID", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, networkAreaIdFlag) }), isValid: false, }, { - description: "org-id missing", + description: "missing organization ID", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, organizationIdFlag) }), isValid: false, }, { - description: "labels missing", + description: "missing labels", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, labelSelectorFlag) }), @@ -100,7 +103,7 @@ func TestParseInput(t *testing.T) { }), }, { - description: "limit missing", + description: "missing limit", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, limitFlag) }), @@ -127,52 +130,13 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } func TestOutputResult(t *testing.T) { - dummyRouteTable := iaasalpha.RoutingTable{ + dummyRouteTable := iaas.RoutingTable{ CreatedAt: utils.Ptr(time.Now()), Default: nil, Description: utils.Ptr("description"), @@ -186,19 +150,19 @@ func TestOutputResult(t *testing.T) { tests := []struct { name string outputFormat string - routingTable []iaasalpha.RoutingTable + routingTable []iaas.RoutingTable wantErr bool }{ { name: "json output with one route", outputFormat: print.JSONOutputFormat, - routingTable: []iaasalpha.RoutingTable{dummyRouteTable}, + routingTable: []iaas.RoutingTable{dummyRouteTable}, wantErr: false, }, { name: "yaml output with one route", outputFormat: print.YAMLOutputFormat, - routingTable: []iaasalpha.RoutingTable{dummyRouteTable}, + routingTable: []iaas.RoutingTable{dummyRouteTable}, wantErr: false, }, } diff --git a/internal/cmd/beta/routingtable/route/create/create.go b/internal/cmd/routingtable/route/create/create.go similarity index 79% rename from internal/cmd/beta/routingtable/route/create/create.go rename to internal/cmd/routingtable/route/create/create.go index f67a49fc5..86566d831 100644 --- a/internal/cmd/beta/routingtable/route/create/create.go +++ b/internal/cmd/routingtable/route/create/create.go @@ -2,12 +2,11 @@ package create import ( "context" - "encoding/json" "errors" "fmt" "strings" + "time" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -19,30 +18,30 @@ import ( routeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/routing-table/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( - organizationIdFlag = "organization-id" - networkAreaIdFlag = "network-area-id" - routingTableIdFlag = "routing-table-id" destinationTypeFlag = "destination-type" destinationValueFlag = "destination-value" + labelFlag = "labels" + networkAreaIdFlag = "network-area-id" nextHopTypeFlag = "nexthop-type" nextHopValueFlag = "nexthop-value" - labelFlag = "labels" + organizationIdFlag = "organization-id" + routingTableIdFlag = "routing-table-id" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string - NetworkAreaId *string - RoutingTableId *string DestinationType *string DestinationValue *string + Labels *map[string]string + NetworkAreaId *string NextHopType *string NextHopValue *string - Labels *map[string]string + OrganizationId *string + RoutingTableId *string } func NewCmd(params *params.CmdParams) *cobra.Command { @@ -53,31 +52,31 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample("Create a route with CIDRv4 destination and IPv4 nexthop", - `stackit beta routing-tables route create \ + `stackit routing-table route create \ --routing-table-id xxx --organization-id yyy --network-area-id zzz \ --destination-type cidrv4 --destination-value \ --nexthop-type ipv4 --nexthop-value `), examples.NewExample("Create a route with CIDRv6 destination and IPv6 nexthop", - `stackit beta routing-tables route create \ + `stackit routing-table route create \ --routing-table-id xxx --organization-id yyy --network-area-id zzz \ --destination-type cidrv6 --destination-value \ --nexthop-type ipv6 --nexthop-value `), examples.NewExample("Create a route with CIDRv6 destination and Nexthop Internet", - `stackit beta routing-tables route create \ + `stackit routing-table route create \ --routing-table-id xxx --organization-id yyy --network-area-id zzz \ --destination-type cidrv6 --destination-value \ --nexthop-type internet`), ), RunE: func(cmd *cobra.Command, _ []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, nil) if err != nil { return err } - apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -107,11 +106,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") - cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") - cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") cmd.Flags().Var(flags.CIDRFlag(), destinationValueFlag, "Destination value") cmd.Flags().String(nextHopValueFlag, "", "NextHop value") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") cmd.Flags().Var( flags.EnumFlag(true, "", "cidrv4", "cidrv6"), @@ -129,19 +128,19 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) model := &inputModel{ GlobalFlagModel: globalFlags, - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), - RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), DestinationType: flags.FlagToStringPointer(p, cmd, destinationTypeFlag), DestinationValue: flags.FlagToStringPointer(p, cmd, destinationValueFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), NextHopType: flags.FlagToStringPointer(p, cmd, nextHopTypeFlag), NextHopValue: flags.FlagToStringPointer(p, cmd, nextHopValueFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), } // Next Hop validation logic @@ -162,13 +161,13 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { return model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaasalpha.APIClient) (iaasalpha.ApiAddRoutesToRoutingTableRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) (iaas.ApiAddRoutesToRoutingTableRequest, error) { destination := buildDestination(model) nextHop := buildNextHop(model) if destination != nil && nextHop != nil { - payload := iaasalpha.AddRoutesToRoutingTablePayload{ - Items: &[]iaasalpha.Route{ + payload := iaas.AddRoutesToRoutingTablePayload{ + Items: &[]iaas.Route{ { Destination: destination, Nexthop: nextHop, @@ -189,7 +188,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaasalpha.A return nil, fmt.Errorf("invalid input") } -func buildDestination(model *inputModel) *iaasalpha.RouteDestination { +func buildDestination(model *inputModel) *iaas.RouteDestination { if model.DestinationValue == nil { return nil } @@ -197,15 +196,15 @@ func buildDestination(model *inputModel) *iaasalpha.RouteDestination { destinationType := strings.ToLower(*model.DestinationType) switch destinationType { case "cidrv4": - return &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + return &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: model.DestinationType, Value: model.DestinationValue, }, } case "cidrv6": - return &iaasalpha.RouteDestination{ - DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + return &iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ Type: model.DestinationType, Value: model.DestinationValue, }, @@ -215,32 +214,32 @@ func buildDestination(model *inputModel) *iaasalpha.RouteDestination { } } -func buildNextHop(model *inputModel) *iaasalpha.RouteNexthop { +func buildNextHop(model *inputModel) *iaas.RouteNexthop { nextHopType := strings.ToLower(*model.NextHopType) switch nextHopType { case "ipv4": - return &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + return &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: model.NextHopType, Value: model.NextHopValue, }, } case "ipv6": - return &iaasalpha.RouteNexthop{ - NexthopIPv6: &iaasalpha.NexthopIPv6{ + return &iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ Type: model.NextHopType, Value: model.NextHopValue, }, } case "internet": - return &iaasalpha.RouteNexthop{ - NexthopInternet: &iaasalpha.NexthopInternet{ + return &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ Type: model.NextHopType, }, } case "blackhole": - return &iaasalpha.RouteNexthop{ - NexthopBlackhole: &iaasalpha.NexthopBlackhole{ + return &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ Type: model.NextHopType, }, } @@ -249,30 +248,27 @@ func buildNextHop(model *inputModel) *iaasalpha.RouteNexthop { } } -func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.Route) error { +func outputResult(p *print.Printer, outputFormat string, items []iaas.Route) error { if len(items) == 0 { return fmt.Errorf("create routes response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - data, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal routes: %w", err) - } - p.Outputln(string(data)) - case print.YAMLOutputFormat: - data, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal routes: %w", err) - } - p.Outputln(string(data)) - default: + return p.OutputResult(outputFormat, items, func() error { table := tables.NewTable() table.SetHeader("ID", "DEST. TYPE", "DEST. VALUE", "NEXTHOP TYPE", "NEXTHOP VALUE", "LABELS", "CREATED", "UPDATED") for _, item := range items { destType, destValue, hopType, hopValue, labels := routeUtils.ExtractRouteDetails(item) + createdAt := "" + if item.CreatedAt != nil { + createdAt = item.CreatedAt.Format(time.RFC3339) + } + + updatedAt := "" + if item.UpdatedAt != nil { + updatedAt = item.UpdatedAt.Format(time.RFC3339) + } + table.AddRow( utils.PtrString(item.Id), destType, @@ -280,11 +276,14 @@ func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.Route hopType, hopValue, labels, - item.CreatedAt.String(), - item.UpdatedAt.String(), + createdAt, + updatedAt, ) } - return table.Display(p) - } - return nil + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) } diff --git a/internal/cmd/beta/routingtable/route/create/create_test.go b/internal/cmd/routingtable/route/create/create_test.go similarity index 70% rename from internal/cmd/beta/routingtable/route/create/create_test.go rename to internal/cmd/routingtable/route/create/create_test.go index 89af7afdc..64b5eaced 100644 --- a/internal/cmd/beta/routingtable/route/create/create_test.go +++ b/internal/cmd/routingtable/route/create/create_test.go @@ -11,25 +11,28 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaasalpha.APIClient{} +var testClient = &iaas.APIClient{} + +const testRegion = "eu01" -var testRegion = "eu01" var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() var testRoutingTableId = uuid.NewString() -var testDestinationTypeFlag = "cidrv4" -var testDestinationValueFlag = "1.1.1.0/24" -var testNextHopTypeFlag = "ipv4" -var testNextHopValueFlag = "1.1.1.1" -var testLabelSelectorFlag = "key1=value1,key2=value2" +const testDestinationTypeFlag = "cidrv4" +const testDestinationValueFlag = "1.1.1.0/24" +const testNextHopTypeFlag = "ipv4" +const testNextHopValueFlag = "1.1.1.1" +const testLabelSelectorFlag = "key1=value1,key2=value2" + var testLabels = &map[string]string{ "key1": "value1", "key2": "value2", @@ -74,7 +77,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureRequest(mods ...func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest)) iaasalpha.ApiAddRoutesToRoutingTableRequest { +func fixtureRequest(mods ...func(request *iaas.ApiAddRoutesToRoutingTableRequest)) iaas.ApiAddRoutesToRoutingTableRequest { request := testClient.AddRoutesToRoutingTable(testCtx, testOrgId, testNetworkAreaId, testRegion, testRoutingTableId) request = request.AddRoutesToRoutingTablePayload(fixturePayload()) for _, mod := range mods { @@ -83,18 +86,18 @@ func fixtureRequest(mods ...func(request *iaasalpha.ApiAddRoutesToRoutingTableRe return request } -func fixturePayload(mods ...func(payload *iaasalpha.AddRoutesToRoutingTablePayload)) iaasalpha.AddRoutesToRoutingTablePayload { - payload := iaasalpha.AddRoutesToRoutingTablePayload{ - Items: &[]iaasalpha.Route{ +func fixturePayload(mods ...func(payload *iaas.AddRoutesToRoutingTablePayload)) iaas.AddRoutesToRoutingTablePayload { + payload := iaas.AddRoutesToRoutingTablePayload{ + Items: &[]iaas.Route{ { - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: utils.Ptr(testDestinationTypeFlag), Value: utils.Ptr(testDestinationValueFlag), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: utils.Ptr(testNextHopTypeFlag), Value: utils.Ptr(testNextHopValueFlag), }, @@ -112,46 +115,47 @@ func fixturePayload(mods ...func(payload *iaasalpha.AddRoutesToRoutingTablePaylo func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel }{ { - description: "base", + description: "valid input", flagValues: fixtureFlagValues(), isValid: true, expectedModel: fixtureInputModel(), }, { - description: "routing-table-id missing", + description: "routing-table ID missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, routingTableIdFlag) }), isValid: false, }, { - description: "destination-value missing", + description: "destination value missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, destinationValueFlag) }), isValid: false, }, { - description: "destination-type missing", + description: "destination type missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, destinationTypeFlag) }), isValid: false, }, { - description: "nexthop-type missing", + description: "next hop type missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, nextHopTypeFlag) }), isValid: false, }, { - description: "nexthop-value missing", + description: "next hop value missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, nextHopValueFlag) }), @@ -163,42 +167,42 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "org id missing", + description: "organization ID missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, organizationIdFlag) }), isValid: false, }, { - description: "org id invalid 1", + description: "organization ID invalid - empty", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[organizationIdFlag] = "" }), isValid: false, }, { - description: "org area id invalid 2", + description: "organization ID invalid - format", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[organizationIdFlag] = "invalid-uuid" }), isValid: false, }, { - description: "network area id missing", + description: "network area ID missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, networkAreaIdFlag) }), isValid: false, }, { - description: "network area id invalid 1", + description: "network area ID invalid - empty", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[networkAreaIdFlag] = "" }), isValid: false, }, { - description: "network area id invalid 2", + description: "network area ID invalid - format", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[networkAreaIdFlag] = "invalid-uuid" }), @@ -212,14 +216,14 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "destination value not ipv4 cidr", + description: "destination value not IPv4 CIDR", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[destinationValueFlag] = "0.0.0.0" }), isValid: false, }, { - description: "destination value not ipv6 cidr", + description: "destination value not IPv6 CIDR", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[destinationTypeFlag] = "cidrv6" flagValues[destinationValueFlag] = "2001:db8::" @@ -227,7 +231,7 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "destination value is ipv6 cidr", + description: "destination value is IPv6 CIDR", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[destinationTypeFlag] = "cidrv6" flagValues[destinationValueFlag] = "2001:db8::/32" @@ -246,7 +250,7 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "nexthop-type is internet and nexthop-value is provided", + description: "next hop type is internet and next hop value is provided", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[nextHopTypeFlag] = "internet" flagValues[nextHopValueFlag] = "1.1.1.1" // should not be allowed @@ -254,7 +258,7 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "nexthop-type is blackhole and nexthop-value is provided", + description: "next hop type is blackhole and next hop value is provided", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[nextHopTypeFlag] = "blackhole" flagValues[nextHopValueFlag] = "1.1.1.1" @@ -262,7 +266,7 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "nexthop-type is internet and nexthop-value is not provided", + description: "next hop type is internet and next hop value is not provided", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[nextHopTypeFlag] = "internet" delete(flagValues, nextHopValueFlag) @@ -274,7 +278,7 @@ func TestParseInput(t *testing.T) { isValid: true, }, { - description: "nexthop-type is blackhole and nexthop-value is not provided", + description: "next hop type is blackhole and next hop value is not provided", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[nextHopTypeFlag] = "blackhole" delete(flagValues, nextHopValueFlag) @@ -286,7 +290,7 @@ func TestParseInput(t *testing.T) { isValid: true, }, { - description: "nexthop-type is ipv4 and nexthop-value is missing", + description: "next hop type is IPv4 and next hop value is missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[nextHopTypeFlag] = "ipv4" delete(flagValues, nextHopValueFlag) @@ -294,7 +298,7 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "nexthop-type is ipv6 and nexthop-value is missing", + description: "next hop type is IPv6 and next hop value is missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[nextHopTypeFlag] = "ipv6" delete(flagValues, nextHopValueFlag) @@ -302,14 +306,14 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "invalid nexthop-type provided", + description: "invalid next hop type provided", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[nextHopTypeFlag] = "invalid-type" }), isValid: false, }, { - description: "optional labels is provided", + description: "optional labels are provided", flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[labelFlag] = "key=value" }), @@ -318,50 +322,25 @@ func TestParseInput(t *testing.T) { }), isValid: true, }, + { + description: "argument value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "argument value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -370,7 +349,7 @@ func TestBuildNextHop(t *testing.T) { tests := []struct { description string model *inputModel - expected *iaasalpha.RouteNexthop + expected *iaas.RouteNexthop }{ { description: "IPv4 next hop", @@ -378,8 +357,8 @@ func TestBuildNextHop(t *testing.T) { m.NextHopType = utils.Ptr("ipv4") m.NextHopValue = utils.Ptr("1.1.1.1") }), - expected: &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + expected: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: utils.Ptr("ipv4"), Value: utils.Ptr("1.1.1.1"), }, @@ -391,8 +370,8 @@ func TestBuildNextHop(t *testing.T) { m.NextHopType = utils.Ptr("ipv6") m.NextHopValue = utils.Ptr("::1") }), - expected: &iaasalpha.RouteNexthop{ - NexthopIPv6: &iaasalpha.NexthopIPv6{ + expected: &iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ Type: utils.Ptr("ipv6"), Value: utils.Ptr("::1"), }, @@ -404,8 +383,8 @@ func TestBuildNextHop(t *testing.T) { m.NextHopType = utils.Ptr("internet") m.NextHopValue = nil }), - expected: &iaasalpha.RouteNexthop{ - NexthopInternet: &iaasalpha.NexthopInternet{ + expected: &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ Type: utils.Ptr("internet"), }, }, @@ -416,8 +395,8 @@ func TestBuildNextHop(t *testing.T) { m.NextHopType = utils.Ptr("blackhole") m.NextHopValue = nil }), - expected: &iaasalpha.RouteNexthop{ - NexthopBlackhole: &iaasalpha.NexthopBlackhole{ + expected: &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ Type: utils.Ptr("blackhole"), }, }, @@ -445,7 +424,7 @@ func TestBuildDestination(t *testing.T) { tests := []struct { description string model *inputModel - expected *iaasalpha.RouteDestination + expected *iaas.RouteDestination }{ { description: "CIDRv4 destination", @@ -453,8 +432,8 @@ func TestBuildDestination(t *testing.T) { m.DestinationType = utils.Ptr("cidrv4") m.DestinationValue = utils.Ptr("192.168.1.0/24") }), - expected: &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + expected: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: utils.Ptr("cidrv4"), Value: utils.Ptr("192.168.1.0/24"), }, @@ -466,8 +445,8 @@ func TestBuildDestination(t *testing.T) { m.DestinationType = utils.Ptr("cidrv6") m.DestinationValue = utils.Ptr("2001:db8::/32") }), - expected: &iaasalpha.RouteDestination{ - DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + expected: &iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ Type: utils.Ptr("cidrv6"), Value: utils.Ptr("2001:db8::/32"), }, @@ -504,7 +483,7 @@ func TestBuildRequest(t *testing.T) { tests := []struct { description string model *inputModel - expectedRequest iaasalpha.ApiAddRoutesToRoutingTableRequest + expectedRequest iaas.ApiAddRoutesToRoutingTableRequest }{ { description: "base", @@ -516,8 +495,8 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(func(model *inputModel) { model.Labels = utils.Ptr(map[string]string{"key": "value"}) }), - expectedRequest: fixtureRequest(func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest) { - *request = (*request).AddRoutesToRoutingTablePayload(fixturePayload(func(payload *iaasalpha.AddRoutesToRoutingTablePayload) { + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + *request = (*request).AddRoutesToRoutingTablePayload(fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { (*payload.Items)[0].Labels = utils.ConvertStringMapToInterfaceMap(utils.Ptr(map[string]string{"key": "value"})) })) }), @@ -530,18 +509,18 @@ func TestBuildRequest(t *testing.T) { model.NextHopType = utils.Ptr("ipv6") model.NextHopValue = utils.Ptr("2001:db8::1") }), - expectedRequest: fixtureRequest(func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest) { - *request = (*request).AddRoutesToRoutingTablePayload(iaasalpha.AddRoutesToRoutingTablePayload{ - Items: &[]iaasalpha.Route{ + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + *request = (*request).AddRoutesToRoutingTablePayload(iaas.AddRoutesToRoutingTablePayload{ + Items: &[]iaas.Route{ { - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ Type: utils.Ptr("cidrv6"), Value: utils.Ptr("2001:db8::/32"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopIPv6: &iaasalpha.NexthopIPv6{ + Nexthop: &iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ Type: utils.Ptr("ipv6"), Value: utils.Ptr("2001:db8::1"), }, @@ -558,10 +537,10 @@ func TestBuildRequest(t *testing.T) { model.NextHopType = utils.Ptr("internet") model.NextHopValue = nil }), - expectedRequest: fixtureRequest(func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest) { - payload := fixturePayload(func(payload *iaasalpha.AddRoutesToRoutingTablePayload) { - (*payload.Items)[0].Nexthop = &iaasalpha.RouteNexthop{ - NexthopInternet: &iaasalpha.NexthopInternet{ + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + (*payload.Items)[0].Nexthop = &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ Type: utils.Ptr("internet"), }, } @@ -575,10 +554,10 @@ func TestBuildRequest(t *testing.T) { model.NextHopType = utils.Ptr("blackhole") model.NextHopValue = nil }), - expectedRequest: fixtureRequest(func(request *iaasalpha.ApiAddRoutesToRoutingTableRequest) { - payload := fixturePayload(func(payload *iaasalpha.AddRoutesToRoutingTablePayload) { - (*payload.Items)[0].Nexthop = &iaasalpha.RouteNexthop{ - NexthopBlackhole: &iaasalpha.NexthopBlackhole{ + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + (*payload.Items)[0].Nexthop = &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ Type: utils.Ptr("blackhole"), }, } @@ -586,6 +565,42 @@ func TestBuildRequest(t *testing.T) { *request = (*request).AddRoutesToRoutingTablePayload(payload) }), }, + { + description: "nexthop type is ipv4 with value", + model: fixtureInputModel(func(model *inputModel) { + model.NextHopType = utils.Ptr("ipv4") + model.NextHopValue = utils.Ptr("1.2.3.4") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + (*payload.Items)[0].Nexthop = &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("1.2.3.4"), + }, + } + }) + *request = (*request).AddRoutesToRoutingTablePayload(payload) + }), + }, + { + description: "nexthop type is ipv6 with value", + model: fixtureInputModel(func(model *inputModel) { + model.NextHopType = utils.Ptr("ipv6") + model.NextHopValue = utils.Ptr("2001:db8::1") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + (*payload.Items)[0].Nexthop = &iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ + Type: utils.Ptr("ipv6"), + Value: utils.Ptr("2001:db8::1"), + }, + } + }) + *request = (*request).AddRoutesToRoutingTablePayload(payload) + }), + }, } for _, tt := range tests { @@ -605,16 +620,16 @@ func TestBuildRequest(t *testing.T) { } func TestOutputResult(t *testing.T) { - dummyRoute := iaasalpha.Route{ + dummyRoute := iaas.Route{ Id: utils.Ptr("route-foo"), - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: utils.Ptr("cidrv4"), Value: utils.Ptr("10.0.0.0/24"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: utils.Ptr("ipv4"), Value: utils.Ptr("10.0.0.1"), }, @@ -627,7 +642,7 @@ func TestOutputResult(t *testing.T) { tests := []struct { name string outputFormat string - items []iaasalpha.Route + items []iaas.Route wantErr bool }{ { @@ -639,25 +654,25 @@ func TestOutputResult(t *testing.T) { { name: "empty items list", outputFormat: "", - items: []iaasalpha.Route{}, + items: []iaas.Route{}, wantErr: true, }, { name: "table output with one route", outputFormat: "", - items: []iaasalpha.Route{dummyRoute}, + items: []iaas.Route{dummyRoute}, wantErr: false, }, { name: "json output with one route", outputFormat: print.JSONOutputFormat, - items: []iaasalpha.Route{dummyRoute}, + items: []iaas.Route{dummyRoute}, wantErr: false, }, { name: "yaml output with one route", outputFormat: print.YAMLOutputFormat, - items: []iaasalpha.Route{dummyRoute}, + items: []iaas.Route{dummyRoute}, wantErr: false, }, } diff --git a/internal/cmd/beta/routingtable/route/delete/delete.go b/internal/cmd/routingtable/route/delete/delete.go similarity index 90% rename from internal/cmd/beta/routingtable/route/delete/delete.go rename to internal/cmd/routingtable/route/delete/delete.go index cf329ca6a..699d5d06c 100644 --- a/internal/cmd/beta/routingtable/route/delete/delete.go +++ b/internal/cmd/routingtable/route/delete/delete.go @@ -12,21 +12,22 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( - organizationIdFlag = "organization-id" networkAreaIdFlag = "network-area-id" - routingTableIdFlag = "routing-table-id" + organizationIdFlag = "organization-id" routeIdArg = "ROUTE_ID_ARG" + routingTableIdFlag = "routing-table-id" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string NetworkAreaId *string - RoutingTableId *string + OrganizationId *string RouteID *string + RoutingTableId *string } func NewCmd(params *params.CmdParams) *cobra.Command { @@ -34,11 +35,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Use: fmt.Sprintf("delete %s", routingTableIdFlag), Short: "Deletes a route within a routing-table", Long: "Deletes a route within a routing-table", - Args: args.SingleArg(routeIdArg, nil), + Args: args.SingleArg(routeIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Deletes a route within a routing-table`, - `$ stackit beta routing-table route delete xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + `$ stackit routing-table route delete xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz`, ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -49,7 +50,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Configure API client - apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -86,8 +87,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) @@ -106,8 +107,8 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu GlobalFlagModel: globalFlags, NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), RouteID: &routeId, + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), } p.DebugInputModel(model) diff --git a/internal/cmd/beta/routingtable/route/delete/delete_test.go b/internal/cmd/routingtable/route/delete/delete_test.go similarity index 58% rename from internal/cmd/beta/routingtable/route/delete/delete_test.go rename to internal/cmd/routingtable/route/delete/delete_test.go index ab842a7fb..9d6146963 100644 --- a/internal/cmd/beta/routingtable/route/delete/delete_test.go +++ b/internal/cmd/routingtable/route/delete/delete_test.go @@ -3,11 +3,9 @@ package delete import ( "testing" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) var ( @@ -48,29 +46,29 @@ func fixtureInputModel(mods ...func(*inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string - args []string + argValues []string flagValues map[string]string isValid bool - expectedRoute *inputModel + expectedModel *inputModel }{ { description: "valid input", - args: []string{testRouteId}, + argValues: []string{testRouteId}, flagValues: fixtureFlagValues(), isValid: true, - expectedRoute: fixtureInputModel(func(m *inputModel) { + expectedModel: fixtureInputModel(func(m *inputModel) { m.RouteID = &testRouteId }), }, { description: "missing route id arg", - args: []string{}, + argValues: []string{}, flagValues: fixtureFlagValues(), isValid: false, }, { description: "missing organization-id flag", - args: []string{testRouteId}, + argValues: []string{testRouteId}, flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, "organization-id") }), @@ -78,7 +76,7 @@ func TestParseInput(t *testing.T) { }, { description: "missing network-area-id flag", - args: []string{testRouteId}, + argValues: []string{testRouteId}, flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, "network-area-id") }), @@ -86,56 +84,49 @@ func TestParseInput(t *testing.T) { }, { description: "missing routing-table-id flag", - args: []string{testRouteId}, + argValues: []string{testRouteId}, flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, "routing-table-id") }), isValid: false, }, + { + description: "arg value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "arg value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "invalid organization-id flag", + argValues: []string{testRouteId}, + flagValues: map[string]string{"organization-id": "invalid-org"}, + isValid: false, + }, + { + description: "invalid network-area-id flag", + argValues: []string{testRouteId}, + flagValues: map[string]string{"network-area-id": "invalid-area"}, + isValid: false, + }, + { + description: "invalid routing-table-id flag", + argValues: []string{testRouteId}, + flagValues: map[string]string{"routing-table-id": "invalid-table"}, + isValid: false, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.args) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedRoute) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/beta/routingtable/route/describe/describe.go b/internal/cmd/routingtable/route/describe/describe.go similarity index 78% rename from internal/cmd/beta/routingtable/route/describe/describe.go rename to internal/cmd/routingtable/route/describe/describe.go index 4a3a1c7d9..68c6e6072 100644 --- a/internal/cmd/beta/routingtable/route/describe/describe.go +++ b/internal/cmd/routingtable/route/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" + "time" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -17,34 +16,34 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( - organizationIdFlag = "organization-id" networkAreaIdFlag = "network-area-id" - routingTableIdFlag = "routing-table-id" + organizationIdFlag = "organization-id" routeIdArg = "ROUTE_ID_ARG" + routingTableIdFlag = "routing-table-id" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string NetworkAreaId *string - RoutingTableId *string + OrganizationId *string RouteID *string + RoutingTableId *string } func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", routeIdArg), - Short: "Describe a route within a routing-table", - Long: "Describe a route within a routing-table", - Args: args.SingleArg(routeIdArg, nil), + Short: "Describes a route within a routing-table", + Long: "Describes a route within a routing-table", + Args: args.SingleArg(routeIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Describe a route within a routing-table`, - `$ stackit beta routing-table route describe xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + `$ stackit routing-table route describe xxxx-xxxx-xxxx-xxxx --routing-table-id xxx --organization-id yyy --network-area-id zzz`, ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -55,7 +54,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Configure API client - apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -84,8 +83,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) @@ -104,36 +103,25 @@ func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputMode GlobalFlagModel: globalFlags, NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), RouteID: &routeId, + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), } p.DebugInputModel(model) return &model, nil } -func outputResult(p *print.Printer, outputFormat string, routingTable *iaasalpha.Route) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(routingTable, "", " ") - if err != nil { - return fmt.Errorf("marshal route describe: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(routingTable, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal route describe: %w", err) - } - p.Outputln(string(details)) +func outputResult(p *print.Printer, outputFormat string, routingTable *iaas.Route) error { + if routingTable == nil { + return fmt.Errorf("describe routes response is empty") + } - return nil - default: + return p.OutputResult(outputFormat, routingTable, func() error { var labels []string - for key, value := range *routingTable.Labels { - labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + if routingTable.Labels != nil && len(*routingTable.Labels) > 0 { + for key, value := range *routingTable.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } } destinationType := "" @@ -174,12 +162,22 @@ func outputResult(p *print.Printer, outputFormat string, routingTable *iaasalpha } } + createdAt := "" + if routingTable.CreatedAt != nil { + createdAt = routingTable.CreatedAt.Format(time.RFC3339) + } + + updatedAt := "" + if routingTable.UpdatedAt != nil { + updatedAt = routingTable.UpdatedAt.Format(time.RFC3339) + } + table := tables.NewTable() table.SetHeader("ID", "CREATED_AT", "UPDATED_AT", "DESTINATION TYPE", "DESTINATION VALUE", "NEXTHOP TYPE", "NEXTHOP VALUE", "LABELS") table.AddRow( utils.PtrString(routingTable.Id), - routingTable.CreatedAt.String(), - routingTable.UpdatedAt.String(), + createdAt, + updatedAt, destinationType, destinationValue, nextHopType, @@ -193,5 +191,5 @@ func outputResult(p *print.Printer, outputFormat string, routingTable *iaasalpha } return nil - } + }) } diff --git a/internal/cmd/beta/routingtable/route/describe/describe_test.go b/internal/cmd/routingtable/route/describe/describe_test.go similarity index 74% rename from internal/cmd/beta/routingtable/route/describe/describe_test.go rename to internal/cmd/routingtable/route/describe/describe_test.go index 63706b8d3..03a46071f 100644 --- a/internal/cmd/beta/routingtable/route/describe/describe_test.go +++ b/internal/cmd/routingtable/route/describe/describe_test.go @@ -4,16 +4,17 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var testRegion = "eu01" +const testRegion = "eu01" + var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() var testRoutingTableId = uuid.NewString() @@ -110,18 +111,40 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "routing-table-id missing", + description: "invalid routing-table-id", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, routingTableIdFlag) + flagValues[routingTableIdFlag] = "invalid-id" }), isValid: false, }, { - description: "routing-id missing", - argValues: []string{}, + description: "arg value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "arg value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "invalid organization-id", + argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, routeIdArg) + flagValues[organizationIdFlag] = "invalid-org" + }), + isValid: false, + }, + { + description: "invalid network-area-id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-area" }), isValid: false, }, @@ -129,61 +152,22 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } func TestOutputResult(t *testing.T) { - dummyRoute := iaasalpha.Route{ + dummyRoute := iaas.Route{ Id: utils.Ptr("route-foo"), - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: utils.Ptr("cidrv4"), Value: utils.Ptr("10.0.0.0/24"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: utils.Ptr("ipv4"), Value: utils.Ptr("10.0.0.1"), }, @@ -196,7 +180,7 @@ func TestOutputResult(t *testing.T) { tests := []struct { name string outputFormat string - route iaasalpha.Route + route iaas.Route wantErr bool }{ { diff --git a/internal/cmd/beta/routingtable/route/list/list.go b/internal/cmd/routingtable/route/list/list.go similarity index 77% rename from internal/cmd/beta/routingtable/route/list/list.go rename to internal/cmd/routingtable/route/list/list.go index 86bc78f18..7af433e4b 100644 --- a/internal/cmd/beta/routingtable/route/list/list.go +++ b/internal/cmd/routingtable/route/list/list.go @@ -2,10 +2,9 @@ package list import ( "context" - "encoding/json" "fmt" + "time" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -18,44 +17,44 @@ import ( routeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/routing-table/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( - organizationIdFlag = "organization-id" - networkAreaIdFlag = "network-area-id" - routingTableIdFlag = "routing-table-id" labelSelectorFlag = "label-selector" limitFlag = "limit" + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + routingTableIdFlag = "routing-table-id" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string - NetworkAreaId *string - RoutingTableId *string LabelSelector *string Limit *int64 + NetworkAreaId *string + OrganizationId *string + RoutingTableId *string } func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", - Short: "list all routes within a routing-table", - Long: "list all routes within a routing-table", + Short: "Lists all routes within a routing-table", + Long: "Lists all routes within a routing-table", Args: args.NoArgs, Example: examples.Build( examples.NewExample( `List all routes within a routing-table`, - `$ stackit beta routing-table route list --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + `$ stackit routing-table route list --routing-table-id xxx --organization-id yyy --network-area-id zzz`, ), examples.NewExample( `List all routes within a routing-table with labels`, - `$ stackit beta routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc`, + `$ stackit routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc`, ), examples.NewExample( `List all routes within a routing-tables with labels and limit to 10`, - `$ stackit beta routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc --limit 10`, + `$ stackit routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc --limit 10`, ), ), RunE: func(cmd *cobra.Command, _ []string) error { @@ -66,7 +65,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Configure API client - apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -95,7 +94,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Truncate output - items := *response.Items + items := response.GetItems() if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } @@ -109,11 +108,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().String(labelSelectorFlag, "", "Filter by label") cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") - cmd.Flags().String(labelSelectorFlag, "", "Filter by label") - cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) cobra.CheckErr(err) @@ -132,41 +131,38 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, + LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), Limit: limit, NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), - LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } p.DebugInputModel(model) return &model, nil } -func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.Route) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal routes list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal routes list: %w", err) - } - p.Outputln(string(details)) +func outputResult(p *print.Printer, outputFormat string, items []iaas.Route) error { + if len(items) == 0 { + return fmt.Errorf("create routes response is empty") + } - return nil - default: + return p.OutputResult(outputFormat, items, func() error { table := tables.NewTable() table.SetHeader("ID", "DEST. TYPE", "DEST. VALUE", "NEXTHOP TYPE", "NEXTHOP VALUE", "LABELS", "CREATED", "UPDATED") for _, item := range items { destType, destValue, hopType, hopValue, labels := routeUtils.ExtractRouteDetails(item) + createdAt := "" + if item.CreatedAt != nil { + createdAt = item.CreatedAt.Format(time.RFC3339) + } + + updatedAt := "" + if item.UpdatedAt != nil { + updatedAt = item.UpdatedAt.Format(time.RFC3339) + } + table.AddRow( utils.PtrString(item.Id), destType, @@ -174,10 +170,14 @@ func outputResult(p *print.Printer, outputFormat string, items []iaasalpha.Route hopType, hopValue, labels, - item.CreatedAt.String(), - item.UpdatedAt.String(), + createdAt, + updatedAt, ) } - return table.Display(p) - } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) } diff --git a/internal/cmd/beta/routingtable/route/list/list_test.go b/internal/cmd/routingtable/route/list/list_test.go similarity index 90% rename from internal/cmd/beta/routingtable/route/list/list_test.go rename to internal/cmd/routingtable/route/list/list_test.go index 3a9b07763..ad03fa226 100644 --- a/internal/cmd/beta/routingtable/route/list/list_test.go +++ b/internal/cmd/routingtable/route/list/list_test.go @@ -11,15 +11,17 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var testRegion = "eu01" +const testRegion = "eu01" + var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() var testRoutingTableId = uuid.NewString() -var testLabelSelectorFlag = "key1=value1,key2=value2" +const testLabelSelectorFlag = "key1=value1,key2=value2" + var testLabels = &map[string]string{ "key1": "value1", "key2": "value2", @@ -99,13 +101,6 @@ func TestParseInput(t *testing.T) { }), isValid: false, }, - { - description: "routing-table-id missing", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, routingTableIdFlag) - }), - isValid: false, - }, { description: "labels missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { @@ -140,6 +135,13 @@ func TestParseInput(t *testing.T) { }), isValid: false, }, + { + description: "limit zero flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, } for _, tt := range tests { @@ -189,16 +191,16 @@ func TestParseInput(t *testing.T) { } func TestOutputResult(t *testing.T) { - dummyRoute := iaasalpha.Route{ + dummyRoute := iaas.Route{ Id: utils.Ptr("route-foo"), - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: utils.Ptr("cidrv4"), Value: utils.Ptr("10.0.0.0/24"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: utils.Ptr("ipv4"), Value: utils.Ptr("10.0.0.1"), }, @@ -211,19 +213,19 @@ func TestOutputResult(t *testing.T) { tests := []struct { name string outputFormat string - routes []iaasalpha.Route + routes []iaas.Route wantErr bool }{ { name: "json output with one route", outputFormat: print.JSONOutputFormat, - routes: []iaasalpha.Route{dummyRoute}, + routes: []iaas.Route{dummyRoute}, wantErr: false, }, { name: "yaml output with one route", outputFormat: print.YAMLOutputFormat, - routes: []iaasalpha.Route{dummyRoute}, + routes: []iaas.Route{dummyRoute}, wantErr: false, }, } diff --git a/internal/cmd/beta/routingtable/route/route.go b/internal/cmd/routingtable/route/route.go similarity index 57% rename from internal/cmd/beta/routingtable/route/route.go rename to internal/cmd/routingtable/route/route.go index cfca28e1a..13cbaddd4 100644 --- a/internal/cmd/beta/routingtable/route/route.go +++ b/internal/cmd/routingtable/route/route.go @@ -2,12 +2,12 @@ package route import ( "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/create" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/delete" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/describe" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/routingtable/route/update" "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/route/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/route/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/route/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/route/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/route/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) @@ -15,8 +15,8 @@ import ( func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "route", - Short: "Manage routes of a routing-table", - Long: "Manage routes of a routing-table", + Short: "Manages routes of a routing-table", + Long: "Manages routes of a routing-table", Args: args.NoArgs, Run: utils.CmdHelp, } diff --git a/internal/cmd/beta/routingtable/route/update/update.go b/internal/cmd/routingtable/route/update/update.go similarity index 77% rename from internal/cmd/beta/routingtable/route/update/update.go rename to internal/cmd/routingtable/route/update/update.go index d3847848a..5942af7ef 100644 --- a/internal/cmd/beta/routingtable/route/update/update.go +++ b/internal/cmd/routingtable/route/update/update.go @@ -2,10 +2,8 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -16,24 +14,24 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( - organizationIdFlag = "organization-id" - networkAreaIdFlag = "network-area-id" - routingTableIdFlag = "routing-table-id" labelFlag = "labels" + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" routeIdArg = "ROUTE_ID_ARG" + routingTableIdFlag = "routing-table-id" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string + Labels *map[string]string NetworkAreaId *string + OrganizationId *string + RouteId *string RoutingTableId *string - RouteId string - Labels *map[string]string } func NewCmd(params *params.CmdParams) *cobra.Command { @@ -45,7 +43,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Example: examples.Build( examples.NewExample( `Updates the label(s) of a route with ID "xxx" in a routing-table ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, - "$ stackit beta routing-table route update xxx --labels key=value,foo=bar --routing-table-id xxx --organization-id yyy --network-area-id zzz", + "$ stackit routing-table route update xxx --labels key=value,foo=bar --routing-table-id xxx --organization-id yyy --network-area-id zzz", ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -56,11 +54,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Configure API client - apiClient, err := client.ConfigureAlphaClient(params.Printer, params.CliVersion) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update route %q for routing-table with id %q?", *model.RouteId, *model.RoutingTableId) + if err := params.Printer.PromptForConfirmation(prompt); err != nil { + return err + } + } + // Call API req := apiClient.UpdateRouteOfRoutingTable( ctx, @@ -68,20 +73,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { *model.NetworkAreaId, model.Region, *model.RoutingTableId, - model.RouteId, + *model.RouteId, ) - payload := iaasalpha.UpdateRouteOfRoutingTablePayload{ + payload := iaas.UpdateRouteOfRoutingTablePayload{ Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), } req = req.UpdateRouteOfRoutingTablePayload(payload) resp, err := req.Execute() if err != nil { - return fmt.Errorf("update route %q of routing-table %q : %w", model.RouteId, *model.RoutingTableId, err) + return fmt.Errorf("update route %q of routing-table %q : %w", *model.RouteId, *model.RoutingTableId, err) } - return outputResult(params.Printer, model.OutputFormat, *model.RoutingTableId, *model.NetworkAreaId, *resp) + return outputResult(params.Printer, model.OutputFormat, *model.RoutingTableId, *model.NetworkAreaId, resp) }, } configureFlags(cmd) @@ -89,10 +94,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels") cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") - cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels") err := flags.MarkFlagsRequired(cmd, labelFlag, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) cobra.CheckErr(err) @@ -114,37 +119,24 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + Labels: labels, NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + RouteId: &routeId, RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), - RouteId: routeId, - Labels: labels, } p.DebugInputModel(model) return &model, nil } -func outputResult(p *print.Printer, outputFormat, routingTableId, networkAreaId string, route iaasalpha.Route) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(route, "", " ") - if err != nil { - return fmt.Errorf("marshal route: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(route, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal route: %w", err) - } - p.Outputln(string(details)) +func outputResult(p *print.Printer, outputFormat, routingTableId, networkAreaId string, route *iaas.Route) error { + if route == nil { + return fmt.Errorf("update route response is empty") + } - return nil - default: + return p.OutputResult(outputFormat, route, func() error { p.Outputf("Updated route %q for routing-table %q in network-area %q.", *route.Id, routingTableId, networkAreaId) return nil - } + }) } diff --git a/internal/cmd/beta/routingtable/route/update/update_test.go b/internal/cmd/routingtable/route/update/update_test.go similarity index 72% rename from internal/cmd/beta/routingtable/route/update/update_test.go rename to internal/cmd/routingtable/route/update/update_test.go index 2a0ed75ff..2d0e5b7b2 100644 --- a/internal/cmd/beta/routingtable/route/update/update_test.go +++ b/internal/cmd/routingtable/route/update/update_test.go @@ -4,22 +4,24 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var testRegion = "eu01" +const testRegion = "eu01" + var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() var testRoutingTableId = uuid.NewString() var testRouteId = uuid.NewString() -var testLabelSelectorFlag = "key1=value1,key2=value2" +const testLabelSelectorFlag = "key1=value1,key2=value2" + var testLabels = &map[string]string{ "key1": "value1", "key2": "value2", @@ -48,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), RoutingTableId: utils.Ptr(testRoutingTableId), - RouteId: testRouteId, + RouteId: &testRouteId, Labels: testLabels, } for _, mod := range mods { @@ -121,12 +123,18 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "routing-id missing", - argValues: []string{}, - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, routeIdArg) - }), - isValid: false, + description: "arg value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "arg value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), }, { description: "labels are missing", @@ -136,65 +144,32 @@ func TestParseInput(t *testing.T) { }), isValid: false, }, + { + description: "invalid label format", + argValues: []string{}, + flagValues: map[string]string{labelFlag: "invalid-label"}, + isValid: false, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } func TestOutputResult(t *testing.T) { - dummyRoute := iaasalpha.Route{ + dummyRoute := iaas.Route{ Id: utils.Ptr("route-foo"), - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: utils.Ptr("cidrv4"), Value: utils.Ptr("10.0.0.0/24"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: utils.Ptr("ipv4"), Value: utils.Ptr("10.0.0.1"), }, @@ -207,7 +182,7 @@ func TestOutputResult(t *testing.T) { tests := []struct { name string outputFormat string - route iaasalpha.Route + route iaas.Route wantErr bool }{ { @@ -228,7 +203,7 @@ func TestOutputResult(t *testing.T) { p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.outputFormat, "", "", tt.route); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.outputFormat, "", "", &tt.route); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/routingtable/routingtable.go b/internal/cmd/routingtable/routingtable.go new file mode 100644 index 000000000..301c5a8e3 --- /dev/null +++ b/internal/cmd/routingtable/routingtable.go @@ -0,0 +1,40 @@ +package routingtable + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rtCreate "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/create" + rtDelete "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/delete" + rtDescribe "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/describe" + rtList "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/route" + rtUpdate "github.com/stackitcloud/stackit-cli/internal/cmd/routingtable/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "routing-table", + Short: "Manage routing-tables and its according routes", + Long: `Manage routing-tables and their associated routes. + +This API is currently available only to selected customers. +To request access, please contact your account manager or submit a support ticket.`, + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + cmd.AddCommand( + rtCreate.NewCmd(params), + rtUpdate.NewCmd(params), + rtList.NewCmd(params), + rtDescribe.NewCmd(params), + rtDelete.NewCmd(params), + route.NewCmd(params), + ) +} diff --git a/internal/cmd/routingtable/update/update.go b/internal/cmd/routingtable/update/update.go new file mode 100644 index 000000000..859c516a6 --- /dev/null +++ b/internal/cmd/routingtable/update/update.go @@ -0,0 +1,167 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + descriptionFlag = "description" + labelFlag = "labels" + nameFlag = "name" + networkAreaIdFlag = "network-area-id" + nonDynamicRoutesFlag = "non-dynamic-routes" + organizationIdFlag = "organization-id" + routingTableIdArg = "ROUTE_TABLE_ID_ARG" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId *string + NetworkAreaId *string + NonDynamicRoutes bool + RoutingTableId *string + Description *string + Labels *map[string]string + Name *string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", routingTableIdArg), + Short: "Updates a routing-table", + Long: "Updates a routing-table.", + Args: args.SingleArg(routingTableIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Updates the label(s) of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit routing-table update xxx --labels key=value,foo=bar --organization-id yyy --network-area-id zzz", + ), + examples.NewExample( + `Updates the name of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit routing-table update xxx --name foo --organization-id yyy --network-area-id zzz", + ), + examples.NewExample( + `Updates the description of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit routing-table update xxx --description foo --organization-id yyy --network-area-id zzz", + ), + examples.NewExample( + `Disables the dynamic_routes of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit routing-table update xxx --organization-id yyy --network-area-id zzz --non-dynamic-routes", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update routing-table %q?", *model.RoutingTableId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := apiClient.UpdateRoutingTableOfArea( + ctx, + *model.OrganizationId, + *model.NetworkAreaId, + model.Region, + *model.RoutingTableId, + ) + + dynamicRoutes := true + if model.NonDynamicRoutes { + dynamicRoutes = false + } + + payload := iaas.UpdateRoutingTableOfAreaPayload{ + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Name: model.Name, + Description: model.Description, + DynamicRoutes: &dynamicRoutes, + } + req = req.UpdateRoutingTableOfAreaPayload(payload) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update routing-table %q : %w", *model.RoutingTableId, err) + } + + return outputResult(params.Printer, model.OutputFormat, *model.NetworkAreaId, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(descriptionFlag, "", "Description of the routing-table") + cmd.Flags().String(nameFlag, "", "Name of the routing-table") + cmd.Flags().StringToString(labelFlag, nil, "Key=value labels") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Bool(nonDynamicRoutesFlag, false, "If true, preventing dynamic routes from propagating to the routing-table.") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if len(inputArgs) == 0 { + return nil, fmt.Errorf("at least one argument is required") + } + routeTableId := inputArgs[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + Name: flags.FlagToStringPointer(p, cmd, nameFlag), + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + NonDynamicRoutes: flags.FlagToBoolValue(p, cmd, nonDynamicRoutesFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + RoutingTableId: &routeTableId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat, networkAreaId string, routingTable *iaas.RoutingTable) error { + if routingTable == nil { + return fmt.Errorf("update routing-table response is empty") + } + + if routingTable.Id == nil { + return fmt.Errorf("update routing-table response is empty") + } + + return p.OutputResult(outputFormat, routingTable, func() error { + p.Outputf("Updated routing-table %q in network-area %q.", *routingTable.Id, networkAreaId) + return nil + }) +} diff --git a/internal/cmd/routingtable/update/update_test.go b/internal/cmd/routingtable/update/update_test.go new file mode 100644 index 000000000..aef2de6e7 --- /dev/null +++ b/internal/cmd/routingtable/update/update_test.go @@ -0,0 +1,221 @@ +package update + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + pprint "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +const testRoutingTableName = "test" +const testRoutingTableDescription = "test" +const testLabelSelectorFlag = "key1=value1,key2=value2" + +var testLabels = &map[string]string{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + descriptionFlag: testRoutingTableDescription, + nameFlag: testRoutingTableName, + labelFlag: testLabelSelectorFlag, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: utils.Ptr(testOrgId), + NetworkAreaId: utils.Ptr(testNetworkAreaId), + Name: utils.Ptr(testRoutingTableName), + Description: utils.Ptr(testRoutingTableDescription), + Labels: utils.Ptr(*testLabels), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRoutingTableId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = &testRoutingTableId + }), + }, + { + description: "dynamic_routes disabled", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nonDynamicRoutesFlag] = "true" + }), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NonDynamicRoutes = true + model.RoutingTableId = &testRoutingTableId + }), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "network-area-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "arg value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "arg value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "labels are missing", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelFlag) + }), + isValid: false, + }, + { + description: "invalid label format", + argValues: []string{}, + flagValues: map[string]string{labelFlag: "invalid-label"}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoutingTable := iaas.RoutingTable{ + Id: utils.Ptr("id-foo"), + Name: utils.Ptr("route-table-foo"), + Description: utils.Ptr("description-foo"), + SystemRoutes: utils.Ptr(true), + DynamicRoutes: utils.Ptr(true), + Labels: utils.ConvertStringMapToInterfaceMap(testLabels), + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routingTable *iaas.RoutingTable + wantErr bool + }{ + { + name: "nil routing-table should return error", + outputFormat: "", + routingTable: nil, + wantErr: true, + }, + { + name: "empty routing-table", + outputFormat: "", + routingTable: &iaas.RoutingTable{}, + wantErr: true, + }, + { + name: "table output routing-table", + outputFormat: "", + routingTable: &dummyRoutingTable, + wantErr: false, + }, + { + name: "json output routing-table", + outputFormat: pprint.JSONOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + { + name: "yaml output routing-table", + outputFormat: pprint.YAMLOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + } + + p := pprint.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.outputFormat, "network-area-id", tt.routingTable); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/pkg/services/iaas/client/alphaclient.go b/internal/pkg/services/iaas/client/alphaclient.go deleted file mode 100644 index 0f9db99f9..000000000 --- a/internal/pkg/services/iaas/client/alphaclient.go +++ /dev/null @@ -1,44 +0,0 @@ -package client - -import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - - "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" -) - -func ConfigureAlphaClient(p *print.Printer, cliVersion string) (*iaasalpha.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.IaaSCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := iaasalpha.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil -} diff --git a/internal/pkg/services/routing-table/utils/utils.go b/internal/pkg/services/routing-table/utils/utils.go index 02afac663..b6d38f6c2 100644 --- a/internal/pkg/services/routing-table/utils/utils.go +++ b/internal/pkg/services/routing-table/utils/utils.go @@ -5,10 +5,10 @@ import ( "strings" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -func ExtractRouteDetails(item iaasalpha.Route) (destType, destValue, hopType, hopValue, labels string) { +func ExtractRouteDetails(item iaas.Route) (destType, destValue, hopType, hopValue, labels string) { if item.Destination.DestinationCIDRv4 != nil { destType = utils.PtrString(item.Destination.DestinationCIDRv4.Type) destValue = utils.PtrString(item.Destination.DestinationCIDRv4.Value) diff --git a/internal/pkg/services/routing-table/utils/utils_test.go b/internal/pkg/services/routing-table/utils/utils_test.go index eda0626c8..a9210154e 100644 --- a/internal/pkg/services/routing-table/utils/utils_test.go +++ b/internal/pkg/services/routing-table/utils/utils_test.go @@ -4,13 +4,13 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) func TestExtractRouteDetails(t *testing.T) { tests := []struct { description string - input *iaasalpha.Route + input *iaas.Route wantDestType string wantDestValue string wantHopType string @@ -19,15 +19,15 @@ func TestExtractRouteDetails(t *testing.T) { }{ { description: "CIDRv4 destination, IPv4 nexthop, with labels", - input: &iaasalpha.Route{ - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + input: &iaas.Route{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: utils.Ptr("CIDRv4"), Value: utils.Ptr("10.0.0.0/24"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: utils.Ptr("IPv4"), Value: utils.Ptr("10.0.0.1"), }, @@ -44,15 +44,15 @@ func TestExtractRouteDetails(t *testing.T) { }, { description: "CIDRv6 destination, IPv6 nexthop, with no labels", - input: &iaasalpha.Route{ - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + input: &iaas.Route{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ Type: utils.Ptr("CIDRv6"), Value: utils.Ptr("2001:db8::/32"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopIPv4: &iaasalpha.NexthopIPv4{ + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ Type: utils.Ptr("IPv6"), Value: utils.Ptr("2001:db8::1"), }, @@ -67,15 +67,15 @@ func TestExtractRouteDetails(t *testing.T) { }, { description: "Internet nexthop without value", - input: &iaasalpha.Route{ - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv4: &iaasalpha.DestinationCIDRv4{ + input: &iaas.Route{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ Type: utils.Ptr("CIDRv4"), Value: utils.Ptr("0.0.0.0/0"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopInternet: &iaasalpha.NexthopInternet{ + Nexthop: &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ Type: utils.Ptr("Internet"), }, }, @@ -89,15 +89,15 @@ func TestExtractRouteDetails(t *testing.T) { }, { description: "Blackhole nexthop without value and nil labels map", - input: &iaasalpha.Route{ - Destination: &iaasalpha.RouteDestination{ - DestinationCIDRv6: &iaasalpha.DestinationCIDRv6{ + input: &iaas.Route{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ Type: utils.Ptr("CIDRv6"), Value: utils.Ptr("::/0"), }, }, - Nexthop: &iaasalpha.RouteNexthop{ - NexthopBlackhole: &iaasalpha.NexthopBlackhole{ + Nexthop: &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ Type: utils.Ptr("Blackhole"), }, },