Repository: elastic/ecctl (go)

Issue:
Support optional override_failsafe when doing a vacate (move_only) ES plan
## Overview
It would be great if vacate/move_only plans could optionally override_failsafe since this is the current way of bypassing validations that can/will fail if a cluster is unhealthy and/or the allocator is unhealthy.

## Possible Implementation
<!--- Not obligatory, but suggest a possible implementation, -->
<!--- or ideas how to implement this new feature. -->

## Testing
Ensure that override_failsafe: true is passed in the transient section of the plan.

## Context
Without this it's not always possible to vacate unhealthy allocators.

## Your Environment
<!--- Include as many relevant details about the environment -->
<!--- to which this new feature would be applicable (e.g., -->
<!--- run under automation, executed ad-hoc, etc.), or -->
<!--- anything else of relevance about your environment -->
<!--- (e.g., your operating system, versions you current -->
<!--- run, security requirements, etc.). -->


Pull request:
Support vacate override failsafe
## Description
This PR adds support to the `override_failsafe` api parameter as part of the plan overrides

## Related Issues
Closes #77 

## Motivation and Context
More successful vacates and more flexibility

## How Has This Been Tested?
Manually by adding UTs and observing that the API call is done with the correct parameters

## Types of Changes
<!--- What types of changes does your code introduce? Put an `x` in -->
<!--- all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Refactoring (improves code quality but has no user-facing effect)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation

## Readiness Checklist
<!--- Go over all the following points, and put an `x` in all the boxes -->
<!--- that apply.  If you're unsure about any of these, don't hesitate -->
<!--- to ask. We're here to help! -->
- [x] My code follows the code style of this project
- [x] My change requires a change to the documentation
- [x] I have updated the documentation accordingly
- [x] I have added tests to cover my changes
- [x] All new and existing tests passed


Patch:
diff --git a/cmd/platform/allocator/vacate.go b/cmd/platform/allocator/vacate.go
index 31b21a7..664ec52 100644
--- a/cmd/platform/allocator/vacate.go
+++ b/cmd/platform/allocator/vacate.go
@@ -20,6 +20,7 @@ package cmdallocator
 import (
 	"errors"
 	"fmt"
+	"os"
 	"strconv"
 
 	"github.com/elastic/cloud-sdk-go/pkg/util/ec"
@@ -90,6 +91,14 @@ var vacateAllocatorCmd = &cobra.Command{
 		}
 
 		moveOnly, _ := cmd.Flags().GetBool("move-only")
+
+		overrideFailsafe, _ := cmd.Flags().GetBool("override-failsafe")
+		force, _ := cmd.Flags().GetBool("force")
+		var msg = "--override-failsafe has been flag specified. Are you sure you want to proceed? [y/N]: "
+		if overrideFailsafe && !force && !cmdutil.ConfirmAction(msg, os.Stderr, os.Stdout) {
+			return nil
+		}
+
 		target, _ := cmd.Flags().GetStringSlice("target")
 		kind, _ := cmd.Flags().GetString("kind")
 
@@ -150,6 +159,7 @@ var vacateAllocatorCmd = &cobra.Command{
 			PlanOverrides: allocator.PlanOverrides{
 				SkipSnapshot:      skipSnapshot,
 				SkipDataMigration: skipDataMigration,
+				OverrideFailsafe:  ec.Bool(overrideFailsafe),
 			},
 		}
 		if len(args) == 1 && allocatorDownRaw != "" {
@@ -178,6 +188,7 @@ func init() {
 	vacateAllocatorCmd.Flags().Uint("concurrency", 8, "Maximum number of concurrent moves to perform at any time")
 	vacateAllocatorCmd.Flags().String("allocator-down", "", "Disables the allocator health auto-discovery, setting the allocator-down to either [true|false]")
 	vacateAllocatorCmd.Flags().Bool("move-only", true, "Keeps the cluster in its current -possibly broken- state and just does the bare minimum to move the requested instances across to another allocator. [true|false]")
+	vacateAllocatorCmd.Flags().Bool("override-failsafe", false, "If false (the default) then the plan will fail out if it believes the requested sequence of operations can result in data loss - this flag will override some of these restraints. [true|false]")
 	vacateAllocatorCmd.Flags().String("skip-snapshot", "", "Skips the snapshot operation on the specified cluster IDs. ONLY available when the cluster IDs are specified. [true|false]")
 	vacateAllocatorCmd.Flags().String("skip-data-migration", "", "Skips the data-migration operation on the specified cluster IDs. ONLY available when the cluster IDs are specified and --move-only is true. [true|false]")
 }
diff --git a/docs/ecctl_platform_allocator_vacate.md b/docs/ecctl_platform_allocator_vacate.md
index bded6bb..b159948 100644
--- a/docs/ecctl_platform_allocator_vacate.md
+++ b/docs/ecctl_platform_allocator_vacate.md
@@ -54,6 +54,7 @@ ecctl platform allocator vacate <source> [flags]
   -k, --kind string                  Kind of workload to vacate (elasticsearch|kibana)
   -m, --maintenance                  Whether to set the allocator(s) in maintenance before performing the vacate
       --move-only                    Keeps the cluster in its current -possibly broken- state and just does the bare minimum to move the requested instances across to another allocator. [true|false] (default true)
+      --override-failsafe            If false (the default) then the plan will fail out if it believes the requested sequence of operations can result in data loss - this flag will override some of these restraints. [true|false]
       --skip-data-migration string   Skips the data-migration operation on the specified cluster IDs. ONLY available when the cluster IDs are specified and --move-only is true. [true|false]
       --skip-snapshot string         Skips the snapshot operation on the specified cluster IDs. ONLY available when the cluster IDs are specified. [true|false]
   -t, --target stringArray           Target allocator(s) on which to place the vacated workload
diff --git a/pkg/platform/allocator/vacate.go b/pkg/platform/allocator/vacate.go
index 53f207b..24083e0 100644
--- a/pkg/platform/allocator/vacate.go
+++ b/pkg/platform/allocator/vacate.go
@@ -472,6 +472,10 @@ func ComputeVacateRequest(pr *models.MoveClustersDetails, clusters, to []string,
 			c.CalculatedPlan.PlanConfiguration.SkipDataMigration = overrides.SkipDataMigration
 		}
 
+		if overrides.OverrideFailsafe != nil {
+			c.CalculatedPlan.PlanConfiguration.OverrideFailsafe = overrides.OverrideFailsafe
+		}
+
 		c.CalculatedPlan.PlanConfiguration.PreferredAllocators = to
 		req.ElasticsearchClusters = append(req.ElasticsearchClusters,
 			&models.MoveElasticsearchClusterConfiguration{
diff --git a/pkg/platform/allocator/vacate_params.go b/pkg/platform/allocator/vacate_params.go
index f0372f9..12071a5 100644
--- a/pkg/platform/allocator/vacate_params.go
+++ b/pkg/platform/allocator/vacate_params.go
@@ -193,4 +193,5 @@ type PlanOverrides struct {
 	// SkipSnapshot overwrites the Transient part of an Elastisearch vacate.
 	SkipSnapshot      *bool
 	SkipDataMigration *bool
+	OverrideFailsafe  *bool
 }


Test patch:
diff --git a/pkg/platform/allocator/vacate_test.go b/pkg/platform/allocator/vacate_test.go
index a6c3014..8699a20 100644
--- a/pkg/platform/allocator/vacate_test.go
+++ b/pkg/platform/allocator/vacate_test.go
@@ -242,6 +242,52 @@ func TestComputeVacateRequest(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "No filters with OverrideFailsafe override",
+			args: args{
+				pr: &models.MoveClustersDetails{
+					ElasticsearchClusters: []*models.MoveElasticsearchClusterDetails{
+						{
+							ClusterID: ec.String("63d765d37613423e97b1040257cf20c8"),
+							CalculatedPlan: &models.TransientElasticsearchPlanConfiguration{
+								PlanConfiguration: &models.ElasticsearchPlanControlConfiguration{
+									Timeout:              4096,
+									ReallocateInstances:  ec.Bool(false),
+									ExtendedMaintenance:  ec.Bool(false),
+									OverrideFailsafe:     ec.Bool(false),
+									SkipDataMigration:    ec.Bool(false),
+									SkipPostUpgradeSteps: ec.Bool(false),
+									SkipSnapshot:         ec.Bool(false),
+								},
+							},
+						},
+					},
+				},
+				clusters:  nil,
+				to:        nil,
+				overrides: PlanOverrides{OverrideFailsafe: ec.Bool(true)},
+			},
+			want: &models.MoveClustersRequest{
+				ElasticsearchClusters: []*models.MoveElasticsearchClusterConfiguration{
+					{
+						ClusterIds: []string{
+							"63d765d37613423e97b1040257cf20c8",
+						},
+						PlanOverride: &models.TransientElasticsearchPlanConfiguration{
+							PlanConfiguration: &models.ElasticsearchPlanControlConfiguration{
+								Timeout:              4096,
+								ReallocateInstances:  ec.Bool(false),
+								ExtendedMaintenance:  ec.Bool(false),
+								OverrideFailsafe:     ec.Bool(true),
+								SkipDataMigration:    ec.Bool(false),
+								SkipPostUpgradeSteps: ec.Bool(false),
+								SkipSnapshot:         ec.Bool(false),
+							},
+						},
+					},
+				},
+			},
+		},
 		{
 			name: "Set target allocator",
 			args: args{