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_routing-table.md b/docs/stackit_routing-table.md new file mode 100644 index 000000000..e026af114 --- /dev/null +++ b/docs/stackit_routing-table.md @@ -0,0 +1,39 @@ +## stackit routing-table + +Manage routing-tables and its according routes + +### Synopsis + +Manage routing tables and their associated routes. + +This API is currently in a private alpha stage. To request access, +please contact your Account Manager or open 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 describe](./stackit_routing-table_describe.md) - Describe a routing-table +* [stackit routing-table list](./stackit_routing-table_list.md) - List all routing-tables +* [stackit routing-table route](./stackit_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_routing-table_describe.md b/docs/stackit_routing-table_describe.md new file mode 100644 index 000000000..923a14aef --- /dev/null +++ b/docs/stackit_routing-table_describe.md @@ -0,0 +1,42 @@ +## stackit routing-table describe + +Describe a routing-table + +### Synopsis + +Describe a routing-table + +``` +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 +``` + +### Options + +``` + -h, --help Help for "stackit 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 routing-table](./stackit_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_routing-table_list.md b/docs/stackit_routing-table_list.md new file mode 100644 index 000000000..07e15e5c0 --- /dev/null +++ b/docs/stackit_routing-table_list.md @@ -0,0 +1,50 @@ +## stackit routing-table list + +List all routing-tables + +### Synopsis + +List all routing-tables + +``` +stackit 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 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 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..93b916e5d --- /dev/null +++ b/docs/stackit_routing-table_route.md @@ -0,0 +1,38 @@ +## stackit routing-table route + +Manage routes of a routing-table + +### Synopsis + +Manage 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) - Describe a route within a routing-table +* [stackit routing-table route list](./stackit_routing-table_route_list.md) - list 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_routing-table_route_create.md b/docs/stackit_routing-table_route_create.md new file mode 100644 index 000000000..fc349415e --- /dev/null +++ b/docs/stackit_routing-table_route_create.md @@ -0,0 +1,63 @@ +## stackit routing-table route create + +Creates a route in a routing-table + +### Synopsis + +Creates a route in a routing-table. + +``` +stackit 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 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 routing-table route](./stackit_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_routing-table_route_delete.md b/docs/stackit_routing-table_route_delete.md new file mode 100644 index 000000000..59aef23e0 --- /dev/null +++ b/docs/stackit_routing-table_route_delete.md @@ -0,0 +1,43 @@ +## stackit routing-table route delete + +Deletes a route within a routing-table + +### Synopsis + +Deletes a route within a routing-table + +``` +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 +``` + +### Options + +``` + -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 +``` + +### 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 route](./stackit_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_routing-table_route_describe.md b/docs/stackit_routing-table_route_describe.md new file mode 100644 index 000000000..693db457d --- /dev/null +++ b/docs/stackit_routing-table_route_describe.md @@ -0,0 +1,43 @@ +## stackit routing-table route describe + +Describe a route within a routing-table + +### Synopsis + +Describe a route within a routing-table + +``` +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 +``` + +### Options + +``` + -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 +``` + +### 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 route](./stackit_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_routing-table_route_list.md b/docs/stackit_routing-table_route_list.md new file mode 100644 index 000000000..95e538390 --- /dev/null +++ b/docs/stackit_routing-table_route_list.md @@ -0,0 +1,51 @@ +## stackit routing-table route list + +list all routes within a routing-table + +### Synopsis + +list all routes within a routing-table + +``` +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 + + 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 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 routing-table route](./stackit_routing-table_route.md) - Manage routes of a routing-table + diff --git a/docs/stackit_routing-table_route_update.md b/docs/stackit_routing-table_route_update.md new file mode 100644 index 000000000..c8d537e52 --- /dev/null +++ b/docs/stackit_routing-table_route_update.md @@ -0,0 +1,44 @@ +## stackit routing-table route update + +Updates a route in a routing-table + +### Synopsis + +Updates a route in a routing-table. + +``` +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 +``` + +### Options + +``` + -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 + --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 routing-table route](./stackit_routing-table_route.md) - Manage routes of a routing-table + diff --git a/go.mod b/go.mod index fe8258d95..fb6814142 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 github.com/stackitcloud/stackit-sdk-go/services/git v0.8.0 github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0 + github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2 github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.1 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 diff --git a/go.sum b/go.sum index 64b7cd08f..4eaf0d492 100644 --- a/go.sum +++ b/go.sum @@ -575,6 +575,8 @@ github.com/stackitcloud/stackit-sdk-go/services/git v0.8.0 h1:/weT7P5Uwy1Qlhw0Ni github.com/stackitcloud/stackit-sdk-go/services/git v0.8.0/go.mod h1:AXFfYBJZIW1o0W0zZEb/proQMhMsb3Nn5E1htS8NDPE= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0 h1:dnEjyapuv8WwRN5vE2z6+4/+ZqQTBx+bX27x2nOF7Jw= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0/go.mod h1:854gnLR92NvAbJAA1xZEumrtNh1DoBP1FXTMvhwYA6w= +github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha h1:m1jq6a8dbUe+suFuUNdHmM/cSehpGLUtDbK1CqLqydg= +github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha/go.mod h1:Nu1b5Phsv8plgZ51+fkxPVsU91ZJ5Ayz+cthilxdmQ8= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 h1:q33ZaCBVEBUsnMDxYyuJKtJvGcE5nKgvuPed3s8zXNI= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0/go.mod h1:20QOZ3rBC9wTGgzXzLz9M6YheX0VaxWE0/JI+s8On7k= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1 h1:hv5WrRU9rN6Jx4OwdOGJRyaQrfA9p1tzEoQK6/CDyoA= diff --git a/internal/cmd/beta/alb/create/create_test.go b/internal/cmd/beta/alb/create/create_test.go index a3b7c0725..8fd5d9ef9 100644 --- a/internal/cmd/beta/alb/create/create_test.go +++ b/internal/cmd/beta/alb/create/create_test.go @@ -28,7 +28,9 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &alb.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" + var testConfig = "testdata/testconfig.json" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { diff --git a/internal/cmd/beta/sqlserverflex/database/create/create_test.go b/internal/cmd/beta/sqlserverflex/database/create/create_test.go index 23001c28c..dc9877e29 100644 --- a/internal/cmd/beta/sqlserverflex/database/create/create_test.go +++ b/internal/cmd/beta/sqlserverflex/database/create/create_test.go @@ -22,7 +22,8 @@ var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testDatabaseName = "my-database" var testOwner = "owner" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/beta/sqlserverflex/database/delete/delete_test.go b/internal/cmd/beta/sqlserverflex/database/delete/delete_test.go index ab137dcd0..038e28221 100644 --- a/internal/cmd/beta/sqlserverflex/database/delete/delete_test.go +++ b/internal/cmd/beta/sqlserverflex/database/delete/delete_test.go @@ -20,7 +20,8 @@ var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testDatabaseName = "my-database" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go b/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go index b77f11679..2a4511171 100644 --- a/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go +++ b/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go @@ -21,7 +21,8 @@ var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testDatabaseName = "my-database" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/beta/sqlserverflex/database/list/list_test.go b/internal/cmd/beta/sqlserverflex/database/list/list_test.go index ae340d0db..4aa5d06f2 100644 --- a/internal/cmd/beta/sqlserverflex/database/list/list_test.go +++ b/internal/cmd/beta/sqlserverflex/database/list/list_test.go @@ -21,7 +21,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/beta/sqlserverflex/instance/create/create_test.go b/internal/cmd/beta/sqlserverflex/instance/create/create_test.go index 40481f963..fd5991af2 100644 --- a/internal/cmd/beta/sqlserverflex/instance/create/create_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/create/create_test.go @@ -20,7 +20,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &sqlserverflex.APIClient{} -var testRegion = "eu01" + +const testRegion = "eu01" // enforce implementation of interfaces var ( diff --git a/internal/cmd/beta/sqlserverflex/instance/delete/delete_test.go b/internal/cmd/beta/sqlserverflex/instance/delete/delete_test.go index fe66b190b..212719c28 100644 --- a/internal/cmd/beta/sqlserverflex/instance/delete/delete_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/delete/delete_test.go @@ -19,7 +19,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/beta/sqlserverflex/instance/describe/describe_test.go b/internal/cmd/beta/sqlserverflex/instance/describe/describe_test.go index 66676b001..5984f24af 100644 --- a/internal/cmd/beta/sqlserverflex/instance/describe/describe_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/describe/describe_test.go @@ -20,7 +20,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/beta/sqlserverflex/instance/list/list_test.go b/internal/cmd/beta/sqlserverflex/instance/list/list_test.go index 59711594b..a13de50b0 100644 --- a/internal/cmd/beta/sqlserverflex/instance/list/list_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/list/list_test.go @@ -20,7 +20,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/beta/sqlserverflex/instance/update/update_test.go b/internal/cmd/beta/sqlserverflex/instance/update/update_test.go index 5db0655ba..fa84944bf 100644 --- a/internal/cmd/beta/sqlserverflex/instance/update/update_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/update/update_test.go @@ -19,7 +19,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &sqlserverflex.APIClient{} -var testRegion = "eu01" + +const testRegion = "eu01" // enforce implementation of interfaces var ( diff --git a/internal/cmd/beta/sqlserverflex/user/create/create_test.go b/internal/cmd/beta/sqlserverflex/user/create/create_test.go index ad5df9a27..218ab5514 100644 --- a/internal/cmd/beta/sqlserverflex/user/create/create_test.go +++ b/internal/cmd/beta/sqlserverflex/user/create/create_test.go @@ -21,7 +21,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/beta/sqlserverflex/user/delete/delete_test.go b/internal/cmd/beta/sqlserverflex/user/delete/delete_test.go index 9220bcbfc..ee797c8c4 100644 --- a/internal/cmd/beta/sqlserverflex/user/delete/delete_test.go +++ b/internal/cmd/beta/sqlserverflex/user/delete/delete_test.go @@ -20,7 +20,8 @@ var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "my-user-id" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/beta/sqlserverflex/user/describe/describe_test.go b/internal/cmd/beta/sqlserverflex/user/describe/describe_test.go index e67aeb3b9..76fedb359 100644 --- a/internal/cmd/beta/sqlserverflex/user/describe/describe_test.go +++ b/internal/cmd/beta/sqlserverflex/user/describe/describe_test.go @@ -21,7 +21,8 @@ var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "my-user-id" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/beta/sqlserverflex/user/list/list_test.go b/internal/cmd/beta/sqlserverflex/user/list/list_test.go index ef80e6252..f52154311 100644 --- a/internal/cmd/beta/sqlserverflex/user/list/list_test.go +++ b/internal/cmd/beta/sqlserverflex/user/list/list_test.go @@ -21,7 +21,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password_test.go b/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password_test.go index b15b7069e..37a29ff42 100644 --- a/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password_test.go +++ b/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password_test.go @@ -21,7 +21,8 @@ var testClient = &sqlserverflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "my-user-id" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/object-storage/bucket/create/create_test.go b/internal/cmd/object-storage/bucket/create/create_test.go index b6e926cd2..03ec3d874 100644 --- a/internal/cmd/object-storage/bucket/create/create_test.go +++ b/internal/cmd/object-storage/bucket/create/create_test.go @@ -23,7 +23,9 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" + var testBucketName = "my-bucket" func fixtureArgValues(mods ...func(argValues []string)) []string { diff --git a/internal/cmd/object-storage/bucket/delete/delete_test.go b/internal/cmd/object-storage/bucket/delete/delete_test.go index 2dfe69ec6..bf546d178 100644 --- a/internal/cmd/object-storage/bucket/delete/delete_test.go +++ b/internal/cmd/object-storage/bucket/delete/delete_test.go @@ -21,7 +21,9 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" + var testBucketName = "my-bucket" func fixtureArgValues(mods ...func(argValues []string)) []string { diff --git a/internal/cmd/object-storage/bucket/describe/describe_test.go b/internal/cmd/object-storage/bucket/describe/describe_test.go index df76123d2..d84cdec6e 100644 --- a/internal/cmd/object-storage/bucket/describe/describe_test.go +++ b/internal/cmd/object-storage/bucket/describe/describe_test.go @@ -23,7 +23,9 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" + var testBucketName = "my-bucket" func fixtureArgValues(mods ...func(argValues []string)) []string { diff --git a/internal/cmd/object-storage/bucket/list/list_test.go b/internal/cmd/object-storage/bucket/list/list_test.go index 3d70ff50b..7591d6517 100644 --- a/internal/cmd/object-storage/bucket/list/list_test.go +++ b/internal/cmd/object-storage/bucket/list/list_test.go @@ -24,7 +24,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/object-storage/credentials-group/create/create_test.go b/internal/cmd/object-storage/credentials-group/create/create_test.go index 23908c876..4b6d939a2 100644 --- a/internal/cmd/object-storage/credentials-group/create/create_test.go +++ b/internal/cmd/object-storage/credentials-group/create/create_test.go @@ -25,7 +25,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() var testCredentialsGroupName = "test-name" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/object-storage/credentials-group/delete/delete_test.go b/internal/cmd/object-storage/credentials-group/delete/delete_test.go index 901fc4304..651bf2232 100644 --- a/internal/cmd/object-storage/credentials-group/delete/delete_test.go +++ b/internal/cmd/object-storage/credentials-group/delete/delete_test.go @@ -22,7 +22,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() var testCredentialsGroupId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/object-storage/credentials-group/list/list_test.go b/internal/cmd/object-storage/credentials-group/list/list_test.go index 587e6842a..e7d690633 100644 --- a/internal/cmd/object-storage/credentials-group/list/list_test.go +++ b/internal/cmd/object-storage/credentials-group/list/list_test.go @@ -24,7 +24,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/object-storage/credentials/create/create_test.go b/internal/cmd/object-storage/credentials/create/create_test.go index f3c0bdd60..873de4878 100644 --- a/internal/cmd/object-storage/credentials/create/create_test.go +++ b/internal/cmd/object-storage/credentials/create/create_test.go @@ -27,7 +27,8 @@ var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() var testCredentialsGroupId = uuid.NewString() var testExpirationDate = "2024-01-01T00:00:00Z" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/object-storage/credentials/delete/delete_test.go b/internal/cmd/object-storage/credentials/delete/delete_test.go index d5ff9e4e5..a207f06fe 100644 --- a/internal/cmd/object-storage/credentials/delete/delete_test.go +++ b/internal/cmd/object-storage/credentials/delete/delete_test.go @@ -23,7 +23,8 @@ var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() var testCredentialsGroupId = uuid.NewString() var testCredentialsId = "keyID" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/object-storage/credentials/list/list_test.go b/internal/cmd/object-storage/credentials/list/list_test.go index 119f7c7c0..249ea3df2 100644 --- a/internal/cmd/object-storage/credentials/list/list_test.go +++ b/internal/cmd/object-storage/credentials/list/list_test.go @@ -25,7 +25,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() var testCredentialsGroupId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/object-storage/disable/disable_test.go b/internal/cmd/object-storage/disable/disable_test.go index 5b5a2ea7f..4f9abe380 100644 --- a/internal/cmd/object-storage/disable/disable_test.go +++ b/internal/cmd/object-storage/disable/disable_test.go @@ -21,7 +21,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/object-storage/enable/enable_test.go b/internal/cmd/object-storage/enable/enable_test.go index 59e88e9e0..cd982b8d1 100644 --- a/internal/cmd/object-storage/enable/enable_test.go +++ b/internal/cmd/object-storage/enable/enable_test.go @@ -21,7 +21,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &objectstorage.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/postgresflex/backup/describe/describe_test.go b/internal/cmd/postgresflex/backup/describe/describe_test.go index 6e47eabb0..2cd39279f 100644 --- a/internal/cmd/postgresflex/backup/describe/describe_test.go +++ b/internal/cmd/postgresflex/backup/describe/describe_test.go @@ -22,7 +22,8 @@ var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testBackupId = "backupID" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/postgresflex/backup/list/list_test.go b/internal/cmd/postgresflex/backup/list/list_test.go index 4ac63c37c..418701b5b 100644 --- a/internal/cmd/postgresflex/backup/list/list_test.go +++ b/internal/cmd/postgresflex/backup/list/list_test.go @@ -23,7 +23,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go b/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go index 18ed95ae6..13a5072fb 100644 --- a/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go +++ b/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go @@ -21,7 +21,8 @@ var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testSchedule = "0 0 * * *" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/postgresflex/instance/clone/clone_test.go b/internal/cmd/postgresflex/instance/clone/clone_test.go index 5f2a60189..8dfaedfd3 100644 --- a/internal/cmd/postgresflex/instance/clone/clone_test.go +++ b/internal/cmd/postgresflex/instance/clone/clone_test.go @@ -53,7 +53,8 @@ var testRecoveryTimestamp = "2024-03-08T09:28:00+00:00" var testFlavorId = uuid.NewString() var testStorageClass = "premium-perf4-stackit" var testStorageSize = int64(10) -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/postgresflex/instance/create/create_test.go b/internal/cmd/postgresflex/instance/create/create_test.go index 09b6b2311..a3c716f26 100644 --- a/internal/cmd/postgresflex/instance/create/create_test.go +++ b/internal/cmd/postgresflex/instance/create/create_test.go @@ -20,7 +20,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{} -var testRegion = "eu01" + +const testRegion = "eu01" type postgresFlexClientMocked struct { listFlavorsFails bool diff --git a/internal/cmd/postgresflex/instance/delete/delete_test.go b/internal/cmd/postgresflex/instance/delete/delete_test.go index fdbe26f4c..23a9689a1 100644 --- a/internal/cmd/postgresflex/instance/delete/delete_test.go +++ b/internal/cmd/postgresflex/instance/delete/delete_test.go @@ -22,7 +22,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" type postgresFlexClientMocked struct { getInstanceFails bool diff --git a/internal/cmd/postgresflex/instance/describe/describe_test.go b/internal/cmd/postgresflex/instance/describe/describe_test.go index 9331b81d4..c6a50b7cf 100644 --- a/internal/cmd/postgresflex/instance/describe/describe_test.go +++ b/internal/cmd/postgresflex/instance/describe/describe_test.go @@ -20,7 +20,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/postgresflex/instance/list/list_test.go b/internal/cmd/postgresflex/instance/list/list_test.go index c8c2d8a7a..341863b27 100644 --- a/internal/cmd/postgresflex/instance/list/list_test.go +++ b/internal/cmd/postgresflex/instance/list/list_test.go @@ -20,7 +20,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/postgresflex/instance/update/update_test.go b/internal/cmd/postgresflex/instance/update/update_test.go index 0c4ed96b6..efc66090e 100644 --- a/internal/cmd/postgresflex/instance/update/update_test.go +++ b/internal/cmd/postgresflex/instance/update/update_test.go @@ -19,7 +19,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{} -var testRegion = "eu01" + +const testRegion = "eu01" type postgresFlexClientMocked struct { listFlavorsFails bool diff --git a/internal/cmd/postgresflex/user/create/create_test.go b/internal/cmd/postgresflex/user/create/create_test.go index bbd08e214..bfeded7ad 100644 --- a/internal/cmd/postgresflex/user/create/create_test.go +++ b/internal/cmd/postgresflex/user/create/create_test.go @@ -21,7 +21,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/postgresflex/user/delete/delete_test.go b/internal/cmd/postgresflex/user/delete/delete_test.go index a3ff66105..8b91f23eb 100644 --- a/internal/cmd/postgresflex/user/delete/delete_test.go +++ b/internal/cmd/postgresflex/user/delete/delete_test.go @@ -20,7 +20,8 @@ var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "12345" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/postgresflex/user/describe/describe_test.go b/internal/cmd/postgresflex/user/describe/describe_test.go index ad6a2377f..b137bf2c1 100644 --- a/internal/cmd/postgresflex/user/describe/describe_test.go +++ b/internal/cmd/postgresflex/user/describe/describe_test.go @@ -21,7 +21,8 @@ var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "12345" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/postgresflex/user/list/list_test.go b/internal/cmd/postgresflex/user/list/list_test.go index 8985f945f..fb077b494 100644 --- a/internal/cmd/postgresflex/user/list/list_test.go +++ b/internal/cmd/postgresflex/user/list/list_test.go @@ -21,7 +21,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/postgresflex/user/reset-password/reset_password_test.go b/internal/cmd/postgresflex/user/reset-password/reset_password_test.go index 51572b406..3692a0564 100644 --- a/internal/cmd/postgresflex/user/reset-password/reset_password_test.go +++ b/internal/cmd/postgresflex/user/reset-password/reset_password_test.go @@ -21,7 +21,8 @@ var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "12345" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/postgresflex/user/update/update_test.go b/internal/cmd/postgresflex/user/update/update_test.go index c20ded9b7..b7467b83d 100644 --- a/internal/cmd/postgresflex/user/update/update_test.go +++ b/internal/cmd/postgresflex/user/update/update_test.go @@ -21,7 +21,8 @@ var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "12345" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ 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/describe/describe.go b/internal/cmd/routingtable/describe/describe.go new file mode 100644 index 000000000..d6820b49f --- /dev/null +++ b/internal/cmd/routingtable/describe/describe.go @@ -0,0 +1,138 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "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 { + if routingTable == nil { + return fmt.Errorf("describe routingtable response is empty") + } + + 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)) + } + + 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/routingtable/describe/describe_test.go b/internal/cmd/routingtable/describe/describe_test.go new file mode 100644 index 000000000..e2cf88299 --- /dev/null +++ b/internal/cmd/routingtable/describe/describe_test.go @@ -0,0 +1,152 @@ +package describe + +import ( + "testing" + "time" + + "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" +) + +const 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) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +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/routingtable/list/list.go b/internal/cmd/routingtable/list/list.go new file mode 100644 index 000000000..beb66878f --- /dev/null +++ b/internal/cmd/routingtable/list/list.go @@ -0,0 +1,186 @@ +package list + +import ( + "context" + "fmt" + "strings" + + "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, nil) + 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, _ []string) (*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 { + if len(items) == 0 { + return fmt.Errorf("list routingtable response is empty") + } + + return p.OutputResult(outputFormat, items, func() error { + 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/routingtable/list/list_test.go b/internal/cmd/routingtable/list/list_test.go new file mode 100644 index 000000000..716ad324f --- /dev/null +++ b/internal/cmd/routingtable/list/list_test.go @@ -0,0 +1,179 @@ +package list + +import ( + "strconv" + "testing" + "time" + + "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" +) + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() + +const 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 + argValues []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) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +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/routingtable/route/create/create.go b/internal/cmd/routingtable/route/create/create.go new file mode 100644 index 000000000..7d496d2ae --- /dev/null +++ b/internal/cmd/routingtable/route/create/create.go @@ -0,0 +1,278 @@ +package create + +import ( + "context" + "errors" + "fmt" + "strings" + + "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, nil) + 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, _ []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), + 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") + } + + 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) + + table.AddRow( + utils.PtrString(item.Id), + destType, + destValue, + hopType, + hopValue, + labels, + item.CreatedAt.String(), + item.UpdatedAt.String(), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/routingtable/route/create/create_test.go b/internal/cmd/routingtable/route/create/create_test.go new file mode 100644 index 000000000..926b60b28 --- /dev/null +++ b/internal/cmd/routingtable/route/create/create_test.go @@ -0,0 +1,639 @@ +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/testutils" + "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{} + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +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", +} + +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 + argValues []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) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +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/routingtable/route/delete/delete.go b/internal/cmd/routingtable/route/delete/delete.go new file mode 100644 index 000000000..cf329ca6a --- /dev/null +++ b/internal/cmd/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/routingtable/route/delete/delete_test.go b/internal/cmd/routingtable/route/delete/delete_test.go new file mode 100644 index 000000000..d80b53a78 --- /dev/null +++ b/internal/cmd/routingtable/route/delete/delete_test.go @@ -0,0 +1,100 @@ +package delete + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +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 + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + argValues: []string{testRouteId}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(func(m *inputModel) { + m.RouteID = &testRouteId + }), + }, + { + description: "missing route id arg", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing organization-id flag", + argValues: []string{testRouteId}, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, "organization-id") + }), + isValid: false, + }, + { + description: "missing network-area-id flag", + argValues: []string{testRouteId}, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, "network-area-id") + }), + isValid: false, + }, + { + description: "missing routing-table-id flag", + argValues: []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) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} diff --git a/internal/cmd/routingtable/route/describe/describe.go b/internal/cmd/routingtable/route/describe/describe.go new file mode 100644 index 000000000..da28a38b6 --- /dev/null +++ b/internal/cmd/routingtable/route/describe/describe.go @@ -0,0 +1,266 @@ +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 { + if routingTable == nil { + return fmt.Errorf("describe routes response is empty") + } + + 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)) + } + + 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 + }) + + 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/routingtable/route/describe/describe_test.go b/internal/cmd/routingtable/route/describe/describe_test.go new file mode 100644 index 000000000..dd407da6a --- /dev/null +++ b/internal/cmd/routingtable/route/describe/describe_test.go @@ -0,0 +1,187 @@ +package describe + +import ( + "testing" + "time" + + "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" +) + +const 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) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +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/routingtable/route/list/list.go b/internal/cmd/routingtable/route/list/list.go new file mode 100644 index 000000000..cdbd8d31a --- /dev/null +++ b/internal/cmd/routingtable/route/list/list.go @@ -0,0 +1,172 @@ +package list + +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/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.GetItems() + 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 { + if len(items) == 0 { + return fmt.Errorf("create routes response is empty") + } + + 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) + + table.AddRow( + utils.PtrString(item.Id), + destType, + destValue, + hopType, + hopValue, + labels, + item.CreatedAt.String(), + item.UpdatedAt.String(), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/routingtable/route/list/list_test.go b/internal/cmd/routingtable/route/list/list_test.go new file mode 100644 index 000000000..2b840146c --- /dev/null +++ b/internal/cmd/routingtable/route/list/list_test.go @@ -0,0 +1,242 @@ +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" +) + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +const 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/routingtable/route/route.go b/internal/cmd/routingtable/route/route.go new file mode 100644 index 000000000..b3203e239 --- /dev/null +++ b/internal/cmd/routingtable/route/route.go @@ -0,0 +1,33 @@ +package route + +import ( + "github.com/spf13/cobra" + "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" +) + +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/routingtable/route/update/update.go b/internal/cmd/routingtable/route/update/update.go new file mode 100644 index 000000000..b9bffaf4c --- /dev/null +++ b/internal/cmd/routingtable/route/update/update.go @@ -0,0 +1,135 @@ +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" + 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 { + if &route == nil { + return fmt.Errorf("update route response is empty") + } + + 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/routingtable/route/update/update_test.go b/internal/cmd/routingtable/route/update/update_test.go new file mode 100644 index 000000000..91e9e46d4 --- /dev/null +++ b/internal/cmd/routingtable/route/update/update_test.go @@ -0,0 +1,199 @@ +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" + "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" +) + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() +var testRouteId = uuid.NewString() + +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, + 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) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +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/routingtable/routingtable.go b/internal/cmd/routingtable/routingtable.go new file mode 100644 index 000000000..c130bd942 --- /dev/null +++ b/internal/cmd/routingtable/routingtable.go @@ -0,0 +1,34 @@ +package routingtable + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + 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" + "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 in a private alpha stage. To request access, +please contact your Account Manager or open a support ticket.`, + 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/server/backup/create/create_test.go b/internal/cmd/server/backup/create/create_test.go index 9f7799a7f..d8c72d8c0 100644 --- a/internal/cmd/server/backup/create/create_test.go +++ b/internal/cmd/server/backup/create/create_test.go @@ -24,7 +24,8 @@ var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupVolumeId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/server/backup/delete/delete_test.go b/internal/cmd/server/backup/delete/delete_test.go index 1b90f5c00..fcd996e55 100644 --- a/internal/cmd/server/backup/delete/delete_test.go +++ b/internal/cmd/server/backup/delete/delete_test.go @@ -20,7 +20,8 @@ var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/server/backup/describe/describe_test.go b/internal/cmd/server/backup/describe/describe_test.go index 2e3c10af2..3b319e083 100644 --- a/internal/cmd/server/backup/describe/describe_test.go +++ b/internal/cmd/server/backup/describe/describe_test.go @@ -22,7 +22,8 @@ var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/server/backup/disable/disable_test.go b/internal/cmd/server/backup/disable/disable_test.go index 9e74ae6ee..d52723811 100644 --- a/internal/cmd/server/backup/disable/disable_test.go +++ b/internal/cmd/server/backup/disable/disable_test.go @@ -19,7 +19,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/server/backup/enable/enable_test.go b/internal/cmd/server/backup/enable/enable_test.go index b7035a762..36db40cb6 100644 --- a/internal/cmd/server/backup/enable/enable_test.go +++ b/internal/cmd/server/backup/enable/enable_test.go @@ -19,7 +19,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/server/backup/list/list_test.go b/internal/cmd/server/backup/list/list_test.go index a265c6779..e827aa3bc 100644 --- a/internal/cmd/server/backup/list/list_test.go +++ b/internal/cmd/server/backup/list/list_test.go @@ -22,7 +22,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/server/backup/restore/restore_test.go b/internal/cmd/server/backup/restore/restore_test.go index 9f890f3c2..26cae55c8 100644 --- a/internal/cmd/server/backup/restore/restore_test.go +++ b/internal/cmd/server/backup/restore/restore_test.go @@ -20,7 +20,8 @@ var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/server/backup/schedule/create/create_test.go b/internal/cmd/server/backup/schedule/create/create_test.go index 5f4d51733..b0a92437f 100644 --- a/internal/cmd/server/backup/schedule/create/create_test.go +++ b/internal/cmd/server/backup/schedule/create/create_test.go @@ -24,7 +24,8 @@ var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testVolumeId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/server/backup/schedule/delete/delete_test.go b/internal/cmd/server/backup/schedule/delete/delete_test.go index 5e0f108ca..7cd8999ff 100644 --- a/internal/cmd/server/backup/schedule/delete/delete_test.go +++ b/internal/cmd/server/backup/schedule/delete/delete_test.go @@ -20,7 +20,8 @@ var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupScheduleId = "5" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/server/backup/schedule/describe/describe_test.go b/internal/cmd/server/backup/schedule/describe/describe_test.go index 5d1f51e3a..8a3087770 100644 --- a/internal/cmd/server/backup/schedule/describe/describe_test.go +++ b/internal/cmd/server/backup/schedule/describe/describe_test.go @@ -21,7 +21,8 @@ var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupScheduleId = "5" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/server/backup/schedule/list/list_test.go b/internal/cmd/server/backup/schedule/list/list_test.go index 2234b0397..20d2b1b02 100644 --- a/internal/cmd/server/backup/schedule/list/list_test.go +++ b/internal/cmd/server/backup/schedule/list/list_test.go @@ -22,7 +22,8 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &serverbackup.APIClient{} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/server/backup/schedule/update/update_test.go b/internal/cmd/server/backup/schedule/update/update_test.go index 263a9188a..3f069897a 100644 --- a/internal/cmd/server/backup/schedule/update/update_test.go +++ b/internal/cmd/server/backup/schedule/update/update_test.go @@ -24,7 +24,8 @@ var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testVolumeId = uuid.NewString() var testBackupScheduleId = "5" -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/server/backup/volume-backup/delete/delete_test.go b/internal/cmd/server/backup/volume-backup/delete/delete_test.go index 0a1da5857..428b4cb6f 100644 --- a/internal/cmd/server/backup/volume-backup/delete/delete_test.go +++ b/internal/cmd/server/backup/volume-backup/delete/delete_test.go @@ -21,7 +21,8 @@ var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() var testVolumeId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/server/backup/volume-backup/restore/restore_test.go b/internal/cmd/server/backup/volume-backup/restore/restore_test.go index f329161db..6096b7048 100644 --- a/internal/cmd/server/backup/volume-backup/restore/restore_test.go +++ b/internal/cmd/server/backup/volume-backup/restore/restore_test.go @@ -22,7 +22,8 @@ var testServerId = uuid.NewString() var testBackupId = uuid.NewString() var testVolumeBackupId = uuid.NewString() var testRestoreVolumeId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ diff --git a/internal/cmd/ske/describe/describe_test.go b/internal/cmd/ske/describe/describe_test.go index 3a0283a6c..e73bd5a9b 100644 --- a/internal/cmd/ske/describe/describe_test.go +++ b/internal/cmd/ske/describe/describe_test.go @@ -21,7 +21,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &serviceenablement.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/ske/disable/disable_test.go b/internal/cmd/ske/disable/disable_test.go index 978e383ed..415795aab 100644 --- a/internal/cmd/ske/disable/disable_test.go +++ b/internal/cmd/ske/disable/disable_test.go @@ -19,7 +19,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &serviceenablement.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ diff --git a/internal/cmd/ske/enable/enable_test.go b/internal/cmd/ske/enable/enable_test.go index add7b850b..09fab6d98 100644 --- a/internal/cmd/ske/enable/enable_test.go +++ b/internal/cmd/ske/enable/enable_test.go @@ -19,7 +19,8 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &serviceenablement.APIClient{} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ 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) + } + }) + } +}