You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli

Package Overview
Dependencies
Versions
174
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli - go Package Compare versions

Comparing version
v0.5.0
to
v0.6.0-prerelease.1
+39
docs/stackit_load-...er_observability-credentials_cleanup.md
## 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
+13
-5
## 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
}