diff --git a/docs/stackit_load-balancer_observability-credentials.md b/docs/stackit_load-balancer_observability-credentials.md index 111523c5f..c11fbfd0a 100644 --- a/docs/stackit_load-balancer_observability-credentials.md +++ b/docs/stackit_load-balancer_observability-credentials.md @@ -32,6 +32,6 @@ stackit load-balancer observability-credentials [flags] * [stackit load-balancer observability-credentials add](./stackit_load-balancer_observability-credentials_add.md) - Adds observability credentials to 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 diff --git a/docs/stackit_load-balancer_observability-credentials_list.md b/docs/stackit_load-balancer_observability-credentials_list.md index ee183e01f..581aa37a8 100644 --- a/docs/stackit_load-balancer_observability-credentials_list.md +++ b/docs/stackit_load-balancer_observability-credentials_list.md @@ -1,10 +1,10 @@ ## 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. ``` stackit load-balancer observability-credentials list [flags] @@ -13,13 +13,19 @@ stackit load-balancer observability-credentials list [flags] ### Examples ``` - 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 ``` @@ -28,6 +34,8 @@ stackit load-balancer observability-credentials list [flags] ``` -h, --help Help for "stackit load-balancer observability-credentials list" --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 ``` ### Options inherited from parent commands diff --git a/internal/cmd/load-balancer/observability-credentials/list/list.go b/internal/cmd/load-balancer/observability-credentials/list/list.go index 21cb4dcad..237ef420c 100644 --- a/internal/cmd/load-balancer/observability-credentials/list/list.go +++ b/internal/cmd/load-balancer/observability-credentials/list/list.go @@ -13,6 +13,7 @@ import ( "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/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/spf13/cobra" @@ -22,28 +23,38 @@ import ( const ( instanceIdFlag = "instance-id" limitFlag = "limit" + usedFlag = "used" + unusedFlag = "unused" ) type inputModel struct { *globalflags.GlobalFlagModel - Limit *int64 + Limit *int64 + Used bool + Unused bool } func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ 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"), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -72,13 +83,23 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("list Load Balancer observability credentials: %w", err) } credentialsPtr := resp.Credentials - if credentialsPtr == nil || (credentialsPtr != nil && len(*credentialsPtr) == 0) { + if credentialsPtr == nil || len(*credentialsPtr) == 0 { p.Info("No observability credentials found for Load Balancer on project %q\n", projectLabel) return nil } 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) + } + // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { credentials = credentials[:*model.Limit] @@ -92,6 +113,10 @@ func NewCmd(p *print.Printer) *cobra.Command { func configureFlags(cmd *cobra.Command) { 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) } func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { @@ -111,6 +136,8 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, Limit: limit, + Used: flags.FlagToBoolValue(p, cmd, usedFlag), + Unused: flags.FlagToBoolValue(p, cmd, unusedFlag), } if p.IsVerbosityDebug() { @@ -155,3 +182,20 @@ func outputResult(p *print.Printer, outputFormat string, credentials []loadbalan return nil } } + +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 +} diff --git a/internal/cmd/load-balancer/observability-credentials/list/list_test.go b/internal/cmd/load-balancer/observability-credentials/list/list_test.go index 9c564117e..736adf134 100644 --- a/internal/cmd/load-balancer/observability-credentials/list/list_test.go +++ b/internal/cmd/load-balancer/observability-credentials/list/list_test.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "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" "github.com/google/go-cmp/cmp" @@ -108,6 +109,34 @@ func TestParseInput(t *testing.T) { }), isValid: false, }, + { + 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, + }, } for _, tt := range tests { @@ -137,6 +166,14 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } + err = cmd.ValidateFlagGroups() + 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 { @@ -183,3 +220,52 @@ func TestBuildRequest(t *testing.T) { }) } } + +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) + } + }) + } +} diff --git a/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go b/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go index 128e2c60a..8120dc497 100644 --- a/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go +++ b/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go @@ -57,6 +57,10 @@ func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, project return testClient.UpdateTargetPool(ctx, projectId, loadBalancerName, targetPoolName) } +func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { + return nil, nil +} + func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ testIP, diff --git a/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go b/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go index bec625abd..ec31f13e2 100644 --- a/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go +++ b/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go @@ -57,6 +57,10 @@ func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, project return testClient.UpdateTargetPool(ctx, projectId, loadBalancerName, targetPoolName) } +func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { + return nil, nil +} + func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ testIP, diff --git a/internal/pkg/services/load-balancer/utils/utils.go b/internal/pkg/services/load-balancer/utils/utils.go index 0f173b251..1ad93ce18 100644 --- a/internal/pkg/services/load-balancer/utils/utils.go +++ b/internal/pkg/services/load-balancer/utils/utils.go @@ -3,14 +3,23 @@ package utils import ( "context" "fmt" + "slices" + "sort" "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) +const ( + OP_FILTER_NOP = iota + OP_FILTER_USED + OP_FILTER_UNUSED +) + type LoadBalancerClient interface { GetCredentialsExecute(ctx context.Context, projectId, credentialsRef string) (*loadbalancer.GetCredentialsResponse, error) GetLoadBalancerExecute(ctx context.Context, projectId, name string) (*loadbalancer.LoadBalancer, error) UpdateTargetPool(ctx context.Context, projectId, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest + ListLoadBalancersExecute(ctx context.Context, projectId string) (*loadbalancer.ListLoadBalancersResponse, error) } func GetCredentialsDisplayName(ctx context.Context, apiClient LoadBalancerClient, projectId, credentialsRef string) (string, error) { @@ -130,3 +139,102 @@ func GetTargetName(ctx context.Context, apiClient LoadBalancerClient, projectId, } return "", fmt.Errorf("target not found") } + +// 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 +} diff --git a/internal/pkg/services/load-balancer/utils/utils_test.go b/internal/pkg/services/load-balancer/utils/utils_test.go index 2d4d96c0d..93b8f6ff3 100644 --- a/internal/pkg/services/load-balancer/utils/utils_test.go +++ b/internal/pkg/services/load-balancer/utils/utils_test.go @@ -15,6 +15,7 @@ import ( var ( testProjectId = uuid.NewString() + testCtx = context.Background() ) const ( @@ -24,10 +25,12 @@ const ( ) 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 } func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _ string) (*loadbalancer.GetCredentialsResponse, error) { @@ -44,6 +47,13 @@ func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, return m.getLoadBalancerResp, nil } +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 { return loadbalancer.ApiUpdateTargetPoolRequest{} } @@ -79,6 +89,18 @@ func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer }, }, }, + 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"), + }, + }, + }, } for _, mod := range mods { @@ -87,6 +109,32 @@ func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer return &lb } +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 { targets := &[]loadbalancer.Target{ { @@ -793,3 +841,316 @@ func TestGetTargetName(t *testing.T) { }) } } + +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) + } + }) + } +}