mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| ## stackit load-balancer observability-credentials cleanup | ||
| Deletes observability credentials unused by any Load Balancer | ||
| ### Synopsis | ||
| Deletes observability credentials unused by any Load Balancer. | ||
| ``` | ||
| stackit load-balancer observability-credentials cleanup [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete observability credentials unused by any Load Balancer | ||
| $ stackit load-balancer observability-credentials cleanup | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit load-balancer observability-credentials cleanup" | ||
| ``` | ||
| ### 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"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials | ||
| package cleanup | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| ) | ||
| const testCredentialsRef = "credentials-1" | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &loadbalancer.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureDeleteCredentialRequest(mods ...func(request *loadbalancer.ApiDeleteCredentialsRequest)) loadbalancer.ApiDeleteCredentialsRequest { | ||
| request := testClient.DeleteCredentials(testCtx, testProjectId, testCredentialsRef) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixtureListCredentialsRequest(mods ...func(request *loadbalancer.ApiListCredentialsRequest)) loadbalancer.ApiListCredentialsRequest { | ||
| request := testClient.ListCredentials(testCtx, testProjectId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildDeleteCredentialRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest loadbalancer.ApiDeleteCredentialsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureDeleteCredentialRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildDeleteCredentialRequest(testCtx, tt.model, testClient, testCredentialsRef) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestListCredentialsRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest loadbalancer.ApiListCredentialsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureListCredentialsRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildListCredentialsRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package cleanup | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "cleanup", | ||
| Short: "Deletes observability credentials unused by any Load Balancer", | ||
| Long: "Deletes observability credentials unused by any Load Balancer.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete observability credentials unused by any Load Balancer`, | ||
| "$ stackit load-balancer observability-credentials cleanup"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } | ||
| listReq := buildListCredentialsRequest(ctx, model, apiClient) | ||
| resp, err := listReq.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("list Load Balancer observability credentials: %w", err) | ||
| } | ||
| var credentials []loadbalancer.CredentialsResponse | ||
| if resp.Credentials != nil && len(*resp.Credentials) > 0 { | ||
| credentials, err = utils.FilterCredentials(ctx, apiClient, *resp.Credentials, model.ProjectId, utils.OP_FILTER_UNUSED) | ||
| if err != nil { | ||
| return fmt.Errorf("filter Load Balancer observability credentials: %w", err) | ||
| } | ||
| } | ||
| if len(credentials) == 0 { | ||
| p.Info("No unused observability credentials found on project %q\n", projectLabel) | ||
| return nil | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := "Will delete the following unused observability credentials: \n" | ||
| for _, credential := range credentials { | ||
| if credential.DisplayName == nil || credential.Username == nil { | ||
| return fmt.Errorf("list unused Load Balancer observability credentials: credentials %q missing display name or username", *credential.CredentialsRef) | ||
| } | ||
| name := *credential.DisplayName | ||
| username := *credential.Username | ||
| prompt += fmt.Sprintf(" - %s (username: %q)\n", name, username) | ||
| } | ||
| prompt += fmt.Sprintf("Are you sure you want to delete unused observability credentials on project %q? (This cannot be undone)", projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| for _, credential := range credentials { | ||
| if credential.CredentialsRef == nil { | ||
| return fmt.Errorf("delete Load Balancer observability credentials: missing credentials reference") | ||
| } | ||
| credentialsRef := *credential.CredentialsRef | ||
| // Call API | ||
| req := buildDeleteCredentialRequest(ctx, model, apiClient, credentialsRef) | ||
| _, err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("delete Load Balancer observability credentials: %w", err) | ||
| } | ||
| } | ||
| p.Info("Deleted unused Load Balancer observability credentials on project %q\n", projectLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildDeleteCredentialRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient, credentialsRef string) loadbalancer.ApiDeleteCredentialsRequest { | ||
| req := apiClient.DeleteCredentials(ctx, model.ProjectId, credentialsRef) | ||
| return req | ||
| } | ||
| func buildListCredentialsRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiListCredentialsRequest { | ||
| req := apiClient.ListCredentials(ctx, model.ProjectId) | ||
| return req | ||
| } |
+24
-1
@@ -104,3 +104,3 @@ before: | ||
| description: "A command-line interface to manage STACKIT resources.\nThis CLI is in a BETA state. More services and functionality will be supported soon." | ||
| folder: Formula | ||
| directory: Formula | ||
| license: "Apache-2.0" | ||
@@ -129,1 +129,24 @@ # If set to auto, the release will not be uploaded to the homebrew tap repo | ||
| publish: true | ||
| winget: | ||
| - name: stackit | ||
| publisher: stackitcloud | ||
| short_description: A command-line interface to manage STACKIT resources. | ||
| license: Apache-2.0 | ||
| publisher_support_url: "https://github.com/stackitcloud/stackit-cli/issues" | ||
| package_identifier: stackitcloud.stackit | ||
| homepage: "https://github.com/stackitcloud/stackit-cli" | ||
| # If set to auto, the release will not be uploaded to the homebrew tap repo | ||
| # if the tag has a prerelease indicator (e.g. v0.0.1-alpha1) | ||
| # Temporarily not skipping prereleases to test integration with Winget | ||
| # skip_upload: auto | ||
| repository: | ||
| owner: stackitcloud | ||
| name: winget-pkgs | ||
| pull_request: | ||
| enabled: true | ||
| draft: true | ||
| base: | ||
| owner: microsoft | ||
| name: winget-pkgs | ||
| branch: master |
| ## stackit load-balancer observability-credentials list | ||
| Lists all observability credentials for Load Balancer | ||
| Lists observability credentials for Load Balancer | ||
| ### Synopsis | ||
| Lists all observability credentials for Load Balancer. | ||
| Lists observability credentials for Load Balancer. | ||
@@ -16,9 +16,15 @@ ``` | ||
| ``` | ||
| List all observability credentials for Load Balancer | ||
| List all Load Balancer observability credentials | ||
| $ stackit load-balancer observability-credentials list | ||
| List all observability credentials for Load Balancer in JSON format | ||
| List all observability credentials being used by Load Balancer | ||
| $ stackit load-balancer observability-credentials list --used | ||
| List all observability credentials not being used by Load Balancer | ||
| $ stackit load-balancer observability-credentials list --unused | ||
| List all Load Balancer observability credentials in JSON format | ||
| $ stackit load-balancer observability-credentials list --output-format json | ||
| List up to 10 observability credentials for Load Balancer | ||
| List up to 10 Load Balancer observability credentials | ||
| $ stackit load-balancer observability-credentials list --limit 10 | ||
@@ -32,2 +38,4 @@ ``` | ||
| --limit int Maximum number of entries to list | ||
| --unused List only credentials not being used by a Load Balancer | ||
| --used List only credentials being used by a Load Balancer | ||
| ``` | ||
@@ -34,0 +42,0 @@ |
@@ -33,6 +33,7 @@ ## stackit load-balancer observability-credentials | ||
| * [stackit load-balancer observability-credentials add](./stackit_load-balancer_observability-credentials_add.md) - Adds observability credentials to Load Balancer | ||
| * [stackit load-balancer observability-credentials cleanup](./stackit_load-balancer_observability-credentials_cleanup.md) - Deletes observability credentials unused by any Load Balancer | ||
| * [stackit load-balancer observability-credentials delete](./stackit_load-balancer_observability-credentials_delete.md) - Deletes observability credentials for Load Balancer | ||
| * [stackit load-balancer observability-credentials describe](./stackit_load-balancer_observability-credentials_describe.md) - Shows details of observability credentials for Load Balancer | ||
| * [stackit load-balancer observability-credentials list](./stackit_load-balancer_observability-credentials_list.md) - Lists all observability credentials for Load Balancer | ||
| * [stackit load-balancer observability-credentials list](./stackit_load-balancer_observability-credentials_list.md) - Lists observability credentials for Load Balancer | ||
| * [stackit load-balancer observability-credentials update](./stackit_load-balancer_observability-credentials_update.md) - Updates observability credentials for Load Balancer | ||
@@ -9,2 +9,3 @@ package list | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| lbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
@@ -112,2 +113,30 @@ | ||
| }, | ||
| { | ||
| description: "used", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[usedFlag] = "true" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Used = true | ||
| }), | ||
| }, | ||
| { | ||
| description: "unused", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[unusedFlag] = "true" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Unused = true | ||
| }), | ||
| }, | ||
| { | ||
| description: "used and unused", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[usedFlag] = "true" | ||
| flagValues[unusedFlag] = "true" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
@@ -142,2 +171,10 @@ | ||
| err = cmd.ValidateFlagGroups() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
@@ -189,1 +226,50 @@ if err != nil { | ||
| } | ||
| func TestGetFilterOp(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| used bool | ||
| unused bool | ||
| expectedFilterOp int | ||
| isValid bool | ||
| }{ | ||
| { | ||
| description: "used", | ||
| used: true, | ||
| expectedFilterOp: lbUtils.OP_FILTER_USED, | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "unused", | ||
| unused: true, | ||
| expectedFilterOp: lbUtils.OP_FILTER_UNUSED, | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "used and unused", | ||
| used: true, | ||
| unused: true, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "neither used nor unused", | ||
| expectedFilterOp: lbUtils.OP_FILTER_NOP, | ||
| isValid: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| filterOp, err := getFilterOp(tt.used, tt.unused) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error getting filter op: %v", err) | ||
| } | ||
| if filterOp != tt.expectedFilterOp { | ||
| t.Fatalf("Data does not match: %d", filterOp) | ||
| } | ||
| }) | ||
| } | ||
| } |
@@ -16,2 +16,3 @@ package list | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
@@ -26,2 +27,4 @@ | ||
| limitFlag = "limit" | ||
| usedFlag = "used" | ||
| unusedFlag = "unused" | ||
| ) | ||
@@ -31,3 +34,5 @@ | ||
| *globalflags.GlobalFlagModel | ||
| Limit *int64 | ||
| Limit *int64 | ||
| Used bool | ||
| Unused bool | ||
| } | ||
@@ -38,14 +43,20 @@ | ||
| Use: "list", | ||
| Short: "Lists all observability credentials for Load Balancer", | ||
| Long: "Lists all observability credentials for Load Balancer.", | ||
| Short: "Lists observability credentials for Load Balancer", | ||
| Long: "Lists observability credentials for Load Balancer.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `List all observability credentials for Load Balancer`, | ||
| `List all Load Balancer observability credentials`, | ||
| "$ stackit load-balancer observability-credentials list"), | ||
| examples.NewExample( | ||
| `List all observability credentials for Load Balancer in JSON format`, | ||
| `List all observability credentials being used by Load Balancer`, | ||
| "$ stackit load-balancer observability-credentials list --used"), | ||
| examples.NewExample( | ||
| `List all observability credentials not being used by Load Balancer`, | ||
| "$ stackit load-balancer observability-credentials list --unused"), | ||
| examples.NewExample( | ||
| `List all Load Balancer observability credentials in JSON format`, | ||
| "$ stackit load-balancer observability-credentials list --output-format json"), | ||
| examples.NewExample( | ||
| `List up to 10 observability credentials for Load Balancer`, | ||
| `List up to 10 Load Balancer observability credentials`, | ||
| "$ stackit load-balancer observability-credentials list --limit 10"), | ||
@@ -79,9 +90,27 @@ ), | ||
| credentialsPtr := resp.Credentials | ||
| if credentialsPtr == nil || (credentialsPtr != nil && len(*credentialsPtr) == 0) { | ||
| p.Info("No observability credentials found for Load Balancer on project %q\n", projectLabel) | ||
| var credentials []loadbalancer.CredentialsResponse | ||
| if credentialsPtr != nil && len(*credentialsPtr) > 0 { | ||
| credentials = *credentialsPtr | ||
| filterOp, err := getFilterOp(model.Used, model.Unused) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| credentials, err = utils.FilterCredentials(ctx, apiClient, credentials, model.ProjectId, filterOp) | ||
| if err != nil { | ||
| return fmt.Errorf("filter credentials: %w", err) | ||
| } | ||
| } | ||
| if len(credentials) == 0 { | ||
| opLabel := "No " | ||
| if model.Used { | ||
| opLabel += "used" | ||
| } else if model.Unused { | ||
| opLabel += "unused" | ||
| } | ||
| p.Info("%s observability credentials found for Load Balancer on project %q\n", opLabel, projectLabel) | ||
| return nil | ||
| } | ||
| credentials := *credentialsPtr | ||
| // Truncate output | ||
@@ -100,2 +129,6 @@ if model.Limit != nil && len(credentials) > int(*model.Limit) { | ||
| cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") | ||
| cmd.Flags().Bool(usedFlag, false, "List only credentials being used by a Load Balancer") | ||
| cmd.Flags().Bool(unusedFlag, false, "List only credentials not being used by a Load Balancer") | ||
| cmd.MarkFlagsMutuallyExclusive(usedFlag, unusedFlag) | ||
| } | ||
@@ -120,2 +153,4 @@ | ||
| Limit: limit, | ||
| Used: flags.FlagToBoolValue(p, cmd, usedFlag), | ||
| Unused: flags.FlagToBoolValue(p, cmd, unusedFlag), | ||
| } | ||
@@ -165,1 +200,18 @@ | ||
| } | ||
| func getFilterOp(used, unused bool) (int, error) { | ||
| // should not happen, cobra handles this | ||
| if used && unused { | ||
| return 0, fmt.Errorf("used and unused flags are mutually exclusive") | ||
| } | ||
| if !used && !unused { | ||
| return utils.OP_FILTER_NOP, nil | ||
| } | ||
| if used { | ||
| return utils.OP_FILTER_USED, nil | ||
| } | ||
| return utils.OP_FILTER_UNUSED, nil | ||
| } |
@@ -5,2 +5,3 @@ package credentials | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/add" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/cleanup" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/delete" | ||
@@ -36,2 +37,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/describe" | ||
| cmd.AddCommand(list.NewCmd(p)) | ||
| cmd.AddCommand(cleanup.NewCmd(p)) | ||
| } |
@@ -60,2 +60,6 @@ package addtarget | ||
| func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { | ||
| return nil, nil | ||
| } | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
@@ -62,0 +66,0 @@ argValues := []string{ |
@@ -60,2 +60,6 @@ package removetarget | ||
| func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { | ||
| return nil, nil | ||
| } | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
@@ -62,0 +66,0 @@ argValues := []string{ |
@@ -18,2 +18,3 @@ package utils | ||
| testProjectId = uuid.NewString() | ||
| testCtx = context.Background() | ||
| ) | ||
@@ -28,6 +29,8 @@ | ||
| type loadBalancerClientMocked struct { | ||
| getCredentialsFails bool | ||
| getCredentialsResp *loadbalancer.GetCredentialsResponse | ||
| getLoadBalancerFails bool | ||
| getLoadBalancerResp *loadbalancer.LoadBalancer | ||
| getCredentialsFails bool | ||
| getCredentialsResp *loadbalancer.GetCredentialsResponse | ||
| getLoadBalancerFails bool | ||
| getLoadBalancerResp *loadbalancer.LoadBalancer | ||
| listLoadBalancersFails bool | ||
| listLoadBalancersResp *loadbalancer.ListLoadBalancersResponse | ||
| } | ||
@@ -49,2 +52,9 @@ | ||
| func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { | ||
| if m.listLoadBalancersFails { | ||
| return nil, fmt.Errorf("could not list load balancers") | ||
| } | ||
| return m.listLoadBalancersResp, nil | ||
| } | ||
| func (m *loadBalancerClientMocked) UpdateTargetPool(_ context.Context, _, _, _ string) loadbalancer.ApiUpdateTargetPoolRequest { | ||
@@ -85,2 +95,14 @@ return loadbalancer.ApiUpdateTargetPoolRequest{} | ||
| }, | ||
| Options: &loadbalancer.LoadBalancerOptions{ | ||
| Observability: &loadbalancer.LoadbalancerOptionObservability{ | ||
| Logs: &loadbalancer.LoadbalancerOptionLogs{ | ||
| CredentialsRef: utils.Ptr("credentials-ref-1"), | ||
| PushUrl: utils.Ptr("https://logs.stackit.cloud"), | ||
| }, | ||
| Metrics: &loadbalancer.LoadbalancerOptionMetrics{ | ||
| CredentialsRef: utils.Ptr("credentials-ref-2"), | ||
| PushUrl: utils.Ptr("https://metrics.stackit.cloud"), | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
@@ -94,2 +116,28 @@ | ||
| func fixtureCredentials(mod ...func([]loadbalancer.CredentialsResponse)) []loadbalancer.CredentialsResponse { | ||
| credentials := []loadbalancer.CredentialsResponse{ | ||
| { | ||
| CredentialsRef: utils.Ptr("credentials-ref-1"), | ||
| DisplayName: utils.Ptr("credentials-1"), | ||
| Username: utils.Ptr("user-1"), | ||
| }, | ||
| { | ||
| CredentialsRef: utils.Ptr("credentials-ref-2"), | ||
| DisplayName: utils.Ptr("credentials-2"), | ||
| Username: utils.Ptr("user-2"), | ||
| }, | ||
| { | ||
| CredentialsRef: utils.Ptr("credentials-ref-3"), | ||
| DisplayName: utils.Ptr("credentials-3"), | ||
| Username: utils.Ptr("user-3"), | ||
| }, | ||
| } | ||
| for _, m := range mod { | ||
| m(credentials) | ||
| } | ||
| return credentials | ||
| } | ||
| func fixtureTargets(mod ...func(*[]loadbalancer.Target)) *[]loadbalancer.Target { | ||
@@ -801,1 +849,314 @@ targets := &[]loadbalancer.Target{ | ||
| } | ||
| func TestGetUsedObsCredentials(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| allCredentials []loadbalancer.CredentialsResponse | ||
| listLoadBalancersFails bool | ||
| listLoadBalancersResp *loadbalancer.ListLoadBalancersResponse | ||
| isValid bool | ||
| expectedOutput []loadbalancer.CredentialsResponse | ||
| }{ | ||
| { | ||
| description: "base", | ||
| allCredentials: fixtureCredentials(), | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ | ||
| LoadBalancers: &[]loadbalancer.LoadBalancer{ | ||
| *fixtureLoadBalancer(), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| expectedOutput: []loadbalancer.CredentialsResponse{ | ||
| { | ||
| DisplayName: utils.Ptr("credentials-1"), | ||
| CredentialsRef: utils.Ptr("credentials-ref-1"), | ||
| Username: utils.Ptr("user-1"), | ||
| }, | ||
| { | ||
| DisplayName: utils.Ptr("credentials-2"), | ||
| CredentialsRef: utils.Ptr("credentials-ref-2"), | ||
| Username: utils.Ptr("user-2"), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| description: "repeated credentials in different load balancers", | ||
| allCredentials: fixtureCredentials(), | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ | ||
| LoadBalancers: &[]loadbalancer.LoadBalancer{ | ||
| *fixtureLoadBalancer(), | ||
| *fixtureLoadBalancer(), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| expectedOutput: []loadbalancer.CredentialsResponse{ | ||
| { | ||
| DisplayName: utils.Ptr("credentials-1"), | ||
| CredentialsRef: utils.Ptr("credentials-ref-1"), | ||
| Username: utils.Ptr("user-1"), | ||
| }, | ||
| { | ||
| DisplayName: utils.Ptr("credentials-2"), | ||
| CredentialsRef: utils.Ptr("credentials-ref-2"), | ||
| Username: utils.Ptr("user-2"), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| description: "no repeated credentials in different load balancers", | ||
| allCredentials: fixtureCredentials(), | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ | ||
| LoadBalancers: &[]loadbalancer.LoadBalancer{ | ||
| *fixtureLoadBalancer(), | ||
| *fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { | ||
| lb.Options.Observability.Logs.CredentialsRef = utils.Ptr("credentials-ref-3") | ||
| lb.Options.Observability.Metrics.CredentialsRef = utils.Ptr("credentials-ref-3") | ||
| }), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| expectedOutput: fixtureCredentials(), | ||
| }, | ||
| { | ||
| description: "no load balancers, no credentials", | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{}, | ||
| isValid: true, | ||
| expectedOutput: nil, | ||
| }, | ||
| { | ||
| description: "no load balancers", | ||
| allCredentials: fixtureCredentials(), | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{}, | ||
| isValid: true, | ||
| expectedOutput: nil, | ||
| }, | ||
| { | ||
| description: "no credentials", | ||
| allCredentials: []loadbalancer.CredentialsResponse{}, | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ | ||
| LoadBalancers: &[]loadbalancer.LoadBalancer{ | ||
| *fixtureLoadBalancer(), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| expectedOutput: nil, | ||
| }, | ||
| { | ||
| description: "list load balancers fails", | ||
| listLoadBalancersFails: true, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no observability options", | ||
| allCredentials: fixtureCredentials(), | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ | ||
| LoadBalancers: &[]loadbalancer.LoadBalancer{ | ||
| *fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { | ||
| lb.Options = nil | ||
| }), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| expectedOutput: nil, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| client := &loadBalancerClientMocked{ | ||
| listLoadBalancersFails: tt.listLoadBalancersFails, | ||
| listLoadBalancersResp: tt.listLoadBalancersResp, | ||
| } | ||
| output, err := GetUsedObsCredentials(testCtx, client, tt.allCredentials, testProjectId) | ||
| 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 | ||
| } | ||
| diff := cmp.Diff(output, tt.expectedOutput) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestGetUnusedObsCredentials(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| allCredentials []loadbalancer.CredentialsResponse | ||
| usedCredentials []loadbalancer.CredentialsResponse | ||
| isValid bool | ||
| expectedOutput []loadbalancer.CredentialsResponse | ||
| }{ | ||
| { | ||
| description: "base", | ||
| allCredentials: fixtureCredentials(), | ||
| usedCredentials: []loadbalancer.CredentialsResponse{ | ||
| { | ||
| DisplayName: utils.Ptr("credentials-1"), | ||
| CredentialsRef: utils.Ptr("credentials-ref-1"), | ||
| Username: utils.Ptr("user-1"), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| expectedOutput: []loadbalancer.CredentialsResponse{ | ||
| { | ||
| DisplayName: utils.Ptr("credentials-2"), | ||
| CredentialsRef: utils.Ptr("credentials-ref-2"), | ||
| Username: utils.Ptr("user-2"), | ||
| }, | ||
| { | ||
| DisplayName: utils.Ptr("credentials-3"), | ||
| CredentialsRef: utils.Ptr("credentials-ref-3"), | ||
| Username: utils.Ptr("user-3"), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| description: "no used credentials", | ||
| allCredentials: fixtureCredentials(), | ||
| usedCredentials: nil, | ||
| isValid: true, | ||
| expectedOutput: fixtureCredentials(), | ||
| }, | ||
| { | ||
| description: "no credentials", | ||
| allCredentials: []loadbalancer.CredentialsResponse{}, | ||
| usedCredentials: []loadbalancer.CredentialsResponse{ | ||
| { | ||
| DisplayName: utils.Ptr("credentials-1"), | ||
| CredentialsRef: utils.Ptr("credentials-ref-1"), | ||
| Username: utils.Ptr("user-1"), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| expectedOutput: nil, | ||
| }, | ||
| { | ||
| description: "no used credentials, no credentials", | ||
| allCredentials: []loadbalancer.CredentialsResponse{}, | ||
| usedCredentials: nil, | ||
| isValid: true, | ||
| expectedOutput: nil, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| output := GetUnusedObsCredentials(tt.usedCredentials, tt.allCredentials) | ||
| diff := cmp.Diff(output, tt.expectedOutput) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestFilterCredentials(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| filterOp int | ||
| allCredentials []loadbalancer.CredentialsResponse | ||
| listLoadBalancersResp *loadbalancer.ListLoadBalancersResponse | ||
| listLoadBalancersFails bool | ||
| expectedCredentials []loadbalancer.CredentialsResponse | ||
| isValid bool | ||
| }{ | ||
| { | ||
| description: "unfiltered credentials", | ||
| filterOp: OP_FILTER_NOP, | ||
| allCredentials: fixtureCredentials(), | ||
| expectedCredentials: fixtureCredentials(), | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "used credentials", | ||
| filterOp: OP_FILTER_USED, | ||
| allCredentials: fixtureCredentials(), | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ | ||
| LoadBalancers: &[]loadbalancer.LoadBalancer{ | ||
| *fixtureLoadBalancer(), | ||
| }, | ||
| }, | ||
| expectedCredentials: []loadbalancer.CredentialsResponse{ | ||
| { | ||
| CredentialsRef: utils.Ptr("credentials-ref-1"), | ||
| DisplayName: utils.Ptr("credentials-1"), | ||
| Username: utils.Ptr("user-1"), | ||
| }, | ||
| { | ||
| CredentialsRef: utils.Ptr("credentials-ref-2"), | ||
| DisplayName: utils.Ptr("credentials-2"), | ||
| Username: utils.Ptr("user-2"), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "unused credentials", | ||
| filterOp: OP_FILTER_UNUSED, | ||
| allCredentials: fixtureCredentials(), | ||
| listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ | ||
| LoadBalancers: &[]loadbalancer.LoadBalancer{ | ||
| *fixtureLoadBalancer(), | ||
| }, | ||
| }, | ||
| expectedCredentials: []loadbalancer.CredentialsResponse{ | ||
| { | ||
| CredentialsRef: utils.Ptr("credentials-ref-3"), | ||
| DisplayName: utils.Ptr("credentials-3"), | ||
| Username: utils.Ptr("user-3"), | ||
| }, | ||
| }, | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "no credentials", | ||
| filterOp: OP_FILTER_NOP, | ||
| allCredentials: []loadbalancer.CredentialsResponse{}, | ||
| expectedCredentials: []loadbalancer.CredentialsResponse{}, | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "list load balancers fails", | ||
| filterOp: OP_FILTER_USED, | ||
| listLoadBalancersFails: true, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "invalid filter operation", | ||
| filterOp: 999, | ||
| allCredentials: fixtureCredentials(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| client := &loadBalancerClientMocked{ | ||
| listLoadBalancersResp: tt.listLoadBalancersResp, | ||
| listLoadBalancersFails: tt.listLoadBalancersFails, | ||
| } | ||
| filteredCredentials, err := FilterCredentials(testCtx, client, tt.allCredentials, testProjectId, tt.filterOp) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error filtering credentials: %v", err) | ||
| } | ||
| diff := cmp.Diff(filteredCredentials, tt.expectedCredentials) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
@@ -6,2 +6,4 @@ package utils | ||
| "fmt" | ||
| "slices" | ||
| "sort" | ||
@@ -11,2 +13,8 @@ "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" | ||
| const ( | ||
| OP_FILTER_NOP = iota | ||
| OP_FILTER_USED | ||
| OP_FILTER_UNUSED | ||
| ) | ||
| type LoadBalancerClient interface { | ||
@@ -16,2 +24,3 @@ GetCredentialsExecute(ctx context.Context, projectId, credentialsRef string) (*loadbalancer.GetCredentialsResponse, error) | ||
| UpdateTargetPool(ctx context.Context, projectId, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest | ||
| ListLoadBalancersExecute(ctx context.Context, projectId string) (*loadbalancer.ListLoadBalancersResponse, error) | ||
| } | ||
@@ -136,1 +145,100 @@ | ||
| } | ||
| // GetUsedObsCredentials returns a list of credentials that are used by load balancers for observability metrics or logs. | ||
| // It goes through all load balancers and checks what observability credentials are being used, then returns a list of those credentials. | ||
| func GetUsedObsCredentials(ctx context.Context, apiClient LoadBalancerClient, allCredentials []loadbalancer.CredentialsResponse, projectId string) ([]loadbalancer.CredentialsResponse, error) { | ||
| var usedCredentialsSlice []loadbalancer.CredentialsResponse | ||
| loadBalancers, err := apiClient.ListLoadBalancersExecute(ctx, projectId) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("list load balancers: %w", err) | ||
| } | ||
| if loadBalancers == nil || loadBalancers.LoadBalancers == nil { | ||
| return usedCredentialsSlice, nil | ||
| } | ||
| var usedCredentialsRefs []string | ||
| for _, loadBalancer := range *loadBalancers.LoadBalancers { | ||
| if loadBalancer.Options == nil || loadBalancer.Options.Observability == nil { | ||
| continue | ||
| } | ||
| if loadBalancer.Options != nil && loadBalancer.Options.Observability != nil && loadBalancer.Options.Observability.Logs != nil && loadBalancer.Options.Observability.Logs.CredentialsRef != nil { | ||
| usedCredentialsRefs = append(usedCredentialsRefs, *loadBalancer.Options.Observability.Logs.CredentialsRef) | ||
| } | ||
| if loadBalancer.Options != nil && loadBalancer.Options.Observability != nil && loadBalancer.Options.Observability.Metrics != nil && loadBalancer.Options.Observability.Metrics.CredentialsRef != nil { | ||
| usedCredentialsRefs = append(usedCredentialsRefs, *loadBalancer.Options.Observability.Metrics.CredentialsRef) | ||
| } | ||
| } | ||
| usedCredentialsMap := make(map[string]loadbalancer.CredentialsResponse) | ||
| for _, credential := range allCredentials { | ||
| if credential.CredentialsRef == nil { | ||
| continue | ||
| } | ||
| ref := *credential.CredentialsRef | ||
| if slices.Contains(usedCredentialsRefs, ref) { | ||
| usedCredentialsMap[ref] = credential | ||
| } | ||
| } | ||
| for _, credential := range usedCredentialsMap { | ||
| usedCredentialsSlice = append(usedCredentialsSlice, credential) | ||
| } | ||
| // sort credentials by reference to make output deterministic | ||
| sort.Slice(usedCredentialsSlice, func(i, j int) bool { | ||
| return *usedCredentialsSlice[i].CredentialsRef < *usedCredentialsSlice[j].CredentialsRef | ||
| }) | ||
| return usedCredentialsSlice, nil | ||
| } | ||
| // GetUnusedObsCredentials returns a list of credentials that are not used by any load balancer for observability metrics or logs. | ||
| // It compares the list of all credentials with the list of used credentials and returns a list of credentials that are not used. | ||
| func GetUnusedObsCredentials(usedCredentials, allCredentials []loadbalancer.CredentialsResponse) []loadbalancer.CredentialsResponse { | ||
| var unusedCredentials []loadbalancer.CredentialsResponse | ||
| usedCredentialsRefs := make(map[string]bool) | ||
| for _, credential := range usedCredentials { | ||
| if credential.CredentialsRef != nil { | ||
| usedCredentialsRefs[*credential.CredentialsRef] = true | ||
| } | ||
| } | ||
| for _, credential := range allCredentials { | ||
| if credential.CredentialsRef == nil { | ||
| continue | ||
| } | ||
| if !usedCredentialsRefs[*credential.CredentialsRef] { | ||
| unusedCredentials = append(unusedCredentials, credential) | ||
| } | ||
| } | ||
| return unusedCredentials | ||
| } | ||
| // FilterCredentials filters a list of credentials based on the used and unused flags. | ||
| // If used is true, it returns only the credentials that are used by load balancers for observability metrics or logs. | ||
| // If unused is true, it returns only the credentials that are not used by any load balancer for observability metrics or logs. | ||
| // If both used and unused are true, it returns an error. | ||
| // If both used and unused are false, it returns the original list of credentials. | ||
| func FilterCredentials(ctx context.Context, client LoadBalancerClient, allCredentials []loadbalancer.CredentialsResponse, projectId string, filterOp int) ([]loadbalancer.CredentialsResponse, error) { | ||
| // check that filter OP is valid | ||
| if filterOp != OP_FILTER_USED && filterOp != OP_FILTER_UNUSED && filterOp != OP_FILTER_NOP { | ||
| return nil, fmt.Errorf("invalid filter operation") | ||
| } | ||
| if filterOp == OP_FILTER_NOP { | ||
| return allCredentials, nil | ||
| } | ||
| usedCredentials, err := GetUsedObsCredentials(ctx, client, allCredentials, projectId) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("get used observability credentials: %w", err) | ||
| } | ||
| if filterOp == OP_FILTER_UNUSED { | ||
| return GetUnusedObsCredentials(usedCredentials, allCredentials), nil | ||
| } | ||
| return usedCredentials, nil | ||
| } |