From 15f41709dcf00b64571799c6479305554deb59f7 Mon Sep 17 00:00:00 2001 From: Adrian Nackov Date: Wed, 15 Jan 2025 07:22:39 +0000 Subject: [PATCH] ref 700848 - server os-update Signed-off-by: Adrian Nackov --- docs/stackit_beta_server.md | 1 + docs/stackit_beta_server_os-update.md | 39 +++ docs/stackit_beta_server_os-update_create.md | 45 +++ .../stackit_beta_server_os-update_describe.md | 44 +++ docs/stackit_beta_server_os-update_disable.md | 41 +++ docs/stackit_beta_server_os-update_enable.md | 41 +++ docs/stackit_beta_server_os-update_list.md | 45 +++ .../stackit_beta_server_os-update_schedule.md | 38 +++ ...t_beta_server_os-update_schedule_create.md | 48 ++++ ...t_beta_server_os-update_schedule_delete.md | 41 +++ ...beta_server_os-update_schedule_describe.md | 44 +++ ...kit_beta_server_os-update_schedule_list.md | 45 +++ ...t_beta_server_os-update_schedule_update.md | 45 +++ docs/stackit_config_set.md | 1 + docs/stackit_config_unset.md | 1 + go.mod | 1 + go.sum | 2 + .../beta/server/os-update/create/create.go | 147 ++++++++++ .../server/os-update/create/create_test.go | 205 ++++++++++++++ .../server/os-update/describe/describe.go | 162 +++++++++++ .../os-update/describe/describe_test.go | 211 ++++++++++++++ .../beta/server/os-update/disable/disable.go | 108 +++++++ .../server/os-update/disable/disable_test.go | 173 ++++++++++++ .../beta/server/os-update/enable/enable.go | 112 ++++++++ .../server/os-update/enable/enable_test.go | 173 ++++++++++++ .../cmd/beta/server/os-update/list/list.go | 177 ++++++++++++ .../beta/server/os-update/list/list_test.go | 188 +++++++++++++ .../cmd/beta/server/os-update/os-update.go | 36 +++ .../os-update/schedule/create/create.go | 167 +++++++++++ .../os-update/schedule/create/create_test.go | 212 ++++++++++++++ .../os-update/schedule/delete/delete.go | 113 ++++++++ .../os-update/schedule/delete/delete_test.go | 209 ++++++++++++++ .../os-update/schedule/describe/describe.go | 149 ++++++++++ .../schedule/describe/describe_test.go | 211 ++++++++++++++ .../server/os-update/schedule/list/list.go | 160 +++++++++++ .../os-update/schedule/list/list_test.go | 188 +++++++++++++ .../server/os-update/schedule/schedule.go | 34 +++ .../os-update/schedule/update/update.go | 191 +++++++++++++ .../os-update/schedule/update/update_test.go | 265 ++++++++++++++++++ internal/cmd/beta/server/server.go | 2 + internal/cmd/config/set/set.go | 4 + internal/cmd/config/unset/unset.go | 7 + internal/cmd/config/unset/unset_test.go | 13 + internal/pkg/config/config.go | 3 + .../services/serverosupdate/client/client.go | 46 +++ 45 files changed, 4188 insertions(+) create mode 100644 docs/stackit_beta_server_os-update.md create mode 100644 docs/stackit_beta_server_os-update_create.md create mode 100644 docs/stackit_beta_server_os-update_describe.md create mode 100644 docs/stackit_beta_server_os-update_disable.md create mode 100644 docs/stackit_beta_server_os-update_enable.md create mode 100644 docs/stackit_beta_server_os-update_list.md create mode 100644 docs/stackit_beta_server_os-update_schedule.md create mode 100644 docs/stackit_beta_server_os-update_schedule_create.md create mode 100644 docs/stackit_beta_server_os-update_schedule_delete.md create mode 100644 docs/stackit_beta_server_os-update_schedule_describe.md create mode 100644 docs/stackit_beta_server_os-update_schedule_list.md create mode 100644 docs/stackit_beta_server_os-update_schedule_update.md create mode 100644 internal/cmd/beta/server/os-update/create/create.go create mode 100644 internal/cmd/beta/server/os-update/create/create_test.go create mode 100644 internal/cmd/beta/server/os-update/describe/describe.go create mode 100644 internal/cmd/beta/server/os-update/describe/describe_test.go create mode 100644 internal/cmd/beta/server/os-update/disable/disable.go create mode 100644 internal/cmd/beta/server/os-update/disable/disable_test.go create mode 100644 internal/cmd/beta/server/os-update/enable/enable.go create mode 100644 internal/cmd/beta/server/os-update/enable/enable_test.go create mode 100644 internal/cmd/beta/server/os-update/list/list.go create mode 100644 internal/cmd/beta/server/os-update/list/list_test.go create mode 100644 internal/cmd/beta/server/os-update/os-update.go create mode 100644 internal/cmd/beta/server/os-update/schedule/create/create.go create mode 100644 internal/cmd/beta/server/os-update/schedule/create/create_test.go create mode 100644 internal/cmd/beta/server/os-update/schedule/delete/delete.go create mode 100644 internal/cmd/beta/server/os-update/schedule/delete/delete_test.go create mode 100644 internal/cmd/beta/server/os-update/schedule/describe/describe.go create mode 100644 internal/cmd/beta/server/os-update/schedule/describe/describe_test.go create mode 100644 internal/cmd/beta/server/os-update/schedule/list/list.go create mode 100644 internal/cmd/beta/server/os-update/schedule/list/list_test.go create mode 100644 internal/cmd/beta/server/os-update/schedule/schedule.go create mode 100644 internal/cmd/beta/server/os-update/schedule/update/update.go create mode 100644 internal/cmd/beta/server/os-update/schedule/update/update_test.go create mode 100644 internal/pkg/services/serverosupdate/client/client.go diff --git a/docs/stackit_beta_server.md b/docs/stackit_beta_server.md index d3abb34f4..a56612e08 100644 --- a/docs/stackit_beta_server.md +++ b/docs/stackit_beta_server.md @@ -40,6 +40,7 @@ stackit beta server [flags] * [stackit beta server list](./stackit_beta_server_list.md) - Lists all servers of a project * [stackit beta server log](./stackit_beta_server_log.md) - Gets server console log * [stackit beta server network-interface](./stackit_beta_server_network-interface.md) - Allows attaching/detaching network interfaces to servers +* [stackit beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates * [stackit beta server public-ip](./stackit_beta_server_public-ip.md) - Allows attaching/detaching public IPs to servers * [stackit beta server reboot](./stackit_beta_server_reboot.md) - Reboots a server * [stackit beta server rescue](./stackit_beta_server_rescue.md) - Rescues an existing server diff --git a/docs/stackit_beta_server_os-update.md b/docs/stackit_beta_server_os-update.md new file mode 100644 index 000000000..0cf8358b9 --- /dev/null +++ b/docs/stackit_beta_server_os-update.md @@ -0,0 +1,39 @@ +## stackit beta server os-update + +Provides functionality for managed server updates + +### Synopsis + +Provides functionality for managed server updates. + +``` +stackit beta server os-update [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers +* [stackit beta server os-update create](./stackit_beta_server_os-update_create.md) - Creates a Server os-update. +* [stackit beta server os-update describe](./stackit_beta_server_os-update_describe.md) - Shows details of a Server os-update +* [stackit beta server os-update disable](./stackit_beta_server_os-update_disable.md) - Disables server os-update service +* [stackit beta server os-update enable](./stackit_beta_server_os-update_enable.md) - Enables Server os-update service +* [stackit beta server os-update list](./stackit_beta_server_os-update_list.md) - Lists all server os-updates +* [stackit beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule + diff --git a/docs/stackit_beta_server_os-update_create.md b/docs/stackit_beta_server_os-update_create.md new file mode 100644 index 000000000..fcfa3efb6 --- /dev/null +++ b/docs/stackit_beta_server_os-update_create.md @@ -0,0 +1,45 @@ +## stackit beta server os-update create + +Creates a Server os-update. + +### Synopsis + +Creates a Server os-update. Operation always is async. + +``` +stackit beta server os-update create [flags] +``` + +### Examples + +``` + Create a Server os-update with name "myupdate" + $ stackit beta server os-update create --server-id xxx --name=myupdate + + Create a Server os-update with name "myupdate" and maintenance window for 13 o'clock. + $ stackit beta server os-update create --server-id xxx --name=mybupdate --maintenance-window=13 +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update create" + -m, --maintenance-window int Maintenance window (in hours, 1-24) (default 23) + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates + diff --git a/docs/stackit_beta_server_os-update_describe.md b/docs/stackit_beta_server_os-update_describe.md new file mode 100644 index 000000000..a73f04dd7 --- /dev/null +++ b/docs/stackit_beta_server_os-update_describe.md @@ -0,0 +1,44 @@ +## stackit beta server os-update describe + +Shows details of a Server os-update + +### Synopsis + +Shows details of a Server os-update. + +``` +stackit beta server os-update describe UPDATE_ID [flags] +``` + +### Examples + +``` + Get details of a Server os-update with id "my-os-update-id" + $ stackit beta server os-update describe my-os-update-id + + Get details of a Server os-update with id "my-os-update-id" in JSON format + $ stackit beta server os-update describe my-os-update-id --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update describe" + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates + diff --git a/docs/stackit_beta_server_os-update_disable.md b/docs/stackit_beta_server_os-update_disable.md new file mode 100644 index 000000000..477d42214 --- /dev/null +++ b/docs/stackit_beta_server_os-update_disable.md @@ -0,0 +1,41 @@ +## stackit beta server os-update disable + +Disables server os-update service + +### Synopsis + +Disables server os-update service. + +``` +stackit beta server os-update disable [flags] +``` + +### Examples + +``` + Disable os-update functionality for your server. + $ stackit beta server os-update disable --server-id=zzz +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update disable" + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates + diff --git a/docs/stackit_beta_server_os-update_enable.md b/docs/stackit_beta_server_os-update_enable.md new file mode 100644 index 000000000..661ddaf47 --- /dev/null +++ b/docs/stackit_beta_server_os-update_enable.md @@ -0,0 +1,41 @@ +## stackit beta server os-update enable + +Enables Server os-update service + +### Synopsis + +Enables Server os-update service. + +``` +stackit beta server os-update enable [flags] +``` + +### Examples + +``` + Enable os-update functionality for your server + $ stackit beta server os-update enable --server-id=zzz +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update enable" + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates + diff --git a/docs/stackit_beta_server_os-update_list.md b/docs/stackit_beta_server_os-update_list.md new file mode 100644 index 000000000..57c46d3df --- /dev/null +++ b/docs/stackit_beta_server_os-update_list.md @@ -0,0 +1,45 @@ +## stackit beta server os-update list + +Lists all server os-updates + +### Synopsis + +Lists all server os-updates. + +``` +stackit beta server os-update list [flags] +``` + +### Examples + +``` + List all os-updates for a server with ID "xxx" + $ stackit beta server os-update list --server-id xxx + + List all os-updates for a server with ID "xxx" in JSON format + $ stackit beta server os-update list --server-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update list" + --limit int Maximum number of entries to list + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates + diff --git a/docs/stackit_beta_server_os-update_schedule.md b/docs/stackit_beta_server_os-update_schedule.md new file mode 100644 index 000000000..9579ba4b9 --- /dev/null +++ b/docs/stackit_beta_server_os-update_schedule.md @@ -0,0 +1,38 @@ +## stackit beta server os-update schedule + +Provides functionality for Server os-update Schedule + +### Synopsis + +Provides functionality for Server os-update Schedule. + +``` +stackit beta server os-update schedule [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update schedule" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates +* [stackit beta server os-update schedule create](./stackit_beta_server_os-update_schedule_create.md) - Creates a Server os-update Schedule +* [stackit beta server os-update schedule delete](./stackit_beta_server_os-update_schedule_delete.md) - Deletes a Server os-update Schedule +* [stackit beta server os-update schedule describe](./stackit_beta_server_os-update_schedule_describe.md) - Shows details of a Server os-update Schedule +* [stackit beta server os-update schedule list](./stackit_beta_server_os-update_schedule_list.md) - Lists all server os-update schedules +* [stackit beta server os-update schedule update](./stackit_beta_server_os-update_schedule_update.md) - Updates a Server os-update Schedule + diff --git a/docs/stackit_beta_server_os-update_schedule_create.md b/docs/stackit_beta_server_os-update_schedule_create.md new file mode 100644 index 000000000..529fe2b87 --- /dev/null +++ b/docs/stackit_beta_server_os-update_schedule_create.md @@ -0,0 +1,48 @@ +## stackit beta server os-update schedule create + +Creates a Server os-update Schedule + +### Synopsis + +Creates a Server os-update Schedule. + +``` +stackit beta server os-update schedule create [flags] +``` + +### Examples + +``` + Create a Server os-update Schedule with name "myschedule" + $ stackit beta server os-update schedule create --server-id xxx --name=myschedule + + Create a Server os-update Schedule with name "myschedule" and maintenance window for 14 o'clock + $ stackit beta server os-update schedule create --server-id xxx --name=myschedule --maintenance-window=14 +``` + +### Options + +``` + -e, --enabled Is the server os-update schedule enabled (default true) + -h, --help Help for "stackit beta server os-update schedule create" + -d, --maintenance-window int os-update maintenance window (in hours, 1-24) (default 23) + -n, --name string os-update schedule name + -r, --rrule string os-update RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule + diff --git a/docs/stackit_beta_server_os-update_schedule_delete.md b/docs/stackit_beta_server_os-update_schedule_delete.md new file mode 100644 index 000000000..d2e5e2bc9 --- /dev/null +++ b/docs/stackit_beta_server_os-update_schedule_delete.md @@ -0,0 +1,41 @@ +## stackit beta server os-update schedule delete + +Deletes a Server os-update Schedule + +### Synopsis + +Deletes a Server os-update Schedule. + +``` +stackit beta server os-update schedule delete SCHEDULE_ID [flags] +``` + +### Examples + +``` + Delete a Server os-update Schedule with ID "xxx" for server "zzz" + $ stackit beta server os-update schedule delete xxx --server-id=zzz +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update schedule delete" + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule + diff --git a/docs/stackit_beta_server_os-update_schedule_describe.md b/docs/stackit_beta_server_os-update_schedule_describe.md new file mode 100644 index 000000000..fc6542fd2 --- /dev/null +++ b/docs/stackit_beta_server_os-update_schedule_describe.md @@ -0,0 +1,44 @@ +## stackit beta server os-update schedule describe + +Shows details of a Server os-update Schedule + +### Synopsis + +Shows details of a Server os-update Schedule. + +``` +stackit beta server os-update schedule describe SCHEDULE_ID [flags] +``` + +### Examples + +``` + Get details of a Server os-update Schedule with id "my-schedule-id" + $ stackit beta server os-update schedule describe my-schedule-id + + Get details of a Server os-update Schedule with id "my-schedule-id" in JSON format + $ stackit beta server os-update schedule describe my-schedule-id --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update schedule describe" + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule + diff --git a/docs/stackit_beta_server_os-update_schedule_list.md b/docs/stackit_beta_server_os-update_schedule_list.md new file mode 100644 index 000000000..5352184bb --- /dev/null +++ b/docs/stackit_beta_server_os-update_schedule_list.md @@ -0,0 +1,45 @@ +## stackit beta server os-update schedule list + +Lists all server os-update schedules + +### Synopsis + +Lists all server os-update schedules. + +``` +stackit beta server os-update schedule list [flags] +``` + +### Examples + +``` + List all os-update schedules for a server with ID "xxx" + $ stackit beta server os-update schedule list --server-id xxx + + List all os-update schedules for a server with ID "xxx" in JSON format + $ stackit beta server os-update schedule list --server-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta server os-update schedule list" + --limit int Maximum number of entries to list + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule + diff --git a/docs/stackit_beta_server_os-update_schedule_update.md b/docs/stackit_beta_server_os-update_schedule_update.md new file mode 100644 index 000000000..4b901269f --- /dev/null +++ b/docs/stackit_beta_server_os-update_schedule_update.md @@ -0,0 +1,45 @@ +## stackit beta server os-update schedule update + +Updates a Server os-update Schedule + +### Synopsis + +Updates a Server os-update Schedule. + +``` +stackit beta server os-update schedule update SCHEDULE_ID [flags] +``` + +### Examples + +``` + Update the name of the os-update schedule "zzz" of server "xxx" + $ stackit beta server os-update schedule update zzz --server-id=xxx --name=newname +``` + +### Options + +``` + -e, --enabled Is the server os-update schedule enabled (default true) + -h, --help Help for "stackit beta server os-update schedule update" + -d, --maintenance-window int Maintenance window (in hours, 1-24) (default 23) + -n, --name string os-update schedule name + -r, --rrule string os-update RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") + -s, --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule + diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index d8e1f20a5..b1abf5662 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -49,6 +49,7 @@ stackit config set [flags] --resource-manager-custom-endpoint string Resource Manager API base URL, used in calls to this API --runcommand-custom-endpoint string Run Command API base URL, used in calls to this API --secrets-manager-custom-endpoint string Secrets Manager API base URL, used in calls to this API + --server-osupdate-custom-endpoint string Server Update Management API base URL, used in calls to this API --serverbackup-custom-endpoint string Server Backup API base URL, used in calls to this API --service-account-custom-endpoint string Service Account API base URL, used in calls to this API --service-enablement-custom-endpoint string Service Enablement API base URL, used in calls to this API diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index 933bacc3d..4a48b759e 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -50,6 +50,7 @@ stackit config unset [flags] --resource-manager-custom-endpoint Resource Manager API base URL. If unset, uses the default base URL --runcommand-custom-endpoint Server Command base URL. If unset, uses the default base URL --secrets-manager-custom-endpoint Secrets Manager API base URL. If unset, uses the default base URL + --server-osupdate-custom-endpoint Server Update Management base URL. If unset, uses the default base URL --serverbackup-custom-endpoint Server Backup base URL. If unset, uses the default base URL --service-account-custom-endpoint Service Account API base URL. If unset, uses the default base URL --service-enablement-custom-endpoint Service Enablement API base URL. If unset, uses the default base URL diff --git a/go.mod b/go.mod index 5f512f130..cd2cec4dc 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.2.3 // indirect github.com/x448/float16 v0.8.4 // indirect ) diff --git a/go.sum b/go.sum index 781c3b93c..a1f298913 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,8 @@ github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.1 h1:qShB0O github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.1/go.mod h1:268uoY2gKCa5xcDL169TGVjLUNTcZ2En77YdfYOcR1w= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.5.0 h1:33zKhcNS1bZ2usGnYZ6YE6Vxm7c9U0aC8lDg96UN+e4= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.5.0/go.mod h1:thrCIDBjEHAcihjWUOMJ5mbYVhOWfS/bLTBJ+yl5W4g= +github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.2.3 h1:+D7NWWP/FF1asAZhntwzaFaSnHExgwta7/k+mRGmzrQ= +github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.2.3/go.mod h1:etidTptNDvvCPA1FGC7T9DXHxXA4bYW3qIUzWG8wVcc= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.5.0 h1:yf9BxAZEG2hdaekWxaNt2BOX/4qmGkl0d268ggR+tCU= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.5.0/go.mod h1:Wpqj80yGruCNYGr2yxqhRaLLj4gPSkhJqZyWRXUh/QU= github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.4.0 h1:K5fVTcJxjOVwJBa3kiWRsYNAq+I3jAYdU1U+f6no5lE= diff --git a/internal/cmd/beta/server/os-update/create/create.go b/internal/cmd/beta/server/os-update/create/create.go new file mode 100644 index 000000000..bf0bf664d --- /dev/null +++ b/internal/cmd/beta/server/os-update/create/create.go @@ -0,0 +1,147 @@ +package create + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/serverosupdate/client" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + serverIdFlag = "server-id" + maintenanceWindowFlag = "maintenance-window" + defaultMaintenanceWindow = 23 +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + ServerId string + MaintenanceWindow int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a Server os-update.", + Long: "Creates a Server os-update. Operation always is async.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a Server os-update with name "myupdate"`, + `$ stackit beta server os-update create --server-id xxx --name=myupdate`), + examples.NewExample( + `Create a Server os-update with name "myupdate" and maintenance window for 13 o'clock.`, + `$ stackit beta server os-update create --server-id xxx --name=mybupdate --maintenance-window=13`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create a os-update for server %s?", model.ServerId) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create Server os-update: %w", err) + } + + return outputResult(p, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + cmd.Flags().Int64P(maintenanceWindowFlag, "m", defaultMaintenanceWindow, "Maintenance window (in hours, 1-24)") +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + MaintenanceWindow: flags.FlagWithDefaultToInt64Value(p, cmd, maintenanceWindowFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) (serverupdate.ApiCreateUpdateRequest, error) { + req := apiClient.CreateUpdate(ctx, model.ProjectId, model.ServerId) + payload := serverupdate.CreateUpdatePayload{ + MaintenanceWindow: &model.MaintenanceWindow, + } + req = req.CreateUpdatePayload(payload) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *serverupdate.Update) error { + switch model.OutputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return fmt.Errorf("marshal server os-update: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal server os-update: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + p.Outputf("Triggered creation of server os-update for server %s. Update ID: %d\n", model.ServerId, *resp.Id) + return nil + } +} diff --git a/internal/cmd/beta/server/os-update/create/create_test.go b/internal/cmd/beta/server/os-update/create/create_test.go new file mode 100644 index 000000000..2402f81a8 --- /dev/null +++ b/internal/cmd/beta/server/os-update/create/create_test.go @@ -0,0 +1,205 @@ +package create + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} + +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + serverIdFlag: testServerId, + maintenanceWindowFlag: "13", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + ServerId: testServerId, + MaintenanceWindow: int64(13), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiCreateUpdateRequest)) serverupdate.ApiCreateUpdateRequest { + request := testClient.CreateUpdate(testCtx, testProjectId, testServerId) + request = request.CreateUpdatePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *serverupdate.CreateUpdatePayload)) serverupdate.CreateUpdatePayload { + payload := serverupdate.CreateUpdatePayload{ + MaintenanceWindow: utils.Ptr(int64(13)), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "with defaults", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, maintenanceWindowFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.MaintenanceWindow = 23 + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(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 TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest serverupdate.ApiCreateUpdateRequest + isValid bool + }{ + { + description: "base", + model: fixtureInputModel(), + isValid: true, + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/describe/describe.go b/internal/cmd/beta/server/os-update/describe/describe.go new file mode 100644 index 000000000..3a92dd3dc --- /dev/null +++ b/internal/cmd/beta/server/os-update/describe/describe.go @@ -0,0 +1,162 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/serverosupdate/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + updateIdArg = "UPDATE_ID" + serverIdFlag = "server-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ServerId string + UpdateId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", updateIdArg), + Short: "Shows details of a Server os-update", + Long: "Shows details of a Server os-update.", + Args: args.SingleArg(updateIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Get details of a Server os-update with id "my-os-update-id"`, + "$ stackit beta server os-update describe my-os-update-id"), + examples.NewExample( + `Get details of a Server os-update with id "my-os-update-id" in JSON format`, + "$ stackit beta server os-update describe my-os-update-id --output-format json"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read server os-update: %w", err) + } + + return outputResult(p, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + updateId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + UpdateId: updateId, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiGetUpdateRequest { + req := apiClient.GetUpdate(ctx, model.ProjectId, model.ServerId, model.UpdateId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, update *serverupdate.Update) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(update, "", " ") + if err != nil { + return fmt.Errorf("marshal server update: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(update, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal server update: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.AddRow("ID", *update.Id) + table.AddSeparator() + table.AddRow("STATUS", *update.Status) + table.AddSeparator() + if update.InstalledUpdates != nil { + table.AddRow("INSTALLED UPDATES", *update.InstalledUpdates) + } else { + table.AddRow("INSTALLED UPDATES", "...") + } + table.AddSeparator() + if update.FailedUpdates != nil { + table.AddRow("FAILED UPDATES", *update.FailedUpdates) + } else { + table.AddRow("FAILED UPDATES", "...") + } + table.AddRow("START DATE", *update.StartDate) + table.AddSeparator() + if update.EndDate != nil { + table.AddRow("END DATE", *update.EndDate) + } else { + table.AddRow("END DATE", "...") + } + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/server/os-update/describe/describe_test.go b/internal/cmd/beta/server/os-update/describe/describe_test.go new file mode 100644 index 000000000..e02f0298f --- /dev/null +++ b/internal/cmd/beta/server/os-update/describe/describe_test.go @@ -0,0 +1,211 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() +var testUpdateId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testUpdateId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + serverIdFlag: testServerId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + ServerId: testServerId, + UpdateId: testUpdateId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiGetUpdateRequest)) serverupdate.ApiGetUpdateRequest { + request := testClient.GetUpdate(testCtx, testProjectId, testServerId, testUpdateId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(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.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + isValid bool + expectedRequest serverupdate.ApiGetUpdateRequest + }{ + { + description: "base", + model: fixtureInputModel(), + isValid: true, + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/disable/disable.go b/internal/cmd/beta/server/os-update/disable/disable.go new file mode 100644 index 000000000..1fbc89823 --- /dev/null +++ b/internal/cmd/beta/server/os-update/disable/disable.go @@ -0,0 +1,108 @@ +package disable + +import ( + "context" + "fmt" + + "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/serverosupdate/client" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + serverIdFlag = "server-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ServerId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "disable", + Short: "Disables server os-update service", + Long: "Disables server os-update service.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Disable os-update functionality for your server.`, + "$ stackit beta server os-update disable --server-id=zzz"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to disable the os-update service for server %s?", model.ServerId) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("disable server os-update service: %w", err) + } + + p.Info("Disabled Server os-update service for server %s\n", model.ServerId) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiDisableServiceRequest { + req := apiClient.DisableService(ctx, model.ProjectId, model.ServerId) + return req +} diff --git a/internal/cmd/beta/server/os-update/disable/disable_test.go b/internal/cmd/beta/server/os-update/disable/disable_test.go new file mode 100644 index 000000000..6560a7fae --- /dev/null +++ b/internal/cmd/beta/server/os-update/disable/disable_test.go @@ -0,0 +1,173 @@ +package disable + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + ServerId: testServerId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiDisableServiceRequest)) serverupdate.ApiDisableServiceRequest { + request := testClient.DisableService(testCtx, testProjectId, testServerId) + for _, mod := range mods { + mod(&request) + } + return request +} + +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(func(model *inputModel) { + model.ServerId = "" + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + 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) + } + + p := print.NewPrinter() + 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 TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest serverupdate.ApiDisableServiceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/enable/enable.go b/internal/cmd/beta/server/os-update/enable/enable.go new file mode 100644 index 000000000..c16d19318 --- /dev/null +++ b/internal/cmd/beta/server/os-update/enable/enable.go @@ -0,0 +1,112 @@ +package enable + +import ( + "context" + "fmt" + "strings" + + "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/serverosupdate/client" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + serverIdFlag = "server-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ServerId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "enable", + Short: "Enables Server os-update service", + Long: "Enables Server os-update service.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Enable os-update functionality for your server`, + "$ stackit beta server os-update enable --server-id=zzz"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to enable the server os-update service for server %s?", model.ServerId) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + if !strings.Contains(err.Error(), "Tried to activate already active service") { + return fmt.Errorf("enable server os-update: %w", err) + } + } + + p.Info("Enabled os-update service for server %s\n", model.ServerId) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiEnableServiceRequest { + payload := serverupdate.EnableServicePayload{} + req := apiClient.EnableService(ctx, model.ProjectId, model.ServerId).EnableServicePayload(payload) + return req +} diff --git a/internal/cmd/beta/server/os-update/enable/enable_test.go b/internal/cmd/beta/server/os-update/enable/enable_test.go new file mode 100644 index 000000000..6e55c1c77 --- /dev/null +++ b/internal/cmd/beta/server/os-update/enable/enable_test.go @@ -0,0 +1,173 @@ +package enable + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + ServerId: testServerId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiEnableServiceRequest)) serverupdate.ApiEnableServiceRequest { + request := testClient.EnableService(testCtx, testProjectId, testServerId).EnableServicePayload(serverupdate.EnableServicePayload{}) + for _, mod := range mods { + mod(&request) + } + return request +} + +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(func(model *inputModel) { + model.ServerId = "" + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + 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) + } + + p := print.NewPrinter() + 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 TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest serverupdate.ApiEnableServiceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/list/list.go b/internal/cmd/beta/server/os-update/list/list.go new file mode 100644 index 000000000..2f41b0ab1 --- /dev/null +++ b/internal/cmd/beta/server/os-update/list/list.go @@ -0,0 +1,177 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "github.com/goccy/go-yaml" + + "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/serverosupdate/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + limitFlag = "limit" + serverIdFlag = "server-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ServerId string + Limit *int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all server os-updates", + Long: "Lists all server os-updates.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all os-updates for a server with ID "xxx"`, + "$ stackit beta server os-update list --server-id xxx"), + examples.NewExample( + `List all os-updates for a server with ID "xxx" in JSON format`, + "$ stackit beta server os-update list --server-id xxx --output-format json"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list server os-update: %w", err) + } + updates := *resp.Items + if len(updates) == 0 { + p.Info("No os-updates found for server %s\n", model.ServerId) + return nil + } + + // Truncate output + if model.Limit != nil && len(updates) > int(*model.Limit) { + updates = updates[:*model.Limit] + } + return outputResult(p, model.OutputFormat, updates) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + 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, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + Limit: limit, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiListUpdatesRequest { + req := apiClient.ListUpdates(ctx, model.ProjectId, model.ServerId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, updates []serverupdate.Update) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(updates, "", " ") + if err != nil { + return fmt.Errorf("marshal server os-update list: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(updates, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal server os-update list: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID", "STATUS", "INSTALLED UPDATES", "FAILED UPDATES", "START DATE", "END DATE") + for i := range updates { + s := updates[i] + + endDate := "..." + if s.EndDate != nil { + endDate = *s.EndDate + } + + installed := "..." + if s.InstalledUpdates != nil { + installed = strconv.FormatInt(*s.InstalledUpdates, 10) + } + + failed := "..." + if s.FailedUpdates != nil { + failed = strconv.FormatInt(*s.FailedUpdates, 10) + } + + table.AddRow(*s.Id, *s.Status, installed, failed, *s.StartDate, endDate) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + } +} diff --git a/internal/cmd/beta/server/os-update/list/list_test.go b/internal/cmd/beta/server/os-update/list/list_test.go new file mode 100644 index 000000000..32f75311c --- /dev/null +++ b/internal/cmd/beta/server/os-update/list/list_test.go @@ -0,0 +1,188 @@ +package list + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + limitFlag: "10", + serverIdFlag: testServerId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + Limit: utils.Ptr(int64(10)), + ServerId: testServerId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiListUpdatesRequest)) serverupdate.ApiListUpdatesRequest { + request := testClient.ListUpdates(testCtx, testProjectId, testServerId) + for _, mod := range mods { + mod(&request) + } + return request +} + +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: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(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 TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest serverupdate.ApiListUpdatesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/os-update.go b/internal/cmd/beta/server/os-update/os-update.go new file mode 100644 index 000000000..b4100b3d6 --- /dev/null +++ b/internal/cmd/beta/server/os-update/os-update.go @@ -0,0 +1,36 @@ +package osupdate + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/disable" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/enable" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "os-update", + Short: "Provides functionality for managed server updates", + Long: "Provides functionality for managed server updates.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, p) + return cmd +} + +func addSubcommands(cmd *cobra.Command, p *print.Printer) { + cmd.AddCommand(create.NewCmd(p)) + cmd.AddCommand(describe.NewCmd(p)) + cmd.AddCommand(list.NewCmd(p)) + cmd.AddCommand(enable.NewCmd(p)) + cmd.AddCommand(disable.NewCmd(p)) + cmd.AddCommand(schedule.NewCmd(p)) +} diff --git a/internal/cmd/beta/server/os-update/schedule/create/create.go b/internal/cmd/beta/server/os-update/schedule/create/create.go new file mode 100644 index 000000000..5a4d7ce93 --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/create/create.go @@ -0,0 +1,167 @@ +package create + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/serverosupdate/client" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + nameFlag = "name" + enabledFlag = "enabled" + rruleFlag = "rrule" + maintenanceWindowFlag = "maintenance-window" + serverIdFlag = "server-id" + + defaultRrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1" + defaultMaintenanceWindow = 23 + defaultEnabled = true +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + ServerId string + ScheduleName string + Enabled bool + Rrule string + MaintenanceWindow int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a Server os-update Schedule", + Long: "Creates a Server os-update Schedule.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a Server os-update Schedule with name "myschedule"`, + `$ stackit beta server os-update schedule create --server-id xxx --name=myschedule`), + examples.NewExample( + `Create a Server os-update Schedule with name "myschedule" and maintenance window for 14 o'clock`, + `$ stackit beta server os-update schedule create --server-id xxx --name=myschedule --maintenance-window=14`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create a os-update Schedule for server %s?", model.ServerId) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create Server os-update Schedule: %w", err) + } + + return outputResult(p, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + cmd.Flags().StringP(nameFlag, "n", "", "os-update schedule name") + cmd.Flags().Int64P(maintenanceWindowFlag, "d", defaultMaintenanceWindow, "os-update maintenance window (in hours, 1-24)") + cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server os-update schedule enabled") + cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "os-update RRULE (recurrence rule)") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag, nameFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + MaintenanceWindow: flags.FlagWithDefaultToInt64Value(p, cmd, maintenanceWindowFlag), + ScheduleName: flags.FlagToStringValue(p, cmd, nameFlag), + Rrule: flags.FlagWithDefaultToStringValue(p, cmd, rruleFlag), + Enabled: flags.FlagToBoolValue(p, cmd, enabledFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) (serverupdate.ApiCreateUpdateScheduleRequest, error) { + req := apiClient.CreateUpdateSchedule(ctx, model.ProjectId, model.ServerId) + req = req.CreateUpdateSchedulePayload(serverupdate.CreateUpdateSchedulePayload{ + Enabled: &model.Enabled, + Name: &model.ScheduleName, + Rrule: &model.Rrule, + MaintenanceWindow: &model.MaintenanceWindow, + }) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *serverupdate.UpdateSchedule) error { + switch model.OutputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return fmt.Errorf("marshal server os-update schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal server os-update schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + p.Outputf("Created server os-update schedule for server %s. os-update Schedule ID: %d\n", model.ServerId, *resp.Id) + return nil + } +} diff --git a/internal/cmd/beta/server/os-update/schedule/create/create_test.go b/internal/cmd/beta/server/os-update/schedule/create/create_test.go new file mode 100644 index 000000000..31e6f781b --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/create/create_test.go @@ -0,0 +1,212 @@ +package create + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} + +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + serverIdFlag: testServerId, + nameFlag: "example-schedule-name", + enabledFlag: "true", + rruleFlag: defaultRrule, + maintenanceWindowFlag: "23", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + ServerId: testServerId, + ScheduleName: "example-schedule-name", + Enabled: defaultEnabled, + Rrule: defaultRrule, + MaintenanceWindow: int64(23), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiCreateUpdateScheduleRequest)) serverupdate.ApiCreateUpdateScheduleRequest { + request := testClient.CreateUpdateSchedule(testCtx, testProjectId, testServerId) + request = request.CreateUpdateSchedulePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *serverupdate.CreateUpdateSchedulePayload)) serverupdate.CreateUpdateSchedulePayload { + payload := serverupdate.CreateUpdateSchedulePayload{ + Name: utils.Ptr("example-schedule-name"), + Enabled: utils.Ptr(defaultEnabled), + Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), + MaintenanceWindow: utils.Ptr(int64(23)), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "with defaults", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, maintenanceWindowFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(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 TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest serverupdate.ApiCreateUpdateScheduleRequest + isValid bool + }{ + { + description: "base", + model: fixtureInputModel(), + isValid: true, + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/schedule/delete/delete.go b/internal/cmd/beta/server/os-update/schedule/delete/delete.go new file mode 100644 index 000000000..d19482752 --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/delete/delete.go @@ -0,0 +1,113 @@ +package delete + +import ( + "context" + "fmt" + + "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/serverosupdate/client" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + scheduleIdArg = "SCHEDULE_ID" + serverIdFlag = "server-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ScheduleId string + ServerId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", scheduleIdArg), + Short: "Deletes a Server os-update Schedule", + Long: "Deletes a Server os-update Schedule.", + Args: args.SingleArg(scheduleIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Delete a Server os-update Schedule with ID "xxx" for server "zzz"`, + "$ stackit beta server os-update schedule delete xxx --server-id=zzz"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete server os-update schedule %q? (This cannot be undone)", model.ScheduleId) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete Server os-update Schedule: %w", err) + } + + p.Info("Deleted server os-update schedule %q\n", model.ScheduleId) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + scheduleId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ScheduleId: scheduleId, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiDeleteUpdateScheduleRequest { + req := apiClient.DeleteUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId) + return req +} diff --git a/internal/cmd/beta/server/os-update/schedule/delete/delete_test.go b/internal/cmd/beta/server/os-update/schedule/delete/delete_test.go new file mode 100644 index 000000000..60b00b97c --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/delete/delete_test.go @@ -0,0 +1,209 @@ +package delete + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() +var testUpdateScheduleId = "5" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testUpdateScheduleId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + serverIdFlag: testServerId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + ServerId: testServerId, + ScheduleId: testUpdateScheduleId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiDeleteUpdateScheduleRequest)) serverupdate.ApiDeleteUpdateScheduleRequest { + request := testClient.DeleteUpdateSchedule(testCtx, testProjectId, testServerId, testUpdateScheduleId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(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.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %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 TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest serverupdate.ApiDeleteUpdateScheduleRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/schedule/describe/describe.go b/internal/cmd/beta/server/os-update/schedule/describe/describe.go new file mode 100644 index 000000000..d46bf0a1e --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/describe/describe.go @@ -0,0 +1,149 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/serverosupdate/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + scheduleIdArg = "SCHEDULE_ID" + serverIdFlag = "server-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ServerId string + ScheduleId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", scheduleIdArg), + Short: "Shows details of a Server os-update Schedule", + Long: "Shows details of a Server os-update Schedule.", + Args: args.SingleArg(scheduleIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Get details of a Server os-update Schedule with id "my-schedule-id"`, + "$ stackit beta server os-update schedule describe my-schedule-id"), + examples.NewExample( + `Get details of a Server os-update Schedule with id "my-schedule-id" in JSON format`, + "$ stackit beta server os-update schedule describe my-schedule-id --output-format json"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read server os-update schedule: %w", err) + } + + return outputResult(p, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + scheduleId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + ScheduleId: scheduleId, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiGetUpdateScheduleRequest { + req := apiClient.GetUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, schedule *serverupdate.UpdateSchedule) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(schedule, "", " ") + if err != nil { + return fmt.Errorf("marshal server os-update schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(schedule, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal server os-update schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.AddRow("SCHEDULE ID", *schedule.Id) + table.AddSeparator() + table.AddRow("SCHEDULE NAME", *schedule.Name) + table.AddSeparator() + table.AddRow("ENABLED", *schedule.Enabled) + table.AddSeparator() + table.AddRow("RRULE", *schedule.Rrule) + table.AddSeparator() + table.AddRow("MAINTENANCE WINDOW", *schedule.MaintenanceWindow) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/server/os-update/schedule/describe/describe_test.go b/internal/cmd/beta/server/os-update/schedule/describe/describe_test.go new file mode 100644 index 000000000..f1165bb00 --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/describe/describe_test.go @@ -0,0 +1,211 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() +var testScheduleId = "5" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testScheduleId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + serverIdFlag: testServerId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + ServerId: testServerId, + ScheduleId: testScheduleId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiGetUpdateScheduleRequest)) serverupdate.ApiGetUpdateScheduleRequest { + request := testClient.GetUpdateSchedule(testCtx, testProjectId, testServerId, testScheduleId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(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.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + isValid bool + expectedRequest serverupdate.ApiGetUpdateScheduleRequest + }{ + { + description: "base", + model: fixtureInputModel(), + isValid: true, + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/schedule/list/list.go b/internal/cmd/beta/server/os-update/schedule/list/list.go new file mode 100644 index 000000000..f24d8becf --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/list/list.go @@ -0,0 +1,160 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + + "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/serverosupdate/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + limitFlag = "limit" + serverIdFlag = "server-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ServerId string + Limit *int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all server os-update schedules", + Long: "Lists all server os-update schedules.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all os-update schedules for a server with ID "xxx"`, + "$ stackit beta server os-update schedule list --server-id xxx"), + examples.NewExample( + `List all os-update schedules for a server with ID "xxx" in JSON format`, + "$ stackit beta server os-update schedule list --server-id xxx --output-format json"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list server os-update schedules: %w", err) + } + schedules := *resp.Items + if len(schedules) == 0 { + p.Info("No os-update schedules found for server %s\n", model.ServerId) + return nil + } + + // Truncate output + if model.Limit != nil && len(schedules) > int(*model.Limit) { + schedules = schedules[:*model.Limit] + } + return outputResult(p, model.OutputFormat, schedules) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + 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, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + Limit: limit, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiListUpdateSchedulesRequest { + req := apiClient.ListUpdateSchedules(ctx, model.ProjectId, model.ServerId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, schedules []serverupdate.UpdateSchedule) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(schedules, "", " ") + if err != nil { + return fmt.Errorf("marshal Server os-update Schedules list: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(schedules, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Server os-update Schedules list: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("SCHEDULE ID", "SCHEDULE NAME", "ENABLED", "RRULE", "MAINTENANCE WINDOW") + for i := range schedules { + s := schedules[i] + table.AddRow(*s.Id, *s.Name, *s.Enabled, *s.Rrule, *s.MaintenanceWindow) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + } +} diff --git a/internal/cmd/beta/server/os-update/schedule/list/list_test.go b/internal/cmd/beta/server/os-update/schedule/list/list_test.go new file mode 100644 index 000000000..6c0ce72f7 --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/list/list_test.go @@ -0,0 +1,188 @@ +package list + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + limitFlag: "10", + serverIdFlag: testServerId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + Limit: utils.Ptr(int64(10)), + ServerId: testServerId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiListUpdateSchedulesRequest)) serverupdate.ApiListUpdateSchedulesRequest { + request := testClient.ListUpdateSchedules(testCtx, testProjectId, testServerId) + for _, mod := range mods { + mod(&request) + } + return request +} + +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: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(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 TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest serverupdate.ApiListUpdateSchedulesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/os-update/schedule/schedule.go b/internal/cmd/beta/server/os-update/schedule/schedule.go new file mode 100644 index 000000000..60c9e503c --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/schedule.go @@ -0,0 +1,34 @@ +package schedule + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/create" + del "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "schedule", + Short: "Provides functionality for Server os-update Schedule", + Long: "Provides functionality for Server os-update Schedule.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, p) + return cmd +} + +func addSubcommands(cmd *cobra.Command, p *print.Printer) { + cmd.AddCommand(list.NewCmd(p)) + cmd.AddCommand(describe.NewCmd(p)) + cmd.AddCommand(create.NewCmd(p)) + cmd.AddCommand(del.NewCmd(p)) + cmd.AddCommand(update.NewCmd(p)) +} diff --git a/internal/cmd/beta/server/os-update/schedule/update/update.go b/internal/cmd/beta/server/os-update/schedule/update/update.go new file mode 100644 index 000000000..c85a429f2 --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/update/update.go @@ -0,0 +1,191 @@ +package update + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/serverosupdate/client" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +const ( + scheduleIdArg = "SCHEDULE_ID" + + nameFlag = "name" + enabledFlag = "enabled" + rruleFlag = "rrule" + maintenanceWindowFlag = "maintenance-window" + serverIdFlag = "server-id" + + defaultRrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1" + defaultMaintenanceWindow = 23 + defaultEnabled = true +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + ServerId string + ScheduleId string + ScheduleName *string + Enabled *bool + Rrule *string + MaintenanceWindow *int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", scheduleIdArg), + Short: "Updates a Server os-update Schedule", + Long: "Updates a Server os-update Schedule.", + Example: examples.Build( + examples.NewExample( + `Update the name of the os-update schedule "zzz" of server "xxx"`, + "$ stackit beta server os-update schedule update zzz --server-id=xxx --name=newname"), + ), + Args: args.SingleArg(scheduleIdArg, nil), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + currentSchedule, err := apiClient.GetUpdateScheduleExecute(ctx, model.ProjectId, model.ServerId, model.ScheduleId) + if err != nil { + p.Debug(print.ErrorLevel, "get current server os-update schedule: %v", err) + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update Server os-update Schedule %q?", model.ScheduleId) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req, err := buildRequest(ctx, model, apiClient, *currentSchedule) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update Server os-update Schedule: %w", err) + } + + return outputResult(p, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") + + cmd.Flags().StringP(nameFlag, "n", "", "os-update schedule name") + cmd.Flags().Int64P(maintenanceWindowFlag, "d", defaultMaintenanceWindow, "Maintenance window (in hours, 1-24)") + cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server os-update schedule enabled") + cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "os-update RRULE (recurrence rule)") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + scheduleId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ScheduleId: scheduleId, + ScheduleName: flags.FlagToStringPointer(p, cmd, nameFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + MaintenanceWindow: flags.FlagToInt64Pointer(p, cmd, maintenanceWindowFlag), + Rrule: flags.FlagToStringPointer(p, cmd, rruleFlag), + Enabled: flags.FlagToBoolPointer(p, cmd, enabledFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient, old serverupdate.UpdateSchedule) (serverupdate.ApiUpdateUpdateScheduleRequest, error) { + req := apiClient.UpdateUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId) + + if model.MaintenanceWindow != nil { + old.MaintenanceWindow = model.MaintenanceWindow + } + if model.Enabled != nil { + old.Enabled = model.Enabled + } + if model.ScheduleName != nil { + old.Name = model.ScheduleName + } + if model.Rrule != nil { + old.Rrule = model.Rrule + } + + req = req.UpdateUpdateSchedulePayload(serverupdate.UpdateUpdateSchedulePayload{ + Enabled: old.Enabled, + Name: old.Name, + Rrule: old.Rrule, + MaintenanceWindow: old.MaintenanceWindow, + }) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *serverupdate.UpdateSchedule) error { + switch model.OutputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return fmt.Errorf("marshal update server os-update schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal update server os-update schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + p.Info("Updated server os-update schedule %d\n", *resp.Id) + return nil + } +} diff --git a/internal/cmd/beta/server/os-update/schedule/update/update_test.go b/internal/cmd/beta/server/os-update/schedule/update/update_test.go new file mode 100644 index 000000000..20d8afe36 --- /dev/null +++ b/internal/cmd/beta/server/os-update/schedule/update/update_test.go @@ -0,0 +1,265 @@ +package update + +import ( + "context" + "strconv" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &serverupdate.APIClient{} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() +var testScheduleId = "5" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testScheduleId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + serverIdFlag: testServerId, + nameFlag: "example-schedule-name", + enabledFlag: "true", + rruleFlag: defaultRrule, + maintenanceWindowFlag: "23", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + ScheduleId: testScheduleId, + ServerId: testServerId, + ScheduleName: utils.Ptr("example-schedule-name"), + Enabled: utils.Ptr(defaultEnabled), + Rrule: utils.Ptr(defaultRrule), + MaintenanceWindow: utils.Ptr(int64(23)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureUpdateSchedule(mods ...func(schedule *serverupdate.UpdateSchedule)) *serverupdate.UpdateSchedule { + id, _ := strconv.ParseInt(testScheduleId, 10, 64) + schedule := &serverupdate.UpdateSchedule{ + Name: utils.Ptr("example-schedule-name"), + Id: utils.Ptr(id), + Enabled: utils.Ptr(defaultEnabled), + Rrule: utils.Ptr(defaultRrule), + MaintenanceWindow: utils.Ptr(int64(23)), + } + for _, mod := range mods { + mod(schedule) + } + return schedule +} + +func fixturePayload(mods ...func(payload *serverupdate.UpdateUpdateSchedulePayload)) serverupdate.UpdateUpdateSchedulePayload { + payload := serverupdate.UpdateUpdateSchedulePayload{ + Name: utils.Ptr("example-schedule-name"), + Enabled: utils.Ptr(defaultEnabled), + Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), + MaintenanceWindow: utils.Ptr(int64(23)), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func fixtureRequest(mods ...func(request *serverupdate.ApiUpdateUpdateScheduleRequest)) serverupdate.ApiUpdateUpdateScheduleRequest { + request := testClient.UpdateUpdateSchedule(testCtx, testProjectId, testServerId, testScheduleId) + request = request.UpdateUpdateSchedulePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "schedule id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(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.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + err = cmd.ValidateFlagGroups() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flag groups: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest serverupdate.ApiUpdateUpdateScheduleRequest + isValid bool + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient, *fixtureUpdateSchedule()) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/server/server.go b/internal/cmd/beta/server/server.go index 6d75b27d5..ef8922220 100644 --- a/internal/cmd/beta/server/server.go +++ b/internal/cmd/beta/server/server.go @@ -11,6 +11,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/list" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/log" networkinterface "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/network-interface" + osUpdate "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update" publicip "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/public-ip" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/reboot" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/rescue" @@ -62,4 +63,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(resize.NewCmd(p)) cmd.AddCommand(rescue.NewCmd(p)) cmd.AddCommand(unrescue.NewCmd(p)) + cmd.AddCommand(osUpdate.NewCmd(p)) } diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 5f983cd88..b8defa692 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -37,6 +37,7 @@ const ( resourceManagerCustomEndpointFlag = "resource-manager-custom-endpoint" secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint" serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint" + serverOsUpdateCustomEndpointFlag = "server-osupdate-custom-endpoint" runCommandCustomEndpointFlag = "runcommand-custom-endpoint" serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" serviceEnablementCustomEndpointFlag = "service-enablement-custom-endpoint" @@ -151,6 +152,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(serviceAccountCustomEndpointFlag, "", "Service Account API base URL, used in calls to this API") cmd.Flags().String(serviceEnablementCustomEndpointFlag, "", "Service Enablement API base URL, used in calls to this API") cmd.Flags().String(serverBackupCustomEndpointFlag, "", "Server Backup API base URL, used in calls to this API") + cmd.Flags().String(serverOsUpdateCustomEndpointFlag, "", "Server Update Management API base URL, used in calls to this API") cmd.Flags().String(runCommandCustomEndpointFlag, "", "Run Command API base URL, used in calls to this API") cmd.Flags().String(skeCustomEndpointFlag, "", "SKE API base URL, used in calls to this API") cmd.Flags().String(sqlServerFlexCustomEndpointFlag, "", "SQLServer Flex API base URL, used in calls to this API") @@ -196,6 +198,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.ServerBackupCustomEndpointKey, cmd.Flags().Lookup(serverBackupCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.ServerOsUpdateCustomEndpointKey, cmd.Flags().Lookup(serverOsUpdateCustomEndpointFlag)) + cobra.CheckErr(err) err = viper.BindPFlag(config.RunCommandCustomEndpointKey, cmd.Flags().Lookup(runCommandCustomEndpointFlag)) cobra.CheckErr(err) err = viper.BindPFlag(config.ServiceAccountCustomEndpointKey, cmd.Flags().Lookup(serviceAccountCustomEndpointFlag)) diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index 0890dbc8a..72d79aeb3 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -43,6 +43,7 @@ const ( serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" serviceEnablementCustomEndpointFlag = "service-enablement-custom-endpoint" serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint" + serverOsUpdateCustomEndpointFlag = "server-osupdate-custom-endpoint" runCommandCustomEndpointFlag = "runcommand-custom-endpoint" skeCustomEndpointFlag = "ske-custom-endpoint" sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" @@ -77,6 +78,7 @@ type inputModel struct { ResourceManagerCustomEndpoint bool SecretsManagerCustomEndpoint bool ServerBackupCustomEndpoint bool + ServerOsUpdateCustomEndpoint bool RunCommandCustomEndpoint bool ServiceAccountCustomEndpoint bool ServiceEnablementCustomEndpoint bool @@ -186,6 +188,9 @@ func NewCmd(p *print.Printer) *cobra.Command { if model.ServerBackupCustomEndpoint { viper.Set(config.ServerBackupCustomEndpointKey, "") } + if model.ServerOsUpdateCustomEndpoint { + viper.Set(config.ServerOsUpdateCustomEndpointKey, "") + } if model.RunCommandCustomEndpoint { viper.Set(config.RunCommandCustomEndpointKey, "") } @@ -242,6 +247,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(serviceAccountCustomEndpointFlag, false, "Service Account API base URL. If unset, uses the default base URL") cmd.Flags().Bool(serviceEnablementCustomEndpointFlag, false, "Service Enablement API base URL. If unset, uses the default base URL") cmd.Flags().Bool(serverBackupCustomEndpointFlag, false, "Server Backup base URL. If unset, uses the default base URL") + cmd.Flags().Bool(serverOsUpdateCustomEndpointFlag, false, "Server Update Management base URL. If unset, uses the default base URL") cmd.Flags().Bool(runCommandCustomEndpointFlag, false, "Server Command base URL. If unset, uses the default base URL") cmd.Flags().Bool(skeCustomEndpointFlag, false, "SKE API base URL. If unset, uses the default base URL") cmd.Flags().Bool(sqlServerFlexCustomEndpointFlag, false, "SQLServer Flex API base URL. If unset, uses the default base URL") @@ -279,6 +285,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { ServiceAccountCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceAccountCustomEndpointFlag), ServiceEnablementCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceEnablementCustomEndpointFlag), ServerBackupCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverBackupCustomEndpointFlag), + ServerOsUpdateCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverOsUpdateCustomEndpointFlag), RunCommandCustomEndpoint: flags.FlagToBoolValue(p, cmd, runCommandCustomEndpointFlag), SKECustomEndpoint: flags.FlagToBoolValue(p, cmd, skeCustomEndpointFlag), SQLServerFlexCustomEndpoint: flags.FlagToBoolValue(p, cmd, sqlServerFlexCustomEndpointFlag), diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index 2bf703677..bc262c002 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -35,6 +35,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool secretsManagerCustomEndpointFlag: true, serviceAccountCustomEndpointFlag: true, serverBackupCustomEndpointFlag: true, + serverOsUpdateCustomEndpointFlag: true, runCommandCustomEndpointFlag: true, skeCustomEndpointFlag: true, sqlServerFlexCustomEndpointFlag: true, @@ -73,6 +74,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { SecretsManagerCustomEndpoint: true, ServiceAccountCustomEndpoint: true, ServerBackupCustomEndpoint: true, + ServerOsUpdateCustomEndpoint: true, RunCommandCustomEndpoint: true, SKECustomEndpoint: true, SQLServerFlexCustomEndpoint: true, @@ -127,6 +129,7 @@ func TestParseInput(t *testing.T) { model.SecretsManagerCustomEndpoint = false model.ServiceAccountCustomEndpoint = false model.ServerBackupCustomEndpoint = false + model.ServerOsUpdateCustomEndpoint = false model.RunCommandCustomEndpoint = false model.SKECustomEndpoint = false model.SQLServerFlexCustomEndpoint = false @@ -254,6 +257,16 @@ func TestParseInput(t *testing.T) { model.ServerBackupCustomEndpoint = false }), }, + { + description: "serverosupdate custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[serverOsUpdateCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ServerOsUpdateCustomEndpoint = false + }), + }, { description: "runcommand custom endpoint empty", flagValues: fixtureFlagValues(func(flagValues map[string]bool) { diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index f72154d92..8de467877 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -39,6 +39,7 @@ const ( ServiceAccountCustomEndpointKey = "service_account_custom_endpoint" ServiceEnablementCustomEndpointKey = "service_enablement_custom_endpoint" ServerBackupCustomEndpointKey = "serverbackup_custom_endpoint" + ServerOsUpdateCustomEndpointKey = "serverosupdate_custom_endpoint" RunCommandCustomEndpointKey = "runcommand_custom_endpoint" SKECustomEndpointKey = "ske_custom_endpoint" SQLServerFlexCustomEndpointKey = "sqlserverflex_custom_endpoint" @@ -96,6 +97,7 @@ var ConfigKeys = []string{ ServiceAccountCustomEndpointKey, ServiceEnablementCustomEndpointKey, ServerBackupCustomEndpointKey, + ServerOsUpdateCustomEndpointKey, RunCommandCustomEndpointKey, SKECustomEndpointKey, SQLServerFlexCustomEndpointKey, @@ -178,6 +180,7 @@ func setConfigDefaults() { viper.SetDefault(ServiceAccountCustomEndpointKey, "") viper.SetDefault(ServiceEnablementCustomEndpointKey, "") viper.SetDefault(ServerBackupCustomEndpointKey, "") + viper.SetDefault(ServerOsUpdateCustomEndpointKey, "") viper.SetDefault(RunCommandCustomEndpointKey, "") viper.SetDefault(SKECustomEndpointKey, "") viper.SetDefault(SQLServerFlexCustomEndpointKey, "") diff --git a/internal/pkg/services/serverosupdate/client/client.go b/internal/pkg/services/serverosupdate/client/client.go new file mode 100644 index 000000000..8b46d6f1d --- /dev/null +++ b/internal/pkg/services/serverosupdate/client/client.go @@ -0,0 +1,46 @@ +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/spf13/viper" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +func ConfigureClient(p *print.Printer) (*serverupdate.APIClient, error) { + var err error + var apiClient *serverupdate.APIClient + var cfgOptions []sdkConfig.ConfigurationOption + + authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) + if err != nil { + p.Debug(print.ErrorLevel, "configure authentication: %v", err) + return nil, &errors.AuthError{} + } + cfgOptions = append(cfgOptions, authCfgOption) + + customEndpoint := viper.GetString(config.ServerOsUpdateCustomEndpointKey) + if customEndpoint != "" { + cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) + } else { + cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01")) + } + + if p.IsVerbosityDebug() { + cfgOptions = append(cfgOptions, + sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), + ) + } + + apiClient, err = serverupdate.NewAPIClient(cfgOptions...) + if err != nil { + p.Debug(print.ErrorLevel, "create new API client: %v", err) + return nil, &errors.AuthError{} + } + + return apiClient, nil +}