diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md index 76757a410..4fa4783a8 100644 --- a/docs/stackit_beta.md +++ b/docs/stackit_beta.md @@ -47,6 +47,7 @@ stackit beta [flags] * [stackit beta edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for Edge Cloud services. * [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake * [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) +* [stackit beta telemetrylink](./stackit_beta_telemetrylink.md) - Provides functionality for Telemetry Link * [stackit beta volume](./stackit_beta_volume.md) - Provides functionality for Volume * [stackit beta vpn](./stackit_beta_vpn.md) - Provides functionality for VPN diff --git a/docs/stackit_beta_telemetrylink.md b/docs/stackit_beta_telemetrylink.md new file mode 100644 index 000000000..3b0892ef2 --- /dev/null +++ b/docs/stackit_beta_telemetrylink.md @@ -0,0 +1,37 @@ +## stackit beta telemetrylink + +Provides functionality for Telemetry Link + +### Synopsis + +Provides functionality for Telemetry Link. + +``` +stackit beta telemetrylink [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta telemetrylink" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta telemetrylink create](./stackit_beta_telemetrylink_create.md) - Creates a Telemetry Link with the specified resource +* [stackit beta telemetrylink delete](./stackit_beta_telemetrylink_delete.md) - Deletes the given Telemetry Link within the specified resource +* [stackit beta telemetrylink describe](./stackit_beta_telemetrylink_describe.md) - Shows details of a Telemetry Link within the specified resource +* [stackit beta telemetrylink update](./stackit_beta_telemetrylink_update.md) - Updates a Telemetry Link within the specified resource + diff --git a/docs/stackit_beta_telemetrylink_create.md b/docs/stackit_beta_telemetrylink_create.md new file mode 100644 index 000000000..9b49f6eca --- /dev/null +++ b/docs/stackit_beta_telemetrylink_create.md @@ -0,0 +1,56 @@ +## stackit beta telemetrylink create + +Creates a Telemetry Link with the specified resource + +### Synopsis + +Creates a Telemetry Link with the specified resource. + +``` +stackit beta telemetrylink create [flags] +``` + +### Examples + +``` + Create a Telemetry Link with name "my-link" for project with ID "xxx" and for telemetry router with ID "yyy" + $ stackit beta telemetrylink create --display-name my-link --resource-type project --resource-id xxx --telemetry-router-id yyy --access-token my-token + + Create a Telemetry Link with name "my-link" for folder with ID "xxx" and for telemetry router with ID "yyy" + $ stackit beta telemetrylink create --display-name my-link --resource-type folder --resource-id xxx --telemetry-router-id yyy --access-token my-token + + Create a Telemetry Link with name "my-link" for organization with ID "xxx" and for telemetry router with ID "yyy" + $ stackit beta telemetrylink create --display-name my-link --resource-type organization --resource-id xxx --telemetry-router-id yyy --access-token my-token + + Create a Telemetry Link with name "my-link" for organization with ID "xxx" and for telemetry router with ID "yyy", and disable routing + $ stackit beta telemetrylink create --display-name my-link --resource-type organization --resource-id xxx --telemetry-router-id yyy --enabled=false --access-token my-token +``` + +### Options + +``` + --access-token string The access token of the Telemetry Router instance + --description string The description of the Telemetry Link resource + --display-name string The displayed name of the Telemetry Link resource + --enabled Enable routing through the link to a telemetry router (default true) + -h, --help Help for "stackit beta telemetrylink create" + --resource-id string STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource + --resource-type string The resource type of the TelemetryLink resource, possible values are [organization folder project] + --telemetry-router-id string The ID of the telemetry-router to route the telemetry data +``` + +### 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 telemetrylink](./stackit_beta_telemetrylink.md) - Provides functionality for Telemetry Link + diff --git a/docs/stackit_beta_telemetrylink_delete.md b/docs/stackit_beta_telemetrylink_delete.md new file mode 100644 index 000000000..d54e96568 --- /dev/null +++ b/docs/stackit_beta_telemetrylink_delete.md @@ -0,0 +1,48 @@ +## stackit beta telemetrylink delete + +Deletes the given Telemetry Link within the specified resource + +### Synopsis + +Deletes the given Telemetry Link within the specified resource. + +``` +stackit beta telemetrylink delete [flags] +``` + +### Examples + +``` + Delete the Telemetry Link for project with ID "xxx" + $ stackit beta telemetrylink delete --resource-type project --resource-id xxx + + Delete the Telemetry Link for folder with ID "xxx" + $ stackit beta telemetrylink delete --resource-type folder --resource-id xxx + + Delete the Telemetry Link for organization with ID "xxx" + $ stackit beta telemetrylink delete --resource-type organization --resource-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta telemetrylink delete" + --resource-id string STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource + --resource-type string The resource type of the TelemetryLink resource, possible values are [organization folder project] +``` + +### 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 telemetrylink](./stackit_beta_telemetrylink.md) - Provides functionality for Telemetry Link + diff --git a/docs/stackit_beta_telemetrylink_describe.md b/docs/stackit_beta_telemetrylink_describe.md new file mode 100644 index 000000000..02604dce4 --- /dev/null +++ b/docs/stackit_beta_telemetrylink_describe.md @@ -0,0 +1,51 @@ +## stackit beta telemetrylink describe + +Shows details of a Telemetry Link within the specified resource + +### Synopsis + +Shows details of a Telemetry Link within the specified resource. + +``` +stackit beta telemetrylink describe [flags] +``` + +### Examples + +``` + Get details of a Telemetry Link for project with ID "xxx" + $ stackit beta telemetrylink describe --resource-type project --resource-id xxx + + Get details of a Telemetry Link for folder with ID "xxx" + $ stackit beta telemetrylink describe --resource-type folder --resource-id xxx + + Get details of a Telemetry Link for organization with ID "xxx" + $ stackit beta telemetrylink describe --resource-type organization --resource-id xxx + + Get details of a Telemetry Link for organization with ID "xxx" in JSON format + $ stackit beta telemetrylink describe --resource-type organization --resource-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta telemetrylink describe" + --resource-id string STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource + --resource-type string The resource type of the TelemetryLink resource, possible values are [organization folder project] +``` + +### 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 telemetrylink](./stackit_beta_telemetrylink.md) - Provides functionality for Telemetry Link + diff --git a/docs/stackit_beta_telemetrylink_update.md b/docs/stackit_beta_telemetrylink_update.md new file mode 100644 index 000000000..444afbdaf --- /dev/null +++ b/docs/stackit_beta_telemetrylink_update.md @@ -0,0 +1,53 @@ +## stackit beta telemetrylink update + +Updates a Telemetry Link within the specified resource + +### Synopsis + +Updates a Telemetry Link within the specified resource. + +``` +stackit beta telemetrylink update [flags] +``` + +### Examples + +``` + Update a Telemetry Link with name "my-new-link" for project with ID "xxx" + $ stackit beta telemetrylink update --display-name my-new-link --resource-type project --resource-id xxx + + Update a Telemetry Link with new access token for folder with ID "xxx" + $ stackit beta telemetrylink update --resource-type folder --resource-id xxx --access-token my-new-token + + Update a Telemetry Link with new telemetry router ID "xxx" for organization with ID "yyy" + $ stackit beta telemetrylink update --resource-type organization --resource-id yyy --telemetry-router-id xxx +``` + +### Options + +``` + --access-token string The access token of the Telemetry Router instance + --description string The description of the Telemetry Link resource + --display-name string The displayed name of the Telemetry Link resource + --enabled Enable routing through the link to a telemetry router (default true) + -h, --help Help for "stackit beta telemetrylink update" + --resource-id string STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource + --resource-type string The resource type of the TelemetryLink resource, possible values are [organization folder project] + --telemetry-router-id string The ID of the telemetry-router to route the telemetry data +``` + +### 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 telemetrylink](./stackit_beta_telemetrylink.md) - Provides functionality for Telemetry Link + diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index b371db82e..f3407ab1f 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -64,6 +64,7 @@ stackit config set [flags] --sfs-custom-endpoint string SFS API base URL, used in calls to this API --ske-custom-endpoint string SKE API base URL, used in calls to this API --sqlserverflex-custom-endpoint string SQLServer Flex API base URL, used in calls to this API + --telemetrylink-custom-endpoint string TelemetryLink API base URL, used in calls to this API --token-custom-endpoint string Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication. --vpn-custom-endpoint string VPN API base URL, used in calls to this API ``` diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index d809e5e30..65c96c959 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -66,6 +66,7 @@ stackit config unset [flags] --sfs-custom-endpoint SFS API base URL. If unset, uses the default base URL --ske-custom-endpoint SKE API base URL. If unset, uses the default base URL --sqlserverflex-custom-endpoint SQLServer Flex API base URL. If unset, uses the default base URL + --telemetrylink-custom-endpoint TelemetryLink API base URL. If unset, uses the default base URL --token-custom-endpoint Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication. --verbosity Verbosity of the CLI --vpn-custom-endpoint VPN API base URL. If unset, uses the default base URL diff --git a/go.mod b/go.mod index 0a83d85b5..3007a2bb3 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.2 github.com/stackitcloud/stackit-sdk-go/services/ske v1.21.2 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.18.1 + github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.6.0 github.com/stackitcloud/stackit-sdk-go/services/valkey v0.3.1 github.com/stackitcloud/stackit-sdk-go/services/vpn v0.15.2 github.com/zalando/go-keyring v0.2.8 diff --git a/go.sum b/go.sum index 15ecaadf9..fa4605dfd 100644 --- a/go.sum +++ b/go.sum @@ -674,6 +674,8 @@ github.com/stackitcloud/stackit-sdk-go/services/ske v1.21.2 h1:4ApmRQGkcf/G/h1s1 github.com/stackitcloud/stackit-sdk-go/services/ske v1.21.2/go.mod h1:YYwFQNYrYUpa4FeT/6pxky8vp7E7pPy2F6+Ptf469hg= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.18.1 h1:EU8vhBTmlqvQGPEeakXrzjpCsYHVaD3XU3sJO9Glj38= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.18.1/go.mod h1:T1xYY3XabHJW6HppYk486ECBRswNwB3tRFM/ZOfQft0= +github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.6.0 h1:glsamuWGVTwzBk3yoWYmq8gDcDkJncepHRn/tDwSwjA= +github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.6.0/go.mod h1:JyuStufMDibWblEo4wPOrh3B02aMgIUEhDF7YK5WGas= github.com/stackitcloud/stackit-sdk-go/services/valkey v0.3.1 h1:zAdxoRrUB5sIz73xAyXXQlZZ46Yc1p3rW1PerOLHNOk= github.com/stackitcloud/stackit-sdk-go/services/valkey v0.3.1/go.mod h1:8znHnd2b6rdEHSmW/36LQa4dMMaLuN8dwo3tD6arEic= github.com/stackitcloud/stackit-sdk-go/services/vpn v0.15.2 h1:82VZJaJPwcBKSj8WV8v0ujBvbkbMxFXIvvv0wU+0Be0= diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index 55787fba1..df5ab51a7 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -11,6 +11,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/telemetrylink" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/volume" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -51,4 +52,5 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(intake.NewCmd(params)) cmd.AddCommand(cdn.NewCmd(params)) cmd.AddCommand(vpn.NewCmd(params)) + cmd.AddCommand(telemetrylink.NewCmd(params)) } diff --git a/internal/cmd/beta/telemetrylink/create/create.go b/internal/cmd/beta/telemetrylink/create/create.go new file mode 100644 index 000000000..ba2a26e21 --- /dev/null +++ b/internal/cmd/beta/telemetrylink/create/create.go @@ -0,0 +1,258 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/telemetrylink/utils" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/telemetrylink/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" + + rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" +) + +const ( + displayNameFlag = "display-name" + resourceIdFlag = "resource-id" + resourceTypeFlag = "resource-type" + descriptionFlag = "description" + telemetryRouterIdFlag = "telemetry-router-id" + accessTokenFlag = "access-token" + enabledFlag = "enabled" +) + +var ( + resourceTypes = []string{"organization", "folder", "project"} +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + DisplayName string + ResourceId string + ResourceType string + Description *string + TelemetryRouterId string + AccessToken string + Enabled *bool +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a Telemetry Link with the specified resource", + Long: "Creates a Telemetry Link with the specified resource.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a Telemetry Link with name "my-link" for project with ID "xxx" and for telemetry router with ID "yyy"`, + "$ stackit beta telemetrylink create --display-name my-link --resource-type project --resource-id xxx --telemetry-router-id yyy --access-token my-token"), + examples.NewExample( + `Create a Telemetry Link with name "my-link" for folder with ID "xxx" and for telemetry router with ID "yyy"`, + "$ stackit beta telemetrylink create --display-name my-link --resource-type folder --resource-id xxx --telemetry-router-id yyy --access-token my-token"), + examples.NewExample( + `Create a Telemetry Link with name "my-link" for organization with ID "xxx" and for telemetry router with ID "yyy"`, + "$ stackit beta telemetrylink create --display-name my-link --resource-type organization --resource-id xxx --telemetry-router-id yyy --access-token my-token"), + examples.NewExample( + `Create a Telemetry Link with name "my-link" for organization with ID "xxx" and for telemetry router with ID "yyy", and disable routing`, + "$ stackit beta telemetrylink create --display-name my-link --resource-type organization --resource-id xxx --telemetry-router-id yyy --enabled=false --access-token my-token"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + var resourceLabel string + rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) + if err == nil { + resourceLabel, err = utils.GetResourceLabel(ctx, rmApiClient.DefaultAPI, model.ResourceId, model.ResourceType) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get %v name: %v", model.ResourceType, err) + resourceLabel = model.ResourceId + } + } else { + params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) + } + + if resourceLabel == "" { + resourceLabel = model.ResourceId + } + + prompt := fmt.Sprintf("Are you sure you want to create a Telemetry Link for %q %q?", model.ResourceType, resourceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + switch model.ResourceType { + case "project": + projectReq, errReq := buildProjectRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + _, err = projectReq.Execute() + case "organization": + orgReq, errReq := buildOrganizationRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + _, err = orgReq.Execute() + case "folder": + folderReq, errReq := buildFolderRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + _, err = folderReq.Execute() + default: + params.Printer.Debug(print.ErrorLevel, "unknown resource type: %v", model.ResourceType) + } + + if err != nil { + return fmt.Errorf("create Telemetry Link: %w", err) + } + + var response *telemetrylink.TelemetryLinkResponse + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Creating Telemetry Link", func() error { + switch model.ResourceType { + case "project": + response, err = wait.CreateProjectTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + case "organization": + response, err = wait.CreateOrganizationTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + case "folder": + response, err = wait.CreateFolderTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + default: + params.Printer.Debug(print.ErrorLevel, "unknown resource type: %v", model.ResourceType) + } + if err != nil { + params.Printer.Debug(print.ErrorLevel, "error creating telemetry link: %v", err) + } + return err + }) + if err != nil { + return fmt.Errorf("wait for Telemetry Link creation: %w", err) + } + } + + return outputResult(params.Printer, model, model.Async, resourceLabel, response) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(displayNameFlag, "", "The displayed name of the Telemetry Link resource") + cmd.Flags().String(descriptionFlag, "", "The description of the Telemetry Link resource") + cmd.Flags().String(accessTokenFlag, "", "The access token of the Telemetry Router instance") + cmd.Flags().String(resourceTypeFlag, "", fmt.Sprintf("The resource type of the TelemetryLink resource, possible values are %s", resourceTypes)) + cmd.Flags().Var(flags.UUIDFlag(), resourceIdFlag, "STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource") + cmd.Flags().Var(flags.UUIDFlag(), telemetryRouterIdFlag, "The ID of the telemetry-router to route the telemetry data") + cmd.Flags().Bool(enabledFlag, true, "Enable routing through the link to a telemetry router") + + err := flags.MarkFlagsRequired(cmd, accessTokenFlag, displayNameFlag, telemetryRouterIdFlag, resourceIdFlag, resourceTypeFlag) + 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, + DisplayName: flags.FlagToStringValue(p, cmd, displayNameFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Enabled: flags.FlagToBoolPointer(p, cmd, enabledFlag), + AccessToken: flags.FlagToStringValue(p, cmd, accessTokenFlag), + ResourceType: flags.FlagToStringValue(p, cmd, resourceTypeFlag), + ResourceId: flags.FlagToStringValue(p, cmd, resourceIdFlag), + TelemetryRouterId: flags.FlagToStringValue(p, cmd, telemetryRouterIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildProjectRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiCreateOrUpdateProjectTelemetryLinkRequest, error) { + req := apiClient.DefaultAPI.CreateOrUpdateProjectTelemetryLink(ctx, model.ResourceId, model.Region).CreateOrUpdateProjectTelemetryLinkPayload( + telemetrylink.CreateOrUpdateProjectTelemetryLinkPayload{ + AccessToken: model.AccessToken, + Description: model.Description, + DisplayName: model.DisplayName, + Enabled: model.Enabled, + TelemetryRouterId: model.TelemetryRouterId, + }) + + return req, nil +} + +func buildOrganizationRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiCreateOrUpdateOrganizationTelemetryLinkRequest, error) { + req := apiClient.DefaultAPI.CreateOrUpdateOrganizationTelemetryLink(ctx, model.ResourceId, model.Region).CreateOrUpdateOrganizationTelemetryLinkPayload( + telemetrylink.CreateOrUpdateOrganizationTelemetryLinkPayload{ + AccessToken: model.AccessToken, + Description: model.Description, + DisplayName: model.DisplayName, + Enabled: model.Enabled, + TelemetryRouterId: model.TelemetryRouterId, + }) + + return req, nil +} + +func buildFolderRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiCreateOrUpdateFolderTelemetryLinkRequest, error) { + req := apiClient.DefaultAPI.CreateOrUpdateFolderTelemetryLink(ctx, model.ResourceId, model.Region).CreateOrUpdateFolderTelemetryLinkPayload( + telemetrylink.CreateOrUpdateFolderTelemetryLinkPayload{ + AccessToken: model.AccessToken, + Description: model.Description, + DisplayName: model.DisplayName, + Enabled: model.Enabled, + TelemetryRouterId: model.TelemetryRouterId, + }) + + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, async bool, resourceLabel string, resp *telemetrylink.TelemetryLinkResponse) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + if model == nil || model.GlobalFlagModel == nil { + return fmt.Errorf("input model is nil") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + operationState := "Created" + if async { + operationState = "Triggered creation of" + } + p.Outputf("%s telemetry link for %q %q. Link ID: %s\n", operationState, model.ResourceType, resourceLabel, resp.Id) + return nil + }) +} diff --git a/internal/cmd/beta/telemetrylink/create/create_test.go b/internal/cmd/beta/telemetrylink/create/create_test.go new file mode 100644 index 000000000..007a52e00 --- /dev/null +++ b/internal/cmd/beta/telemetrylink/create/create_test.go @@ -0,0 +1,390 @@ +package create + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu01" + testDisplayName = "my-telemetrylink-instance" + testDescription = "my link description" + testAccessToken = "test-access-token" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &telemetrylink.APIClient{DefaultAPI: &telemetrylink.DefaultAPIService{}} + testResourceId = uuid.NewString() + testProjectId = uuid.NewString() + testTelemetryRouterId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + descriptionFlag: testDescription, + enabledFlag: "true", + resourceIdFlag: testResourceId, + resourceTypeFlag: "project", + telemetryRouterIdFlag: testTelemetryRouterId, + accessTokenFlag: testAccessToken, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + DisplayName: testDisplayName, + Description: new(testDescription), + Enabled: new(true), + ResourceId: testResourceId, + ResourceType: "project", + TelemetryRouterId: testTelemetryRouterId, + AccessToken: testAccessToken, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureProjectRequest(mods ...func(request *telemetrylink.ApiCreateOrUpdateProjectTelemetryLinkRequest)) telemetrylink.ApiCreateOrUpdateProjectTelemetryLinkRequest { + request := testClient.DefaultAPI.CreateOrUpdateProjectTelemetryLink(testCtx, testResourceId, testRegion) + request = request.CreateOrUpdateProjectTelemetryLinkPayload(telemetrylink.CreateOrUpdateProjectTelemetryLinkPayload{ + DisplayName: testDisplayName, + Description: new(testDescription), + Enabled: new(true), + TelemetryRouterId: testTelemetryRouterId, + AccessToken: testAccessToken, + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureOrganizationRequest(mods ...func(request *telemetrylink.ApiCreateOrUpdateOrganizationTelemetryLinkRequest)) telemetrylink.ApiCreateOrUpdateOrganizationTelemetryLinkRequest { + request := testClient.DefaultAPI.CreateOrUpdateOrganizationTelemetryLink(testCtx, testResourceId, testRegion) + request = request.CreateOrUpdateOrganizationTelemetryLinkPayload(telemetrylink.CreateOrUpdateOrganizationTelemetryLinkPayload{ + DisplayName: testDisplayName, + Description: new(testDescription), + Enabled: new(true), + TelemetryRouterId: testTelemetryRouterId, + AccessToken: testAccessToken, + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureFolderRequest(mods ...func(request *telemetrylink.ApiCreateOrUpdateFolderTelemetryLinkRequest)) telemetrylink.ApiCreateOrUpdateFolderTelemetryLinkRequest { + request := testClient.DefaultAPI.CreateOrUpdateFolderTelemetryLink(testCtx, testResourceId, testRegion) + request = request.CreateOrUpdateFolderTelemetryLinkPayload(telemetrylink.CreateOrUpdateFolderTelemetryLinkPayload{ + DisplayName: testDisplayName, + Description: new(testDescription), + Enabled: new(true), + TelemetryRouterId: testTelemetryRouterId, + AccessToken: testAccessToken, + }) + + 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: "optional flags omitted", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + }), + }, + { + description: "no values provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "telemetry router id missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, telemetryRouterIdFlag) + }), + isValid: false, + }, + { + description: "access token missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, accessTokenFlag) + }), + isValid: false, + }, + { + description: "resource type missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourceTypeFlag) + }), + isValid: false, + }, + { + description: "resource id missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourceIdFlag) + }), + isValid: false, + }, + { + description: "resource id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourceIdFlag] = "" + }), + isValid: false, + }, + { + description: "resource id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "display name missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, displayNameFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildProjectRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiCreateOrUpdateProjectTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureProjectRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + }), + expectedRequest: fixtureProjectRequest().CreateOrUpdateProjectTelemetryLinkPayload(telemetrylink.CreateOrUpdateProjectTelemetryLinkPayload{ + DisplayName: testDisplayName, + Description: nil, + Enabled: new(true), + TelemetryRouterId: testTelemetryRouterId, + AccessToken: testAccessToken, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildProjectRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildFolderRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiCreateOrUpdateFolderTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureFolderRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + }), + expectedRequest: fixtureFolderRequest().CreateOrUpdateFolderTelemetryLinkPayload(telemetrylink.CreateOrUpdateFolderTelemetryLinkPayload{ + DisplayName: testDisplayName, + Description: nil, + Enabled: new(true), + TelemetryRouterId: testTelemetryRouterId, + AccessToken: testAccessToken, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildFolderRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildOrganizationRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiCreateOrUpdateOrganizationTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureOrganizationRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + }), + expectedRequest: fixtureOrganizationRequest().CreateOrUpdateOrganizationTelemetryLinkPayload(telemetrylink.CreateOrUpdateOrganizationTelemetryLinkPayload{ + DisplayName: testDisplayName, + Description: nil, + Enabled: new(true), + TelemetryRouterId: testTelemetryRouterId, + AccessToken: testAccessToken, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildOrganizationRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + instance *telemetrylink.TelemetryLinkResponse + wantErr bool + }{ + { + description: "nil response", + instance: nil, + wantErr: true, + }, + { + description: "model is nil", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: nil, + wantErr: true, + }, + { + description: "global flag nil", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: nil}, + wantErr: true, + }, + { + description: "default output", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, + wantErr: false, + }, + { + description: "json output", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}}, + wantErr: false, + }, + { + description: "yaml output", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.model, true, "label", tt.instance) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/telemetrylink/delete/delete.go b/internal/cmd/beta/telemetrylink/delete/delete.go new file mode 100644 index 000000000..4de10d3f0 --- /dev/null +++ b/internal/cmd/beta/telemetrylink/delete/delete.go @@ -0,0 +1,192 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/telemetrylink/utils" + + "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" + rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/telemetrylink/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" +) + +const ( + resourceIdFlag = "resource-id" + resourceTypeFlag = "resource-type" +) + +var ( + resourceTypes = []string{"organization", "folder", "project"} +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + ResourceId string + ResourceType string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Deletes the given Telemetry Link within the specified resource", + Long: "Deletes the given Telemetry Link within the specified resource.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete the Telemetry Link for project with ID "xxx"`, + "$ stackit beta telemetrylink delete --resource-type project --resource-id xxx"), + examples.NewExample( + `Delete the Telemetry Link for folder with ID "xxx"`, + "$ stackit beta telemetrylink delete --resource-type folder --resource-id xxx"), + examples.NewExample( + `Delete the Telemetry Link for organization with ID "xxx"`, + "$ stackit beta telemetrylink delete --resource-type organization --resource-id xxx"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + var resourceLabel string + rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) + if err == nil { + resourceLabel, err = utils.GetResourceLabel(ctx, rmApiClient.DefaultAPI, model.ResourceId, model.ResourceType) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get %v name: %v", model.ResourceType, err) + resourceLabel = model.ResourceId + } + } else { + params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) + } + + if resourceLabel == "" { + resourceLabel = model.ResourceId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the Telemetry Link for %q %q?", model.ResourceType, resourceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + switch model.ResourceType { + case "project": + projectReq, errReq := buildProjectRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + err = projectReq.Execute() + case "organization": + orgReq, errReq := buildOrganizationRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + err = orgReq.Execute() + case "folder": + folderReq, errReq := buildFolderRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + err = folderReq.Execute() + default: + params.Printer.Debug(print.ErrorLevel, "unknown resource type: %v", model.ResourceType) + } + + if err != nil { + return fmt.Errorf("delete Telemetry Link: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Deleting Telemetry Link", func() error { + switch model.ResourceType { + case "project": + _, err = wait.DeleteProjectTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + case "organization": + _, err = wait.DeleteOrganizationTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + case "folder": + _, err = wait.DeleteFolderTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + default: + params.Printer.Debug(print.ErrorLevel, "unknown resource type: %v", model.ResourceType) + } + if err != nil { + params.Printer.Debug(print.ErrorLevel, "error deleting telemetry link: %v", err) + } + return err + }) + if err != nil { + return fmt.Errorf("wait for Telemetry Link deletion: %w", err) + } + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + params.Printer.Outputf("%s telemetry link for %q %q. \n", operationState, model.ResourceType, resourceLabel) + + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(resourceTypeFlag, "", fmt.Sprintf("The resource type of the TelemetryLink resource, possible values are %s", resourceTypes)) + cmd.Flags().Var(flags.UUIDFlag(), resourceIdFlag, "STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource") + + err := flags.MarkFlagsRequired(cmd, resourceIdFlag, resourceTypeFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &cliErr.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ResourceType: flags.FlagToStringValue(p, cmd, resourceTypeFlag), + ResourceId: flags.FlagToStringValue(p, cmd, resourceIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildProjectRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiDeleteProjectTelemetryLinkRequest, error) { + return apiClient.DefaultAPI.DeleteProjectTelemetryLink(ctx, model.ResourceId, model.Region), nil +} + +func buildOrganizationRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiDeleteOrganizationTelemetryLinkRequest, error) { + return apiClient.DefaultAPI.DeleteOrganizationTelemetryLink(ctx, model.ResourceId, model.Region), nil +} + +func buildFolderRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiDeleteFolderTelemetryLinkRequest, error) { + return apiClient.DefaultAPI.DeleteFolderTelemetryLink(ctx, model.ResourceId, model.Region), nil +} diff --git a/internal/cmd/beta/telemetrylink/delete/delete_test.go b/internal/cmd/beta/telemetrylink/delete/delete_test.go new file mode 100644 index 000000000..914650875 --- /dev/null +++ b/internal/cmd/beta/telemetrylink/delete/delete_test.go @@ -0,0 +1,221 @@ +package delete + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &telemetrylink.APIClient{DefaultAPI: &telemetrylink.DefaultAPIService{}} + testResourceId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + resourceIdFlag: testResourceId, + resourceTypeFlag: "project", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + ResourceId: testResourceId, + ResourceType: "project", + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureProjectRequest(mods ...func(request *telemetrylink.ApiDeleteProjectTelemetryLinkRequest)) telemetrylink.ApiDeleteProjectTelemetryLinkRequest { + request := testClient.DefaultAPI.DeleteProjectTelemetryLink(testCtx, testResourceId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureOrganizationRequest(mods ...func(request *telemetrylink.ApiDeleteOrganizationTelemetryLinkRequest)) telemetrylink.ApiDeleteOrganizationTelemetryLinkRequest { + request := testClient.DefaultAPI.DeleteOrganizationTelemetryLink(testCtx, testResourceId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureFolderRequest(mods ...func(request *telemetrylink.ApiDeleteFolderTelemetryLinkRequest)) telemetrylink.ApiDeleteFolderTelemetryLinkRequest { + request := testClient.DefaultAPI.DeleteFolderTelemetryLink(testCtx, testResourceId, testRegion) + 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 provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "resource type missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourceTypeFlag) + }), + isValid: false, + }, + { + description: "resource id missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourceIdFlag) + }), + isValid: false, + }, + { + description: "resource id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourceIdFlag] = "" + }), + isValid: false, + }, + { + description: "resource id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildProjectRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiDeleteProjectTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureProjectRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildProjectRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildFolderRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiDeleteFolderTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureFolderRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildFolderRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildOrganizationRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiDeleteOrganizationTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureOrganizationRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildOrganizationRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/telemetrylink/describe/describe.go b/internal/cmd/beta/telemetrylink/describe/describe.go new file mode 100644 index 000000000..09844b061 --- /dev/null +++ b/internal/cmd/beta/telemetrylink/describe/describe.go @@ -0,0 +1,174 @@ +package describe + +import ( + "context" + "fmt" + + "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/telemetrylink/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" +) + +const ( + resourceIdFlag = "resource-id" + resourceTypeFlag = "resource-type" +) + +var ( + resourceTypes = []string{"organization", "folder", "project"} +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + ResourceId string + ResourceType string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Shows details of a Telemetry Link within the specified resource", + Long: "Shows details of a Telemetry Link within the specified resource.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Get details of a Telemetry Link for project with ID "xxx"`, + "$ stackit beta telemetrylink describe --resource-type project --resource-id xxx"), + examples.NewExample( + `Get details of a Telemetry Link for folder with ID "xxx"`, + "$ stackit beta telemetrylink describe --resource-type folder --resource-id xxx"), + examples.NewExample( + `Get details of a Telemetry Link for organization with ID "xxx"`, + "$ stackit beta telemetrylink describe --resource-type organization --resource-id xxx"), + examples.NewExample( + `Get details of a Telemetry Link for organization with ID "xxx" in JSON format`, + "$ stackit beta telemetrylink describe --resource-type organization --resource-id xxx --output-format json"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + var response *telemetrylink.TelemetryLinkResponse + switch model.ResourceType { + case "project": + projectReq, errReq := buildProjectRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + response, err = projectReq.Execute() + case "organization": + orgReq, errReq := buildOrganizationRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + response, err = orgReq.Execute() + case "folder": + folderReq, errReq := buildFolderRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + response, err = folderReq.Execute() + default: + params.Printer.Debug(print.ErrorLevel, "unknown resource type: %v", model.ResourceType) + } + + if err != nil { + return fmt.Errorf("describe Telemetry Link: %w", err) + } + + return outputResult(params.Printer, model, response) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(resourceTypeFlag, "", fmt.Sprintf("The resource type of the TelemetryLink resource, possible values are %s", resourceTypes)) + cmd.Flags().Var(flags.UUIDFlag(), resourceIdFlag, "STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource") + + err := flags.MarkFlagsRequired(cmd, resourceIdFlag, resourceTypeFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &cliErr.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ResourceType: flags.FlagToStringValue(p, cmd, resourceTypeFlag), + ResourceId: flags.FlagToStringValue(p, cmd, resourceIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildProjectRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiGetProjectTelemetryLinkRequest, error) { + return apiClient.DefaultAPI.GetProjectTelemetryLink(ctx, model.ResourceId, model.Region), nil +} + +func buildOrganizationRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiGetOrganizationTelemetryLinkRequest, error) { + return apiClient.DefaultAPI.GetOrganizationTelemetryLink(ctx, model.ResourceId, model.Region), nil +} + +func buildFolderRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiGetFolderTelemetryLinkRequest, error) { + return apiClient.DefaultAPI.GetFolderTelemetryLink(ctx, model.ResourceId, model.Region), nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *telemetrylink.TelemetryLinkResponse) error { + if resp == nil { + return fmt.Errorf("received nil response, could not display details") + } + + if model == nil || model.GlobalFlagModel == nil { + return fmt.Errorf("input model is nil") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + table := tables.NewTable() + table.AddRow("ID", resp.Id) + table.AddSeparator() + table.AddRow("NAME", resp.DisplayName) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(resp.Description)) + table.AddSeparator() + table.AddRow("TELEMETRY ROUTER ID", resp.TelemetryRouterId) + table.AddSeparator() + table.AddRow("STATUS", resp.Status) + table.AddSeparator() + table.AddRow("ENABLED", resp.Enabled) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/beta/telemetrylink/describe/describe_test.go b/internal/cmd/beta/telemetrylink/describe/describe_test.go new file mode 100644 index 000000000..09018c89a --- /dev/null +++ b/internal/cmd/beta/telemetrylink/describe/describe_test.go @@ -0,0 +1,278 @@ +package describe + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &telemetrylink.APIClient{DefaultAPI: &telemetrylink.DefaultAPIService{}} + testResourceId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + resourceIdFlag: testResourceId, + resourceTypeFlag: "project", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + ResourceId: testResourceId, + ResourceType: "project", + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureProjectRequest(mods ...func(request *telemetrylink.ApiGetProjectTelemetryLinkRequest)) telemetrylink.ApiGetProjectTelemetryLinkRequest { + request := testClient.DefaultAPI.GetProjectTelemetryLink(testCtx, testResourceId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureOrganizationRequest(mods ...func(request *telemetrylink.ApiGetOrganizationTelemetryLinkRequest)) telemetrylink.ApiGetOrganizationTelemetryLinkRequest { + request := testClient.DefaultAPI.GetOrganizationTelemetryLink(testCtx, testResourceId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureFolderRequest(mods ...func(request *telemetrylink.ApiGetFolderTelemetryLinkRequest)) telemetrylink.ApiGetFolderTelemetryLinkRequest { + request := testClient.DefaultAPI.GetFolderTelemetryLink(testCtx, testResourceId, testRegion) + 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 provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "resource type missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourceTypeFlag) + }), + isValid: false, + }, + { + description: "resource id missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourceIdFlag) + }), + isValid: false, + }, + { + description: "resource id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourceIdFlag] = "" + }), + isValid: false, + }, + { + description: "resource id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildProjectRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiGetProjectTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureProjectRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildProjectRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildFolderRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiGetFolderTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureFolderRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildFolderRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildOrganizationRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiGetOrganizationTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureOrganizationRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildOrganizationRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + response *telemetrylink.TelemetryLinkResponse + wantErr bool + }{ + { + description: "nil response", + response: nil, + wantErr: true, + }, + { + description: "model is nil", + response: &telemetrylink.TelemetryLinkResponse{}, + model: nil, + wantErr: true, + }, + { + description: "global flag nil", + response: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: nil}, + wantErr: true, + }, + { + description: "default output", + response: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, + wantErr: false, + }, + { + description: "json output", + response: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}}, + wantErr: false, + }, + { + description: "yaml output", + response: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.model, tt.response) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/telemetrylink/telemetrylink.go b/internal/cmd/beta/telemetrylink/telemetrylink.go new file mode 100644 index 000000000..facf5127c --- /dev/null +++ b/internal/cmd/beta/telemetrylink/telemetrylink.go @@ -0,0 +1,32 @@ +package telemetrylink + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/telemetrylink/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/telemetrylink/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/telemetrylink/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/telemetrylink/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "telemetrylink", + Short: "Provides functionality for Telemetry Link", + Long: "Provides functionality for Telemetry Link.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/beta/telemetrylink/update/update.go b/internal/cmd/beta/telemetrylink/update/update.go new file mode 100644 index 000000000..8f0f14eca --- /dev/null +++ b/internal/cmd/beta/telemetrylink/update/update.go @@ -0,0 +1,250 @@ +package update + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/telemetrylink/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/telemetrylink/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" + + rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" +) + +const ( + displayNameFlag = "display-name" + resourceIdFlag = "resource-id" + resourceTypeFlag = "resource-type" + descriptionFlag = "description" + telemetryRouterIdFlag = "telemetry-router-id" + accessTokenFlag = "access-token" + enabledFlag = "enabled" +) + +var ( + resourceTypes = []string{"organization", "folder", "project"} +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + DisplayName *string + ResourceId string + ResourceType string + Description *string + TelemetryRouterId *string + AccessToken *string + Enabled *bool +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Updates a Telemetry Link within the specified resource", + Long: "Updates a Telemetry Link within the specified resource.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Update a Telemetry Link with name "my-new-link" for project with ID "xxx"`, + "$ stackit beta telemetrylink update --display-name my-new-link --resource-type project --resource-id xxx"), + examples.NewExample( + `Update a Telemetry Link with new access token for folder with ID "xxx"`, + "$ stackit beta telemetrylink update --resource-type folder --resource-id xxx --access-token my-new-token"), + examples.NewExample( + `Update a Telemetry Link with new telemetry router ID "xxx" for organization with ID "yyy"`, + "$ stackit beta telemetrylink update --resource-type organization --resource-id yyy --telemetry-router-id xxx"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + var resourceLabel string + rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) + if err == nil { + resourceLabel, err = utils.GetResourceLabel(ctx, rmApiClient.DefaultAPI, model.ResourceId, model.ResourceType) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get %v name: %v", model.ResourceType, err) + resourceLabel = model.ResourceId + } + } else { + params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) + } + + if resourceLabel == "" { + resourceLabel = model.ResourceId + } + + prompt := fmt.Sprintf("Are you sure you want to update the Telemetry Link for %q %q?", model.ResourceType, resourceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + switch model.ResourceType { + case "project": + projectReq, errReq := buildProjectRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + _, err = projectReq.Execute() + case "organization": + orgReq, errReq := buildOrganizationRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + _, err = orgReq.Execute() + case "folder": + folderReq, errReq := buildFolderRequest(ctx, model, apiClient) + if errReq != nil { + return errReq + } + _, err = folderReq.Execute() + default: + params.Printer.Debug(print.ErrorLevel, "unknown resource type: %v", model.ResourceType) + } + + if err != nil { + return fmt.Errorf("update Telemetry Link: %w", err) + } + + var response *telemetrylink.TelemetryLinkResponse + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Updating Telemetry Link", func() error { + switch model.ResourceType { + case "project": + response, err = wait.PartialUpdateProjectTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + case "organization": + response, err = wait.PartialUpdateOrganizationTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + case "folder": + response, err = wait.PartialUpdateFolderTelemetryLinkWaitHandler(ctx, apiClient.DefaultAPI, model.ResourceId, model.Region).WaitWithContext(ctx) + default: + params.Printer.Debug(print.ErrorLevel, "unknown resource type: %v", model.ResourceType) + } + if err != nil { + params.Printer.Debug(print.ErrorLevel, "error updating telemetry link: %v", err) + } + return err + }) + if err != nil { + return fmt.Errorf("wait for Telemetry Link update: %w", err) + } + } + + return outputResult(params.Printer, model, model.Async, resourceLabel, response) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(displayNameFlag, "", "The displayed name of the Telemetry Link resource") + cmd.Flags().String(descriptionFlag, "", "The description of the Telemetry Link resource") + cmd.Flags().String(accessTokenFlag, "", "The access token of the Telemetry Router instance") + cmd.Flags().String(resourceTypeFlag, "", fmt.Sprintf("The resource type of the TelemetryLink resource, possible values are %s", resourceTypes)) + cmd.Flags().Var(flags.UUIDFlag(), resourceIdFlag, "STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource") + cmd.Flags().Var(flags.UUIDFlag(), telemetryRouterIdFlag, "The ID of the telemetry-router to route the telemetry data") + cmd.Flags().Bool(enabledFlag, true, "Enable routing through the link to a telemetry router") + + err := flags.MarkFlagsRequired(cmd, resourceIdFlag, resourceTypeFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: globalFlags, + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Enabled: flags.FlagToBoolPointer(p, cmd, enabledFlag), + AccessToken: flags.FlagToStringPointer(p, cmd, accessTokenFlag), + ResourceType: flags.FlagToStringValue(p, cmd, resourceTypeFlag), + ResourceId: flags.FlagToStringValue(p, cmd, resourceIdFlag), + TelemetryRouterId: flags.FlagToStringPointer(p, cmd, telemetryRouterIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildProjectRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiPartialUpdateProjectTelemetryLinkRequest, error) { + req := apiClient.DefaultAPI.PartialUpdateProjectTelemetryLink(ctx, model.ResourceId, model.Region).PartialUpdateProjectTelemetryLinkPayload( + telemetrylink.PartialUpdateProjectTelemetryLinkPayload{ + AccessToken: model.AccessToken, + Description: model.Description, + DisplayName: model.DisplayName, + Enabled: model.Enabled, + TelemetryRouterId: model.TelemetryRouterId, + }) + + return req, nil +} + +func buildOrganizationRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiPartialUpdateOrganizationTelemetryLinkRequest, error) { + req := apiClient.DefaultAPI.PartialUpdateOrganizationTelemetryLink(ctx, model.ResourceId, model.Region).PartialUpdateOrganizationTelemetryLinkPayload( + telemetrylink.PartialUpdateOrganizationTelemetryLinkPayload{ + AccessToken: model.AccessToken, + Description: model.Description, + DisplayName: model.DisplayName, + Enabled: model.Enabled, + TelemetryRouterId: model.TelemetryRouterId, + }) + + return req, nil +} + +func buildFolderRequest(ctx context.Context, model *inputModel, apiClient *telemetrylink.APIClient) (telemetrylink.ApiPartialUpdateFolderTelemetryLinkRequest, error) { + req := apiClient.DefaultAPI.PartialUpdateFolderTelemetryLink(ctx, model.ResourceId, model.Region).PartialUpdateFolderTelemetryLinkPayload( + telemetrylink.PartialUpdateFolderTelemetryLinkPayload{ + AccessToken: model.AccessToken, + Description: model.Description, + DisplayName: model.DisplayName, + Enabled: model.Enabled, + TelemetryRouterId: model.TelemetryRouterId, + }) + + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, async bool, resourceLabel string, resp *telemetrylink.TelemetryLinkResponse) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + if model == nil || model.GlobalFlagModel == nil { + return fmt.Errorf("input model is nil") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + operationState := "Updated" + if async { + operationState = "Triggered update of" + } + p.Outputf("%s telemetry link for %q %q. Link ID: %s\n", operationState, model.ResourceType, resourceLabel, resp.Id) + return nil + }) +} diff --git a/internal/cmd/beta/telemetrylink/update/update_test.go b/internal/cmd/beta/telemetrylink/update/update_test.go new file mode 100644 index 000000000..c1e2039fc --- /dev/null +++ b/internal/cmd/beta/telemetrylink/update/update_test.go @@ -0,0 +1,389 @@ +package update + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu01" + testDisplayName = "my-telemetrylink-instance" + testDescription = "my link description" + testAccessToken = "test-access-token" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &telemetrylink.APIClient{DefaultAPI: &telemetrylink.DefaultAPIService{}} + testResourceId = uuid.NewString() + testProjectId = uuid.NewString() + testTelemetryRouterId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + descriptionFlag: testDescription, + enabledFlag: "true", + resourceIdFlag: testResourceId, + resourceTypeFlag: "project", + telemetryRouterIdFlag: testTelemetryRouterId, + accessTokenFlag: testAccessToken, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + DisplayName: new(testDisplayName), + Description: new(testDescription), + Enabled: new(true), + ResourceId: testResourceId, + ResourceType: "project", + TelemetryRouterId: new(testTelemetryRouterId), + AccessToken: new(testAccessToken), + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureProjectRequest(mods ...func(request *telemetrylink.ApiPartialUpdateProjectTelemetryLinkRequest)) telemetrylink.ApiPartialUpdateProjectTelemetryLinkRequest { + request := testClient.DefaultAPI.PartialUpdateProjectTelemetryLink(testCtx, testResourceId, testRegion) + request = request.PartialUpdateProjectTelemetryLinkPayload(telemetrylink.PartialUpdateProjectTelemetryLinkPayload{ + DisplayName: new(testDisplayName), + Description: new(testDescription), + Enabled: new(true), + TelemetryRouterId: new(testTelemetryRouterId), + AccessToken: new(testAccessToken), + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureOrganizationRequest(mods ...func(request *telemetrylink.ApiPartialUpdateOrganizationTelemetryLinkRequest)) telemetrylink.ApiPartialUpdateOrganizationTelemetryLinkRequest { + request := testClient.DefaultAPI.PartialUpdateOrganizationTelemetryLink(testCtx, testResourceId, testRegion) + request = request.PartialUpdateOrganizationTelemetryLinkPayload(telemetrylink.PartialUpdateOrganizationTelemetryLinkPayload{ + DisplayName: new(testDisplayName), + Description: new(testDescription), + Enabled: new(true), + TelemetryRouterId: new(testTelemetryRouterId), + AccessToken: new(testAccessToken), + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureFolderRequest(mods ...func(request *telemetrylink.ApiPartialUpdateFolderTelemetryLinkRequest)) telemetrylink.ApiPartialUpdateFolderTelemetryLinkRequest { + request := testClient.DefaultAPI.PartialUpdateFolderTelemetryLink(testCtx, testResourceId, testRegion) + request = request.PartialUpdateFolderTelemetryLinkPayload(telemetrylink.PartialUpdateFolderTelemetryLinkPayload{ + DisplayName: new(testDisplayName), + Description: new(testDescription), + Enabled: new(true), + TelemetryRouterId: new(testTelemetryRouterId), + AccessToken: new(testAccessToken), + }) + + 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: "optional flags omitted", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + delete(flagValues, displayNameFlag) + delete(flagValues, telemetryRouterIdFlag) + delete(flagValues, accessTokenFlag) + delete(flagValues, enabledFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.TelemetryRouterId = nil + model.AccessToken = nil + model.Enabled = nil + model.DisplayName = nil + }), + }, + { + description: "no values provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "resource type missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourceTypeFlag) + }), + isValid: false, + }, + { + description: "resource id missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourceIdFlag) + }), + isValid: false, + }, + { + description: "resource id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourceIdFlag] = "" + }), + isValid: false, + }, + { + description: "resource id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildProjectRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiPartialUpdateProjectTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureProjectRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.DisplayName = nil + model.Enabled = nil + model.TelemetryRouterId = nil + model.AccessToken = nil + }), + expectedRequest: fixtureProjectRequest().PartialUpdateProjectTelemetryLinkPayload(telemetrylink.PartialUpdateProjectTelemetryLinkPayload{ + DisplayName: nil, + Description: nil, + Enabled: nil, + TelemetryRouterId: nil, + AccessToken: nil, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildProjectRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildFolderRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiPartialUpdateFolderTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureFolderRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.DisplayName = nil + model.Enabled = nil + model.TelemetryRouterId = nil + model.AccessToken = nil + }), + expectedRequest: fixtureFolderRequest().PartialUpdateFolderTelemetryLinkPayload(telemetrylink.PartialUpdateFolderTelemetryLinkPayload{ + DisplayName: nil, + Description: nil, + Enabled: nil, + TelemetryRouterId: nil, + AccessToken: nil, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildFolderRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildOrganizationRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest telemetrylink.ApiPartialUpdateOrganizationTelemetryLinkRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureOrganizationRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.DisplayName = nil + model.Enabled = nil + model.TelemetryRouterId = nil + model.AccessToken = nil + }), + expectedRequest: fixtureOrganizationRequest().PartialUpdateOrganizationTelemetryLinkPayload(telemetrylink.PartialUpdateOrganizationTelemetryLinkPayload{ + DisplayName: nil, + Description: nil, + Enabled: nil, + TelemetryRouterId: nil, + AccessToken: nil, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildOrganizationRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, telemetrylink.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + instance *telemetrylink.TelemetryLinkResponse + wantErr bool + }{ + { + description: "nil response", + instance: nil, + wantErr: true, + }, + { + description: "model is nil", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: nil, + wantErr: true, + }, + { + description: "global flag nil", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: nil}, + wantErr: true, + }, + { + description: "default output", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, + wantErr: false, + }, + { + description: "json output", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}}, + wantErr: false, + }, + { + description: "yaml output", + instance: &telemetrylink.TelemetryLinkResponse{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.model, true, "label", tt.instance) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 5a37ee68c..6863f5c31 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -56,6 +56,7 @@ const ( sfsCustomEndpointFlag = "sfs-custom-endpoint" cdnCustomEndpointFlag = "cdn-custom-endpoint" vpnCustomEndpointFlag = "vpn-custom-endpoint" + telemetryLinkCustomEndpointFlag = "telemetrylink-custom-endpoint" ) type inputModel struct { @@ -178,6 +179,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(sfsCustomEndpointFlag, "", "SFS API base URL, used in calls to this API") cmd.Flags().String(cdnCustomEndpointFlag, "", "CDN API base URL, used in calls to this API") cmd.Flags().String(vpnCustomEndpointFlag, "", "VPN API base URL, used in calls to this API") + cmd.Flags().String(telemetryLinkCustomEndpointFlag, "", "TelemetryLink API base URL, used in calls to this API") err := viper.BindPFlag(config.SessionTimeLimitKey, cmd.Flags().Lookup(sessionTimeLimitFlag)) cobra.CheckErr(err) @@ -252,6 +254,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.VPNCustomEndpointKey, cmd.Flags().Lookup(vpnCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.TelemetryLinkCustomEndpointKey, cmd.Flags().Lookup(telemetryLinkCustomEndpointFlag)) + cobra.CheckErr(err) } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index ba128c62f..110071d73 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -59,6 +59,7 @@ const ( logsCustomEndpointFlag = "logs-custom-endpoint" cdnCustomEndpointFlag = "cdn-custom-endpoint" vpnCustomEndpointFlag = "vpn-custom-endpoint" + telemetryLinkCustomEndpointFlag = "telemetrylink-custom-endpoint" ) var ( @@ -111,6 +112,7 @@ type inputModel struct { LogsCustomEndpoint bool CDNCustomEndpoint bool VpnCustomEndpoint bool + TelemetryLinkCustomEndpoint bool } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -261,6 +263,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if model.VpnCustomEndpoint { viper.Set(config.VPNCustomEndpointKey, "") } + if model.TelemetryLinkCustomEndpoint { + viper.Set(config.TelemetryLinkCustomEndpointKey, "") + } err := config.Write() if err != nil { @@ -318,6 +323,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(sfsCustomEndpointFlag, false, "SFS API base URL. If unset, uses the default base URL") cmd.Flags().Bool(cdnCustomEndpointFlag, false, "Custom CDN endpoint URL. If unset, uses the default base URL") cmd.Flags().Bool(vpnCustomEndpointFlag, false, "VPN API base URL. If unset, uses the default base URL") + cmd.Flags().Bool(telemetryLinkCustomEndpointFlag, false, "TelemetryLink API base URL. If unset, uses the default base URL") } func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { @@ -366,6 +372,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { LogsCustomEndpoint: flags.FlagToBoolValue(p, cmd, logsCustomEndpointFlag), CDNCustomEndpoint: flags.FlagToBoolValue(p, cmd, cdnCustomEndpointFlag), VpnCustomEndpoint: flags.FlagToBoolValue(p, cmd, vpnCustomEndpointFlag), + TelemetryLinkCustomEndpoint: flags.FlagToBoolValue(p, cmd, telemetryLinkCustomEndpointFlag), } p.DebugInputModel(model) diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index 5ca73e03a..3427111ed 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -51,6 +51,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool logsCustomEndpointFlag: true, cdnCustomEndpointFlag: true, vpnCustomEndpointFlag: true, + telemetryLinkCustomEndpointFlag: true, } for _, mod := range mods { mod(flagValues) @@ -100,6 +101,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { LogsCustomEndpoint: true, CDNCustomEndpoint: true, VpnCustomEndpoint: true, + TelemetryLinkCustomEndpoint: true, } for _, mod := range mods { mod(model) @@ -165,6 +167,7 @@ func TestParseInput(t *testing.T) { model.LogsCustomEndpoint = false model.CDNCustomEndpoint = false model.VpnCustomEndpoint = false + model.TelemetryLinkCustomEndpoint = false }), }, { @@ -397,6 +400,16 @@ func TestParseInput(t *testing.T) { model.VpnCustomEndpoint = false }), }, + { + description: "telemetrylink custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[telemetryLinkCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.TelemetryLinkCustomEndpoint = false + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index b0ad97036..92553200f 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -59,6 +59,7 @@ const ( LogsCustomEndpointKey = "logs_custom_endpoint" ValkeyCustomEndpointKey = "valkey_custom_endpoint" VPNCustomEndpointKey = "vpn_custom_endpoint" + TelemetryLinkCustomEndpointKey = "telemetrylink_custom_endpoint" ProjectNameKey = "project_name" DefaultProfileName = "default" @@ -130,6 +131,7 @@ var ConfigKeys = []string{ TokenCustomEndpointKey, ValkeyCustomEndpointKey, VPNCustomEndpointKey, + TelemetryLinkCustomEndpointKey, } var defaultConfigFolderPath string @@ -225,6 +227,7 @@ func setConfigDefaults() { viper.SetDefault(CDNCustomEndpointKey, "") viper.SetDefault(ValkeyCustomEndpointKey, "") viper.SetDefault(VPNCustomEndpointKey, "") + viper.SetDefault(TelemetryLinkCustomEndpointKey, "") } func getConfigFilePath(configFolder string) string { diff --git a/internal/pkg/services/resourcemanager/utils/utils.go b/internal/pkg/services/resourcemanager/utils/utils.go index b6144e4e2..ca5c57dd3 100644 --- a/internal/pkg/services/resourcemanager/utils/utils.go +++ b/internal/pkg/services/resourcemanager/utils/utils.go @@ -25,3 +25,13 @@ func GetProjectName(ctx context.Context, apiClient resourcemanager.DefaultAPI, p return resp.Name, nil } + +// GetFolderName returns the name of a folder by its ID. +func GetFolderName(ctx context.Context, apiClient resourcemanager.DefaultAPI, folderId string) (string, error) { + resp, err := apiClient.GetFolderDetails(ctx, folderId).Execute() + if err != nil { + return "", fmt.Errorf("get folder details: %w", err) + } + + return resp.Name, nil +} diff --git a/internal/pkg/services/resourcemanager/utils/utils_test.go b/internal/pkg/services/resourcemanager/utils/utils_test.go index ec53935a7..c0d392ce2 100644 --- a/internal/pkg/services/resourcemanager/utils/utils_test.go +++ b/internal/pkg/services/resourcemanager/utils/utils_test.go @@ -12,11 +12,13 @@ import ( ) var ( - testOrgId = uuid.NewString() + testOrgId = uuid.NewString() + testFolderId = uuid.NewString() ) const ( - testOrgName = "organization" + testOrgName = "organization" + testFolderName = "folder" ) type resourceManagerClientMocked struct { @@ -24,6 +26,8 @@ type resourceManagerClientMocked struct { getOrganizationResp *resourcemanager.OrganizationResponse getProjectFails bool getProjectResp *resourcemanager.GetProjectResponse + getFolderFails bool + getFolderResp *resourcemanager.GetFolderDetailsResponse } func (s *resourceManagerClientMocked) newMock() resourcemanager.DefaultAPI { @@ -40,6 +44,12 @@ func (s *resourceManagerClientMocked) newMock() resourcemanager.DefaultAPI { } return s.getProjectResp, nil }), + GetFolderDetailsExecuteMock: utils.Ptr(func(_ resourcemanager.ApiGetFolderDetailsRequest) (*resourcemanager.GetFolderDetailsResponse, error) { + if s.getFolderFails { + return nil, fmt.Errorf("could not get folder") + } + return s.getFolderResp, nil + }), } } @@ -138,3 +148,51 @@ func TestGetProjectName(t *testing.T) { }) } } + +func TestGetFolderName(t *testing.T) { + tests := []struct { + description string + getFolderFails bool + getFolderResp *resourcemanager.GetFolderDetailsResponse + isValid bool + expectedOutput string + }{ + { + description: "base", + getFolderResp: &resourcemanager.GetFolderDetailsResponse{ + Name: testFolderName, + }, + isValid: true, + expectedOutput: testFolderName, + }, + { + description: "get organization fails", + getFolderFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &resourceManagerClientMocked{ + getFolderFails: tt.getFolderFails, + getFolderResp: tt.getFolderResp, + } + + output, err := GetFolderName(context.Background(), client.newMock(), testFolderId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} diff --git a/internal/pkg/services/telemetrylink/client/client.go b/internal/pkg/services/telemetrylink/client/client.go new file mode 100644 index 000000000..6f110b8c4 --- /dev/null +++ b/internal/pkg/services/telemetrylink/client/client.go @@ -0,0 +1,15 @@ +package client + +import ( + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/spf13/viper" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*telemetrylink.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.TelemetryLinkCustomEndpointKey), false, telemetrylink.NewAPIClient) +} diff --git a/internal/pkg/services/telemetrylink/utils/utils.go b/internal/pkg/services/telemetrylink/utils/utils.go new file mode 100644 index 000000000..68285f141 --- /dev/null +++ b/internal/pkg/services/telemetrylink/utils/utils.go @@ -0,0 +1,35 @@ +package utils + +import ( + "context" + "fmt" + + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + + rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils" +) + +// GetResourceLabel returns the name of the given resource +func GetResourceLabel(ctx context.Context, apiClient resourcemanager.DefaultAPI, resourceId, resourceType string) (string, error) { + var resourceLabel string + var err error + switch resourceType { + case "project": + resourceLabel, err = rmUtils.GetProjectName(ctx, apiClient, resourceId) + case "organization": + resourceLabel, err = rmUtils.GetOrganizationName(ctx, apiClient, resourceId) + case "folder": + resourceLabel, err = rmUtils.GetFolderName(ctx, apiClient, resourceId) + default: + return "", fmt.Errorf("unknown resource type: %v", resourceType) + } + if err != nil { + return "", fmt.Errorf("get %v name: %w", resourceType, err) + } + + if resourceLabel == "" { + resourceLabel = resourceId + } + + return resourceLabel, nil +} diff --git a/internal/pkg/services/telemetrylink/utils/utils_test.go b/internal/pkg/services/telemetrylink/utils/utils_test.go new file mode 100644 index 000000000..93537edeb --- /dev/null +++ b/internal/pkg/services/telemetrylink/utils/utils_test.go @@ -0,0 +1,149 @@ +package utils + +import ( + "context" + "fmt" + "testing" + + "github.com/google/uuid" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var ( + testOrgId = uuid.NewString() + testFolderId = uuid.NewString() + testProjectId = uuid.NewString() +) + +const ( + testOrgName = "organization" + testFolderName = "folder" + testProjectName = "project" +) + +type resourceManagerClientMocked struct { + getOrganizationFails bool + getOrganizationResp *resourcemanager.OrganizationResponse + getProjectFails bool + getProjectResp *resourcemanager.GetProjectResponse + getFolderFails bool + getFolderResp *resourcemanager.GetFolderDetailsResponse +} + +func (s *resourceManagerClientMocked) newMock() resourcemanager.DefaultAPI { + return resourcemanager.DefaultAPIServiceMock{ + GetOrganizationExecuteMock: utils.Ptr(func(_ resourcemanager.ApiGetOrganizationRequest) (*resourcemanager.OrganizationResponse, error) { + if s.getOrganizationFails { + return nil, fmt.Errorf("could not get organization") + } + return s.getOrganizationResp, nil + }), + GetProjectExecuteMock: utils.Ptr(func(_ resourcemanager.ApiGetProjectRequest) (*resourcemanager.GetProjectResponse, error) { + if s.getProjectFails { + return nil, fmt.Errorf("could not get project") + } + return s.getProjectResp, nil + }), + GetFolderDetailsExecuteMock: utils.Ptr(func(_ resourcemanager.ApiGetFolderDetailsRequest) (*resourcemanager.GetFolderDetailsResponse, error) { + if s.getFolderFails { + return nil, fmt.Errorf("could not get folder") + } + return s.getFolderResp, nil + }), + } +} + +func TestGetResourceLabel(t *testing.T) { + tests := []struct { + description string + isValid bool + expectedOutput string + resourceType string + resourceId string + getProjectNameFails bool + getOrganizationNameFails bool + getFolderNameFails bool + getOrganizationResp *resourcemanager.OrganizationResponse + getProjectResp *resourcemanager.GetProjectResponse + getFolderResp *resourcemanager.GetFolderDetailsResponse + }{ + { + description: "get project label", + isValid: true, + resourceType: "project", + resourceId: testProjectId, + getProjectNameFails: false, + expectedOutput: testProjectName, + getProjectResp: &resourcemanager.GetProjectResponse{ + Name: "project", + }, + }, + { + description: "get project name fails", + getProjectNameFails: true, + isValid: false, + }, + { + description: "get folder label", + isValid: true, + resourceType: "folder", + resourceId: testFolderId, + getProjectNameFails: false, + expectedOutput: testFolderName, + getFolderResp: &resourcemanager.GetFolderDetailsResponse{ + Name: testFolderName, + }, + }, + { + description: "get folder name fails", + getFolderNameFails: true, + isValid: false, + }, + { + description: "get organization label", + isValid: true, + resourceType: "organization", + resourceId: testOrgId, + getProjectNameFails: false, + expectedOutput: testOrgName, + getOrganizationResp: &resourcemanager.OrganizationResponse{ + Name: testOrgName, + }, + }, + { + description: "get organization name fails", + getOrganizationNameFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &resourceManagerClientMocked{ + getFolderFails: tt.getFolderNameFails, + getOrganizationFails: tt.getOrganizationNameFails, + getProjectFails: tt.getProjectNameFails, + getProjectResp: tt.getProjectResp, + getOrganizationResp: tt.getOrganizationResp, + getFolderResp: tt.getFolderResp, + } + + output, err := GetResourceLabel(context.Background(), client.newMock(), tt.resourceId, tt.resourceType) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +}