diff --git a/cmd/create_recovery_plan.go b/cmd/create_recovery_plan.go index 5e681a445..52d5201b4 100644 --- a/cmd/create_recovery_plan.go +++ b/cmd/create_recovery_plan.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "sort" + "strconv" "gopkg.in/yaml.v2" @@ -17,9 +18,9 @@ import ( ) type InstanceGroupPlan struct { - Name string `yaml:"name"` - MaxInFlight string `yaml:"max_in_flight,omitempty"` - PlannedResolutions map[string]string `yaml:"planned_resolutions"` + Name string `yaml:"name"` + MaxInFlightOverride string `yaml:"max_in_flight_override,omitempty"` + PlannedResolutions map[string]string `yaml:"planned_resolutions"` } type RecoveryPlan struct { @@ -47,6 +48,11 @@ func (c CreateRecoveryPlanCmd) Run(opts CreateRecoveryPlanOpts) error { return nil } + maxInFlightByInstanceGroup, err := c.getMaxInFlightByInstanceGroup() + if err != nil { + return err + } + var plan RecoveryPlan for _, instanceGroup := range sortedInstanceGroups(problemsByInstanceGroup) { c.ui.PrintLinef("Instance Group '%s'\n", instanceGroup) @@ -56,9 +62,24 @@ func (c CreateRecoveryPlanCmd) Run(opts CreateRecoveryPlanOpts) error { return err } + instanceGroupCurrentMaxInFlight := maxInFlightByInstanceGroup[instanceGroup] + var instanceGroupMaxInFlightOverride string + if c.ui.AskForConfirmationWithLabel( + fmt.Sprintf("Override current max_in_flight value of '%s'?", instanceGroupCurrentMaxInFlight), + ) == nil { + instanceGroupMaxInFlightOverride, err = c.ui.AskForTextWithDefaultValue( + fmt.Sprintf("max_in_flight override for '%s'", instanceGroup), + instanceGroupCurrentMaxInFlight, + ) + if err != nil { + return err + } + } + plan.InstanceGroupsPlan = append(plan.InstanceGroupsPlan, InstanceGroupPlan{ - Name: instanceGroup, - PlannedResolutions: instanceGroupResolutions, + Name: instanceGroup, + MaxInFlightOverride: instanceGroupMaxInFlightOverride, + PlannedResolutions: instanceGroupResolutions, }) } @@ -70,6 +91,52 @@ func (c CreateRecoveryPlanCmd) Run(opts CreateRecoveryPlanOpts) error { return c.fs.WriteFile(opts.Args.RecoveryPlan.ExpandedPath, bytes) } +type updateInstanceGroup struct { + Name string `yaml:"name"` + Update map[string]interface{} `yaml:"update"` +} + +type updateManifest struct { + InstanceGroups []updateInstanceGroup `yaml:"instance_groups"` + Update map[string]interface{} `yaml:"update"` +} + +func (c CreateRecoveryPlanCmd) getMaxInFlightByInstanceGroup() (map[string]string, error) { + rawManifest, err := c.deployment.Manifest() + if err != nil { + return nil, err + } + + var updateManifest updateManifest + err = yaml.Unmarshal([]byte(rawManifest), &updateManifest) + if err != nil { + return nil, err + } + + globalMaxInFlight := updateManifest.Update["max_in_flight"] + flightMap := make(map[string]string) + for _, instanceGroup := range updateManifest.InstanceGroups { + groupMaxInFlight := instanceGroup.Update["max_in_flight"] + if groupMaxInFlight == nil { + groupMaxInFlight = globalMaxInFlight + } + flightMap[instanceGroup.Name] = ensureString(groupMaxInFlight) + } + + return flightMap, nil +} + +func ensureString(i interface{}) string { + switch v := i.(type) { + case int: + return strconv.Itoa(v) + case string: + return v + } + + return i.(string) +} + func sortedInstanceGroups(problemsByInstanceGroup map[string][]boshdir.Problem) []string { var instanceGroups []string for k := range problemsByInstanceGroup { diff --git a/cmd/create_recovery_plan_test.go b/cmd/create_recovery_plan_test.go index f01365c73..d0e6b06dd 100644 --- a/cmd/create_recovery_plan_test.go +++ b/cmd/create_recovery_plan_test.go @@ -152,6 +152,11 @@ var _ = Describe("CreateRecoveryPlanCmd", func() { deployment.ScanForProblemsReturns(severalProbs, nil) ui.AskedChoiceChosens = []int{0, 1, 2} ui.AskedChoiceErrs = []error{nil, nil, nil} + ui.AskedConfirmationErr = nil + ui.AskedText = []fakeui.Answer{ + {Text: "10", Error: nil}, + {Text: "50%", Error: nil}, + } }) It("shows problems by instance group and type", func() { @@ -239,10 +244,12 @@ var _ = Describe("CreateRecoveryPlanCmd", func() { Expect(actualPlan.InstanceGroupsPlan).To(HaveLen(2)) Expect(actualPlan.InstanceGroupsPlan[0].Name).To(Equal("diego_cell")) + Expect(actualPlan.InstanceGroupsPlan[0].MaxInFlightOverride).To(Equal("10")) Expect(actualPlan.InstanceGroupsPlan[0].PlannedResolutions).To(HaveLen(1)) Expect(actualPlan.InstanceGroupsPlan[0].PlannedResolutions).To(HaveKeyWithValue("unresponsive_agent", *skipResolution.Name)) Expect(actualPlan.InstanceGroupsPlan[1].Name).To(Equal("router")) + Expect(actualPlan.InstanceGroupsPlan[1].MaxInFlightOverride).To(Equal("50%")) Expect(actualPlan.InstanceGroupsPlan[1].PlannedResolutions).To(HaveLen(2)) Expect(actualPlan.InstanceGroupsPlan[1].PlannedResolutions).To(HaveKeyWithValue("missing_vm", *recreateResolution.Name)) Expect(actualPlan.InstanceGroupsPlan[1].PlannedResolutions).To(HaveKeyWithValue("mount_info_mismatch", *reattachDiskAndRebootResolution.Name)) @@ -256,6 +263,29 @@ var _ = Describe("CreateRecoveryPlanCmd", func() { Expect(err.Error()).To(ContainSubstring("fake-err")) }) + It("does not override max_in_flight if not confirmed", func() { + ui.AskedConfirmationErr = errors.New("fake-err") + + err := act() + Expect(err).ToNot(HaveOccurred()) + + Expect(fakeFS.WriteFileCallCount).To(Equal(1)) + Expect(fakeFS.FileExists("/tmp/foo.yml")).To(BeTrue()) + bytes, err := fakeFS.ReadFile("/tmp/foo.yml") + Expect(err).ToNot(HaveOccurred()) + + var actualPlan RecoveryPlan + Expect(yaml.Unmarshal(bytes, &actualPlan)).ToNot(HaveOccurred()) + + Expect(actualPlan.InstanceGroupsPlan).To(HaveLen(2)) + + Expect(actualPlan.InstanceGroupsPlan[0].Name).To(Equal("diego_cell")) + Expect(actualPlan.InstanceGroupsPlan[0].MaxInFlightOverride).To(BeEmpty()) + + Expect(actualPlan.InstanceGroupsPlan[1].Name).To(Equal("router")) + Expect(actualPlan.InstanceGroupsPlan[1].MaxInFlightOverride).To(BeEmpty()) + }) + Context("director does not return instance group", func() { BeforeEach(func() { grouplessProbs := severalProbs