diff --git a/docs/stackit_load-balancer_target-pool.md b/docs/stackit_load-balancer_target-pool.md index e72d26734..ddcb4c895 100644 --- a/docs/stackit_load-balancer_target-pool.md +++ b/docs/stackit_load-balancer_target-pool.md @@ -30,4 +30,5 @@ stackit load-balancer target-pool [flags] * [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer * [stackit load-balancer target-pool add-target](./stackit_load-balancer_target-pool_add-target.md) - Adds a target to a target pool +* [stackit load-balancer target-pool remove-target](./stackit_load-balancer_target-pool_remove-target.md) - Removes a target from a target pool diff --git a/docs/stackit_load-balancer_target-pool_add-target.md b/docs/stackit_load-balancer_target-pool_add-target.md index 28aaf2202..d70d09e66 100644 --- a/docs/stackit_load-balancer_target-pool_add-target.md +++ b/docs/stackit_load-balancer_target-pool_add-target.md @@ -7,23 +7,23 @@ Adds a target to a target pool Adds a target to a target pool. ``` -stackit load-balancer target-pool add-target LOAD_BALANCER_NAME [flags] +stackit load-balancer target-pool add-target TARGET_POOL_NAME [flags] ``` ### Examples ``` Add a target to target pool "my-target-pool" of load balancer with name "my-load-balancer" - $ stackit load-balancer target-pool add-target my-load-balancer --target-pool-name my-target-pool --target-name my-new-target --ip 1.2.3.4 + $ stackit load-balancer target-pool add-target my-target-pool --lb-name my-load-balancer --target-name my-new-target --ip 1.2.3.4 ``` ### Options ``` - -h, --help Help for "stackit load-balancer target-pool add-target" - --ip string Target IP. Must by unique within a target pool. Must be a valid IPv4 or IPv6 - --target-name string Target name - --target-pool-name string Target pool name + -h, --help Help for "stackit load-balancer target-pool add-target" + --ip string Target IP. Must by unique within a target pool. Must be a valid IPv4 or IPv6 + --lb-name string Load balancer name + --target-name string Target name ``` ### Options inherited from parent commands diff --git a/docs/stackit_load-balancer_target-pool_remove-target.md b/docs/stackit_load-balancer_target-pool_remove-target.md new file mode 100644 index 000000000..184c2acc2 --- /dev/null +++ b/docs/stackit_load-balancer_target-pool_remove-target.md @@ -0,0 +1,41 @@ +## stackit load-balancer target-pool remove-target + +Removes a target from a target pool + +### Synopsis + +Removes a target from a target pool. + +``` +stackit load-balancer target-pool remove-target TARGET_POOL_NAME [flags] +``` + +### Examples + +``` + Remove target with IP 1.2.3.4 from target pool "my-target-pool" of load balancer with name "my-load-balancer" + $ stackit load-balancer target-pool remove-target my-target-pool lb-name my-load-balancer --ip 1.2.3.4 +``` + +### Options + +``` + -h, --help Help for "stackit load-balancer target-pool remove-target" + --ip string Target IP of the target to remove. Must be a valid IPv4 or IPv6 + --lb-name string Load balancer name +``` + +### 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 target-pool](./stackit_load-balancer_target-pool.md) - Provides functionality for target pools + diff --git a/internal/cmd/load-balancer/target-pool/add-target/add_target.go b/internal/cmd/load-balancer/target-pool/add-target/add_target.go index b94549793..f846857fa 100644 --- a/internal/cmd/load-balancer/target-pool/add-target/add_target.go +++ b/internal/cmd/load-balancer/target-pool/add-target/add_target.go @@ -18,32 +18,32 @@ import ( ) const ( - loadBalancerNameArg = "LOAD_BALANCER_NAME" + targetPoolNameArg = "TARGET_POOL_NAME" - targetPoolNameFlag = "target-pool-name" - targetNameFlag = "target-name" - ipFlag = "ip" + lbNameFlag = "lb-name" + targetNameFlag = "target-name" + ipFlag = "ip" ) type inputModel struct { *globalflags.GlobalFlagModel - LoadBalancerName string - TargetPoolName string - TargetName string - Ip string + TargetPoolName string + LBName string + TargetName string + IP string } func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ - Use: fmt.Sprintf("add-target %s", loadBalancerNameArg), + Use: fmt.Sprintf("add-target %s", targetPoolNameArg), Short: "Adds a target to a target pool", Long: "Adds a target to a target pool.", - Args: args.SingleArg(loadBalancerNameArg, nil), + Args: args.SingleArg(targetPoolNameArg, nil), Example: examples.Build( examples.NewExample( `Add a target to target pool "my-target-pool" of load balancer with name "my-load-balancer"`, - "$ stackit load-balancer target-pool add-target my-load-balancer --target-pool-name my-target-pool --target-name my-new-target --ip 1.2.3.4"), + "$ stackit load-balancer target-pool add-target my-target-pool --lb-name my-load-balancer --target-name my-new-target --ip 1.2.3.4"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -59,7 +59,7 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to add a target with IP %q to target pool %q of load balancer %q?", model.Ip, model.TargetPoolName, model.LoadBalancerName) + prompt := fmt.Sprintf("Are you sure you want to add a target with IP %q to target pool %q of load balancer %q?", model.IP, model.TargetPoolName, model.LBName) err = p.PromptForConfirmation(prompt) if err != nil { return err @@ -76,7 +76,7 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("add target to target pool: %w", err) } - p.Info("Added target to target pool of load balancer %q\n", model.LoadBalancerName) + p.Info("Added target to target pool of load balancer %q\n", model.LBName) return nil }, } @@ -85,16 +85,16 @@ func NewCmd(p *print.Printer) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().String(targetPoolNameFlag, "", "Target pool name") + cmd.Flags().String(lbNameFlag, "", "Load balancer name") cmd.Flags().String(targetNameFlag, "", "Target name") cmd.Flags().String(ipFlag, "", "Target IP. Must by unique within a target pool. Must be a valid IPv4 or IPv6") - err := flags.MarkFlagsRequired(cmd, targetPoolNameFlag, targetNameFlag, ipFlag) + err := flags.MarkFlagsRequired(cmd, lbNameFlag, targetNameFlag, ipFlag) cobra.CheckErr(err) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { - lbName := inputArgs[0] + targetPoolName := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { @@ -102,11 +102,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } model := inputModel{ - GlobalFlagModel: globalFlags, - LoadBalancerName: lbName, - TargetPoolName: cmd.Flag(targetPoolNameFlag).Value.String(), - TargetName: cmd.Flag(targetNameFlag).Value.String(), - Ip: cmd.Flag(ipFlag).Value.String(), + GlobalFlagModel: globalFlags, + TargetPoolName: targetPoolName, + LBName: cmd.Flag(lbNameFlag).Value.String(), + TargetName: cmd.Flag(targetNameFlag).Value.String(), + IP: cmd.Flag(ipFlag).Value.String(), } if p.IsVerbosityDebug() { @@ -122,16 +122,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } func buildRequest(ctx context.Context, model *inputModel, apiClient utils.LoadBalancerClient) (loadbalancer.ApiUpdateTargetPoolRequest, error) { - req := apiClient.UpdateTargetPool(ctx, model.ProjectId, model.LoadBalancerName, model.TargetPoolName) + req := apiClient.UpdateTargetPool(ctx, model.ProjectId, model.LBName, model.TargetPoolName) - targetPool, err := utils.GetLoadBalancerTargetPool(ctx, apiClient, model.ProjectId, model.LoadBalancerName, model.TargetPoolName) + targetPool, err := utils.GetLoadBalancerTargetPool(ctx, apiClient, model.ProjectId, model.LBName, model.TargetPoolName) if err != nil { return req, fmt.Errorf("get load balancer target pool: %w", err) } newTarget := &loadbalancer.Target{ DisplayName: &model.TargetName, - Ip: &model.Ip, + Ip: &model.IP, } err = utils.AddTargetToTargetPool(targetPool, newTarget) if err != nil { 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 60fbbafd1..679ab6023 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 @@ -26,10 +26,10 @@ var ( ) const ( - testLoadBalancerName = "my-load-balancer" - testTargetPoolName = "target-pool-1" - testTargetName = "my-target" - testIp = "1.2.3.4" + testLBName = "my-load-balancer" + testTargetPoolName = "target-pool-1" + testTargetName = "my-target" + testIP = "1.2.3.4" ) type loadBalancerClientMocked struct { @@ -59,7 +59,7 @@ func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, project func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ - testLoadBalancerName, + testTargetPoolName, } for _, mod := range mods { mod(argValues) @@ -69,10 +69,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - targetPoolNameFlag: testTargetPoolName, - targetNameFlag: testTargetName, - ipFlag: testIp, + projectIdFlag: testProjectId, + lbNameFlag: testLBName, + targetNameFlag: testTargetName, + ipFlag: testIP, } for _, mod := range mods { mod(flagValues) @@ -86,10 +86,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - LoadBalancerName: testLoadBalancerName, - TargetPoolName: testTargetPoolName, - TargetName: testTargetName, - Ip: testIp, + TargetPoolName: testTargetPoolName, + LBName: testLBName, + TargetName: testTargetName, + IP: testIP, } for _, mod := range mods { mod(model) @@ -112,7 +112,7 @@ func fixtureTargets() *[]loadbalancer.Target { func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer.LoadBalancer { lb := loadbalancer.LoadBalancer{ - Name: utils.Ptr(testLoadBalancerName), + Name: utils.Ptr(testLBName), TargetPools: &[]loadbalancer.TargetPool{ { Name: utils.Ptr(testTargetPoolName), @@ -167,7 +167,7 @@ func fixturePayload(mods ...func(payload *loadbalancer.UpdateTargetPoolPayload)) } func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateTargetPoolRequest)) loadbalancer.ApiUpdateTargetPoolRequest { - request := testClient.UpdateTargetPool(testCtx, testProjectId, testLoadBalancerName, testTargetPoolName) + request := testClient.UpdateTargetPool(testCtx, testProjectId, testLBName, testTargetPoolName) request = request.UpdateTargetPoolPayload(*fixturePayload()) for _, mod := range mods { mod(&request) @@ -221,16 +221,16 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "load balancer name empty", + description: "target pool name missing", argValues: []string{""}, flagValues: fixtureFlagValues(), isValid: false, }, { - description: "target pool name missing", + description: "load balancer name missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, targetPoolNameFlag) + delete(flagValues, lbNameFlag) }), isValid: false, }, @@ -328,7 +328,7 @@ func TestBuildRequest(t *testing.T) { (*fixtureTargets())[1], { DisplayName: utils.Ptr(testTargetName), - Ip: utils.Ptr(testIp), + Ip: utils.Ptr(testIP), }, } }) @@ -347,7 +347,7 @@ func TestBuildRequest(t *testing.T) { payload.Targets = &[]loadbalancer.Target{ { DisplayName: utils.Ptr(testTargetName), - Ip: utils.Ptr(testIp), + Ip: utils.Ptr(testIP), }, } }) @@ -366,7 +366,7 @@ func TestBuildRequest(t *testing.T) { payload.Targets = &[]loadbalancer.Target{ { DisplayName: utils.Ptr(testTargetName), - Ip: utils.Ptr(testIp), + Ip: utils.Ptr(testIP), }, } }) diff --git a/internal/cmd/load-balancer/target-pool/remove-target/remove_target.go b/internal/cmd/load-balancer/target-pool/remove-target/remove_target.go new file mode 100644 index 000000000..9e7a8f8a6 --- /dev/null +++ b/internal/cmd/load-balancer/target-pool/remove-target/remove_target.go @@ -0,0 +1,145 @@ +package removetarget + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + + "github.com/spf13/cobra" +) + +const ( + targetPoolNameArg = "TARGET_POOL_NAME" + + lbNameFlag = "lb-name" + ipFlag = "ip" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + TargetPoolName string + LBName string + IP string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("remove-target %s", targetPoolNameArg), + Short: "Removes a target from a target pool", + Long: "Removes a target from a target pool.", + Args: args.SingleArg(targetPoolNameArg, nil), + Example: examples.Build( + examples.NewExample( + `Remove target with IP 1.2.3.4 from target pool "my-target-pool" of load balancer with name "my-load-balancer"`, + "$ stackit load-balancer target-pool remove-target my-target-pool lb-name my-load-balancer --ip 1.2.3.4"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + targetLabel, err := utils.GetTargetName(ctx, apiClient, model.ProjectId, model.LBName, model.TargetPoolName, model.IP) + if err != nil { + p.Debug(print.ErrorLevel, "get target name: %v", err) + targetLabel = model.IP + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to remove target %q from target pool %q of load balancer %q?", targetLabel, model.TargetPoolName, model.LBName) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + _, err = req.Execute() + if err != nil { + return fmt.Errorf("remove target from target pool: %w", err) + } + + p.Info("Removed target from target pool of load balancer %q\n", model.LBName) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(lbNameFlag, "", "Load balancer name") + cmd.Flags().String(ipFlag, "", "Target IP of the target to remove. Must be a valid IPv4 or IPv6") + + err := flags.MarkFlagsRequired(cmd, lbNameFlag, ipFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + targetPoolName := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + TargetPoolName: targetPoolName, + LBName: cmd.Flag(lbNameFlag).Value.String(), + IP: cmd.Flag(ipFlag).Value.String(), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient utils.LoadBalancerClient) (loadbalancer.ApiUpdateTargetPoolRequest, error) { + req := apiClient.UpdateTargetPool(ctx, model.ProjectId, model.LBName, model.TargetPoolName) + + targetPool, err := utils.GetLoadBalancerTargetPool(ctx, apiClient, model.ProjectId, model.LBName, model.TargetPoolName) + if err != nil { + return req, fmt.Errorf("get load balancer target pool: %w", err) + } + + err = utils.RemoveTargetFromTargetPool(targetPool, model.IP) + if err != nil { + return req, fmt.Errorf("remove target to target pool: %w", err) + } + + payload := utils.ToPayloadTargetPool(targetPool) + if payload == nil { + return req, fmt.Errorf("nil payload") + } + + return req.UpdateTargetPoolPayload(*payload), nil +} 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 new file mode 100644 index 000000000..6bd1ab994 --- /dev/null +++ b/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go @@ -0,0 +1,394 @@ +package removetarget + +import ( + "context" + "fmt" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &loadbalancer.APIClient{} + testProjectId = uuid.NewString() +) + +const ( + testLBName = "my-load-balancer" + testTargetPoolName = "target-pool-1" + testTargetName = "my-target" + testIP = "1.2.3.4" +) + +type loadBalancerClientMocked struct { + getCredentialsFails bool + getCredentialsResp *loadbalancer.GetCredentialsResponse + getLoadBalancerFails bool + getLoadBalancerResp *loadbalancer.LoadBalancer +} + +func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _ string) (*loadbalancer.GetCredentialsResponse, error) { + if m.getCredentialsFails { + return nil, fmt.Errorf("could not get credentials") + } + return m.getCredentialsResp, nil +} + +func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, _ string) (*loadbalancer.LoadBalancer, error) { + if m.getLoadBalancerFails { + return nil, fmt.Errorf("could not get load balancer") + } + return m.getLoadBalancerResp, nil +} + +func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, projectId, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest { + return testClient.UpdateTargetPool(ctx, projectId, loadBalancerName, targetPoolName) +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testTargetPoolName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + lbNameFlag: testLBName, + ipFlag: testIP, + } + 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, + }, + LBName: testLBName, + TargetPoolName: testTargetPoolName, + IP: testIP, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureTargets() *[]loadbalancer.Target { + return &[]loadbalancer.Target{ + { + DisplayName: utils.Ptr("target-1"), + Ip: utils.Ptr("1.2.3.4"), + }, + { + DisplayName: utils.Ptr("target-2"), + Ip: utils.Ptr("4.3.2.1"), + }, + } +} + +func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer.LoadBalancer { + lb := loadbalancer.LoadBalancer{ + Name: utils.Ptr(testLBName), + TargetPools: &[]loadbalancer.TargetPool{ + { + Name: utils.Ptr(testTargetPoolName), + Targets: fixtureTargets(), + ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ + UnhealthyThreshold: utils.Ptr(int64(3)), + }, + SessionPersistence: &loadbalancer.SessionPersistence{ + UseSourceIpAddress: utils.Ptr(true), + }, + TargetPort: utils.Ptr(int64(80)), + }, + { + Name: utils.Ptr("target-pool-2"), + Targets: &[]loadbalancer.Target{ + { + DisplayName: utils.Ptr("target-1"), + Ip: utils.Ptr("6.7.8.9"), + }, + { + DisplayName: utils.Ptr("target-2"), + Ip: utils.Ptr("9.8.7.6"), + }, + }, + }, + }, + } + + for _, mod := range mods { + mod(&lb) + } + return &lb +} + +func fixturePayload(mods ...func(payload *loadbalancer.UpdateTargetPoolPayload)) *loadbalancer.UpdateTargetPoolPayload { + payload := &loadbalancer.UpdateTargetPoolPayload{ + Name: utils.Ptr("target-pool-1"), + ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ + UnhealthyThreshold: utils.Ptr(int64(3)), + }, + SessionPersistence: &loadbalancer.SessionPersistence{ + UseSourceIpAddress: utils.Ptr(true), + }, + TargetPort: utils.Ptr(int64(80)), + Targets: fixtureTargets(), + } + + for _, mod := range mods { + mod(payload) + } + return payload +} + +func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateTargetPoolRequest)) loadbalancer.ApiUpdateTargetPoolRequest { + request := testClient.UpdateTargetPool(testCtx, testProjectId, testLBName, testTargetPoolName) + request = request.UpdateTargetPoolPayload(*fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "target pool name missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "load balancer name missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, lbNameFlag) + }), + isValid: false, + }, + { + description: "ip missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + isValid bool + getLoadBalancerFails bool + getLoadBalancerResp *loadbalancer.LoadBalancer + expectedRequest loadbalancer.ApiUpdateTargetPoolRequest + }{ + { + description: "base", + model: fixtureInputModel(), + getLoadBalancerResp: fixtureLoadBalancer(), + isValid: true, + expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) { + payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) { + payload.Targets = utils.Ptr((*payload.Targets)[1:]) + }) + *request = request.UpdateTargetPoolPayload(*payload) + }), + }, + { + description: "empty targets", + model: fixtureInputModel(), + getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { + (*lb.TargetPools)[0].Targets = &[]loadbalancer.Target{} + }), + isValid: false, + }, + { + description: "target not found", + model: fixtureInputModel( + func(model *inputModel) { + model.IP = "9.9.9.9" + }), + getLoadBalancerResp: fixtureLoadBalancer(), + isValid: false, + }, + { + description: "nil targets", + model: fixtureInputModel(), + getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { + (*lb.TargetPools)[0].Targets = nil + }), + isValid: false, + }, + { + description: "get load balancer fails", + model: fixtureInputModel(), + getLoadBalancerFails: true, + isValid: false, + }, + { + description: "target pool not found", + model: fixtureInputModel( + func(model *inputModel) { + model.TargetPoolName = "not-existent" + }), + getLoadBalancerResp: fixtureLoadBalancer(), + isValid: false, + }, + { + description: "nil target pool", + model: fixtureInputModel(), + getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { + *lb.TargetPools = nil + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &loadBalancerClientMocked{ + getLoadBalancerFails: tt.getLoadBalancerFails, + getLoadBalancerResp: tt.getLoadBalancerResp, + } + request, err := buildRequest(testCtx, tt.model, client) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/load-balancer/target-pool/target_pool.go b/internal/cmd/load-balancer/target-pool/target_pool.go index 4bbbe1a91..e990bb5f9 100644 --- a/internal/cmd/load-balancer/target-pool/target_pool.go +++ b/internal/cmd/load-balancer/target-pool/target_pool.go @@ -2,6 +2,7 @@ package targetpool import ( addtarget "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool/add-target" + removetarget "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool/remove-target" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -23,4 +24,5 @@ func NewCmd(p *print.Printer) *cobra.Command { func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(addtarget.NewCmd(p)) + cmd.AddCommand(removetarget.NewCmd(p)) } diff --git a/internal/pkg/services/load-balancer/utils/utils.go b/internal/pkg/services/load-balancer/utils/utils.go index 4a500ff03..05157ae7c 100644 --- a/internal/pkg/services/load-balancer/utils/utils.go +++ b/internal/pkg/services/load-balancer/utils/utils.go @@ -58,6 +58,25 @@ func AddTargetToTargetPool(targetPool *loadbalancer.TargetPool, target *loadbala return nil } +func RemoveTargetFromTargetPool(targetPool *loadbalancer.TargetPool, ip string) error { + if targetPool == nil { + return fmt.Errorf("target pool is nil") + } + if targetPool.Targets == nil { + return fmt.Errorf("no targets found") + } + targets := *targetPool.Targets + for i, target := range targets { + if target.Ip != nil && *target.Ip == ip { + newTargets := targets[:i] + newTargets = append(newTargets, targets[i+1:]...) + *targetPool.Targets = newTargets + return nil + } + } + return fmt.Errorf("target not found") +} + func ToPayloadTargetPool(targetPool *loadbalancer.TargetPool) *loadbalancer.UpdateTargetPoolPayload { if targetPool == nil { return nil @@ -70,3 +89,22 @@ func ToPayloadTargetPool(targetPool *loadbalancer.TargetPool) *loadbalancer.Upda Targets: targetPool.Targets, } } + +func GetTargetName(ctx context.Context, apiClient LoadBalancerClient, projectId, loadBalancerName, targetPoolName, targetIp string) (string, error) { + targetPool, err := GetLoadBalancerTargetPool(ctx, apiClient, projectId, loadBalancerName, targetPoolName) + if err != nil { + return "", fmt.Errorf("get target pool: %w", err) + } + if targetPool.Targets == nil { + return "", fmt.Errorf("no targets found") + } + for _, target := range *targetPool.Targets { + if target.Ip != nil && *target.Ip == targetIp { + if target.DisplayName == nil { + return "", fmt.Errorf("nil target display name") + } + return *target.DisplayName, nil + } + } + return "", fmt.Errorf("target not found") +} diff --git a/internal/pkg/services/load-balancer/utils/utils_test.go b/internal/pkg/services/load-balancer/utils/utils_test.go index df2a96e0d..86bdbd85b 100644 --- a/internal/pkg/services/load-balancer/utils/utils_test.go +++ b/internal/pkg/services/load-balancer/utils/utils_test.go @@ -87,6 +87,29 @@ func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer return &lb } +func fixtureTargets(mod ...func(*[]loadbalancer.Target)) *[]loadbalancer.Target { + targets := &[]loadbalancer.Target{ + { + DisplayName: utils.Ptr("target-1"), + Ip: utils.Ptr("1.2.3.4"), + }, + { + DisplayName: utils.Ptr("target-2"), + Ip: utils.Ptr("2.2.2.2"), + }, + { + DisplayName: utils.Ptr("target-3"), + Ip: utils.Ptr("6.6.6.6"), + }, + } + + for _, m := range mod { + m(targets) + } + + return targets +} + func TestGetCredentialsDisplayName(t *testing.T) { tests := []struct { description string @@ -344,6 +367,145 @@ func TestAddTargetToTargetPool(t *testing.T) { } } +func TestRemoveTargetFromTargetPool(t *testing.T) { + tests := []struct { + description string + targetPool *loadbalancer.TargetPool + targetIp string + isValid bool + expectedTargetPool *loadbalancer.TargetPool + }{ + { + description: "remove first target", + targetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: fixtureTargets(), + }, + targetIp: "1.2.3.4", + isValid: true, + expectedTargetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: &[]loadbalancer.Target{ + { + DisplayName: utils.Ptr("target-2"), + Ip: utils.Ptr("2.2.2.2"), + }, + { + DisplayName: utils.Ptr("target-3"), + Ip: utils.Ptr("6.6.6.6"), + }, + }, + }, + }, + { + description: "remove last target", + targetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: fixtureTargets(), + }, + targetIp: "6.6.6.6", + isValid: true, + expectedTargetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: &[]loadbalancer.Target{ + { + DisplayName: utils.Ptr("target-1"), + Ip: utils.Ptr("1.2.3.4"), + }, + { + DisplayName: utils.Ptr("target-2"), + Ip: utils.Ptr("2.2.2.2"), + }, + }, + }, + }, + { + description: "remove middle target", + targetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: fixtureTargets(), + }, + targetIp: "2.2.2.2", + isValid: true, + expectedTargetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: &[]loadbalancer.Target{ + { + DisplayName: utils.Ptr("target-1"), + Ip: utils.Ptr("1.2.3.4"), + }, + { + DisplayName: utils.Ptr("target-3"), + Ip: utils.Ptr("6.6.6.6"), + }, + }, + }, + }, + { + description: "remove only target", + targetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: &[]loadbalancer.Target{ + { + DisplayName: utils.Ptr("target-1"), + Ip: utils.Ptr("1.2.3.4"), + }, + }, + }, + targetIp: "1.2.3.4", + isValid: true, + expectedTargetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: &[]loadbalancer.Target{}, + }, + }, + { + description: "no target pool targets", + targetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: &[]loadbalancer.Target{}, + }, + targetIp: "2.2.2.2", + isValid: false, + }, + { + description: "nil target pool targets", + targetPool: &loadbalancer.TargetPool{ + Name: utils.Ptr("target-pool-1"), + Targets: nil, + }, + targetIp: "2.2.2.2", + isValid: false, + }, + { + description: "nil target pool", + targetPool: nil, + targetIp: "2.2.2.2", + expectedTargetPool: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := RemoveTargetFromTargetPool(tt.targetPool, tt.targetIp) + + 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(tt.targetPool, tt.expectedTargetPool) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + func TestToPayloadTargetPool(t *testing.T) { tests := []struct { description string @@ -403,3 +565,109 @@ func TestToPayloadTargetPool(t *testing.T) { }) } } + +func TestGetTargetName(t *testing.T) { + tests := []struct { + description string + targetPoolName string + targetIp string + getLoadBalancerFails bool + getLoadBalancerResp *loadbalancer.LoadBalancer + isValid bool + expectedOutput string + }{ + { + description: "base", + targetPoolName: "target-pool-1", + targetIp: "1.2.3.4", + getLoadBalancerResp: fixtureLoadBalancer(), + isValid: true, + expectedOutput: "target-1", + }, + { + description: "target not found", + targetPoolName: "target-pool-1", + targetIp: "9.9.9.9", + getLoadBalancerResp: fixtureLoadBalancer(), + isValid: false, + }, + { + description: "no targets", + targetPoolName: "target-pool-1", + targetIp: "1.2.3.4", + getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { + lb.TargetPools = &[]loadbalancer.TargetPool{ + { + Name: utils.Ptr("target-pool-1"), + Targets: &[]loadbalancer.Target{}, + }, + } + }), + isValid: false, + }, + { + description: "nil targets", + targetPoolName: "target-pool-1", + targetIp: "1.2.3.4", + getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { + lb.TargetPools = &[]loadbalancer.TargetPool{ + { + Name: utils.Ptr("target-pool-1"), + Targets: nil, + }, + } + }), + isValid: false, + }, + { + description: "nil target name", + targetPoolName: "target-pool-1", + targetIp: "1.2.3.4", + getLoadBalancerResp: fixtureLoadBalancer( + func(lb *loadbalancer.LoadBalancer) { + lb.TargetPools = &[]loadbalancer.TargetPool{ + { + Name: utils.Ptr("target-pool-1"), + Targets: &[]loadbalancer.Target{ + { + DisplayName: nil, + Ip: utils.Ptr("1.2.3.4"), + }, + }, + }, + } + }), + isValid: false, + }, + { + description: "get target pool fails", + targetPoolName: "target-pool-1", + targetIp: "1.2.3.4", + getLoadBalancerFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &loadBalancerClientMocked{ + getLoadBalancerResp: tt.getLoadBalancerResp, + } + + output, err := GetTargetName(context.Background(), client, testProjectId, testLoadBalancerName, tt.targetPoolName, tt.targetIp) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +}