From ed85db10901e31ad78a1249044a6f565752b75c0 Mon Sep 17 00:00:00 2001 From: romanprog Date: Wed, 29 Nov 2023 00:27:09 +0200 Subject: [PATCH 01/11] tainted, graph refactoring --- pkg/cmd/cdev/apply.go | 7 +- pkg/cmd/cdev/destroy.go | 1 + pkg/cmd/cdev/output.go | 7 +- pkg/colors/main.go | 4 + pkg/config/config.go | 12 +- pkg/config/targetcheck.go | 33 ++ pkg/project/commands.go | 329 +++++++++--------- pkg/project/grapher.go | 29 +- pkg/project/helpers.go | 243 +++++++++++-- pkg/project/project.go | 10 + pkg/project/state.go | 13 +- pkg/project/unit.go | 1 + pkg/units/shell/common/unit.go | 11 + pkg/utils/stats_collector.go | 1 - .../local-tmpl/complex-output/main.tf | 1 + tests/test-project/local-tmpl/template.yaml | 1 + 16 files changed, 460 insertions(+), 243 deletions(-) create mode 100644 pkg/config/targetcheck.go diff --git a/pkg/cmd/cdev/apply.go b/pkg/cmd/cdev/apply.go index 2c59c883..9a35bb8a 100644 --- a/pkg/cmd/cdev/apply.go +++ b/pkg/cmd/cdev/apply.go @@ -1,8 +1,10 @@ package cdev import ( + "github.com/apex/log" "github.com/shalb/cluster.dev/pkg/config" "github.com/shalb/cluster.dev/pkg/project" + "github.com/shalb/cluster.dev/pkg/utils" "github.com/spf13/cobra" ) @@ -14,7 +16,9 @@ var applyCmd = &cobra.Command{ Short: "Deploys or updates infrastructure according to project configuration", RunE: func(cmd *cobra.Command, args []string) error { project, err := project.LoadProjectFull() - + if utils.GetEnv("CDEV_COLLECT_USAGE_STATS", "false") == "true" { + log.Infof("Sending usage statistic. To disable statistics collection, export the CDEV_COLLECT_USAGE_STATS=false environment variable") + } if err != nil { return NewCmdErr(project, "apply", err) } @@ -40,4 +44,5 @@ func init() { rootCmd.AddCommand(applyCmd) applyCmd.Flags().BoolVar(&config.Global.IgnoreState, "ignore-state", false, "Apply even if the state has not changed.") applyCmd.Flags().BoolVar(&config.Global.Force, "force", false, "Skip interactive approval.") + applyCmd.Flags().StringArrayVarP(&config.Global.Targets, "target", "t", []string{}, "Units and stack that will be applied. All others will not apply.") } diff --git a/pkg/cmd/cdev/destroy.go b/pkg/cmd/cdev/destroy.go index c0414754..8c5fc586 100644 --- a/pkg/cmd/cdev/destroy.go +++ b/pkg/cmd/cdev/destroy.go @@ -35,4 +35,5 @@ func init() { rootCmd.AddCommand(destroyCmd) destroyCmd.Flags().BoolVar(&config.Global.IgnoreState, "ignore-state", false, "Destroy current configuration and ignore state.") destroyCmd.Flags().BoolVar(&config.Global.Force, "force", false, "Skip interactive approval.") + destroyCmd.Flags().StringArrayVarP(&config.Global.Targets, "target", "t", []string{}, "Units and stack that will be destroyed. All others will not destroy.") } diff --git a/pkg/cmd/cdev/output.go b/pkg/cmd/cdev/output.go index a1532e59..bab66878 100644 --- a/pkg/cmd/cdev/output.go +++ b/pkg/cmd/cdev/output.go @@ -20,12 +20,7 @@ var outputCmd = &cobra.Command{ if err != nil { log.Fatalf("Fatal error: outputs: lock state: %v", err.Error()) } - stProject, err := project.LoadState() - if err != nil { - project.UnLockState() - log.Fatalf("Fatal error: outputs: load state: %v", err.Error()) - } - err = stProject.PrintOutputs() + err = project.OwnState.PrintOutputs() if err != nil { log.Fatalf("Fatal error: outputs: print %v", err.Error()) } diff --git a/pkg/colors/main.go b/pkg/colors/main.go index e388566b..7f062937 100644 --- a/pkg/colors/main.go +++ b/pkg/colors/main.go @@ -39,6 +39,8 @@ const ( LightWhiteBold LightRedBold PurpleBold + Orange + OrangeBold ) var colored = true @@ -65,6 +67,8 @@ var colorsMap map[Color]ColoredFmt = map[Color]ColoredFmt{ WhiteBold: color.New(color.FgWhite, color.BgDefault, color.OpBold), Yellow: color.New(color.FgYellow, color.BgDefault), YellowBold: color.New(color.FgYellow, color.BgDefault, color.OpBold), + Orange: color.RGB(255, 153, 51), + OrangeBold: color.NewRGBStyle(color.RGB(255, 153, 51)).SetOpts(color.Opts{color.OpBold}), } // SetColored set all colors to default, if colored == false. diff --git a/pkg/config/config.go b/pkg/config/config.go index 52df9bc3..e8d938bf 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -64,6 +64,7 @@ type ConfSpec struct { Force bool Interactive bool OutputJSON bool + Targets []string } // Global config for executor. @@ -102,13 +103,8 @@ func InitConfig() { log.Fatal(err.Error()) } } - Interrupted = false -} - -// getEnv Helper for args parse. -func getEnv(key string, defaultVal string) string { - if envVal, ok := os.LookupEnv(key); ok { - return envVal + if Global.MaxParallel == 0 { + log.Fatal("Parallelism should be greater then 0.") } - return defaultVal + Interrupted = false } diff --git a/pkg/config/targetcheck.go b/pkg/config/targetcheck.go new file mode 100644 index 00000000..509e9318 --- /dev/null +++ b/pkg/config/targetcheck.go @@ -0,0 +1,33 @@ +package config + +import "strings" + +type TargetUnits []string + +func NewTargetsChecker(tg []string) *TargetUnits { + res := TargetUnits{} + res = append(res, tg...) + return &res +} + +func (t *TargetUnits) Check(unitKey string) bool { + for _, target := range *t { + tgSplitted := strings.Split(target, ".") + uKeySplitted := strings.Split(unitKey, ".") + if len(tgSplitted) == 0 || len(tgSplitted) > 2 || len(uKeySplitted) != 2 { + return false + } + if len(tgSplitted) == 1 { + // Target is whole stack, check unit stack name only. + if uKeySplitted[0] == tgSplitted[0] { + return true + } + continue + } + // The target is unit, compare unit name and stack name. + if uKeySplitted[0] == tgSplitted[0] && uKeySplitted[1] == tgSplitted[1] { + return false + } + } + return false +} diff --git a/pkg/project/commands.go b/pkg/project/commands.go index 35d63934..e4125664 100644 --- a/pkg/project/commands.go +++ b/pkg/project/commands.go @@ -27,49 +27,46 @@ func (p *Project) Build() error { // Destroy all units. func (p *Project) Destroy() error { - fProject, err := p.LoadState() - if err != nil { - return fmt.Errorf("project destroy: %w", err) - } - graph := grapher{} - if config.Global.IgnoreState { - graph.Init(p, 1, true) - } else { - graph.Init(&fProject.Project, 1, true) - } - defer graph.Close() + planStatus := ProjectPlanningStatus{} + p.planDestroyAll(&planStatus) + graph := planStatus.GetDestroyGraph() if graph.Len() < 1 { log.Info("Nothing to destroy, exiting") return nil } destSeq := graph.GetSequenceSet() if !config.Global.Force { - destList := planDestroy(destSeq, nil) - showPlanResults(nil, nil, destList, nil) + showPlanResults(&planStatus) respond := climenu.GetText("Continue?(yes/no)", "no") if respond != "yes" { log.Info("Destroying cancelled") return nil } } - err = p.ClearCacheDir() + err := p.ClearCacheDir() if err != nil { return fmt.Errorf("project destroy: clear cache dir: %w", err) } log.Info("Destroying...") - for _, md := range destSeq { - log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Destroying unit '%v'", md.Key())) - err = md.Build() + for _, unit := range destSeq { + log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Destroying unit '%v'", unit.Key())) + err = unit.Build() if err != nil { return fmt.Errorf("project destroy: destroying deleted unit: %w", err) } p.ProcessedUnitsCount++ - err = md.Destroy() + err = unit.Destroy() if err != nil { + if unit.IsTainted() { + err = p.OwnState.SaveState() + if err != nil { + return fmt.Errorf("project destroy: saving state: %w", err) + } + } return fmt.Errorf("project destroy: %w", err) } - fProject.DeleteUnit(md) - err = fProject.SaveState() + p.OwnState.DeleteUnit(unit) + err = p.OwnState.SaveState() if err != nil { return fmt.Errorf("project destroy: saving state: %w", err) } @@ -79,17 +76,12 @@ func (p *Project) Destroy() error { // Apply all units. func (p *Project) Apply() error { - grDebug := grapher{} - err := grDebug.Init(p, config.Global.MaxParallel, false) + planningStatus, err := p.Plan() if err != nil { return err } if !config.Global.Force { - hasChanges, err := p.Plan() - if err != nil { - return err - } - if !hasChanges { + if !planningStatus.HasChanges() { return nil } respond := climenu.GetText("Continue?(yes/no)", "no") @@ -103,38 +95,28 @@ func (p *Project) Apply() error { return fmt.Errorf("project apply: clear cache dir: %v", err.Error()) } log.Info("Applying...") - gr := grapher{} - err = gr.Init(p, config.Global.MaxParallel, false) - if err != nil { - return err - } + gr := planningStatus.GetApplyGraph() defer gr.Close() - fProject, err := p.LoadState() - if err != nil { - return err - } - - StateDestroyGraph := grapher{} - err = StateDestroyGraph.Init(&fProject.Project, 1, true) - if err != nil { - return err - } + StateDestroyGraph := planningStatus.GetDestroyGraph() defer StateDestroyGraph.Close() - for _, md := range StateDestroyGraph.GetSequenceSet() { - _, exists := p.Units[md.Key()] - if exists { - continue - } - err = md.Build() + + for _, unit := range StateDestroyGraph.GetSequenceSet() { + err = unit.Build() if err != nil { log.Errorf("project apply: destroying deleted unit: %v", err.Error()) } - err = md.Destroy() + err = unit.Destroy() if err != nil { + if unit.IsTainted() { + err = p.OwnState.SaveState() + if err != nil { + return fmt.Errorf("project apply: saving state: %w", err) + } + } return fmt.Errorf("project apply: destroying deleted unit: %v", err.Error()) } - fProject.DeleteUnit(md) - err = fProject.SaveState() + p.OwnState.DeleteUnit(unit) + err = p.OwnState.SaveState() if err != nil { return fmt.Errorf("project apply: %v", err.Error()) } @@ -159,156 +141,108 @@ func (p *Project) Apply() error { return nil } - go func(unit Unit, finFunc func(error), stateP *StateProject) { - diff, stateUnit := stateP.CheckUnitChanges(unit) - var res error - if len(diff) > 0 || config.Global.IgnoreState { - log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Applying unit '%v':", md.Key())) - log.Debugf("Unit %v diff: \n%v", md.Key(), diff) - err = unit.Build() - if err != nil { - log.Errorf("project apply: unit build error: %v", err.Error()) - finFunc(err) - return - } - p.ProcessedUnitsCount++ - res := unit.Apply() - if res == nil { - stateP.UpdateUnit(unit) - err := stateP.SaveState() - if err != nil { - finFunc(err) - return - } - err = unit.UpdateProjectRuntimeData(p) + go func(unit Unit, finFunc func(error)) { + log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Applying unit '%v':", md.Key())) + err = unit.Build() + if err != nil { + log.Errorf("project apply: unit build error: %v", err.Error()) + finFunc(err) + return + } + p.ProcessedUnitsCount++ + applyError := unit.Apply() + if applyError != nil { + if unit.IsTainted() { + unit.Project().OwnState.UpdateUnit(unit) + err := unit.Project().OwnState.SaveState() if err != nil { finFunc(err) return } + finFunc(applyError) + return } - finFunc(res) + } + unit.Project().OwnState.UpdateUnit(unit) + err := unit.Project().OwnState.SaveState() + if err != nil { + finFunc(err) return - } else { - // Copy unit from state to project (to save raw output data for some units) - p.Units[unit.Key()] = stateUnit } - log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Unit '%v' has not changed. Skip applying.", md.Key())) - finFunc(res) - }(md, fn, fProject) + err = unit.UpdateProjectRuntimeData(p) + if err != nil { + finFunc(err) + return + } + + finFunc(nil) + }(md, fn) } } // Plan and output result. -func (p *Project) Plan() (hasChanges bool, err error) { - fProject, err := p.LoadState() - if err != nil { - return - } - - CurrentGraph := grapher{} - err = CurrentGraph.Init(p, 1, false) - if err != nil { - return - } - defer CurrentGraph.Close() - StateGraph := grapher{} - err = StateGraph.Init(&fProject.Project, 1, true) +func (p *Project) Plan() (*ProjectPlanningStatus, error) { + log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Checking units in state")) + planningSt, err := p.buildPlan() if err != nil { - return + return nil, err } - defer StateGraph.Close() - stateModsSeq := StateGraph.GetSequenceSet() - curModsSeq := CurrentGraph.GetSequenceSet() - modsForApply := []string{} - modsForUpdate := []string{} - modsUnchanged := []string{} - changedUnits := map[string]Unit{} - log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Checking units in state")) - modsForDestroy := planDestroy(stateModsSeq, curModsSeq) - - for _, md := range curModsSeq { - _, exists := fProject.Units[md.Key()] - diff, stateUnit := fProject.CheckUnitChanges(md) - log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Planning unit '%v':", md.Key())) - if len(diff) > 0 || config.Global.IgnoreState { - changedUnits[md.Key()] = md - if len(diff) > 0 { - fmt.Printf("%v\n", diff) - } else { - fmt.Println("") + if config.Global.ShowTerraformPlan { + err = p.ClearCacheDir() + for _, md := range planningSt.GetApplyGraph().GetSequenceSet() { + if err != nil { + return nil, fmt.Errorf("project plan: clear cache dir: %v", err.Error()) } - if exists { - modsForUpdate = append(modsForUpdate, md.Key()) - } else { - modsForApply = append(modsForApply, md.Key()) + allDepsDeployed := true + for _, planModDep := range md.Dependencies().Slice() { + dS := planningSt.FindUnit(planModDep.Unit) + if dS != nil && dS.Status != NotChanged { + allDepsDeployed = false + } } - if config.Global.ShowTerraformPlan { - err = p.ClearCacheDir() + if allDepsDeployed { + err = md.Build() if err != nil { - return false, fmt.Errorf("project plan: clear cache dir: %v", err.Error()) - } - allDepsDeployed := true - for _, planModDep := range md.Dependencies().Slice() { - _, exists := fProject.Units[planModDep.Unit.Key()] - if !exists { - allDepsDeployed = false - break - } + log.Errorf("terraform plan: unit build error: %v", err.Error()) + return nil, err } - if allDepsDeployed { - err = md.Build() - if err != nil { - log.Errorf("terraform plan: unit build error: %v", err.Error()) - return - } - err = md.Plan() - if err != nil { - log.Errorf("unit '%v' terraform plan return an error: %v", md.Key(), err.Error()) - return - } - } else { - log.Warnf("The unit '%v' has dependencies that have not yet been deployed. Can't show terraform plan.", md.Key()) - } - } - } else { - if stateUnit != nil { - stateUnit.UpdateProjectRuntimeData(p) - } - modsUnchanged = append(modsUnchanged, md.Key()) - // Unit was not changed. Copy unit outputs from state. - p.UnitLinks.JoinWithDataReplace(fProject.UnitLinks.ByTargetUnit(md)) - // log.Warnf("Plan after: %+v", p.UnitLinks.List) - log.Infof(colors.Fmt(colors.GreenBold).Sprint("Not changed.")) - } - } - for _, un := range changedUnits { - for _, dep := range un.Dependencies().Map() { - if dep.Unit.ForceApply() { - if _, exists := changedUnits[dep.Unit.Key()]; !exists { - modsForUpdate = append(modsForUpdate, dep.Unit.Key()) - changedUnits[dep.Unit.Key()] = dep.Unit + err = md.Plan() + if err != nil { + log.Errorf("unit '%v' terraform plan return an error: %v", md.Key(), err.Error()) + return nil, err } + } else { + log.Warnf("The unit '%v' has dependencies that have not yet been deployed. Can't show terraform plan.", md.Key()) } } } - showPlanResults(modsForApply, modsForUpdate, modsForDestroy, modsUnchanged) - hasChanges = len(modsForApply)+len(modsForUpdate)+len(modsForDestroy) != 0 - return + showPlanResults(planningSt) + return planningSt, nil } // planDestroy collect and show units for destroying. -func planDestroy(stateList, projList []Unit) []string { - modsForDestroy := []string{} - for _, md := range stateList { - if i := findMod(projList, md); i >= 0 { +func (p *Project) planDestroy(opStatus *ProjectPlanningStatus) { + for _, md := range p.OwnState.UnitsSlice() { + if i := findMod(p.UnitsSlice(), md); i >= 0 { continue } diff := utils.Diff(md.GetDiffData(), nil, true) - log.Info(colors.Fmt(colors.Red).Sprintf("unit '%v' will be destroyed:", md.Key())) - fmt.Printf("%v\n", diff) - modsForDestroy = append(modsForDestroy, md.Key()) + opStatus.Add(md, Destroy, diff, md.IsTainted()) + } +} + +// planDestroyAll add all units from state for destroy. +func (p *Project) planDestroyAll(opStatus *ProjectPlanningStatus) { + var units []Unit + if config.Global.IgnoreState { + units = p.UnitsSlice() + } else { + units = p.OwnState.UnitsSlice() + } + for _, md := range units { + diff := utils.Diff(md.GetDiffData(), nil, true) + opStatus.Add(md, Destroy, diff, md.IsTainted()) } - return modsForDestroy } func findMod(list []Unit, mod Unit) int { @@ -322,3 +256,52 @@ func findMod(list []Unit, mod Unit) int { } return -1 } + +// Plan and output result. +func (p *Project) buildPlan() (planningStatus *ProjectPlanningStatus, err error) { + err = checkUnitDependencies(p) + if err != nil { + return + } + planningStatus = &ProjectPlanningStatus{} + p.planDestroy(planningStatus) + for _, unit := range p.UnitsSlice() { + _, exists := p.OwnState.Units[unit.Key()] + diff, stateUnit, tainted := p.OwnState.CheckUnitChanges(unit) + if len(diff) > 0 || config.Global.IgnoreState { + if len(diff) > 0 { + if exists { + planningStatus.Add(unit, Update, diff, tainted) + } else { + planningStatus.Add(unit, Apply, diff, tainted) + } + } + } else { + if stateUnit != nil { + stateUnit.UpdateProjectRuntimeData(p) + } + planningStatus.Add(unit, NotChanged, "", false) + + // Unit was not changed. Copy unit outputs from state. + p.UnitLinks.JoinWithDataReplace(p.OwnState.UnitLinks.ByTargetUnit(unit)) + } + } + // planningStatus.Print() + changedUnits := planningStatus.OperationFilter(Apply, Update, Destroy) + for _, st := range changedUnits.Slice() { + err = DependenciesRecursiveIterate(st.UnitPtr, func(unitForCheck Unit) error { + if unitForCheck.ForceApply() { + fu := changedUnits.FindUnit(unitForCheck) + if fu == nil { + planningStatus.AddOrUpdate(unitForCheck, UpdateAsDep, colors.Fmt(colors.Yellow).Sprint("")) + } + } + return nil + }) + if err != nil { + return nil, err + } + } + + return +} diff --git a/pkg/project/grapher.go b/pkg/project/grapher.go index 149f75fe..f54b8043 100644 --- a/pkg/project/grapher.go +++ b/pkg/project/grapher.go @@ -34,19 +34,16 @@ type grapher struct { stopChan chan struct{} } -func (g *grapher) Init(project *Project, maxParallel int, reverse bool) error { - if err := checkUnitDependencies(project); err != nil { - return err - } +func (g *grapher) InitP(planningStatus *ProjectPlanningStatus, maxParallel int, reverse bool) { if maxParallel < 1 { - return fmt.Errorf("maxParallel should be greater then 0") + log.Fatal("Internal error, parallelism < 1.") } g.units = make(map[string]Unit) g.unitsErrors = make(map[string]error) g.unFinished = make(map[string]Unit) - for key, mod := range project.Units { - g.units[key] = mod - g.unFinished[key] = mod + for _, uStatus := range planningStatus.Slice() { + g.units[uStatus.UnitPtr.Key()] = uStatus.UnitPtr + g.unFinished[uStatus.UnitPtr.Key()] = uStatus.UnitPtr } g.maxParallel = maxParallel g.queue.Init() @@ -58,7 +55,6 @@ func (g *grapher) Init(project *Project, maxParallel int, reverse bool) error { g.updateQueue() g.stopChan = make(chan struct{}) g.listenHupSig() - return nil } func (g *grapher) HasError() bool { @@ -96,16 +92,13 @@ func (g *grapher) updateDirectQueue() int { func (g *grapher) updateReverseQueue() int { count := 0 for key, mod := range g.units { - isReady := true dependedMods := findDependedUnits(g.unFinished, mod) if len(dependedMods) > 0 { - isReady = false - } - if isReady { - g.queue.PushBack(mod) - delete(g.units, key) - count++ + continue } + g.queue.PushBack(mod) + delete(g.units, key) + count++ } return count } @@ -237,19 +230,15 @@ func checkUnitDependenciesCircle(chain []string) error { func findDependedUnits(modList map[string]Unit, targetMod Unit) map[string]Unit { res := map[string]Unit{} for key, mod := range modList { - // log.Infof("findDependedUnits '%v':", mod.Name()) if mod.Key() == targetMod.Key() { continue } for _, dep := range mod.Dependencies().Slice() { if dep.Unit.Key() == targetMod.Key() { - // log.Infof(" '%v':", dep.TargetUnitName) - // log.Warnf("Tm: %v, M: %v Dependency: %v", targetMod.Name(), mod.Name(), dep.TargetUnitName) res[key] = mod } } } - //log.Debugf("Searching depended from unit: %v\n Result: %v", targetMod.Name(), res) return res } diff --git a/pkg/project/helpers.go b/pkg/project/helpers.go index 83df3c6b..44920f27 100644 --- a/pkg/project/helpers.go +++ b/pkg/project/helpers.go @@ -8,12 +8,139 @@ import ( "regexp" "strings" + "github.com/apex/log" + "github.com/olekukonko/tablewriter" "github.com/shalb/cluster.dev/pkg/colors" "github.com/shalb/cluster.dev/pkg/config" "github.com/shalb/cluster.dev/pkg/utils" ) +type UnitOperation uint16 + +const ( + Apply UnitOperation = iota + 1 + Destroy + Update + NotChanged + UpdateAsDep +) + +func (u UnitOperation) String() string { + mapperStatus := map[uint16]string{ + 1: colors.Fmt(colors.Green).Sprint("Apply"), + 2: colors.Fmt(colors.Red).Sprint("Destroy"), + 3: colors.Fmt(colors.Yellow).Sprint("Update"), + 4: colors.Fmt(colors.White).Sprint("NotChanged"), + 5: colors.Fmt(colors.Yellow).Sprint("UpdateAsDep"), + } + return mapperStatus[uint16(u)] +} + +func (u UnitOperation) HasChanges() bool { + return u != NotChanged +} + +type UnitPlanningStatus struct { + UnitPtr Unit + Diff string + Status UnitOperation + IsTainted bool +} + +type ProjectPlanningStatus struct { + units []*UnitPlanningStatus +} + +func (s *ProjectPlanningStatus) GetApplyGraph() *grapher { + CurrentGraph := grapher{} + CurrentGraph.InitP(s.OperationFilter(Apply, Update, UpdateAsDep), 1, false) + return &CurrentGraph +} + +func (s *ProjectPlanningStatus) FindUnit(unit Unit) *UnitPlanningStatus { + if unit == nil { + return nil + } + for _, us := range s.units { + if us.UnitPtr == unit { + return us + } + } + return nil +} + +func (s *ProjectPlanningStatus) GetDestroyGraph() *grapher { + CurrentGraph := grapher{} + CurrentGraph.InitP(s.OperationFilter(Destroy), 1, false) + return &CurrentGraph +} + +func (s *ProjectPlanningStatus) OperationFilter(ops ...UnitOperation) *ProjectPlanningStatus { + res := ProjectPlanningStatus{ + units: make([]*UnitPlanningStatus, 0), + } + if len(ops) == 0 { + return &res + } + for _, uo := range s.units { + for _, op := range ops { + if uo.Status == op { + res.units = append(res.units, uo) + } + } + } + return &res +} + +func (s *ProjectPlanningStatus) Add(u Unit, op UnitOperation, diff string, isTainted bool) { + uo := UnitPlanningStatus{ + UnitPtr: u, + Status: op, + Diff: diff, + IsTainted: isTainted, + } + s.units = append(s.units, &uo) +} + +func (s *ProjectPlanningStatus) AddOrUpdate(u Unit, op UnitOperation, diff string) { + uo := UnitPlanningStatus{ + UnitPtr: u, + Status: op, + Diff: diff, + } + existingUnit := s.FindUnit(u) + if existingUnit == nil { + s.units = append(s.units, &uo) + } else { + existingUnit.Diff = diff + existingUnit.Status = op + } +} + +func (s *ProjectPlanningStatus) HasChanges() bool { + for _, un := range s.units { + if un.Status != NotChanged { + return true + } + } + return false +} + +func (s *ProjectPlanningStatus) Len() int { + return len(s.units) +} + +func (s *ProjectPlanningStatus) Print() { + for _, unitStatus := range s.units { + fmt.Printf("UnitName: %v, Unit status: %v\n", unitStatus.UnitPtr.Key(), unitStatus.Status.String()) + } +} + +func (s *ProjectPlanningStatus) Slice() []*UnitPlanningStatus { + return s.units +} + // CreateMarker generate hash string for template markers. func CreateMarker(link ULinkT) (string, error) { if link.LinkType == "" { @@ -39,7 +166,7 @@ func CreateMarker(link ULinkT) (string, error) { // EscapeForMarkerStr convert URL to string which can be used as marker. func EscapeForMarkerStr(in string) (string, error) { - reg, err := regexp.Compile("[^A-Za-z0-9_\\-\\.]+") + reg, err := regexp.Compile(`[^A-Za-z0-9_\-\.]+`) if err != nil { return "", err } @@ -206,10 +333,10 @@ func ProjectsFilesExists() bool { return false } -func showPlanResults(deployList, updateList, destroyList, unchangedList []string) { +func showPlanResults(opStatus *ProjectPlanningStatus) { fmt.Println(colors.Fmt(colors.WhiteBold).Sprint("Plan results:")) - if len(deployList)+len(updateList)+len(destroyList) == 0 { + if opStatus.Len() == 0 { fmt.Println(colors.Fmt(colors.WhiteBold).Sprint("No changes, nothing to do.")) return } @@ -219,43 +346,55 @@ func showPlanResults(deployList, updateList, destroyList, unchangedList []string unitsTable := []string{} var deployString, updateString, destroyString, unchangedString string - for i, modName := range deployList { - if i != 0 { - deployString += "\n" - } - deployString += colors.Fmt(colors.Green).Sprint(modName) - } - for i, modName := range updateList { - if i != 0 { - updateString += "\n" - } - updateString += colors.Fmt(colors.Yellow).Sprint(modName) - } - for i, modName := range destroyList { - if i != 0 { - destroyString += "\n" - } - destroyString += colors.Fmt(colors.Red).Sprint(modName) - } - for i, modName := range unchangedList { - if i != 0 { - unchangedString += "\n" + for _, unit := range opStatus.Slice() { + log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Planning unit '%v':", unit.UnitPtr.Key())) + switch unit.Status { + case Apply: + fmt.Printf("%v\n", unit.Diff) + if len(deployString) != 0 { + deployString += "\n" + } + deployString += RenderUnitPlanningString(unit) + case Update: + fmt.Printf("%v\n", unit.Diff) + if len(updateString) != 0 { + updateString += "\n" + } + updateString += RenderUnitPlanningString(unit) + case UpdateAsDep: + fmt.Printf("%v\n", unit.Diff) + if len(updateString) != 0 { + updateString += "\n" + } + updateString += RenderUnitPlanningString(unit) + case Destroy: + fmt.Printf("%v\n", unit.Diff) + if len(destroyString) != 0 { + destroyString += "\n" + } + destroyString += RenderUnitPlanningString(unit) + case NotChanged: + log.Infof(colors.Fmt(colors.GreenBold).Sprint("Not changed.")) + if len(unchangedString) != 0 { + unchangedString += "\n" + } + unchangedString += RenderUnitPlanningString(unit) } - unchangedString += colors.Fmt(colors.White).Sprint(modName) } - if len(deployList) > 0 { + + if opStatus.OperationFilter(Apply).Len() > 0 { headers = append(headers, "Will be deployed") unitsTable = append(unitsTable, deployString) } - if len(updateList) > 0 { + if opStatus.OperationFilter(Update).Len() > 0 { headers = append(headers, "Will be updated") unitsTable = append(unitsTable, updateString) } - if len(destroyList) > 0 { + if opStatus.OperationFilter(Destroy).Len() > 0 { headers = append(headers, "Will be destroyed") unitsTable = append(unitsTable, destroyString) } - if len(unchangedList) > 0 { + if opStatus.OperationFilter(NotChanged).Len() > 0 { headers = append(headers, "Unchanged") unitsTable = append(unitsTable, unchangedString) } @@ -263,3 +402,49 @@ func showPlanResults(deployList, updateList, destroyList, unchangedList []string table.Append(unitsTable) table.Render() } + +func RenderUnitPlanningString(uStatus *UnitPlanningStatus) string { + switch uStatus.Status { + case Update, UpdateAsDep: + if uStatus.IsTainted { + return colors.Fmt(colors.Orange).Sprintf("%s(tainted)", uStatus.UnitPtr.Key()) + } else { + return colors.Fmt(colors.Yellow).Sprint(uStatus.UnitPtr.Key()) + } + case Apply: + if uStatus.IsTainted { + return colors.Fmt(colors.Green).Sprintf("%s(tainted)", uStatus.UnitPtr.Key()) + } else { + return colors.Fmt(colors.Green).Sprint(uStatus.UnitPtr.Key()) + } + case Destroy: + if uStatus.IsTainted { + return colors.Fmt(colors.Red).Sprintf("%s(tainted)", uStatus.UnitPtr.Key()) + } else { + return colors.Fmt(colors.Red).Sprint(uStatus.UnitPtr.Key()) + } + case NotChanged: + return colors.Fmt(colors.White).Sprint(uStatus.UnitPtr.Key()) + } + // Impossible, crush + log.Fatalf("Unexpected internal error. Unknown unit status '%v'", uStatus.Status.String()) + return uStatus.UnitPtr.Key() +} + +func DependenciesRecursiveIterate(u Unit, f func(Unit) error) error { + return dependenciesRecursiveIterateDepth(u, f, 0) +} + +func dependenciesRecursiveIterateDepth(u Unit, f func(Unit) error, depth int) error { + if depth > 20 { + log.Fatalf("Internal error: may be unexpected dependencies loop") + } + for _, dep := range u.Dependencies().Slice() { + err := f(dep.Unit) + if err != nil { + return err + } + dependenciesRecursiveIterateDepth(dep.Unit, f, depth+1) + } + return nil +} diff --git a/pkg/project/project.go b/pkg/project/project.go index 618d75e3..5f92d807 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -362,3 +362,13 @@ func (p *Project) PrintOutputs() (err error) { } return nil } + +func (p *Project) UnitsSlice() []Unit { + res := make([]Unit, len(p.Units)) + i := 0 + for _, unit := range p.Units { + res[i] = unit + i++ + } + return res +} diff --git a/pkg/project/state.go b/pkg/project/state.go index 72fd30cc..1538d6e9 100644 --- a/pkg/project/state.go +++ b/pkg/project/state.go @@ -217,24 +217,27 @@ func (p *Project) LoadState() (*StateProject, error) { return &statePrj, nil } -func (sp *StateProject) CheckUnitChanges(unit Unit) (string, Unit) { +func (sp *StateProject) CheckUnitChanges(unit Unit) (string, Unit, bool) { unitStateCache := map[string]bool{} unitInState, exists := sp.Units[unit.Key()] if !exists { - return utils.Diff(nil, unit.GetDiffData(), true), nil + return utils.Diff(nil, unit.GetDiffData(), true), nil, false } diffData := unit.GetDiffData() stateDiffData := unitInState.GetDiffData() df := utils.Diff(stateDiffData, diffData, true) if len(df) > 0 { - return df, unitInState + return df, unitInState, false + } + if unitInState.IsTainted() { + return colors.Fmt(colors.Yellow).Sprint(utils.Diff(nil, unit.GetDiffData(), false)), unitInState, true } for _, dep := range unit.Dependencies().UniqUnits() { if sp.checkUnitChangesRecursive(dep, unitStateCache) { - return colors.Fmt(colors.Yellow).Sprintf("+/- There are changes in the unit dependencies."), unitInState + return colors.Fmt(colors.Yellow).Sprintf("+/- There are changes in the unit dependencies."), unitInState, false } } - return "", unitInState + return "", unitInState, false } func (sp *StateProject) checkUnitChangesRecursive(unit Unit, cacheUnitChanges map[string]bool) bool { diff --git a/pkg/project/unit.go b/pkg/project/unit.go index c6291c42..699bf457 100644 --- a/pkg/project/unit.go +++ b/pkg/project/unit.go @@ -31,6 +31,7 @@ type Unit interface { WasApplied() bool ForceApply() bool Mux() *sync.Mutex + IsTainted() bool } type UnitDriver interface { diff --git a/pkg/units/shell/common/unit.go b/pkg/units/shell/common/unit.go index 47d1e1fe..2d954904 100644 --- a/pkg/units/shell/common/unit.go +++ b/pkg/units/shell/common/unit.go @@ -90,6 +90,12 @@ type Unit struct { DependsOn interface{} `yaml:"depends_on,omitempty" json:"depends_on,omitempty"` FApply bool `yaml:"force_apply" json:"force_apply"` lockedMux *sync.Mutex `yaml:"-" json:"-"` + Tainted bool `yaml:"-" json:"tainted,omitempty"` +} + +// IsTainted return true if unit have tainted state (failed previous apply or destroy). +func (u *Unit) IsTainted() bool { + return u.Tainted } // Mux return unit mutex to lock apply. @@ -263,6 +269,7 @@ func (u *Unit) Apply() error { } u.OutputRaw, err = u.runCommands(applyCommands, "apply") if err != nil { + u.Tainted = true return fmt.Errorf("apply unit '%v': %w", u.Key(), err) } // Get outputs. @@ -274,12 +281,14 @@ func (u *Unit) Apply() error { } u.OutputRaw, err = u.runCommands(cmdConf, "retrieving outputs") if err != nil { + u.Tainted = true return fmt.Errorf("retrieving unit '%v' outputs: %w", u.Key(), err) } } if u.GetOutputsConf != nil { parser, exists := u.OutputParsers[u.GetOutputsConf.Type] if !exists { + u.Tainted = true return fmt.Errorf("retrieving unit '%v' outputs: parser %v doesn't exists", u.Key(), u.GetOutputsConf.Type) } err = parser(string(u.OutputRaw), u.ProjectPtr.UnitLinks.ByTargetUnit(u).ByLinkTypes(project.OutputLinkType)) @@ -287,11 +296,13 @@ func (u *Unit) Apply() error { //str := fmt.Sprintf("Outputs data: %s", string(u.OutputRaw)) // log.Warnf("Len: %v", len(str)) + u.Tainted = true return fmt.Errorf("parse outputs '%v': %w", u.GetOutputsConf.Type, err) } } if err == nil { + u.Tainted = false u.Applied = true } return err diff --git a/pkg/utils/stats_collector.go b/pkg/utils/stats_collector.go index d3fd197a..43dac57d 100644 --- a/pkg/utils/stats_collector.go +++ b/pkg/utils/stats_collector.go @@ -46,7 +46,6 @@ func (e *StatsExporter) PushStats(stats interface{}) error { client := http.Client{ Timeout: 3 * time.Second, } - log.Warnf("Sending usage statistic. To disable statistics collection, export the CDEV_COLLECT_USAGE_STATS=false environment variable") log.Debugf("Usage stats:\n%v", string(jsonBody)) res, err := client.Do(req) if err != nil { diff --git a/tests/test-project/local-tmpl/complex-output/main.tf b/tests/test-project/local-tmpl/complex-output/main.tf index 103fc131..8939a6e2 100644 --- a/tests/test-project/local-tmpl/complex-output/main.tf +++ b/tests/test-project/local-tmpl/complex-output/main.tf @@ -7,5 +7,6 @@ output "map" { value = { key = "value", key2 = "value2", + key3 = "value3" } } diff --git a/tests/test-project/local-tmpl/template.yaml b/tests/test-project/local-tmpl/template.yaml index dde4d007..ca4e7aab 100644 --- a/tests/test-project/local-tmpl/template.yaml +++ b/tests/test-project/local-tmpl/template.yaml @@ -17,6 +17,7 @@ units: source: ./s3-file/ depends_on: this.create-bucket inputs: + # wrong_input: "taint state" bucket_name: {{ remoteState "this.create-bucket.id" }} data: {{ .variables.data }} - From 2839e95b381cefe8eb54a4002eec1845af462f98 Mon Sep 17 00:00:00 2001 From: romanprog Date: Mon, 4 Dec 2023 16:19:22 +0200 Subject: [PATCH 02/11] save code state before new changes on graph --- pkg/cmd/cdev/apply.go | 1 - pkg/cmd/cdev/project.go | 2 +- pkg/project/commands.go | 102 ++++-- pkg/project/dependencies.go | 280 ++++++++++++++++ pkg/project/graph.go | 311 ++++++++++++++++++ pkg/project/grapher.go | 269 --------------- pkg/project/helpers.go | 67 ++-- pkg/project/stack.go | 15 + pkg/project/state.go | 19 +- pkg/project/unit.go | 3 + pkg/units/shell/common/state.go | 4 +- pkg/units/shell/common/unit.go | 36 +- pkg/units/shell/terraform/printer/main.go | 19 ++ .../{dev-infra.yaml => dev-infra.yaml_} | 0 tests/test-project/graph-test/template.yaml | 96 ++++++ tests/test-project/local.yaml | 12 +- 16 files changed, 889 insertions(+), 347 deletions(-) create mode 100644 pkg/project/dependencies.go create mode 100644 pkg/project/graph.go delete mode 100644 pkg/project/grapher.go rename tests/test-project/{dev-infra.yaml => dev-infra.yaml_} (100%) create mode 100644 tests/test-project/graph-test/template.yaml diff --git a/pkg/cmd/cdev/apply.go b/pkg/cmd/cdev/apply.go index 9a35bb8a..a6fc9185 100644 --- a/pkg/cmd/cdev/apply.go +++ b/pkg/cmd/cdev/apply.go @@ -35,7 +35,6 @@ var applyCmd = &cobra.Command{ if err != nil { return NewCmdErr(project, "apply", err) } - project.UnLockState() return NewCmdErr(project, "apply", nil) }, } diff --git a/pkg/cmd/cdev/project.go b/pkg/cmd/cdev/project.go index 92993f08..00113ca2 100644 --- a/pkg/cmd/cdev/project.go +++ b/pkg/cmd/cdev/project.go @@ -33,7 +33,7 @@ var projectLs = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { p, err := project.LoadProjectFull() if err != nil { - log.Errorf("No project found in the current directory. Configuration error: %v", err.Error()) + log.Errorf("Project configuration error: %v", err.Error()) return } log.Info("Project info:") diff --git a/pkg/project/commands.go b/pkg/project/commands.go index e4125664..36e14a59 100644 --- a/pkg/project/commands.go +++ b/pkg/project/commands.go @@ -29,12 +29,17 @@ func (p *Project) Build() error { func (p *Project) Destroy() error { planStatus := ProjectPlanningStatus{} p.planDestroyAll(&planStatus) - graph := planStatus.GetDestroyGraph() + log.Errorf("Destroy planStatus %++v", planStatus.units) + graph, err := planStatus.GetDestroyGraph() + if err != nil { + return fmt.Errorf("build destroy graph: %w", err) + } if graph.Len() < 1 { log.Info("Nothing to destroy, exiting") return nil } - destSeq := graph.GetSequenceSet() + destSeq := graph.Slice() + log.Warnf("Destroy: destSeq: %++v", destSeq) if !config.Global.Force { showPlanResults(&planStatus) respond := climenu.GetText("Continue?(yes/no)", "no") @@ -43,21 +48,22 @@ func (p *Project) Destroy() error { return nil } } - err := p.ClearCacheDir() + err = p.ClearCacheDir() if err != nil { return fmt.Errorf("project destroy: clear cache dir: %w", err) } log.Info("Destroying...") for _, unit := range destSeq { - log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Destroying unit '%v'", unit.Key())) - err = unit.Build() + log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Destroying unit '%v'", unit.UnitPtr.Key())) + err = unit.UnitPtr.Build() + defer unit.UnitPtr.SetExecStatus(Finished) // Set unit status done on any error if err != nil { return fmt.Errorf("project destroy: destroying deleted unit: %w", err) } p.ProcessedUnitsCount++ - err = unit.Destroy() + err = unit.UnitPtr.Destroy() if err != nil { - if unit.IsTainted() { + if unit.UnitPtr.IsTainted() { err = p.OwnState.SaveState() if err != nil { return fmt.Errorf("project destroy: saving state: %w", err) @@ -65,17 +71,19 @@ func (p *Project) Destroy() error { } return fmt.Errorf("project destroy: %w", err) } - p.OwnState.DeleteUnit(unit) + p.OwnState.DeleteUnit(unit.UnitPtr) err = p.OwnState.SaveState() if err != nil { return fmt.Errorf("project destroy: saving state: %w", err) } + unit.UnitPtr.SetExecStatus(Finished) // Set unit status done if unit destroyed without errors } return nil } // Apply all units. func (p *Project) Apply() error { + var planningStatus *ProjectPlanningStatus planningStatus, err := p.Plan() if err != nil { return err @@ -95,19 +103,22 @@ func (p *Project) Apply() error { return fmt.Errorf("project apply: clear cache dir: %v", err.Error()) } log.Info("Applying...") - gr := planningStatus.GetApplyGraph() - defer gr.Close() - StateDestroyGraph := planningStatus.GetDestroyGraph() - defer StateDestroyGraph.Close() - - for _, unit := range StateDestroyGraph.GetSequenceSet() { - err = unit.Build() + StateDestroyGraph, err := planningStatus.GetDestroyGraph() + if err != nil { + return fmt.Errorf("build destroy greph: %w", err) + } + for _, graphUnit := range StateDestroyGraph.Slice() { + if config.Global.IgnoreState { + break + } + defer graphUnit.UnitPtr.SetExecStatus(Finished) // Set unit status done on any error + err = graphUnit.UnitPtr.Build() if err != nil { log.Errorf("project apply: destroying deleted unit: %v", err.Error()) } - err = unit.Destroy() + err = graphUnit.UnitPtr.Destroy() if err != nil { - if unit.IsTainted() { + if graphUnit.UnitPtr.IsTainted() { err = p.OwnState.SaveState() if err != nil { return fmt.Errorf("project apply: saving state: %w", err) @@ -115,11 +126,16 @@ func (p *Project) Apply() error { } return fmt.Errorf("project apply: destroying deleted unit: %v", err.Error()) } - p.OwnState.DeleteUnit(unit) + p.OwnState.DeleteUnit(graphUnit.UnitPtr) err = p.OwnState.SaveState() if err != nil { return fmt.Errorf("project apply: %v", err.Error()) } + graphUnit.UnitPtr.SetExecStatus(Finished) + } + gr, err := planningStatus.GetApplyGraph() + if err != nil { + return fmt.Errorf("build apply graph: %w", err) } for { // log.Warnf("FOR Project apply. Unit links: %+v", p.UnitLinks) @@ -129,10 +145,14 @@ func (p *Project) Apply() error { } md, fn, err := gr.GetNextAsync() if err != nil { - log.Errorf("error in unit %v, waiting for all running units done.", md.Key()) + unitName := "" + if md != nil { + unitName = md.Key() + } + log.Errorf("error in unit %v, waiting for all running units done.", unitName) gr.Wait() for modKey, e := range gr.Errors() { - log.Errorf("unit: '%v':\n%v", modKey, e.Error()) + log.Errorf("unit: '%v':\n%v", modKey, e.ExecError()) } return fmt.Errorf("applying error") } @@ -151,14 +171,17 @@ func (p *Project) Apply() error { } p.ProcessedUnitsCount++ applyError := unit.Apply() + log.Warnf("apply routine: unit done") if applyError != nil { if unit.IsTainted() { unit.Project().OwnState.UpdateUnit(unit) err := unit.Project().OwnState.SaveState() if err != nil { + log.Warnf("apply routine: send sig 1") finFunc(err) return } + log.Warnf("apply routine: send sig 2") finFunc(applyError) return } @@ -166,15 +189,17 @@ func (p *Project) Apply() error { unit.Project().OwnState.UpdateUnit(unit) err := unit.Project().OwnState.SaveState() if err != nil { + log.Warnf("apply routine: send sig 3") finFunc(err) return } err = unit.UpdateProjectRuntimeData(p) if err != nil { + log.Warnf("apply routine: send sig 4") finFunc(err) return } - + log.Warnf("apply routine: send sig 5") finFunc(nil) }(md, fn) } @@ -189,30 +214,37 @@ func (p *Project) Plan() (*ProjectPlanningStatus, error) { } if config.Global.ShowTerraformPlan { err = p.ClearCacheDir() - for _, md := range planningSt.GetApplyGraph().GetSequenceSet() { + if err != nil { + return nil, fmt.Errorf("build dir for exec terraform plan: %w", err) + } + planSeq, err := planningSt.GetApplyGraph() + if err != nil { + return nil, fmt.Errorf("build graph: %w", err) + } + for _, md := range planSeq.Slice() { if err != nil { return nil, fmt.Errorf("project plan: clear cache dir: %v", err.Error()) } allDepsDeployed := true - for _, planModDep := range md.Dependencies().Slice() { + for _, planModDep := range md.UnitPtr.Dependencies().Slice() { dS := planningSt.FindUnit(planModDep.Unit) if dS != nil && dS.Status != NotChanged { allDepsDeployed = false } } if allDepsDeployed { - err = md.Build() + err = md.UnitPtr.Build() if err != nil { log.Errorf("terraform plan: unit build error: %v", err.Error()) return nil, err } - err = md.Plan() + err = md.UnitPtr.Plan() if err != nil { - log.Errorf("unit '%v' terraform plan return an error: %v", md.Key(), err.Error()) + log.Errorf("unit '%v' terraform plan return an error: %v", md.UnitPtr.Key(), err.Error()) return nil, err } } else { - log.Warnf("The unit '%v' has dependencies that have not yet been deployed. Can't show terraform plan.", md.Key()) + log.Warnf("The unit '%v' has dependencies that have not yet been deployed. Can't show terraform plan.", md.UnitPtr.Key()) } } } @@ -239,6 +271,7 @@ func (p *Project) planDestroyAll(opStatus *ProjectPlanningStatus) { } else { units = p.OwnState.UnitsSlice() } + log.Errorf("planDestroyAll %++v", units) for _, md := range units { diff := utils.Diff(md.GetDiffData(), nil, true) opStatus.Add(md, Destroy, diff, md.IsTainted()) @@ -264,6 +297,12 @@ func (p *Project) buildPlan() (planningStatus *ProjectPlanningStatus, err error) return } planningStatus = &ProjectPlanningStatus{} + if config.Global.IgnoreState { + for _, u := range p.UnitsSlice() { + planningStatus.Add(u, Apply, utils.Diff(nil, u.GetDiffData(), true), false) + } + return planningStatus, nil + } p.planDestroy(planningStatus) for _, unit := range p.UnitsSlice() { _, exists := p.OwnState.Units[unit.Key()] @@ -293,7 +332,8 @@ func (p *Project) buildPlan() (planningStatus *ProjectPlanningStatus, err error) if unitForCheck.ForceApply() { fu := changedUnits.FindUnit(unitForCheck) if fu == nil { - planningStatus.AddOrUpdate(unitForCheck, UpdateAsDep, colors.Fmt(colors.Yellow).Sprint("")) + log.Debugf("Unit '%v' added for update as a force_apply dependency", unitForCheck.Key()) + planningStatus.AddOrUpdate(unitForCheck, Update, colors.Fmt(colors.Yellow).Sprint("+/- Will be applied as a 'force_apply' dependency")) } } return nil @@ -302,6 +342,10 @@ func (p *Project) buildPlan() (planningStatus *ProjectPlanningStatus, err error) return nil, err } } - + // Check graph and set sequence indexes + _, err = planningStatus.OperationFilter(Apply, Update).GetApplyGraph() + if err != nil { + return nil, fmt.Errorf("check apply graph: %w", err) + } return } diff --git a/pkg/project/dependencies.go b/pkg/project/dependencies.go new file mode 100644 index 00000000..bc106be9 --- /dev/null +++ b/pkg/project/dependencies.go @@ -0,0 +1,280 @@ +package project + +import ( + "fmt" + "strings" +) + +// import ( +// "container/list" +// "fmt" +// "os" +// "os/signal" +// "strings" +// "sync" +// "syscall" + +// "github.com/apex/log" +// "github.com/shalb/cluster.dev/pkg/config" +// ) + +// type unitExecuteResult struct { +// Mod Unit +// Error error +// } + +// type grapher struct { +// finished map[string]unitExecuteResult +// inProgress map[string]Unit +// queue list.List +// units map[string]Unit +// unFinished map[string]Unit +// mux sync.Mutex +// waitForModDone chan unitExecuteResult +// maxParallel int +// hasError bool +// reverse bool +// unitsErrors map[string]error +// sigTrap chan os.Signal +// stopChan chan struct{} +// } + +// func (g *grapher) InitP(planningStatus *ProjectPlanningStatus, maxParallel int, reverse bool) { +// if maxParallel < 1 { +// log.Fatal("Internal error, parallelism < 1.") +// } +// g.units = make(map[string]Unit) +// g.unFinished = make(map[string]Unit) +// g.inProgress = make(map[string]Unit) + +// g.unitsErrors = make(map[string]error) + +// for _, uStatus := range planningStatus.Slice() { +// g.units[uStatus.UnitPtr.Key()] = uStatus.UnitPtr +// g.unFinished[uStatus.UnitPtr.Key()] = uStatus.UnitPtr +// } +// g.maxParallel = maxParallel +// g.queue.Init() + +// g.finished = make(map[string]unitExecuteResult) +// g.waitForModDone = make(chan unitExecuteResult) +// g.hasError = false +// g.reverse = reverse +// g.updateQueue() +// g.stopChan = make(chan struct{}) +// g.listenHupSig() +// } + +// func (g *grapher) HasError() bool { +// return g.hasError +// } + +// func (g *grapher) updateQueue() int { +// if g.reverse { +// return g.updateReverseQueue() +// } +// return g.updateDirectQueue() +// } + +// func (g *grapher) UnitFinished(u Unit) bool { +// return g.unFinished[u.Key()] != nil +// } + +// func (g *grapher) updateDirectQueue() int { +// count := 0 +// for key, mod := range g.units { +// isReady := true +// for _, dep := range mod.Dependencies().Slice() { +// if g.UnitFinished(dep.Unit) { +// isReady = false +// break +// } +// } +// if isReady { +// g.queue.PushBack(mod) +// delete(g.units, key) +// count++ +// } +// } +// return count +// } + +// func (g *grapher) updateReverseQueue() int { +// count := 0 +// for key, mod := range g.units { +// dependedMods := findDependedUnits(g.unFinished, mod) +// if len(dependedMods) > 0 { +// continue +// } +// g.queue.PushBack(mod) +// delete(g.units, key) +// count++ +// } +// return count +// } + +// func (g *grapher) GetNextAsync() (Unit, func(error), error) { +// g.mux.Lock() +// defer g.mux.Unlock() +// for { +// if config.Interrupted { +// g.queue.Init() +// g.units = make(map[string]Unit) +// g.updateQueue() +// return nil, nil, fmt.Errorf("interupted") +// } +// if g.queue.Len() > 0 && len(g.inProgress) < g.maxParallel { +// modElem := g.queue.Front() +// mod := modElem.Value.(Unit) +// finFunc := func(err error) { +// g.waitForModDone <- unitExecuteResult{mod, err} +// } +// g.queue.Remove(modElem) +// g.inProgress[mod.Key()] = mod +// return mod, finFunc, nil +// } +// if g.Len() == 0 { +// return nil, nil, nil +// } +// doneMod := <-g.waitForModDone +// g.setUnitDone(doneMod) +// if doneMod.Error != nil { +// return doneMod.Mod, nil, fmt.Errorf("error while unit running") +// } +// } +// } + +// func (g *grapher) GetNextSync() Unit { +// if g.Len() == 0 { +// return nil +// } +// modElem := g.queue.Front() +// mod := modElem.Value.(Unit) +// g.queue.Remove(modElem) +// g.setUnitDone(unitExecuteResult{mod, nil}) +// return mod +// } + +// func (g *grapher) GetSequenceSet() []Unit { +// res := make([]Unit, g.Len()) +// mCount := g.Len() +// for i := 0; i < mCount; i++ { +// md := g.GetNextSync() +// if md == nil { +// log.Fatal("Building apply units set: getting nil unit, undefined behavior") +// } +// res[i] = md +// log.Infof("GetSequenceSet %v %v", i, md.Key()) +// } +// return res +// } + +// func (g *grapher) setUnitDone(doneMod unitExecuteResult) { +// g.finished[doneMod.Mod.Key()] = doneMod +// delete(g.inProgress, doneMod.Mod.Key()) +// delete(g.unFinished, doneMod.Mod.Key()) +// if doneMod.Error != nil { +// g.unitsErrors[doneMod.Mod.Key()] = doneMod.Error +// g.hasError = true +// } +// g.updateQueue() +// } + +// func (g *grapher) Errors() map[string]error { +// return g.unitsErrors +// } + +// func (g *grapher) Wait() { +// for { +// if len(g.inProgress) == 0 { +// return +// } +// doneMod := <-g.waitForModDone +// g.setUnitDone(doneMod) +// } +// } + +// func (g *grapher) Len() int { +// return len(g.units) + g.queue.Len() + len(g.inProgress) +// } + +func checkUnitDependencies(p *Project) error { + for _, uniit := range p.Units { + if err := checkDependenciesRecursive(uniit); err != nil { + return fmt.Errorf("unresolved dependency in unit %v.%v: %w", uniit.Stack().Name, uniit.Name(), err) + } + } + return nil +} + +func checkDependenciesRecursive(unit Unit, chain ...string) error { + if err := checkUnitDependenciesCircle(chain); err != nil { + return err + } + chain = append(chain, unit.Key()) + for _, dep := range unit.Dependencies().Slice() { + if err := checkDependenciesRecursive(dep.Unit, chain...); err != nil { + return err + } + } + return nil +} + +func checkUnitDependenciesCircle(chain []string) error { + if len(chain) < 2 { + return nil + } + circleCheck := []string{} + for _, str := range chain { + for _, comareStr := range circleCheck { + // log.Warnf("Compare: %v == %v", str, ) + if str == comareStr { + circleCheck = append(circleCheck, str) + return fmt.Errorf("loop: %s", strings.Join(circleCheck, " -> ")) + } + } + circleCheck = append(circleCheck, str) + } + return nil +} + +func findDependedUnits(modList map[string]Unit, targetMod Unit) map[string]Unit { + res := map[string]Unit{} + for key, mod := range modList { + if mod.Key() == targetMod.Key() { + continue + } + for _, dep := range mod.Dependencies().Slice() { + if dep.Unit.Key() == targetMod.Key() { + res[key] = mod + } + } + } + return res +} + +// func (g *grapher) listenHupSig() { +// signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT} +// g.sigTrap = make(chan os.Signal, 1) +// signal.Notify(g.sigTrap, signals...) +// // log.Warn("Listening signals...") +// go func() { +// for { +// select { +// case <-g.sigTrap: +// config.Interrupted = true +// case <-g.stopChan: +// // log.Warn("Stop listening") +// signal.Stop(g.sigTrap) +// g.sigTrap <- nil +// close(g.sigTrap) +// return +// } +// } +// }() +// } + +// func (g *grapher) Close() error { +// g.stopChan <- struct{}{} +// return nil +// } diff --git a/pkg/project/graph.go b/pkg/project/graph.go new file mode 100644 index 00000000..7decf34c --- /dev/null +++ b/pkg/project/graph.go @@ -0,0 +1,311 @@ +package project + +import ( + "fmt" + "sync" + + "github.com/apex/log" + "github.com/shalb/cluster.dev/pkg/config" +) + +type ExecutionStatus uint16 + +const ( + Backlog ExecutionStatus = iota + 1 + ReadyForExec + InProgress + Finished +) + +type ExecSet struct { + execUnits []*UnitPlanningStatus +} + +func (e *ExecSet) Find(u Unit) *UnitPlanningStatus { + for _, eu := range e.execUnits { + if eu.UnitPtr == u { + return eu + } + } + return nil +} + +func (e *ExecSet) Index(u Unit) int { + for i, eu := range e.execUnits { + if eu.UnitPtr == u { + return i + } + } + return -1 +} + +func (e *ExecSet) AddUnit(u *UnitPlanningStatus) { + if u == nil { + log.Debug("Internal problem: create exec set, added unit is nil, ignore") + return + } + if e.Find(u.UnitPtr) != nil { + return + } + e.execUnits = append(e.execUnits, u) +} + +func (e *ExecSet) Delete(u Unit) { + index := e.Index(u) + if index < 0 { + return + } + e.execUnits = append(e.execUnits[:index], e.execUnits[index+1:]...) +} + +func (e *ExecSet) Slice() []*UnitPlanningStatus { + return e.execUnits +} + +func (e *ExecSet) Len() int { + return len(e.execUnits) +} + +func (e *ExecSet) IsEmpty() bool { + return len(e.execUnits) == 0 +} + +func (e *ExecSet) Front() *UnitPlanningStatus { + if len(e.execUnits) > 0 { + return e.execUnits[0] + } + return nil +} + +func (e *ExecSet) StatusFilter(statusList ...ExecutionStatus) *ExecSet { + res := ExecSet{ + execUnits: make([]*UnitPlanningStatus, 0), + } + for _, ue := range e.execUnits { + for _, status := range statusList { + if ue.UnitPtr.GetExecStatus() == status { + res.AddUnit(ue) + break + } + } + } + return &res +} + +func NewExecSet(planningStatus *ProjectPlanningStatus) *ExecSet { + res := ExecSet{ + execUnits: make([]*UnitPlanningStatus, 0), + } + for _, unit := range planningStatus.Slice() { + res.AddUnit(unit) + unit.UnitPtr.SetExecStatus(Backlog) + } + return &res +} + +type graph struct { + units *ExecSet + mux sync.Mutex + waitUnitDone chan Unit + maxParallel int + reverse bool + // sigTrap chan os.Signal + // stopChan chan struct{} +} + +func (g *graph) BuildDirect(planningStatus *ProjectPlanningStatus, maxParallel int) error { + return g.build(planningStatus, maxParallel, false) +} + +func (g *graph) BuildReverse(planningStatus *ProjectPlanningStatus, maxParallel int) error { + return g.build(planningStatus, maxParallel, true) +} + +func (g *graph) build(planningStatus *ProjectPlanningStatus, maxParallel int, reverse bool) error { + g.units = NewExecSet(planningStatus) + g.reverse = reverse + g.maxParallel = maxParallel + g.waitUnitDone = make(chan Unit) + return g.checkAndBuildIndexes() + // g.listenHupSig() +} + +func (g *graph) GetNextAsync() (Unit, func(error), error) { + g.mux.Lock() + defer g.mux.Unlock() + for { + g.updateQueue() + if config.Interrupted { + return nil, nil, fmt.Errorf("interrupted") + } + readyFroExecList := g.units.StatusFilter(ReadyForExec) + if readyFroExecList.Len() > 0 && g.units.StatusFilter(InProgress).Len() < g.maxParallel { + unitForExec := readyFroExecList.Front() + finFunc := func(err error) { + g.waitUnitDone <- unitForExec.UnitPtr + } + unitForExec.UnitPtr.SetExecStatus(InProgress) + g.updateQueue() + return unitForExec.UnitPtr, finFunc, nil + } + if g.units.StatusFilter(Backlog, InProgress, ReadyForExec).IsEmpty() { + return nil, nil, nil + } + unitFinished := <-g.waitUnitDone + unitFinished.SetExecStatus(Finished) + g.updateQueue() + if unitFinished.ExecError() != nil { + return unitFinished, nil, fmt.Errorf("error while unit running") + } + } +} + +func (g *graph) checkAndBuildIndexes() error { + i := 0 + for { + readyCount := g.updateQueue() + if readyCount == 0 { + if g.units.StatusFilter(Backlog).Len() > 0 { + return fmt.Errorf("the graph is broken, can't resolve sequence") + } + break + } + for _, u := range g.units.StatusFilter(ReadyForExec).Slice() { + u.Index = i + u.UnitPtr.SetExecStatus(Finished) + } + i++ + } + g.resetUnitsStatus() // back all units to backlog + return nil +} + +func (g *graph) Slice() []*UnitPlanningStatus { + i := 0 + res := []*UnitPlanningStatus{} + for { + readyCount := g.updateQueue() + if readyCount == 0 { + if g.units.StatusFilter(Backlog).Len() > 0 { + return nil + } + break + } + for _, u := range g.units.StatusFilter(ReadyForExec).Slice() { + res = append(res, u) + } + i++ + } + g.resetUnitsStatus() // back all units to backlog + return res +} + +func (g *graph) resetUnitsStatus() { + for _, u := range g.units.Slice() { + u.UnitPtr.SetExecStatus(Backlog) + } +} + +func (g *graph) Errors() []Unit { + res := []Unit{} + for _, u := range g.units.Slice() { + if u.UnitPtr.ExecError() != nil { + res = append(res, u.UnitPtr) + } + } + return res +} + +func (g *graph) Len() int { + return g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Len() +} + +func (g *graph) updateQueue() int { + if g.reverse { + return g.updateReverseQueue() + } + return g.updateDirectQueue() +} + +func (g *graph) updateDirectQueue() int { + count := 0 + for _, unit := range g.units.StatusFilter(Backlog).Slice() { + blockedByDep := false + for _, dep := range unit.UnitPtr.Dependencies().Slice() { + if g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Find(dep.Unit) != nil { + blockedByDep = true + break + } + } + if !blockedByDep { + unit.UnitPtr.SetExecStatus(ReadyForExec) + count++ + } + } + return count +} + +func (g *graph) updateReverseQueue() int { + count := 0 + for _, unit := range g.units.StatusFilter(Backlog).Slice() { + // for _, dep := range unit.Dependencies().Slice() { + graphDepFind := g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Find(unit.UnitPtr) + if graphDepFind == nil || graphDepFind.UnitPtr.GetExecStatus() == Finished { + continue + } + unit.UnitPtr.SetExecStatus(ReadyForExec) + count++ + // } + } + return count +} + +func (g *graph) Wait() { + for { + if g.units.StatusFilter(InProgress).Len() == 0 { + return + } + doneUnit := <-g.waitUnitDone + doneUnit.SetExecStatus(Finished) + } +} + +// func (g *graph) GetSequenceSet() []Unit { +// mCount := len(g.units.Slice()) +// res := make([]Unit, mCount) +// for i := 0; i < mCount; i++ { +// md := g.GetNextSync() +// if md == nil { +// log.Fatal("Building apply units set: getting nil unit, undefined behavior") +// } +// res[i] = md +// log.Infof("GetSequenceSet %v %v", i, md.Key()) +// } +// return res +// } + +// func (g *grapherNew) listenHupSig() { +// signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT} +// g.sigTrap = make(chan os.Signal, 1) +// signal.Notify(g.sigTrap, signals...) +// // log.Warn("Listening signals...") +// go func() { +// for { +// select { +// case <-g.sigTrap: +// config.Interrupted = true +// case <-g.stopChan: +// // log.Warn("Stop listening") +// signal.Stop(g.sigTrap) +// g.sigTrap <- nil +// close(g.sigTrap) +// return +// } +// } +// }() +// } + +// func (g *grapherNew) Close() error { +// g.stopChan <- struct{}{} +// return nil +// } diff --git a/pkg/project/grapher.go b/pkg/project/grapher.go deleted file mode 100644 index f54b8043..00000000 --- a/pkg/project/grapher.go +++ /dev/null @@ -1,269 +0,0 @@ -package project - -import ( - "container/list" - "fmt" - "os" - "os/signal" - "strings" - "sync" - "syscall" - - "github.com/apex/log" - "github.com/shalb/cluster.dev/pkg/config" -) - -type modResult struct { - Mod Unit - Error error -} - -type grapher struct { - finished map[string]modResult - inProgress map[string]Unit - queue list.List - units map[string]Unit - unFinished map[string]Unit - mux sync.Mutex - waitForModDone chan modResult - maxParallel int - hasError bool - reverse bool - unitsErrors map[string]error - sigTrap chan os.Signal - stopChan chan struct{} -} - -func (g *grapher) InitP(planningStatus *ProjectPlanningStatus, maxParallel int, reverse bool) { - if maxParallel < 1 { - log.Fatal("Internal error, parallelism < 1.") - } - g.units = make(map[string]Unit) - g.unitsErrors = make(map[string]error) - g.unFinished = make(map[string]Unit) - for _, uStatus := range planningStatus.Slice() { - g.units[uStatus.UnitPtr.Key()] = uStatus.UnitPtr - g.unFinished[uStatus.UnitPtr.Key()] = uStatus.UnitPtr - } - g.maxParallel = maxParallel - g.queue.Init() - g.inProgress = make(map[string]Unit) - g.finished = make(map[string]modResult) - g.waitForModDone = make(chan modResult) - g.hasError = false - g.reverse = reverse - g.updateQueue() - g.stopChan = make(chan struct{}) - g.listenHupSig() -} - -func (g *grapher) HasError() bool { - return g.hasError -} - -func (g *grapher) updateQueue() int { - if g.reverse { - return g.updateReverseQueue() - } - return g.updateDirectQueue() -} - -func (g *grapher) updateDirectQueue() int { - count := 0 - for key, mod := range g.units { - isReady := true - if !mod.Dependencies().IsEmpty() { - for _, dep := range mod.Dependencies().Slice() { - if er, ok := g.finished[dep.Unit.Key()]; !ok || er.Error != nil { - isReady = false - break - } - } - } - if isReady { - g.queue.PushBack(mod) - delete(g.units, key) - count++ - } - } - return count -} - -func (g *grapher) updateReverseQueue() int { - count := 0 - for key, mod := range g.units { - dependedMods := findDependedUnits(g.unFinished, mod) - if len(dependedMods) > 0 { - continue - } - g.queue.PushBack(mod) - delete(g.units, key) - count++ - } - return count -} - -func (g *grapher) GetNextAsync() (Unit, func(error), error) { - g.mux.Lock() - defer g.mux.Unlock() - for { - if config.Interrupted { - g.queue.Init() - g.units = make(map[string]Unit) - g.updateQueue() - return nil, nil, fmt.Errorf("interupted") - } - if g.queue.Len() > 0 && len(g.inProgress) < g.maxParallel { - modElem := g.queue.Front() - mod := modElem.Value.(Unit) - finFunc := func(err error) { - g.waitForModDone <- modResult{mod, err} - } - g.queue.Remove(modElem) - g.inProgress[mod.Key()] = mod - return mod, finFunc, nil - } - if g.Len() == 0 { - return nil, nil, nil - } - doneMod := <-g.waitForModDone - g.setUnitDone(doneMod) - if doneMod.Error != nil { - return doneMod.Mod, nil, fmt.Errorf("error while unit running") - } - } -} - -func (g *grapher) GetNextSync() Unit { - if g.Len() == 0 { - return nil - } - modElem := g.queue.Front() - mod := modElem.Value.(Unit) - g.queue.Remove(modElem) - g.setUnitDone(modResult{mod, nil}) - return mod -} - -func (g *grapher) GetSequenceSet() []Unit { - res := make([]Unit, g.Len()) - mCount := g.Len() - for i := 0; i < mCount; i++ { - md := g.GetNextSync() - if md == nil { - log.Fatal("Building apply units set: getting nil unit, undefined behavior") - } - res[i] = md - } - return res -} - -func (g *grapher) setUnitDone(doneMod modResult) { - g.finished[doneMod.Mod.Key()] = doneMod - delete(g.inProgress, doneMod.Mod.Key()) - delete(g.unFinished, doneMod.Mod.Key()) - if doneMod.Error != nil { - g.unitsErrors[doneMod.Mod.Key()] = doneMod.Error - g.hasError = true - } - g.updateQueue() -} - -func (g *grapher) Errors() map[string]error { - return g.unitsErrors -} - -func (g *grapher) Wait() { - for { - if len(g.inProgress) == 0 { - return - } - doneMod := <-g.waitForModDone - g.setUnitDone(doneMod) - } -} - -func (g *grapher) Len() int { - return len(g.units) + g.queue.Len() + len(g.inProgress) -} - -func checkUnitDependencies(p *Project) error { - for _, uniit := range p.Units { - if err := checkDependenciesRecursive(uniit); err != nil { - return fmt.Errorf("unresolved dependency in unit %v.%v: %w", uniit.Stack().Name, uniit.Name(), err) - } - } - return nil -} - -func checkDependenciesRecursive(unit Unit, chain ...string) error { - if err := checkUnitDependenciesCircle(chain); err != nil { - return err - } - chain = append(chain, unit.Key()) - for _, dep := range unit.Dependencies().Slice() { - if err := checkDependenciesRecursive(dep.Unit, chain...); err != nil { - return err - } - } - return nil -} - -func checkUnitDependenciesCircle(chain []string) error { - if len(chain) < 2 { - return nil - } - circleCheck := []string{} - for _, str := range chain { - for _, comareStr := range circleCheck { - // log.Warnf("Compare: %v == %v", str, ) - if str == comareStr { - circleCheck = append(circleCheck, str) - return fmt.Errorf("loop: %s", strings.Join(circleCheck, " -> ")) - } - } - circleCheck = append(circleCheck, str) - } - return nil -} - -func findDependedUnits(modList map[string]Unit, targetMod Unit) map[string]Unit { - res := map[string]Unit{} - for key, mod := range modList { - if mod.Key() == targetMod.Key() { - continue - } - for _, dep := range mod.Dependencies().Slice() { - if dep.Unit.Key() == targetMod.Key() { - res[key] = mod - } - } - } - return res -} - -func (g *grapher) listenHupSig() { - signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT} - g.sigTrap = make(chan os.Signal, 1) - signal.Notify(g.sigTrap, signals...) - // log.Warn("Listening signals...") - go func() { - for { - select { - case <-g.sigTrap: - config.Interrupted = true - case <-g.stopChan: - // log.Warn("Stop listening") - signal.Stop(g.sigTrap) - g.sigTrap <- nil - close(g.sigTrap) - return - } - } - }() -} - -func (g *grapher) Close() error { - g.stopChan <- struct{}{} - return nil -} diff --git a/pkg/project/helpers.go b/pkg/project/helpers.go index 44920f27..c29ef5f3 100644 --- a/pkg/project/helpers.go +++ b/pkg/project/helpers.go @@ -23,7 +23,6 @@ const ( Destroy Update NotChanged - UpdateAsDep ) func (u UnitOperation) String() string { @@ -32,7 +31,6 @@ func (u UnitOperation) String() string { 2: colors.Fmt(colors.Red).Sprint("Destroy"), 3: colors.Fmt(colors.Yellow).Sprint("Update"), 4: colors.Fmt(colors.White).Sprint("NotChanged"), - 5: colors.Fmt(colors.Yellow).Sprint("UpdateAsDep"), } return mapperStatus[uint16(u)] } @@ -46,16 +44,23 @@ type UnitPlanningStatus struct { Diff string Status UnitOperation IsTainted bool + Index int } type ProjectPlanningStatus struct { units []*UnitPlanningStatus } -func (s *ProjectPlanningStatus) GetApplyGraph() *grapher { - CurrentGraph := grapher{} - CurrentGraph.InitP(s.OperationFilter(Apply, Update, UpdateAsDep), 1, false) - return &CurrentGraph +func (s *ProjectPlanningStatus) GetApplyGraph() (*graph, error) { + log.Warnf("GetApplyGraph") + CurrentGraph := graph{} + var err error + if config.Global.IgnoreState { + err = CurrentGraph.BuildDirect(s, config.Global.MaxParallel) + } else { + err = CurrentGraph.BuildDirect(s.OperationFilter(Apply, Update), config.Global.MaxParallel) + } + return &CurrentGraph, err } func (s *ProjectPlanningStatus) FindUnit(unit Unit) *UnitPlanningStatus { @@ -70,10 +75,11 @@ func (s *ProjectPlanningStatus) FindUnit(unit Unit) *UnitPlanningStatus { return nil } -func (s *ProjectPlanningStatus) GetDestroyGraph() *grapher { - CurrentGraph := grapher{} - CurrentGraph.InitP(s.OperationFilter(Destroy), 1, false) - return &CurrentGraph +func (s *ProjectPlanningStatus) GetDestroyGraph() (*graph, error) { + log.Warnf("GetDestroyGraph") + CurrentGraph := graph{} + err := CurrentGraph.BuildReverse(s.OperationFilter(Destroy), 1) + return &CurrentGraph, err } func (s *ProjectPlanningStatus) OperationFilter(ops ...UnitOperation) *ProjectPlanningStatus { @@ -99,6 +105,7 @@ func (s *ProjectPlanningStatus) Add(u Unit, op UnitOperation, diff string, isTai Status: op, Diff: diff, IsTainted: isTainted, + Index: -1, } s.units = append(s.units, &uo) } @@ -333,20 +340,25 @@ func ProjectsFilesExists() bool { return false } -func showPlanResults(opStatus *ProjectPlanningStatus) { +func showPlanResults(opStatus *ProjectPlanningStatus) error { fmt.Println(colors.Fmt(colors.WhiteBold).Sprint("Plan results:")) if opStatus.Len() == 0 { fmt.Println(colors.Fmt(colors.WhiteBold).Sprint("No changes, nothing to do.")) - return + return nil } table := tablewriter.NewWriter(os.Stdout) headers := []string{} unitsTable := []string{} + // indexedSlice, err := opStatus.OperationFilter(Apply, Update).GetApplyGraph() + // if err != nil { + // return fmt.Errorf("build graph for plan table: %w", err) + // } + var deployString, updateString, destroyString, unchangedString string - for _, unit := range opStatus.Slice() { + for _, unit := range opStatus.OperationFilter(Apply, Update).Slice() { log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Planning unit '%v':", unit.UnitPtr.Key())) switch unit.Status { case Apply: @@ -361,12 +373,6 @@ func showPlanResults(opStatus *ProjectPlanningStatus) { updateString += "\n" } updateString += RenderUnitPlanningString(unit) - case UpdateAsDep: - fmt.Printf("%v\n", unit.Diff) - if len(updateString) != 0 { - updateString += "\n" - } - updateString += RenderUnitPlanningString(unit) case Destroy: fmt.Printf("%v\n", unit.Diff) if len(destroyString) != 0 { @@ -401,34 +407,39 @@ func showPlanResults(opStatus *ProjectPlanningStatus) { table.SetHeader(headers) table.Append(unitsTable) table.Render() + return nil } func RenderUnitPlanningString(uStatus *UnitPlanningStatus) string { + keyForRender := uStatus.UnitPtr.Key() + if config.Global.LogLevel == "debug" { + keyForRender += fmt.Sprintf("(%v)", uStatus.Index) + } switch uStatus.Status { - case Update, UpdateAsDep: + case Update: if uStatus.IsTainted { - return colors.Fmt(colors.Orange).Sprintf("%s(tainted)", uStatus.UnitPtr.Key()) + return colors.Fmt(colors.Orange).Sprintf("%s(tainted)", keyForRender) } else { - return colors.Fmt(colors.Yellow).Sprint(uStatus.UnitPtr.Key()) + return colors.Fmt(colors.Yellow).Sprint(keyForRender) } case Apply: if uStatus.IsTainted { - return colors.Fmt(colors.Green).Sprintf("%s(tainted)", uStatus.UnitPtr.Key()) + return colors.Fmt(colors.Green).Sprintf("%s(tainted)", keyForRender) } else { - return colors.Fmt(colors.Green).Sprint(uStatus.UnitPtr.Key()) + return colors.Fmt(colors.Green).Sprint(keyForRender) } case Destroy: if uStatus.IsTainted { - return colors.Fmt(colors.Red).Sprintf("%s(tainted)", uStatus.UnitPtr.Key()) + return colors.Fmt(colors.Red).Sprintf("%s(tainted)", keyForRender) } else { - return colors.Fmt(colors.Red).Sprint(uStatus.UnitPtr.Key()) + return colors.Fmt(colors.Red).Sprint(keyForRender) } case NotChanged: - return colors.Fmt(colors.White).Sprint(uStatus.UnitPtr.Key()) + return colors.Fmt(colors.White).Sprint(keyForRender) } // Impossible, crush log.Fatalf("Unexpected internal error. Unknown unit status '%v'", uStatus.Status.String()) - return uStatus.UnitPtr.Key() + return "" } func DependenciesRecursiveIterate(u Unit, f func(Unit) error) error { diff --git a/pkg/project/stack.go b/pkg/project/stack.go index c21ba5df..f7524c3c 100644 --- a/pkg/project/stack.go +++ b/pkg/project/stack.go @@ -43,6 +43,9 @@ func (p *Project) readStacks() error { return err } } + if len(p.Stacks) == 0 { + return fmt.Errorf("no stacks found, at least one needed") + } return nil } @@ -51,6 +54,18 @@ func (p *Project) readStackObj(stackSpec ObjectData) error { if !ok { return fmt.Errorf("stack object must contain field 'name'") } + disabledInt := stackSpec.data["disabled"] + if disabledInt != nil { + disabled, ok := disabledInt.(bool) + if !ok { + return fmt.Errorf("stack option 'disabled' should be bool, not %T", disabledInt) + } + if disabled { + log.Debugf("stack '%v' is disabled, ignore", name) + return nil + } + } + // Check if stack with this name is already exists in project. if _, ok = p.Stacks[name]; ok { return fmt.Errorf("duplicate stack name '%s'", name) diff --git a/pkg/project/state.go b/pkg/project/state.go index 1538d6e9..794bd28b 100644 --- a/pkg/project/state.go +++ b/pkg/project/state.go @@ -15,18 +15,26 @@ import ( "github.com/shalb/cluster.dev/pkg/utils" ) -func (sp *StateProject) UpdateUnit(mod Unit) { +func (sp *StateProject) UpdateUnit(unit Unit) { sp.StateMutex.Lock() defer sp.StateMutex.Unlock() - sp.Units[mod.Key()] = mod - sp.ChangedUnits[mod.Key()] = mod - sp.UnitLinks.Join(sp.LoaderProjectPtr.UnitLinks.ByTargetUnit(mod)) + sp.Units[unit.Key()] = unit + sp.ChangedUnits[unit.Key()] = unit + sp.UnitLinks.Join(sp.LoaderProjectPtr.UnitLinks.ByTargetUnit(unit)) } func (sp *StateProject) DeleteUnit(mod Unit) { delete(sp.Units, mod.Key()) } +func (sp *stateData) ClearULinks() { + for linkKey, link := range sp.UnitLinks.Map() { + if sp.Units[link.UnitKey()] == nil { + sp.UnitLinks.Delete(linkKey) + } + } +} + type StateProject struct { Project LoaderProjectPtr *Project @@ -46,6 +54,8 @@ func (p *Project) SaveState() error { for key, mod := range p.Units { st.Units[key] = mod.GetState() } + // Remove all unit links, that not have a target unit. + st.ClearULinks() buffer := &bytes.Buffer{} encoder := json.NewEncoder(buffer) encoder.SetEscapeHTML(false) @@ -169,6 +179,7 @@ func (p *Project) LoadState() (*StateProject, error) { return nil, fmt.Errorf("load state: %w", err) } } + stateD.ClearULinks() p.UUID = stateD.ProjectUUID if p.UUID == "" { p.UUID = createProjectUUID() diff --git a/pkg/project/unit.go b/pkg/project/unit.go index 699bf457..e5396edd 100644 --- a/pkg/project/unit.go +++ b/pkg/project/unit.go @@ -32,6 +32,9 @@ type Unit interface { ForceApply() bool Mux() *sync.Mutex IsTainted() bool + SetExecStatus(ExecutionStatus) + GetExecStatus() ExecutionStatus + ExecError() error } type UnitDriver interface { diff --git a/pkg/units/shell/common/state.go b/pkg/units/shell/common/state.go index 0bf73f28..08f5be15 100644 --- a/pkg/units/shell/common/state.go +++ b/pkg/units/shell/common/state.go @@ -36,9 +36,9 @@ func (u *Unit) GetState() interface{} { return u.SavedState } res := u.GetStateUnit() - return &res - + return res } + func (u *Unit) GetUnitDiff() UnitDiffSpec { st := UnitDiffSpec{ Outputs: make(map[string]string), diff --git a/pkg/units/shell/common/unit.go b/pkg/units/shell/common/unit.go index 2d954904..3c97d4b2 100644 --- a/pkg/units/shell/common/unit.go +++ b/pkg/units/shell/common/unit.go @@ -91,6 +91,8 @@ type Unit struct { FApply bool `yaml:"force_apply" json:"force_apply"` lockedMux *sync.Mutex `yaml:"-" json:"-"` Tainted bool `yaml:"-" json:"tainted,omitempty"` + ExecStatus project.ExecutionStatus `yaml:"-" json:"-"` + ExecErr error `yaml:"-" json:"-"` } // IsTainted return true if unit have tainted state (failed previous apply or destroy). @@ -122,6 +124,7 @@ func (u *Unit) ReadConfig(spec map[string]interface{}, stack *project.Stack) err if stack == nil { return fmt.Errorf("read shell unit: empty stack or project") } + u.ExecStatus = project.Backlog // Set status 'backlog' by default. u.StackPtr = stack u.ProjectPtr = stack.ProjectPtr u.SpecRaw = spec @@ -269,7 +272,7 @@ func (u *Unit) Apply() error { } u.OutputRaw, err = u.runCommands(applyCommands, "apply") if err != nil { - u.Tainted = true + u.MarkTainted(err) return fmt.Errorf("apply unit '%v': %w", u.Key(), err) } // Get outputs. @@ -281,14 +284,14 @@ func (u *Unit) Apply() error { } u.OutputRaw, err = u.runCommands(cmdConf, "retrieving outputs") if err != nil { - u.Tainted = true + u.MarkTainted(err) return fmt.Errorf("retrieving unit '%v' outputs: %w", u.Key(), err) } } if u.GetOutputsConf != nil { parser, exists := u.OutputParsers[u.GetOutputsConf.Type] if !exists { - u.Tainted = true + u.MarkTainted(err) return fmt.Errorf("retrieving unit '%v' outputs: parser %v doesn't exists", u.Key(), u.GetOutputsConf.Type) } err = parser(string(u.OutputRaw), u.ProjectPtr.UnitLinks.ByTargetUnit(u).ByLinkTypes(project.OutputLinkType)) @@ -296,18 +299,25 @@ func (u *Unit) Apply() error { //str := fmt.Sprintf("Outputs data: %s", string(u.OutputRaw)) // log.Warnf("Len: %v", len(str)) - u.Tainted = true + u.MarkTainted(err) return fmt.Errorf("parse outputs '%v': %w", u.GetOutputsConf.Type, err) } } if err == nil { - u.Tainted = false u.Applied = true } return err } +func (u *Unit) MarkTainted(err error) { + u.ExecErr = err + if u.SavedState != nil { + u.SavedState.(*Unit).Tainted = true + } + u.Tainted = true +} + func (u *Unit) runCommands(commandsCnf OperationConfig, name string) ([]byte, error) { if len(commandsCnf.Commands) == 0 { log.Debugf("configuration for '%v' is empty for unit '%v'. Skip.", name, u.Key()) @@ -381,6 +391,9 @@ func (u *Unit) Destroy() error { destroyCommands.Commands = append(destroyCommands.Commands, "./post_hook.sh") } _, err = u.runCommands(destroyCommands, "destroy") + if err != nil { + u.MarkTainted(err) + } return err } @@ -547,3 +560,16 @@ func (u *Unit) EnvSlice() []string { } return res } + +func (u *Unit) SetExecStatus(status project.ExecutionStatus) { + if u.GetExecStatus() != status { + log.Warnf("Unit '%v' Status changed: %v --> %v", u.Key(), u.GetExecStatus(), status) + u.ExecStatus = status + } +} +func (u *Unit) GetExecStatus() project.ExecutionStatus { + return u.ExecStatus +} +func (u *Unit) ExecError() error { + return u.ExecErr +} diff --git a/pkg/units/shell/terraform/printer/main.go b/pkg/units/shell/terraform/printer/main.go index 53f6c2f4..3975e8d9 100644 --- a/pkg/units/shell/terraform/printer/main.go +++ b/pkg/units/shell/terraform/printer/main.go @@ -114,8 +114,27 @@ func (u *Unit) Build() error { return u.Unit.Build() } +func (u *Unit) Destroy() (err error) { + err = u.Unit.Destroy() + if u.IsTainted() { + if u.SavedState != nil { + u.StateData.MarkTainted(err) + } + } + if err != nil { + return + } + // log.Warnf("Printer OutputRaw: %v", outputs) + return +} + func (u *Unit) Apply() (err error) { err = u.Unit.Apply() + if u.IsTainted() { + if u.SavedState != nil { + u.StateData.MarkTainted(err) + } + } if err != nil { return } diff --git a/tests/test-project/dev-infra.yaml b/tests/test-project/dev-infra.yaml_ similarity index 100% rename from tests/test-project/dev-infra.yaml rename to tests/test-project/dev-infra.yaml_ diff --git a/tests/test-project/graph-test/template.yaml b/tests/test-project/graph-test/template.yaml new file mode 100644 index 00000000..09d7663c --- /dev/null +++ b/tests/test-project/graph-test/template.yaml @@ -0,0 +1,96 @@ +name: graphTest +kind: StackTemplate +units: + - + name: force_apply_zero + type: shell + force_apply: true + apply: + commands: + - echo "Waiting..." + - sleep 5 + - + name: force_apply_unit + type: shell + depends_on: this.force_apply_zero + force_apply: true + apply: + commands: + - echo "Waiting..." + - sleep 4 + - + name: parallelWatcher1 + type: shell + apply: + commands: + - echo "Waiting..." + - sleep 30 + - + name: parallelWatcher2 + type: shell + apply: + commands: + - echo "Waiting..." + - sleep 25 + - + name: parallelWatcher3 + type: shell + apply: + commands: + - echo "Waiting..." + - sleep 35 + - + name: parallelWatcher4 + type: shell + apply: + commands: + - echo "Waiting..." + - sleep 10 + - + name: parallelWatcher5 + type: shell + apply: + commands: + - echo "Waiting..." + - sleep 40 + - + name: parallelWatcher6 + type: shell + apply: + commands: + - echo "Waiting..." + - sleep 20 + - exit 0 + - + name: firstUnit + type: printer + outputs: + longWay: "STEP1" + update_count: "1" + - + name: secondUnit + type: printer + outputs: + longWay: {{ remoteState "this.firstUnit.longWay"}}-STEP2 + - + name: thirdStep + type: printer + depends_on: this.force_apply_unit + outputs: + longWay: {{ remoteState "this.secondUnit.longWay"}}-STEP3 + changed: true + - + name: foursUnit + type: printer + outputs: + longWay: {{ remoteState "this.thirdStep.longWay"}}-STEP4 + - + name: fifthStep + type: printer + outputs: + longWay: {{ remoteState "this.foursUnit.longWay"}}-STEP5 + - + name: outputs + type: printer + outputs: + WayReport: {{ remoteState "this.thirdStep.longWay"}} diff --git a/tests/test-project/local.yaml b/tests/test-project/local.yaml index a85beb6d..3f671cb0 100644 --- a/tests/test-project/local.yaml +++ b/tests/test-project/local.yaml @@ -1,11 +1,7 @@ name: cdev-tests-local -template: ./local-tmpl/ +template: ./graph-test/ kind: Stack backend: aws-backend -variables: - data: {{ remoteState "cdev-tests.create-bucket.test" }} - region: {{ .project.variables.region }} - list_one: - - one - - two - - three +disabled: false +variables: {} + From be5200fc543b2da5b657efd0bb2f8ef8679c0773 Mon Sep 17 00:00:00 2001 From: romanprog Date: Wed, 6 Dec 2023 13:18:48 +0200 Subject: [PATCH 03/11] save --- pkg/cmd/cdev/plan.go | 6 +- pkg/project/commands.go | 11 +-- pkg/project/graph.go | 75 ++++++++++++++++--- pkg/project/helpers.go | 40 +++++----- pkg/project/project.go | 36 ++++----- pkg/project/state.go | 53 +++++++------ tests/test-project/cdev.state | 60 +++++++++++++++ .../{dev-infra.yaml_ => dev-infra.yaml} | 1 + tests/test-project/graph-test/template.yaml | 2 +- 9 files changed, 209 insertions(+), 75 deletions(-) create mode 100644 tests/test-project/cdev.state rename tests/test-project/{dev-infra.yaml_ => dev-infra.yaml} (92%) diff --git a/pkg/cmd/cdev/plan.go b/pkg/cmd/cdev/plan.go index f4e7535b..251e95c9 100644 --- a/pkg/cmd/cdev/plan.go +++ b/pkg/cmd/cdev/plan.go @@ -1,6 +1,8 @@ package cdev import ( + "fmt" + "github.com/apex/log" "github.com/shalb/cluster.dev/pkg/config" "github.com/shalb/cluster.dev/pkg/project" @@ -16,12 +18,12 @@ var planCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { project, err := project.LoadProjectFull() if err != nil { - return NewCmdErr(nil, "plan", err) + return NewCmdErr(nil, "plan", fmt.Errorf("load project configuration: %w", err)) } log.Info("Planning...") _, err = project.Plan() if err != nil { - return NewCmdErr(project, "plan", err) + return NewCmdErr(project, "plan", fmt.Errorf("build plan: %w", err)) } return NewCmdErr(project, "plan", nil) }, diff --git a/pkg/project/commands.go b/pkg/project/commands.go index 36e14a59..919f6c7b 100644 --- a/pkg/project/commands.go +++ b/pkg/project/commands.go @@ -228,7 +228,7 @@ func (p *Project) Plan() (*ProjectPlanningStatus, error) { allDepsDeployed := true for _, planModDep := range md.UnitPtr.Dependencies().Slice() { dS := planningSt.FindUnit(planModDep.Unit) - if dS != nil && dS.Status != NotChanged { + if dS != nil && dS.Operation != NotChanged { allDepsDeployed = false } } @@ -255,7 +255,7 @@ func (p *Project) Plan() (*ProjectPlanningStatus, error) { // planDestroy collect and show units for destroying. func (p *Project) planDestroy(opStatus *ProjectPlanningStatus) { for _, md := range p.OwnState.UnitsSlice() { - if i := findMod(p.UnitsSlice(), md); i >= 0 { + if i := findUnit(p.UnitsSlice(), md); i >= 0 { continue } diff := utils.Diff(md.GetDiffData(), nil, true) @@ -263,7 +263,7 @@ func (p *Project) planDestroy(opStatus *ProjectPlanningStatus) { } } -// planDestroyAll add all units from state for destroy. +// planDestroyAll add all units from state for destroying. func (p *Project) planDestroyAll(opStatus *ProjectPlanningStatus) { var units []Unit if config.Global.IgnoreState { @@ -278,12 +278,13 @@ func (p *Project) planDestroyAll(opStatus *ProjectPlanningStatus) { } } -func findMod(list []Unit, mod Unit) int { +// findUnit returns the index of unitForSearch in the list. Returns -1 if not found. +func findUnit(list []Unit, unitForSearch Unit) int { if len(list) < 1 { return -1 } for index, m := range list { - if mod.Key() == m.Key() { + if unitForSearch.Key() == m.Key() { return index } } diff --git a/pkg/project/graph.go b/pkg/project/graph.go index 7decf34c..135373fb 100644 --- a/pkg/project/graph.go +++ b/pkg/project/graph.go @@ -130,6 +130,14 @@ func (g *graph) build(planningStatus *ProjectPlanningStatus, maxParallel int, re // g.listenHupSig() } +func (g *graph) BuildNew(planningStatus *ProjectPlanningStatus, maxParallel int) error { + g.units = NewExecSet(planningStatus) + g.maxParallel = maxParallel + g.waitUnitDone = make(chan Unit) + return g.checkAndBuildIndexes() + // g.listenHupSig() +} + func (g *graph) GetNextAsync() (Unit, func(error), error) { g.mux.Lock() defer g.mux.Unlock() @@ -161,30 +169,39 @@ func (g *graph) GetNextAsync() (Unit, func(error), error) { } func (g *graph) checkAndBuildIndexes() error { + slice := g.IndexedSlice() + if slice == nil { + return fmt.Errorf("the graph is broken, can't resolve sequence") + } + return nil +} + +func (g *graph) Slice() []*UnitPlanningStatus { i := 0 + res := []*UnitPlanningStatus{} for { readyCount := g.updateQueue() if readyCount == 0 { if g.units.StatusFilter(Backlog).Len() > 0 { - return fmt.Errorf("the graph is broken, can't resolve sequence") + return nil } break } - for _, u := range g.units.StatusFilter(ReadyForExec).Slice() { - u.Index = i - u.UnitPtr.SetExecStatus(Finished) - } + res = append(res, g.units.StatusFilter(ReadyForExec).Slice()...) + i++ } g.resetUnitsStatus() // back all units to backlog - return nil + return res } -func (g *graph) Slice() []*UnitPlanningStatus { +// IndexedSlice return the slice of units in sorted in order ready for exec. +func (g *graph) IndexedSlice() []*UnitPlanningStatus { i := 0 res := []*UnitPlanningStatus{} + apply := []*UnitPlanningStatus{} for { - readyCount := g.updateQueue() + readyCount := g.updateQueueNew() if readyCount == 0 { if g.units.StatusFilter(Backlog).Len() > 0 { return nil @@ -192,11 +209,20 @@ func (g *graph) Slice() []*UnitPlanningStatus { break } for _, u := range g.units.StatusFilter(ReadyForExec).Slice() { - res = append(res, u) + if u.Operation == Destroy { + // Place 'destroy' units first in queue + res = append(res, u) + } else { + // Then place 'apply/update' units + apply = append(apply, u) + } + // Mark unit as finished for next updateQueue + u.UnitPtr.SetExecStatus(Finished) } i++ } g.resetUnitsStatus() // back all units to backlog + res = append(res, apply...) return res } @@ -227,6 +253,37 @@ func (g *graph) updateQueue() int { return g.updateDirectQueue() } +func (g *graph) updateQueueNew() int { + count := 0 + for _, unit := range g.units.StatusFilter(Backlog).Slice() { + blockedByDep := false + switch unit.Operation { + case Apply, Update: + for _, dep := range unit.UnitPtr.Dependencies().Slice() { + if g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Find(dep.Unit) != nil { + blockedByDep = true + break + } + } + case Destroy: + for _, dep := range unit.UnitPtr.Dependencies().Slice() { + if g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Find(dep.Unit) != nil { + blockedByDep = true + break + } + } + case NotChanged: + unit.UnitPtr.SetExecStatus(Finished) + } + if !blockedByDep { + unit.UnitPtr.SetExecStatus(ReadyForExec) + count++ + continue + } + } + return count +} + func (g *graph) updateDirectQueue() int { count := 0 for _, unit := range g.units.StatusFilter(Backlog).Slice() { diff --git a/pkg/project/helpers.go b/pkg/project/helpers.go index c29ef5f3..aec326bf 100644 --- a/pkg/project/helpers.go +++ b/pkg/project/helpers.go @@ -42,7 +42,7 @@ func (u UnitOperation) HasChanges() bool { type UnitPlanningStatus struct { UnitPtr Unit Diff string - Status UnitOperation + Operation UnitOperation IsTainted bool Index int } @@ -51,6 +51,12 @@ type ProjectPlanningStatus struct { units []*UnitPlanningStatus } +func (s *ProjectPlanningStatus) BuildGraph() (*graph, error) { + CurrentGraph := graph{} + err := CurrentGraph.BuildDirect(s, config.Global.MaxParallel) + return &CurrentGraph, err +} + func (s *ProjectPlanningStatus) GetApplyGraph() (*graph, error) { log.Warnf("GetApplyGraph") CurrentGraph := graph{} @@ -91,7 +97,7 @@ func (s *ProjectPlanningStatus) OperationFilter(ops ...UnitOperation) *ProjectPl } for _, uo := range s.units { for _, op := range ops { - if uo.Status == op { + if uo.Operation == op { res.units = append(res.units, uo) } } @@ -102,7 +108,7 @@ func (s *ProjectPlanningStatus) OperationFilter(ops ...UnitOperation) *ProjectPl func (s *ProjectPlanningStatus) Add(u Unit, op UnitOperation, diff string, isTainted bool) { uo := UnitPlanningStatus{ UnitPtr: u, - Status: op, + Operation: op, Diff: diff, IsTainted: isTainted, Index: -1, @@ -112,22 +118,22 @@ func (s *ProjectPlanningStatus) Add(u Unit, op UnitOperation, diff string, isTai func (s *ProjectPlanningStatus) AddOrUpdate(u Unit, op UnitOperation, diff string) { uo := UnitPlanningStatus{ - UnitPtr: u, - Status: op, - Diff: diff, + UnitPtr: u, + Operation: op, + Diff: diff, } existingUnit := s.FindUnit(u) if existingUnit == nil { s.units = append(s.units, &uo) } else { existingUnit.Diff = diff - existingUnit.Status = op + existingUnit.Operation = op } } func (s *ProjectPlanningStatus) HasChanges() bool { for _, un := range s.units { - if un.Status != NotChanged { + if un.Operation != NotChanged { return true } } @@ -140,7 +146,7 @@ func (s *ProjectPlanningStatus) Len() int { func (s *ProjectPlanningStatus) Print() { for _, unitStatus := range s.units { - fmt.Printf("UnitName: %v, Unit status: %v\n", unitStatus.UnitPtr.Key(), unitStatus.Status.String()) + fmt.Printf("UnitName: %v, Unit status: %v\n", unitStatus.UnitPtr.Key(), unitStatus.Operation.String()) } } @@ -352,15 +358,15 @@ func showPlanResults(opStatus *ProjectPlanningStatus) error { headers := []string{} unitsTable := []string{} - // indexedSlice, err := opStatus.OperationFilter(Apply, Update).GetApplyGraph() - // if err != nil { - // return fmt.Errorf("build graph for plan table: %w", err) - // } + gr, err := opStatus.BuildGraph() + if err != nil { + return fmt.Errorf("build graph for plan table: %w", err) + } var deployString, updateString, destroyString, unchangedString string - for _, unit := range opStatus.OperationFilter(Apply, Update).Slice() { + for _, unit := range gr.IndexedSlice() { log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Planning unit '%v':", unit.UnitPtr.Key())) - switch unit.Status { + switch unit.Operation { case Apply: fmt.Printf("%v\n", unit.Diff) if len(deployString) != 0 { @@ -415,7 +421,7 @@ func RenderUnitPlanningString(uStatus *UnitPlanningStatus) string { if config.Global.LogLevel == "debug" { keyForRender += fmt.Sprintf("(%v)", uStatus.Index) } - switch uStatus.Status { + switch uStatus.Operation { case Update: if uStatus.IsTainted { return colors.Fmt(colors.Orange).Sprintf("%s(tainted)", keyForRender) @@ -438,7 +444,7 @@ func RenderUnitPlanningString(uStatus *UnitPlanningStatus) string { return colors.Fmt(colors.White).Sprint(keyForRender) } // Impossible, crush - log.Fatalf("Unexpected internal error. Unknown unit status '%v'", uStatus.Status.String()) + log.Fatalf("Unexpected internal error. Unknown unit status '%v'", uStatus.Operation.String()) return "" } diff --git a/pkg/project/project.go b/pkg/project/project.go index 5f92d807..274a9616 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -106,7 +106,7 @@ func LoadProjectBase() (*Project, error) { func LoadProjectFull() (*Project, error) { project, err := LoadProjectBase() if err != nil { - return nil, err + return nil, fmt.Errorf("load base configuration: %w", err) } for filename, cnf := range project.objectsFiles { templatedConf, isWarn, err := project.TemplateTry(cnf, filename) @@ -119,37 +119,37 @@ func LoadProjectFull() (*Project, error) { rel, _ := filepath.Rel(config.Global.WorkingDir, filename) log.Warnf("File %v has unresolved template key: \n%v", rel, err.Error()) } else { - return nil, err + return nil, fmt.Errorf("render template: %w", err) } } err = project.readObjects(templatedConf, filename) if err != nil { - return nil, fmt.Errorf("load project: %w", err) + return nil, fmt.Errorf("read objects: %w", err) } } err = project.prepareObjects() if err != nil { - return nil, err + return nil, fmt.Errorf("prepare objects: %w", err) } if !config.Global.NotLoadState { - project.OwnState, err = project.LoadState() - if err != nil { - return nil, err + if config.Global.IgnoreState { + project.OwnState = project.NewEmptyState() + } else { + project.OwnState, err = project.LoadState() + if err != nil { + return nil, fmt.Errorf("load state: %w", err) + } } } err = project.readUnits() if err != nil { - return nil, err + return nil, fmt.Errorf("read units: %w", err) } err = project.prepareUnits() if err != nil { - return nil, err + return nil, fmt.Errorf("prepare units: %w", err) } - // for _, un := range project.Units { - // un.PrintEnv() - // } - // log.Errorf("LoadProjectFull: %+v", project.UnitLinks) return project, nil } @@ -199,9 +199,10 @@ func (p *Project) prepareObjects() error { func (p *Project) readUnits() error { // Read units from all stacks. for stackName, stack := range p.Stacks { + log.Warnf("Check units in stack: '%v'", stackName) for _, stackTmpl := range stack.Templates { for _, unitData := range stackTmpl.Units { - mod, err := NewUnit(unitData, stack) + unit, err := NewUnit(unitData, stack) if err != nil { traceUnitView, errYaml := yaml.Marshal(unitData) if errYaml != nil { @@ -209,10 +210,11 @@ func (p *Project) readUnits() error { } return fmt.Errorf("stack '%v', reading units: %v\nUnit data:\n%v", stackName, err.Error(), string(traceUnitView)) } - if _, exists := p.Units[mod.Key()]; exists { - return fmt.Errorf("stack '%v', reading units: duplicate unit name: %v", stackName, mod.Name()) + if _, exists := p.Units[unit.Key()]; exists { + return fmt.Errorf("stack '%v', reading units: duplicate unit name: %v", stackName, unit.Name()) } - p.Units[mod.Key()] = mod + p.Units[unit.Key()] = unit + log.Debugf("Unit added: '%v'", unit.Key()) } } } diff --git a/pkg/project/state.go b/pkg/project/state.go index 794bd28b..ec990c03 100644 --- a/pkg/project/state.go +++ b/pkg/project/state.go @@ -152,6 +152,31 @@ func createProjectUUID() string { return id.String() } +func (p *Project) NewEmptyState() *StateProject { + stateD := stateData{ + UnitLinks: &UnitLinksT{}, + } + statePrj := StateProject{ + Project: Project{ + name: p.Name(), + secrets: p.secrets, + configData: p.configData, + configDataFile: p.configDataFile, + objects: p.objects, + Units: make(map[string]Unit), + UnitLinks: stateD.UnitLinks, + Stacks: make(map[string]*Stack), + Backends: p.Backends, + CodeCacheDir: config.Global.StateCacheDir, + StateBackendName: p.StateBackendName, + UUID: p.UUID, + }, + LoaderProjectPtr: p, + ChangedUnits: make(map[string]Unit), + } + return &statePrj +} + func (p *Project) LoadState() (*StateProject, error) { if _, err := os.Stat(config.Global.StateCacheDir); os.IsNotExist(err) { err := os.Mkdir(config.Global.StateCacheDir, 0755) @@ -187,33 +212,13 @@ func (p *Project) LoadState() (*StateProject, error) { } else { log.Debugf("Project UUID loaded from state: %v", p.UUID) } - statePrj := StateProject{ - Project: Project{ - name: p.Name(), - secrets: p.secrets, - configData: p.configData, - configDataFile: p.configDataFile, - objects: p.objects, - Units: make(map[string]Unit), - UnitLinks: stateD.UnitLinks, - Stacks: make(map[string]*Stack), - Backends: p.Backends, - CodeCacheDir: config.Global.StateCacheDir, - StateBackendName: p.StateBackendName, - UUID: p.UUID, - }, - LoaderProjectPtr: p, - ChangedUnits: make(map[string]Unit), - } - // log.Warnf("StateProject. Unit links: %+v", statePrj.UnitLinks) - // utils.JSONCopy(p.Markers, statePrj.Markers) + statePrj := p.NewEmptyState() + statePrj.UnitLinks = stateD.UnitLinks for mName, mState := range stateD.Units { - // log.Debugf("Loading unit from state: %v", mName) - if mState == nil { continue } - unit, err := NewUnitFromState(mState.(map[string]interface{}), mName, &statePrj) + unit, err := NewUnitFromState(mState.(map[string]interface{}), mName, statePrj) if err != nil { return nil, fmt.Errorf("loading unit from state: %v", err.Error()) } @@ -225,7 +230,7 @@ func (p *Project) LoadState() (*StateProject, error) { return nil, err } - return &statePrj, nil + return statePrj, nil } func (sp *StateProject) CheckUnitChanges(unit Unit) (string, Unit, bool) { diff --git a/tests/test-project/cdev.state b/tests/test-project/cdev.state new file mode 100644 index 00000000..16e89bf9 --- /dev/null +++ b/tests/test-project/cdev.state @@ -0,0 +1,60 @@ +{ + "version": "v0.8.2", + "project_uuid": "4cd452ee-ca12-4c45-a03c-f4674bf1cbf5", + "unit_links": { + "unit_links_list": { + "04bd8a74af4c330a799f39a235f34577.RemoteStateMarkers.cdev-tests-local.thirdStep.longWay.04bd8a74af4c330a799f39a235f34577": { + "link_type": "RemoteStateMarkers", + "target_unit_name": "thirdStep", + "target_stack_name": "cdev-tests-local", + "output_name": "longWay", + "output_data": null + } + }, + "MapMutex": {} + }, + "units": { + "cdev-tests-local.parallelWatcher1": { + "name": "parallelWatcher1", + "create_files": [], + "apply": { + "commands": [ + "echo \"Waiting...\"", + "sleep 30" + ] + }, + "type": "shell", + "backend_name": "aws-backend", + "force_apply": false + }, + "cdev-tests-local.parallelWatcher5": { + "name": "parallelWatcher5", + "create_files": [], + "apply": { + "commands": [ + "echo \"Waiting...\"", + "sleep 40" + ] + }, + "type": "shell", + "backend_name": "aws-backend", + "force_apply": false + }, + "cdev-tests-local.thirdStep": { + "name": "thirdStep", + "outputs_config": { + "command": "terraform output -json", + "type": "terraform" + }, + "backend_name": "aws-backend", + "depends_on": "this.force_apply_unit", + "force_apply": false, + "output": "{\n \"changed\": {\n \"sensitive\": true,\n \"type\": \"bool\",\n \"value\": true\n },\n \"longWay\": {\n \"sensitive\": true,\n \"type\": \"string\",\n \"value\": \"STEP1-STEP2-STEP3\"\n }\n}\n", + "outputs": { + "changed": true, + "longWay": "9d79a12ca098d7d766e970f0756946d7.RemoteStateMarkers.cdev-tests-local.secondUnit.longWay.9d79a12ca098d7d766e970f0756946d7-STEP3" + }, + "type": "printer" + } + } + } diff --git a/tests/test-project/dev-infra.yaml_ b/tests/test-project/dev-infra.yaml similarity index 92% rename from tests/test-project/dev-infra.yaml_ rename to tests/test-project/dev-infra.yaml index 04b9bdb7..ac1eb72d 100644 --- a/tests/test-project/dev-infra.yaml_ +++ b/tests/test-project/dev-infra.yaml @@ -2,6 +2,7 @@ name: cdev-tests template: https://github.com/shalb/cdev-test-template?ref=v0.0.3 kind: Stack backend: aws-backend +disabled: true variables: name: "cdev-gha-test" region: {{ .project.variables.region }} diff --git a/tests/test-project/graph-test/template.yaml b/tests/test-project/graph-test/template.yaml index 09d7663c..5dad6b86 100644 --- a/tests/test-project/graph-test/template.yaml +++ b/tests/test-project/graph-test/template.yaml @@ -75,7 +75,7 @@ units: - name: thirdStep type: printer - depends_on: this.force_apply_unit + # depends_on: this.force_apply_unit outputs: longWay: {{ remoteState "this.secondUnit.longWay"}}-STEP3 changed: true From f42712f2518a9b3324aa00c14b68565916cc6f1e Mon Sep 17 00:00:00 2001 From: romanprog Date: Sun, 10 Dec 2023 18:46:04 +0200 Subject: [PATCH 04/11] ready for tests --- pkg/cmd/cdev/apply.go | 3 +- pkg/cmd/cdev/destroy.go | 2 + pkg/cmd/cdev/plan.go | 2 +- pkg/cmd/cdev/state.go | 5 +- pkg/config/config.go | 19 +- pkg/project/commands.go | 310 +++++++++----------- pkg/project/graph.go | 200 +++---------- pkg/project/helpers.go | 78 ++--- pkg/project/project.go | 17 +- pkg/project/state.go | 15 +- pkg/project/unit.go | 9 + pkg/units/shell/common/factory.go | 2 +- pkg/units/shell/common/unit.go | 102 ++++--- pkg/units/shell/terraform/base/unit.go | 8 - tests/test-project/graph-test/template.yaml | 74 ++--- tests/test-project/local.yaml | 1 - 16 files changed, 366 insertions(+), 481 deletions(-) diff --git a/pkg/cmd/cdev/apply.go b/pkg/cmd/cdev/apply.go index a6fc9185..b9b7461b 100644 --- a/pkg/cmd/cdev/apply.go +++ b/pkg/cmd/cdev/apply.go @@ -16,7 +16,7 @@ var applyCmd = &cobra.Command{ Short: "Deploys or updates infrastructure according to project configuration", RunE: func(cmd *cobra.Command, args []string) error { project, err := project.LoadProjectFull() - if utils.GetEnv("CDEV_COLLECT_USAGE_STATS", "false") == "true" { + if utils.GetEnv("CDEV_COLLECT_USAGE_STATS", "true") != "false" { log.Infof("Sending usage statistic. To disable statistics collection, export the CDEV_COLLECT_USAGE_STATS=false environment variable") } if err != nil { @@ -31,6 +31,7 @@ var applyCmd = &cobra.Command{ if err != nil { return NewCmdErr(project, "apply", err) } + log.Info("The project was successfully applied") err = project.PrintOutputs() if err != nil { return NewCmdErr(project, "apply", err) diff --git a/pkg/cmd/cdev/destroy.go b/pkg/cmd/cdev/destroy.go index 8c5fc586..b8abe55c 100644 --- a/pkg/cmd/cdev/destroy.go +++ b/pkg/cmd/cdev/destroy.go @@ -1,6 +1,7 @@ package cdev import ( + "github.com/apex/log" "github.com/shalb/cluster.dev/pkg/config" "github.com/shalb/cluster.dev/pkg/project" "github.com/spf13/cobra" @@ -26,6 +27,7 @@ var destroyCmd = &cobra.Command{ project.UnLockState() return NewCmdErr(project, "destroy", err) } + log.Info("The project was successfully destroyed") project.UnLockState() return NewCmdErr(project, "destroy", nil) }, diff --git a/pkg/cmd/cdev/plan.go b/pkg/cmd/cdev/plan.go index 251e95c9..220a859e 100644 --- a/pkg/cmd/cdev/plan.go +++ b/pkg/cmd/cdev/plan.go @@ -31,6 +31,6 @@ var planCmd = &cobra.Command{ func init() { rootCmd.AddCommand(planCmd) - planCmd.Flags().BoolVar(&config.Global.ShowTerraformPlan, "tf-plan", false, "Also show units terraform plan if possible.") + // planCmd.Flags().BoolVar(&config.Global.ShowTerraformPlan, "tf-plan", false, "Also show units terraform plan if possible.") planCmd.Flags().BoolVar(&config.Global.IgnoreState, "force", false, "Show plan (if set tf-plan) even if the state has not changed.") } diff --git a/pkg/cmd/cdev/state.go b/pkg/cmd/cdev/state.go index 111ff569..d13570d6 100644 --- a/pkg/cmd/cdev/state.go +++ b/pkg/cmd/cdev/state.go @@ -18,6 +18,7 @@ var stateUnlockCmd = &cobra.Command{ Use: "unlock", Short: "Unlock state force", Run: func(cmd *cobra.Command, args []string) { + config.Global.IgnoreState = true project, err := project.LoadProjectFull() if err != nil { log.Fatalf("Fatal error: state unlock: %v", err.Error()) @@ -35,7 +36,7 @@ var stateUpdateCmd = &cobra.Command{ Use: "update", Short: "Updates the state of the current project to version %v. Make sure that the state of the project is consistent (run cdev apply with the old version before)", Run: func(cmd *cobra.Command, args []string) { - config.Global.NotLoadState = true + config.Global.IgnoreState = true project, err := project.LoadProjectFull() if err != nil { log.Fatalf("Fatal error: state update: %v", err.Error()) @@ -61,7 +62,7 @@ var statePullCmd = &cobra.Command{ Use: "pull", Short: "Downloads the remote state", Run: func(cmd *cobra.Command, args []string) { - config.Global.NotLoadState = true + config.Global.IgnoreState = true project, err := project.LoadProjectFull() if err != nil { log.Fatalf("Fatal error: state pull: %v", err.Error()) diff --git a/pkg/config/config.go b/pkg/config/config.go index e8d938bf..61c7e3fa 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -55,16 +55,15 @@ type ConfSpec struct { UseCache bool OptFooTest bool IgnoreState bool - NotLoadState bool - ShowTerraformPlan bool - StateCacheDir string - TemplatesCacheDir string - CacheDir string - NoColor bool - Force bool - Interactive bool - OutputJSON bool - Targets []string + // ShowTerraformPlan bool + StateCacheDir string + TemplatesCacheDir string + CacheDir string + NoColor bool + Force bool + Interactive bool + OutputJSON bool + Targets []string } // Global config for executor. diff --git a/pkg/project/commands.go b/pkg/project/commands.go index 919f6c7b..0fddbec8 100644 --- a/pkg/project/commands.go +++ b/pkg/project/commands.go @@ -29,19 +29,16 @@ func (p *Project) Build() error { func (p *Project) Destroy() error { planStatus := ProjectPlanningStatus{} p.planDestroyAll(&planStatus) - log.Errorf("Destroy planStatus %++v", planStatus.units) - graph, err := planStatus.GetDestroyGraph() + destroyGraph, err := planStatus.BuildGraph() if err != nil { return fmt.Errorf("build destroy graph: %w", err) } - if graph.Len() < 1 { + if destroyGraph.Len() < 1 { log.Info("Nothing to destroy, exiting") return nil } - destSeq := graph.Slice() - log.Warnf("Destroy: destSeq: %++v", destSeq) if !config.Global.Force { - showPlanResults(&planStatus) + showPlanResults(destroyGraph) respond := climenu.GetText("Continue?(yes/no)", "no") if respond != "yes" { log.Info("Destroying cancelled") @@ -53,43 +50,52 @@ func (p *Project) Destroy() error { return fmt.Errorf("project destroy: clear cache dir: %w", err) } log.Info("Destroying...") - for _, unit := range destSeq { - log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Destroying unit '%v'", unit.UnitPtr.Key())) - err = unit.UnitPtr.Build() - defer unit.UnitPtr.SetExecStatus(Finished) // Set unit status done on any error - if err != nil { - return fmt.Errorf("project destroy: destroying deleted unit: %w", err) + for { + // log.Warnf("FOR Project apply. Unit links: %+v", p.UnitLinks) + if destroyGraph.Len() == 0 { + return p.OwnState.SaveState() } - p.ProcessedUnitsCount++ - err = unit.UnitPtr.Destroy() + gUnit, fn, err := destroyGraph.GetNextAsync() if err != nil { - if unit.UnitPtr.IsTainted() { - err = p.OwnState.SaveState() - if err != nil { - return fmt.Errorf("project destroy: saving state: %w", err) - } + unitName := "" + if gUnit != nil { + unitName = gUnit.UnitPtr.Key() } - return fmt.Errorf("project destroy: %w", err) + log.Errorf("error in unit %v, waiting for all running units done.", unitName) + destroyGraph.Wait() + for _, e := range destroyGraph.Errors() { + log.Errorf("unit: '%v':\n%v", e.Key(), e.ExecError()) + } + err := p.OwnState.SaveState() + if err != nil { + return fmt.Errorf("save state after error: %w", err) + } + return fmt.Errorf("applying error") } - p.OwnState.DeleteUnit(unit.UnitPtr) - err = p.OwnState.SaveState() - if err != nil { - return fmt.Errorf("project destroy: saving state: %w", err) + // Check if graph return nil unit - applying finished, return + if gUnit == nil { + return p.OwnState.SaveState() + } + switch gUnit.Operation { + case Apply, Update: + // TODO remove this check + return fmt.Errorf("destroy: internal error, found unit for apply in destroy command") + case Destroy: + // log.Warnf("DESTROY circle: run DESTROY for unit: %v", gUnit.UnitPtr.Key()) + go destroyRoutine(gUnit, fn, p) } - unit.UnitPtr.SetExecStatus(Finished) // Set unit status done if unit destroyed without errors } - return nil } // Apply all units. func (p *Project) Apply() error { - var planningStatus *ProjectPlanningStatus - planningStatus, err := p.Plan() + // var applyGraph *ProjectPlanningStatus + applyGraph, err := p.Plan() if err != nil { return err } if !config.Global.Force { - if !planningStatus.HasChanges() { + if !applyGraph.planningUnits.HasChanges() { return nil } respond := climenu.GetText("Continue?(yes/no)", "no") @@ -103,151 +109,95 @@ func (p *Project) Apply() error { return fmt.Errorf("project apply: clear cache dir: %v", err.Error()) } log.Info("Applying...") - StateDestroyGraph, err := planningStatus.GetDestroyGraph() - if err != nil { - return fmt.Errorf("build destroy greph: %w", err) - } - for _, graphUnit := range StateDestroyGraph.Slice() { - if config.Global.IgnoreState { - break - } - defer graphUnit.UnitPtr.SetExecStatus(Finished) // Set unit status done on any error - err = graphUnit.UnitPtr.Build() - if err != nil { - log.Errorf("project apply: destroying deleted unit: %v", err.Error()) - } - err = graphUnit.UnitPtr.Destroy() - if err != nil { - if graphUnit.UnitPtr.IsTainted() { - err = p.OwnState.SaveState() - if err != nil { - return fmt.Errorf("project apply: saving state: %w", err) - } - } - return fmt.Errorf("project apply: destroying deleted unit: %v", err.Error()) - } - p.OwnState.DeleteUnit(graphUnit.UnitPtr) - err = p.OwnState.SaveState() - if err != nil { - return fmt.Errorf("project apply: %v", err.Error()) - } - graphUnit.UnitPtr.SetExecStatus(Finished) - } - gr, err := planningStatus.GetApplyGraph() - if err != nil { - return fmt.Errorf("build apply graph: %w", err) - } + for { // log.Warnf("FOR Project apply. Unit links: %+v", p.UnitLinks) - if gr.Len() == 0 { - p.SaveState() - return nil + if applyGraph.Len() == 0 { + return p.SaveState() } - md, fn, err := gr.GetNextAsync() + gUnit, fn, err := applyGraph.GetNextAsync() if err != nil { unitName := "" - if md != nil { - unitName = md.Key() + if gUnit != nil { + unitName = gUnit.UnitPtr.Key() } log.Errorf("error in unit %v, waiting for all running units done.", unitName) - gr.Wait() - for modKey, e := range gr.Errors() { - log.Errorf("unit: '%v':\n%v", modKey, e.ExecError()) + applyGraph.Wait() + for _, e := range applyGraph.Errors() { + log.Errorf("unit: '%v':\n%v", e.Key(), e.ExecError()) + } + err := p.SaveState() + if err != nil { + return fmt.Errorf("save state after error: %w", err) } return fmt.Errorf("applying error") } - if md == nil { + // Check if graph return nil unit - applying finished, return + if gUnit == nil { p.SaveState() return nil } + switch gUnit.Operation { + case Apply, Update: + // log.Warnf("APPLY circle: run APPLY for unit: %v", gUnit.UnitPtr.Key()) + go applyRoutine(gUnit, fn, p) + case Destroy: + // log.Warnf("APPLY circle: run DESTROY for unit: %v", gUnit.UnitPtr.Key()) + go destroyRoutine(gUnit, fn, p) + } + } +} - go func(unit Unit, finFunc func(error)) { - log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Applying unit '%v':", md.Key())) - err = unit.Build() - if err != nil { - log.Errorf("project apply: unit build error: %v", err.Error()) - finFunc(err) - return - } - p.ProcessedUnitsCount++ - applyError := unit.Apply() - log.Warnf("apply routine: unit done") - if applyError != nil { - if unit.IsTainted() { - unit.Project().OwnState.UpdateUnit(unit) - err := unit.Project().OwnState.SaveState() - if err != nil { - log.Warnf("apply routine: send sig 1") - finFunc(err) - return - } - log.Warnf("apply routine: send sig 2") - finFunc(applyError) - return - } - } - unit.Project().OwnState.UpdateUnit(unit) - err := unit.Project().OwnState.SaveState() - if err != nil { - log.Warnf("apply routine: send sig 3") - finFunc(err) - return - } - err = unit.UpdateProjectRuntimeData(p) - if err != nil { - log.Warnf("apply routine: send sig 4") - finFunc(err) - return - } - log.Warnf("apply routine: send sig 5") - finFunc(nil) - }(md, fn) +// applyRoutine function to run unit apply in parallel +func applyRoutine(graphUnit *UnitPlanningStatus, finFunc func(error), p *Project) { + log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Applying unit '%v':", graphUnit.UnitPtr.Key())) + err := graphUnit.UnitPtr.Build() + if err != nil { + log.Errorf("project apply: unit build error: %v", err.Error()) + finFunc(err) + return + } + p.ProcessedUnitsCount++ + err = graphUnit.UnitPtr.Apply() + if err != nil { + if graphUnit.UnitPtr.IsTainted() { + p.OwnState.UpdateUnit(graphUnit.UnitPtr) + } + finFunc(fmt.Errorf("project apply: destroying deleted unit: %v", err.Error())) + return + } + p.OwnState.UpdateUnit(graphUnit.UnitPtr) + graphUnit.UnitPtr.SetExecStatus(Finished) + finFunc(nil) +} + +// destroyRoutine function to run unit destroy in parallel +func destroyRoutine(graphUnit *UnitPlanningStatus, finFunc func(error), p *Project) { + log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Destroying unit '%v':", graphUnit.UnitPtr.Key())) + err := graphUnit.UnitPtr.Build() + if err != nil { + log.Errorf("project apply: unit build error: %v", err.Error()) + finFunc(err) + return + } + p.ProcessedUnitsCount++ + err = graphUnit.UnitPtr.Destroy() + if err != nil { + finFunc(fmt.Errorf("project apply: destroying deleted unit: %v", err.Error())) + return } + p.OwnState.DeleteUnit(graphUnit.UnitPtr) + graphUnit.UnitPtr.SetExecStatus(Finished) + finFunc(nil) } // Plan and output result. -func (p *Project) Plan() (*ProjectPlanningStatus, error) { +func (p *Project) Plan() (*graph, error) { log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Checking units in state")) planningSt, err := p.buildPlan() if err != nil { return nil, err } - if config.Global.ShowTerraformPlan { - err = p.ClearCacheDir() - if err != nil { - return nil, fmt.Errorf("build dir for exec terraform plan: %w", err) - } - planSeq, err := planningSt.GetApplyGraph() - if err != nil { - return nil, fmt.Errorf("build graph: %w", err) - } - for _, md := range planSeq.Slice() { - if err != nil { - return nil, fmt.Errorf("project plan: clear cache dir: %v", err.Error()) - } - allDepsDeployed := true - for _, planModDep := range md.UnitPtr.Dependencies().Slice() { - dS := planningSt.FindUnit(planModDep.Unit) - if dS != nil && dS.Operation != NotChanged { - allDepsDeployed = false - } - } - if allDepsDeployed { - err = md.UnitPtr.Build() - if err != nil { - log.Errorf("terraform plan: unit build error: %v", err.Error()) - return nil, err - } - err = md.UnitPtr.Plan() - if err != nil { - log.Errorf("unit '%v' terraform plan return an error: %v", md.UnitPtr.Key(), err.Error()) - return nil, err - } - } else { - log.Warnf("The unit '%v' has dependencies that have not yet been deployed. Can't show terraform plan.", md.UnitPtr.Key()) - } - } - } showPlanResults(planningSt) return planningSt, nil } @@ -271,7 +221,6 @@ func (p *Project) planDestroyAll(opStatus *ProjectPlanningStatus) { } else { units = p.OwnState.UnitsSlice() } - log.Errorf("planDestroyAll %++v", units) for _, md := range units { diff := utils.Diff(md.GetDiffData(), nil, true) opStatus.Add(md, Destroy, diff, md.IsTainted()) @@ -292,28 +241,28 @@ func findUnit(list []Unit, unitForSearch Unit) int { } // Plan and output result. -func (p *Project) buildPlan() (planningStatus *ProjectPlanningStatus, err error) { +func (p *Project) buildPlan() (resGraph *graph, err error) { err = checkUnitDependencies(p) if err != nil { return } - planningStatus = &ProjectPlanningStatus{} + planningStatus := &ProjectPlanningStatus{} if config.Global.IgnoreState { for _, u := range p.UnitsSlice() { planningStatus.Add(u, Apply, utils.Diff(nil, u.GetDiffData(), true), false) } - return planningStatus, nil + return planningStatus.BuildGraph() } p.planDestroy(planningStatus) for _, unit := range p.UnitsSlice() { _, exists := p.OwnState.Units[unit.Key()] - diff, stateUnit, tainted := p.OwnState.CheckUnitChanges(unit) + diff, stateUnit := p.OwnState.CheckUnitChanges(unit) if len(diff) > 0 || config.Global.IgnoreState { if len(diff) > 0 { if exists { - planningStatus.Add(unit, Update, diff, tainted) + planningStatus.Add(unit, Update, diff, stateUnit.IsTainted()) } else { - planningStatus.Add(unit, Apply, diff, tainted) + planningStatus.Add(unit, Apply, diff, false) } } } else { @@ -326,25 +275,34 @@ func (p *Project) buildPlan() (planningStatus *ProjectPlanningStatus, err error) p.UnitLinks.JoinWithDataReplace(p.OwnState.UnitLinks.ByTargetUnit(unit)) } } - // planningStatus.Print() - changedUnits := planningStatus.OperationFilter(Apply, Update, Destroy) - for _, st := range changedUnits.Slice() { - err = DependenciesRecursiveIterate(st.UnitPtr, func(unitForCheck Unit) error { - if unitForCheck.ForceApply() { - fu := changedUnits.FindUnit(unitForCheck) - if fu == nil { - log.Debugf("Unit '%v' added for update as a force_apply dependency", unitForCheck.Key()) - planningStatus.AddOrUpdate(unitForCheck, Update, colors.Fmt(colors.Yellow).Sprint("+/- Will be applied as a 'force_apply' dependency")) - } - } - return nil - }) - if err != nil { - return nil, err - } - } + // // planningStatus.Print() + // changedUnits := planningStatus.OperationFilter(Apply, Update, Destroy) + // for { + // addedAsForceDepCount := 0 + // for _, st := range changedUnits.Slice() { + // err = DependenciesRecursiveIterate(st.UnitPtr, func(unitForCheck Unit) error { + // if unitForCheck.ForceApply() { + // fu := changedUnits.FindUnit(unitForCheck) + // if fu == nil { + // log.Debugf("Unit '%v' added for update as a force_apply dependency", unitForCheck.Key()) + // if u := planningStatus.FindUnitByKey(unitForCheck); u == nil { + // planningStatus.AddIfNotExists(unitForCheck, Update, colors.Fmt(colors.Yellow).Sprint("+/- Will be applied as a 'force_apply' dependency"), false) + // addedAsForceDepCount++ + // } + // } + // } + // return nil + // }) + // if err != nil { + // return nil, err + // } + // } + // if addedAsForceDepCount == 0 { + // break + // } + // } // Check graph and set sequence indexes - _, err = planningStatus.OperationFilter(Apply, Update).GetApplyGraph() + resGraph, err = planningStatus.BuildGraph() if err != nil { return nil, fmt.Errorf("check apply graph: %w", err) } diff --git a/pkg/project/graph.go b/pkg/project/graph.go index 135373fb..f1b6e540 100644 --- a/pkg/project/graph.go +++ b/pkg/project/graph.go @@ -104,45 +104,30 @@ func NewExecSet(planningStatus *ProjectPlanningStatus) *ExecSet { } type graph struct { - units *ExecSet - mux sync.Mutex - waitUnitDone chan Unit - maxParallel int - reverse bool + units *ExecSet + mux sync.Mutex + waitUnitDone chan *UnitPlanningStatus + maxParallel int + indexedSlice []*UnitPlanningStatus + planningUnits *ProjectPlanningStatus // sigTrap chan os.Signal // stopChan chan struct{} } -func (g *graph) BuildDirect(planningStatus *ProjectPlanningStatus, maxParallel int) error { - return g.build(planningStatus, maxParallel, false) -} - -func (g *graph) BuildReverse(planningStatus *ProjectPlanningStatus, maxParallel int) error { - return g.build(planningStatus, maxParallel, true) -} - -func (g *graph) build(planningStatus *ProjectPlanningStatus, maxParallel int, reverse bool) error { - g.units = NewExecSet(planningStatus) - g.reverse = reverse - g.maxParallel = maxParallel - g.waitUnitDone = make(chan Unit) - return g.checkAndBuildIndexes() - // g.listenHupSig() -} - func (g *graph) BuildNew(planningStatus *ProjectPlanningStatus, maxParallel int) error { g.units = NewExecSet(planningStatus) g.maxParallel = maxParallel - g.waitUnitDone = make(chan Unit) + g.waitUnitDone = make(chan *UnitPlanningStatus) + g.planningUnits = planningStatus return g.checkAndBuildIndexes() // g.listenHupSig() } -func (g *graph) GetNextAsync() (Unit, func(error), error) { +func (g *graph) GetNextAsync() (*UnitPlanningStatus, func(error), error) { g.mux.Lock() defer g.mux.Unlock() for { - g.updateQueue() + g.updateQueueNew() if config.Interrupted { return nil, nil, fmt.Errorf("interrupted") } @@ -150,85 +135,73 @@ func (g *graph) GetNextAsync() (Unit, func(error), error) { if readyFroExecList.Len() > 0 && g.units.StatusFilter(InProgress).Len() < g.maxParallel { unitForExec := readyFroExecList.Front() finFunc := func(err error) { - g.waitUnitDone <- unitForExec.UnitPtr + g.waitUnitDone <- unitForExec } unitForExec.UnitPtr.SetExecStatus(InProgress) - g.updateQueue() - return unitForExec.UnitPtr, finFunc, nil + g.updateQueueNew() + return unitForExec, finFunc, nil } if g.units.StatusFilter(Backlog, InProgress, ReadyForExec).IsEmpty() { return nil, nil, nil } unitFinished := <-g.waitUnitDone - unitFinished.SetExecStatus(Finished) - g.updateQueue() - if unitFinished.ExecError() != nil { + unitFinished.UnitPtr.SetExecStatus(Finished) + g.updateQueueNew() + if unitFinished.UnitPtr.ExecError() != nil { return unitFinished, nil, fmt.Errorf("error while unit running") } } } func (g *graph) checkAndBuildIndexes() error { - slice := g.IndexedSlice() - if slice == nil { - return fmt.Errorf("the graph is broken, can't resolve sequence") - } - return nil -} - -func (g *graph) Slice() []*UnitPlanningStatus { - i := 0 - res := []*UnitPlanningStatus{} - for { - readyCount := g.updateQueue() - if readyCount == 0 { - if g.units.StatusFilter(Backlog).Len() > 0 { - return nil - } - break - } - res = append(res, g.units.StatusFilter(ReadyForExec).Slice()...) - - i++ - } - g.resetUnitsStatus() // back all units to backlog - return res -} - -// IndexedSlice return the slice of units in sorted in order ready for exec. -func (g *graph) IndexedSlice() []*UnitPlanningStatus { i := 0 - res := []*UnitPlanningStatus{} + g.indexedSlice = []*UnitPlanningStatus{} apply := []*UnitPlanningStatus{} + notChanged := []*UnitPlanningStatus{} for { readyCount := g.updateQueueNew() if readyCount == 0 { if g.units.StatusFilter(Backlog).Len() > 0 { - return nil + return fmt.Errorf("the graph is broken, can't resolve sequence") } break } for _, u := range g.units.StatusFilter(ReadyForExec).Slice() { - if u.Operation == Destroy { + switch u.Operation { + case Destroy: // Place 'destroy' units first in queue - res = append(res, u) - } else { + g.indexedSlice = append(g.indexedSlice, u) + u.Index = i + case Apply, Update: // Then place 'apply/update' units apply = append(apply, u) + u.Index = i + default: + // Place notChanged units to the end of queue and mark them as 'Finished' in graph + notChanged = append(notChanged, u) + u.Index = -1 } // Mark unit as finished for next updateQueue u.UnitPtr.SetExecStatus(Finished) } i++ } - g.resetUnitsStatus() // back all units to backlog - res = append(res, apply...) - return res + g.indexedSlice = append(g.indexedSlice, apply...) + g.indexedSlice = append(g.indexedSlice, notChanged...) + g.resetUnitsStatus() + return nil +} + +// IndexedSlice return the slice of units in sorted in order ready for exec. +func (g *graph) IndexedSlice() []*UnitPlanningStatus { + return g.indexedSlice } func (g *graph) resetUnitsStatus() { for _, u := range g.units.Slice() { - u.UnitPtr.SetExecStatus(Backlog) + if u.Operation != NotChanged { + u.UnitPtr.SetExecStatus(Backlog) + } } } @@ -246,13 +219,6 @@ func (g *graph) Len() int { return g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Len() } -func (g *graph) updateQueue() int { - if g.reverse { - return g.updateReverseQueue() - } - return g.updateDirectQueue() -} - func (g *graph) updateQueueNew() int { count := 0 for _, unit := range g.units.StatusFilter(Backlog).Slice() { @@ -266,10 +232,11 @@ func (g *graph) updateQueueNew() int { } } case Destroy: - for _, dep := range unit.UnitPtr.Dependencies().Slice() { - if g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Find(dep.Unit) != nil { - blockedByDep = true - break + for _, unitForCheck := range g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Slice() { + for _, link := range unitForCheck.UnitPtr.Dependencies().Slice() { + if link.UnitKey() == unit.UnitPtr.Key() { + blockedByDep = true + } } } case NotChanged: @@ -284,85 +251,12 @@ func (g *graph) updateQueueNew() int { return count } -func (g *graph) updateDirectQueue() int { - count := 0 - for _, unit := range g.units.StatusFilter(Backlog).Slice() { - blockedByDep := false - for _, dep := range unit.UnitPtr.Dependencies().Slice() { - if g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Find(dep.Unit) != nil { - blockedByDep = true - break - } - } - if !blockedByDep { - unit.UnitPtr.SetExecStatus(ReadyForExec) - count++ - } - } - return count -} - -func (g *graph) updateReverseQueue() int { - count := 0 - for _, unit := range g.units.StatusFilter(Backlog).Slice() { - // for _, dep := range unit.Dependencies().Slice() { - graphDepFind := g.units.StatusFilter(Backlog, InProgress, ReadyForExec).Find(unit.UnitPtr) - if graphDepFind == nil || graphDepFind.UnitPtr.GetExecStatus() == Finished { - continue - } - unit.UnitPtr.SetExecStatus(ReadyForExec) - count++ - // } - } - return count -} - func (g *graph) Wait() { for { if g.units.StatusFilter(InProgress).Len() == 0 { return } doneUnit := <-g.waitUnitDone - doneUnit.SetExecStatus(Finished) + doneUnit.UnitPtr.SetExecStatus(Finished) } } - -// func (g *graph) GetSequenceSet() []Unit { -// mCount := len(g.units.Slice()) -// res := make([]Unit, mCount) -// for i := 0; i < mCount; i++ { -// md := g.GetNextSync() -// if md == nil { -// log.Fatal("Building apply units set: getting nil unit, undefined behavior") -// } -// res[i] = md -// log.Infof("GetSequenceSet %v %v", i, md.Key()) -// } -// return res -// } - -// func (g *grapherNew) listenHupSig() { -// signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT} -// g.sigTrap = make(chan os.Signal, 1) -// signal.Notify(g.sigTrap, signals...) -// // log.Warn("Listening signals...") -// go func() { -// for { -// select { -// case <-g.sigTrap: -// config.Interrupted = true -// case <-g.stopChan: -// // log.Warn("Stop listening") -// signal.Stop(g.sigTrap) -// g.sigTrap <- nil -// close(g.sigTrap) -// return -// } -// } -// }() -// } - -// func (g *grapherNew) Close() error { -// g.stopChan <- struct{}{} -// return nil -// } diff --git a/pkg/project/helpers.go b/pkg/project/helpers.go index aec326bf..f2311153 100644 --- a/pkg/project/helpers.go +++ b/pkg/project/helpers.go @@ -52,23 +52,12 @@ type ProjectPlanningStatus struct { } func (s *ProjectPlanningStatus) BuildGraph() (*graph, error) { - CurrentGraph := graph{} - err := CurrentGraph.BuildDirect(s, config.Global.MaxParallel) - return &CurrentGraph, err -} - -func (s *ProjectPlanningStatus) GetApplyGraph() (*graph, error) { - log.Warnf("GetApplyGraph") - CurrentGraph := graph{} - var err error - if config.Global.IgnoreState { - err = CurrentGraph.BuildDirect(s, config.Global.MaxParallel) - } else { - err = CurrentGraph.BuildDirect(s.OperationFilter(Apply, Update), config.Global.MaxParallel) - } - return &CurrentGraph, err + graphRet := graph{} + err := graphRet.BuildNew(s, config.Global.MaxParallel) + return &graphRet, err } +// FindUnit searching unit by pointer, return *UnitPlanningStatus only for same unit func (s *ProjectPlanningStatus) FindUnit(unit Unit) *UnitPlanningStatus { if unit == nil { return nil @@ -81,11 +70,18 @@ func (s *ProjectPlanningStatus) FindUnit(unit Unit) *UnitPlanningStatus { return nil } -func (s *ProjectPlanningStatus) GetDestroyGraph() (*graph, error) { - log.Warnf("GetDestroyGraph") - CurrentGraph := graph{} - err := CurrentGraph.BuildReverse(s.OperationFilter(Destroy), 1) - return &CurrentGraph, err +// FindUnit searching unit by pointer, return *UnitPlanningStatus if unit with same key +// exists (possible to have 2 different units ptr with same key - project/projectState) +func (s *ProjectPlanningStatus) FindUnitByKey(unit Unit) *UnitPlanningStatus { + if unit == nil { + return nil + } + for _, us := range s.units { + if us.UnitPtr.Key() == unit.Key() { + return us + } + } + return nil } func (s *ProjectPlanningStatus) OperationFilter(ops ...UnitOperation) *ProjectPlanningStatus { @@ -105,22 +101,24 @@ func (s *ProjectPlanningStatus) OperationFilter(ops ...UnitOperation) *ProjectPl return &res } -func (s *ProjectPlanningStatus) Add(u Unit, op UnitOperation, diff string, isTainted bool) { +func (s *ProjectPlanningStatus) Add(u Unit, op UnitOperation, diff string, tainted bool) { uo := UnitPlanningStatus{ UnitPtr: u, Operation: op, Diff: diff, - IsTainted: isTainted, Index: -1, + IsTainted: tainted, } s.units = append(s.units, &uo) } -func (s *ProjectPlanningStatus) AddOrUpdate(u Unit, op UnitOperation, diff string) { +func (s *ProjectPlanningStatus) AddOrUpdate(u Unit, op UnitOperation, diff string, tainted bool) { uo := UnitPlanningStatus{ UnitPtr: u, Operation: op, Diff: diff, + Index: -1, + IsTainted: tainted, } existingUnit := s.FindUnit(u) if existingUnit == nil { @@ -131,6 +129,21 @@ func (s *ProjectPlanningStatus) AddOrUpdate(u Unit, op UnitOperation, diff strin } } +func (s *ProjectPlanningStatus) AddIfNotExists(u Unit, op UnitOperation, diff string, tainted bool) { + existingUnit := s.FindUnitByKey(u) + if existingUnit == nil { + uo := UnitPlanningStatus{ + UnitPtr: u, + Operation: op, + Diff: diff, + IsTainted: tainted, + Index: -1, + } + log.Errorf("ProjectPlanningStatus AddOrUpdate: %v", u.Key()) + s.units = append(s.units, &uo) + } +} + func (s *ProjectPlanningStatus) HasChanges() bool { for _, un := range s.units { if un.Operation != NotChanged { @@ -294,7 +307,7 @@ func ScanMarkers(data interface{}, procFunc MarkerScanner, unit Unit) error { // log.Warn("interface") if reflect.TypeOf(out.Interface()).Kind() == reflect.String { if !out.CanSet() { - return fmt.Errorf("Internal error: can't set interface field.") + return fmt.Errorf("internal error: can't set interface field") } val, err := procFunc(out, unit) if err != nil { @@ -346,7 +359,7 @@ func ProjectsFilesExists() bool { return false } -func showPlanResults(opStatus *ProjectPlanningStatus) error { +func showPlanResults(opStatus *graph) error { fmt.Println(colors.Fmt(colors.WhiteBold).Sprint("Plan results:")) if opStatus.Len() == 0 { @@ -358,13 +371,8 @@ func showPlanResults(opStatus *ProjectPlanningStatus) error { headers := []string{} unitsTable := []string{} - gr, err := opStatus.BuildGraph() - if err != nil { - return fmt.Errorf("build graph for plan table: %w", err) - } - var deployString, updateString, destroyString, unchangedString string - for _, unit := range gr.IndexedSlice() { + for _, unit := range opStatus.IndexedSlice() { log.Infof(colors.Fmt(colors.LightWhiteBold).Sprintf("Planning unit '%v':", unit.UnitPtr.Key())) switch unit.Operation { case Apply: @@ -394,19 +402,19 @@ func showPlanResults(opStatus *ProjectPlanningStatus) error { } } - if opStatus.OperationFilter(Apply).Len() > 0 { + if opStatus.planningUnits.OperationFilter(Apply).Len() > 0 { headers = append(headers, "Will be deployed") unitsTable = append(unitsTable, deployString) } - if opStatus.OperationFilter(Update).Len() > 0 { + if opStatus.planningUnits.OperationFilter(Update).Len() > 0 { headers = append(headers, "Will be updated") unitsTable = append(unitsTable, updateString) } - if opStatus.OperationFilter(Destroy).Len() > 0 { + if opStatus.planningUnits.OperationFilter(Destroy).Len() > 0 { headers = append(headers, "Will be destroyed") unitsTable = append(unitsTable, destroyString) } - if opStatus.OperationFilter(NotChanged).Len() > 0 { + if opStatus.planningUnits.OperationFilter(NotChanged).Len() > 0 { headers = append(headers, "Unchanged") unitsTable = append(unitsTable, unchangedString) } diff --git a/pkg/project/project.go b/pkg/project/project.go index 274a9616..7bfb2c17 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -132,14 +132,12 @@ func LoadProjectFull() (*Project, error) { if err != nil { return nil, fmt.Errorf("prepare objects: %w", err) } - if !config.Global.NotLoadState { - if config.Global.IgnoreState { - project.OwnState = project.NewEmptyState() - } else { - project.OwnState, err = project.LoadState() - if err != nil { - return nil, fmt.Errorf("load state: %w", err) - } + if config.Global.IgnoreState { + project.OwnState = project.NewEmptyState() + } else { + project.OwnState, err = project.LoadState() + if err != nil { + return nil, fmt.Errorf("load state: %w", err) } } err = project.readUnits() @@ -199,7 +197,6 @@ func (p *Project) prepareObjects() error { func (p *Project) readUnits() error { // Read units from all stacks. for stackName, stack := range p.Stacks { - log.Warnf("Check units in stack: '%v'", stackName) for _, stackTmpl := range stack.Templates { for _, unitData := range stackTmpl.Units { unit, err := NewUnit(unitData, stack) @@ -214,7 +211,7 @@ func (p *Project) readUnits() error { return fmt.Errorf("stack '%v', reading units: duplicate unit name: %v", stackName, unit.Name()) } p.Units[unit.Key()] = unit - log.Debugf("Unit added: '%v'", unit.Key()) + log.Debugf("Unit added: '%v', tainted: %v", unit.Key(), unit.IsTainted()) } } } diff --git a/pkg/project/state.go b/pkg/project/state.go index ec990c03..5e30726d 100644 --- a/pkg/project/state.go +++ b/pkg/project/state.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "sync" "time" "github.com/apex/log" @@ -169,6 +170,8 @@ func (p *Project) NewEmptyState() *StateProject { Backends: p.Backends, CodeCacheDir: config.Global.StateCacheDir, StateBackendName: p.StateBackendName, + StateMutex: sync.Mutex{}, + InitLock: sync.Mutex{}, UUID: p.UUID, }, LoaderProjectPtr: p, @@ -233,27 +236,27 @@ func (p *Project) LoadState() (*StateProject, error) { return statePrj, nil } -func (sp *StateProject) CheckUnitChanges(unit Unit) (string, Unit, bool) { +func (sp *StateProject) CheckUnitChanges(unit Unit) (string, Unit) { unitStateCache := map[string]bool{} unitInState, exists := sp.Units[unit.Key()] if !exists { - return utils.Diff(nil, unit.GetDiffData(), true), nil, false + return utils.Diff(nil, unit.GetDiffData(), true), nil } diffData := unit.GetDiffData() stateDiffData := unitInState.GetDiffData() df := utils.Diff(stateDiffData, diffData, true) if len(df) > 0 { - return df, unitInState, false + return df, unitInState } if unitInState.IsTainted() { - return colors.Fmt(colors.Yellow).Sprint(utils.Diff(nil, unit.GetDiffData(), false)), unitInState, true + return colors.Fmt(colors.Yellow).Sprintf("Unit is tainted!\n%v", utils.Diff(nil, unit.GetDiffData(), false)), unitInState } for _, dep := range unit.Dependencies().UniqUnits() { if sp.checkUnitChangesRecursive(dep, unitStateCache) { - return colors.Fmt(colors.Yellow).Sprintf("+/- There are changes in the unit dependencies."), unitInState, false + return colors.Fmt(colors.Yellow).Sprintf("+/- There are changes in the unit dependencies."), unitInState } } - return "", unitInState, false + return "", unitInState } func (sp *StateProject) checkUnitChangesRecursive(unit Unit, cacheUnitChanges map[string]bool) bool { diff --git a/pkg/project/unit.go b/pkg/project/unit.go index e5396edd..bb0bab40 100644 --- a/pkg/project/unit.go +++ b/pkg/project/unit.go @@ -32,6 +32,7 @@ type Unit interface { ForceApply() bool Mux() *sync.Mutex IsTainted() bool + SetTainted(newValue bool) SetExecStatus(ExecutionStatus) GetExecStatus() ExecutionStatus ExecError() error @@ -100,6 +101,14 @@ func NewUnitFromState(state map[string]interface{}, name string, p *StateProject return nil, fmt.Errorf("internal error: bad unit type in state '%v'", mType) } } + // stateUnit, err := modDrv.NewFromState(state, name, p) + // if err != nil { + // return nil, err + // } + // curUnit := p.LoaderProjectPtr.Units[stateUnit.Key()] + // if curUnit != nil { + // curUnit.SetTainted() + // } return modDrv.NewFromState(state, name, p) } diff --git a/pkg/units/shell/common/factory.go b/pkg/units/shell/common/factory.go index c7489bf1..74b8e97e 100644 --- a/pkg/units/shell/common/factory.go +++ b/pkg/units/shell/common/factory.go @@ -15,7 +15,7 @@ const unitKind string = "shell" func NewEmptyUnit() *Unit { unit := Unit{ //UnitMarkers: make(map[string]interface{}), - Applied: false, + AlreadyApplied: false, UnitKind: unitKind, CreateFiles: &FilesListT{}, DependenciesList: project.NewUnitLinksT(), diff --git a/pkg/units/shell/common/unit.go b/pkg/units/shell/common/unit.go index 3c97d4b2..df18f6bb 100644 --- a/pkg/units/shell/common/unit.go +++ b/pkg/units/shell/common/unit.go @@ -40,25 +40,6 @@ type OutputsConfigSpec struct { Separator string `yaml:"separator,omitempty" json:"separator,omitempty"` } -// type StateConfigFileSpec struct { -// Mask string `yaml:"mask"` -// Dir string `yaml:"dir"` -// Recursive bool `yaml:"recursive"` -// } - -// StateConfigSpec describes what data to save to the state. -// type StateConfigSpec struct { -// CreateFiles bool -// ApplyConf bool -// DestroyConf bool -// InitConf bool -// PlanConf bool -// Hooks bool -// Env bool -// WorkDir bool -// GetOutputConf bool -// } - // OutputParser represents function for parsing unit output. type OutputParser func(string, *project.UnitLinksT) error @@ -80,7 +61,8 @@ type Unit struct { DestroyConf *OperationConfig `yaml:"destroy,omitempty" json:"destroy,omitempty"` GetOutputsConf *OutputsConfigSpec `yaml:"outputs,omitempty" json:"outputs_config,omitempty"` OutputParsers map[string]OutputParser `yaml:"-" json:"-"` - Applied bool `yaml:"-" json:"-"` + AlreadyApplied bool `yaml:"-" json:"-"` + AlreadyDestroyed bool `yaml:"-" json:"-"` PreHook *HookSpec `yaml:"-" json:"pre_hook,omitempty"` PostHook *HookSpec `yaml:"-" json:"post_hook,omitempty"` UnitKind string `yaml:"type" json:"type"` @@ -95,6 +77,11 @@ type Unit struct { ExecErr error `yaml:"-" json:"-"` } +// IsTainted return true if unit have tainted state (failed previous apply or destroy). +func (u *Unit) SetTainted(newValue bool) { + u.Tainted = newValue +} + // IsTainted return true if unit have tainted state (failed previous apply or destroy). func (u *Unit) IsTainted() bool { return u.Tainted @@ -115,7 +102,7 @@ func (u *Unit) ForceApply() bool { // WasApplied return true if unit's method Apply was runned. func (u *Unit) WasApplied() bool { - return u.Applied + return u.AlreadyApplied } // ReadConfig reads unit spec (unmarshaled YAML) and init the unit. @@ -209,6 +196,9 @@ func (u *Unit) Project() *project.Project { // StackName return unit stack name. func (u *Unit) StackName() string { + if u.StackPtr == nil { + return "" + } return u.StackPtr.Name } @@ -232,32 +222,52 @@ func (u *Unit) Init() error { } func (u *Unit) forceApplyDependencies() (err error) { - for _, dep := range u.DependenciesList.Map() { - if dep.Unit.ForceApply() { - dep.Unit.Mux().Lock() - if dep.Unit.WasApplied() { - dep.Unit.Mux().Unlock() - continue - } - log.Debugf("Force applying dependency '%v'...", dep.Unit.Key()) - err = dep.Unit.Build() - if err != nil { - dep.Unit.Mux().Unlock() - return err - } - err = dep.Unit.Apply() - if err != nil { - dep.Unit.Mux().Unlock() - return err - } - dep.Unit.Mux().Unlock() + for _, dep := range findForceApplyDependencies(u) { + if !dep.ForceApply() { + continue + } + log.Debugf("Force applying dependency '%v'...", dep.Key()) + err = dep.Build() + if err != nil { + return err + } + err = dep.Apply() + if err != nil { + return err } } return } +func findForceApplyDependencies(u project.Unit) []project.Unit { + resMap := map[string]project.Unit{} + findForceApplyDependenciesRecursive(u, &resMap) + res := make([]project.Unit, len(resMap)) + i := 0 + for _, unit := range resMap { + res[i] = unit + } + return res +} + +func findForceApplyDependenciesRecursive(u project.Unit, res *map[string]project.Unit) { + for _, dep := range u.Dependencies().Slice() { + if dep.Unit != nil && dep.Unit.ForceApply() { + // log.Warnf("findForceApplyDependenciesRecursive, unit added: %v", dep.Unit.Key()) + (*res)[dep.Unit.Key()] = dep.Unit + } + findForceApplyDependenciesRecursive(dep.Unit, res) + } +} + // Apply runs unit apply procedure. func (u *Unit) Apply() error { + u.Mux().Lock() + defer u.Mux().Unlock() + if u.AlreadyApplied { + log.Debugf("A duplicate apply detected. Unit: '%v', skip...", u.Key()) + return nil + } err := u.forceApplyDependencies() if err != nil { return err @@ -305,7 +315,7 @@ func (u *Unit) Apply() error { } if err == nil { - u.Applied = true + u.AlreadyApplied = true } return err } @@ -315,6 +325,7 @@ func (u *Unit) MarkTainted(err error) { if u.SavedState != nil { u.SavedState.(*Unit).Tainted = true } + // log.Errorf("MarkTainted: %v, %v", u.Key(), err.Error()) u.Tainted = true } @@ -374,6 +385,12 @@ func (u *Unit) Plan() error { // Destroy unit. func (u *Unit) Destroy() error { + u.Mux().Lock() + defer u.Mux().Unlock() + if u.AlreadyDestroyed { + log.Debugf("A duplicate destroy detected. Unit: '%v', skip...", u.Key()) + return nil + } err := u.forceApplyDependencies() if err != nil { return err @@ -393,6 +410,8 @@ func (u *Unit) Destroy() error { _, err = u.runCommands(destroyCommands, "destroy") if err != nil { u.MarkTainted(err) + } else { + u.AlreadyDestroyed = true } return err } @@ -563,7 +582,6 @@ func (u *Unit) EnvSlice() []string { func (u *Unit) SetExecStatus(status project.ExecutionStatus) { if u.GetExecStatus() != status { - log.Warnf("Unit '%v' Status changed: %v --> %v", u.Key(), u.GetExecStatus(), status) u.ExecStatus = status } } diff --git a/pkg/units/shell/terraform/base/unit.go b/pkg/units/shell/terraform/base/unit.go index 7cae0096..5198f9d2 100644 --- a/pkg/units/shell/terraform/base/unit.go +++ b/pkg/units/shell/terraform/base/unit.go @@ -104,10 +104,6 @@ func (u *Unit) Init() error { // Apply unit. func (u *Unit) Apply() error { - if u.Applied { - log.Debugf("Unit '%v.%v' is already applied, skipping") - return nil - } if !u.InitDone { if err := u.Init(); err != nil { return err @@ -128,10 +124,6 @@ func (u *Unit) Plan() error { // Destroy unit. func (u *Unit) Destroy() error { - if u.Applied { - log.Debugf("Unit '%v.%v' is already applied, skipping") - return nil - } if !u.InitDone { if err := u.Init(); err != nil { return err diff --git a/tests/test-project/graph-test/template.yaml b/tests/test-project/graph-test/template.yaml index 5dad6b86..6025958a 100644 --- a/tests/test-project/graph-test/template.yaml +++ b/tests/test-project/graph-test/template.yaml @@ -17,7 +17,7 @@ units: apply: commands: - echo "Waiting..." - - sleep 4 + - sleep 10 - name: parallelWatcher1 type: shell @@ -32,35 +32,39 @@ units: commands: - echo "Waiting..." - sleep 25 - - - name: parallelWatcher3 - type: shell - apply: - commands: - - echo "Waiting..." - - sleep 35 - - - name: parallelWatcher4 - type: shell - apply: - commands: - - echo "Waiting..." - - sleep 10 + # - + # name: parallelWatcher3 + # type: shell + # depends_on: this.force_apply_unit + # apply: + # commands: + # - echo "Waiting..." + # - sleep 35 + # - + # name: parallelWatcher4 + # type: shell + # depends_on: this.force_apply_unit + # apply: + # commands: + # - echo "Waiting..." + # - sleep 10 - name: parallelWatcher5 type: shell + depends_on: this.force_apply_unit apply: commands: - echo "Waiting..." - sleep 40 - - - name: parallelWatcher6 - type: shell - apply: - commands: - - echo "Waiting..." - - sleep 20 - - exit 0 + # - + # name: parallelWatcher6 + # type: shell + # depends_on: this.force_apply_unit + # apply: + # commands: + # - echo "Waiting..." + # - sleep 20 + # - exit 0 - name: firstUnit type: printer @@ -75,20 +79,20 @@ units: - name: thirdStep type: printer - # depends_on: this.force_apply_unit + depends_on: this.force_apply_unit outputs: longWay: {{ remoteState "this.secondUnit.longWay"}}-STEP3 - changed: true - - - name: foursUnit - type: printer - outputs: - longWay: {{ remoteState "this.thirdStep.longWay"}}-STEP4 - - - name: fifthStep - type: printer - outputs: - longWay: {{ remoteState "this.foursUnit.longWay"}}-STEP5 + changed: false + # - + # name: foursUnit + # type: printer + # outputs: + # longWay: {{ remoteState "this.thirdStep.longWay"}}-STEP4 + # - + # name: fifthStep + # type: printer + # outputs: + # longWay: {{ remoteState "this.foursUnit.longWay"}}-STEP5 - name: outputs type: printer diff --git a/tests/test-project/local.yaml b/tests/test-project/local.yaml index 9514f8ff..f734a0a5 100644 --- a/tests/test-project/local.yaml +++ b/tests/test-project/local.yaml @@ -3,6 +3,5 @@ template: ./graph-test/ kind: Stack disabled: false backend: aws-backend -disabled: false variables: {} From f40390ef14880be83d4c42173e72aa7c10be460a Mon Sep 17 00:00:00 2001 From: romanprog Date: Fri, 15 Dec 2023 13:56:50 +0200 Subject: [PATCH 05/11] fixes --- pkg/project/commands.go | 5 ++ pkg/units/shell/common/unit.go | 2 +- pkg/units/shell/terraform/printer/main.go | 16 +++- tests/test-project/cdev.state | 60 --------------- tests/test-project/graph-test/template.yaml | 85 +++++++++++---------- tests/test-project/local-tmpl/template.yaml | 1 + tests/test-project/local.yaml | 12 ++- 7 files changed, 77 insertions(+), 104 deletions(-) delete mode 100644 tests/test-project/cdev.state diff --git a/pkg/project/commands.go b/pkg/project/commands.go index 0fddbec8..017eaeab 100644 --- a/pkg/project/commands.go +++ b/pkg/project/commands.go @@ -166,6 +166,11 @@ func applyRoutine(graphUnit *UnitPlanningStatus, finFunc func(error), p *Project finFunc(fmt.Errorf("project apply: destroying deleted unit: %v", err.Error())) return } + err = graphUnit.UnitPtr.UpdateProjectRuntimeData(p) + if err != nil { + finFunc(err) + return + } p.OwnState.UpdateUnit(graphUnit.UnitPtr) graphUnit.UnitPtr.SetExecStatus(Finished) finFunc(nil) diff --git a/pkg/units/shell/common/unit.go b/pkg/units/shell/common/unit.go index df18f6bb..66527460 100644 --- a/pkg/units/shell/common/unit.go +++ b/pkg/units/shell/common/unit.go @@ -246,6 +246,7 @@ func findForceApplyDependencies(u project.Unit) []project.Unit { i := 0 for _, unit := range resMap { res[i] = unit + i++ } return res } @@ -253,7 +254,6 @@ func findForceApplyDependencies(u project.Unit) []project.Unit { func findForceApplyDependenciesRecursive(u project.Unit, res *map[string]project.Unit) { for _, dep := range u.Dependencies().Slice() { if dep.Unit != nil && dep.Unit.ForceApply() { - // log.Warnf("findForceApplyDependenciesRecursive, unit added: %v", dep.Unit.Key()) (*res)[dep.Unit.Key()] = dep.Unit } findForceApplyDependenciesRecursive(dep.Unit, res) diff --git a/pkg/units/shell/terraform/printer/main.go b/pkg/units/shell/terraform/printer/main.go index 3975e8d9..bb704278 100644 --- a/pkg/units/shell/terraform/printer/main.go +++ b/pkg/units/shell/terraform/printer/main.go @@ -53,20 +53,30 @@ func (u *Unit) ReadConfig(spec map[string]interface{}, stack *project.Stack) err modType, ok := spec["type"].(string) if !ok { - return fmt.Errorf("Incorrect unit type") + return fmt.Errorf("incorrect unit type") } if modType != u.KindKey() { - return fmt.Errorf("Incorrect unit type") + return fmt.Errorf("incorrect unit type") } mOutputs, ok := spec["outputs"].(map[string]interface{}) if !ok { mOutputs, ok = spec["inputs"].(map[string]interface{}) if !ok { - return fmt.Errorf("Incorrect unit inputs") + return fmt.Errorf("incorrect unit inputs") } log.Warnf("Printer unit '%v' has field 'inputs', this field is deprecated and will be removed in future. Use 'outputs' instead", u.Key()) } u.Outputs = mOutputs + if stack.ProjectPtr.OwnState != nil { + myStateIntf := stack.ProjectPtr.OwnState.Units[u.Key()] + if myStateIntf == nil { + return nil + } + switch stateOutput := myStateIntf.(type) { + case *Unit: + u.OutputRaw = stateOutput.OutputRaw + } + } return nil } diff --git a/tests/test-project/cdev.state b/tests/test-project/cdev.state deleted file mode 100644 index 16e89bf9..00000000 --- a/tests/test-project/cdev.state +++ /dev/null @@ -1,60 +0,0 @@ -{ - "version": "v0.8.2", - "project_uuid": "4cd452ee-ca12-4c45-a03c-f4674bf1cbf5", - "unit_links": { - "unit_links_list": { - "04bd8a74af4c330a799f39a235f34577.RemoteStateMarkers.cdev-tests-local.thirdStep.longWay.04bd8a74af4c330a799f39a235f34577": { - "link_type": "RemoteStateMarkers", - "target_unit_name": "thirdStep", - "target_stack_name": "cdev-tests-local", - "output_name": "longWay", - "output_data": null - } - }, - "MapMutex": {} - }, - "units": { - "cdev-tests-local.parallelWatcher1": { - "name": "parallelWatcher1", - "create_files": [], - "apply": { - "commands": [ - "echo \"Waiting...\"", - "sleep 30" - ] - }, - "type": "shell", - "backend_name": "aws-backend", - "force_apply": false - }, - "cdev-tests-local.parallelWatcher5": { - "name": "parallelWatcher5", - "create_files": [], - "apply": { - "commands": [ - "echo \"Waiting...\"", - "sleep 40" - ] - }, - "type": "shell", - "backend_name": "aws-backend", - "force_apply": false - }, - "cdev-tests-local.thirdStep": { - "name": "thirdStep", - "outputs_config": { - "command": "terraform output -json", - "type": "terraform" - }, - "backend_name": "aws-backend", - "depends_on": "this.force_apply_unit", - "force_apply": false, - "output": "{\n \"changed\": {\n \"sensitive\": true,\n \"type\": \"bool\",\n \"value\": true\n },\n \"longWay\": {\n \"sensitive\": true,\n \"type\": \"string\",\n \"value\": \"STEP1-STEP2-STEP3\"\n }\n}\n", - "outputs": { - "changed": true, - "longWay": "9d79a12ca098d7d766e970f0756946d7.RemoteStateMarkers.cdev-tests-local.secondUnit.longWay.9d79a12ca098d7d766e970f0756946d7-STEP3" - }, - "type": "printer" - } - } - } diff --git a/tests/test-project/graph-test/template.yaml b/tests/test-project/graph-test/template.yaml index 6025958a..4bd2c051 100644 --- a/tests/test-project/graph-test/template.yaml +++ b/tests/test-project/graph-test/template.yaml @@ -17,37 +17,37 @@ units: apply: commands: - echo "Waiting..." - - sleep 10 + - sleep 4 - name: parallelWatcher1 type: shell apply: commands: - echo "Waiting..." - - sleep 30 + - sleep 8 - name: parallelWatcher2 type: shell apply: commands: - echo "Waiting..." - - sleep 25 - # - - # name: parallelWatcher3 - # type: shell - # depends_on: this.force_apply_unit - # apply: - # commands: - # - echo "Waiting..." - # - sleep 35 - # - - # name: parallelWatcher4 - # type: shell - # depends_on: this.force_apply_unit - # apply: - # commands: - # - echo "Waiting..." - # - sleep 10 + - sleep 5 + - + name: parallelWatcher3 + type: shell + depends_on: this.force_apply_unit + apply: + commands: + - echo "Waiting..." + - sleep 2 + - + name: parallelWatcher4 + type: shell + depends_on: this.force_apply_unit + apply: + commands: + - echo "Waiting..." + - sleep 10 - name: parallelWatcher5 type: shell @@ -55,16 +55,16 @@ units: apply: commands: - echo "Waiting..." - - sleep 40 - # - - # name: parallelWatcher6 - # type: shell - # depends_on: this.force_apply_unit - # apply: - # commands: - # - echo "Waiting..." - # - sleep 20 - # - exit 0 + - sleep 7 + - + name: parallelWatcher6 + type: shell + depends_on: this.force_apply_unit + apply: + commands: + - echo "Waiting..." + - sleep 10 + - exit 0 - name: firstUnit type: printer @@ -83,18 +83,25 @@ units: outputs: longWay: {{ remoteState "this.secondUnit.longWay"}}-STEP3 changed: false - # - - # name: foursUnit - # type: printer - # outputs: - # longWay: {{ remoteState "this.thirdStep.longWay"}}-STEP4 - # - - # name: fifthStep - # type: printer - # outputs: - # longWay: {{ remoteState "this.foursUnit.longWay"}}-STEP5 + - + name: foursUnit + type: printer + outputs: + NewKey: "foo" + longWay: {{ remoteState "this.thirdStep.longWay"}}-STEP4 + - + name: fifthStep + type: printer + outputs: + longWay: {{ remoteState "this.foursUnit.longWay"}}-STEP5 - name: outputs type: printer outputs: WayReport: {{ remoteState "this.thirdStep.longWay"}} + Text: | + Yes, there are several games where the world is created + based on the player's responses. These games are often referred to as + "procedural" games, and they use AI algorithms to generate new content on the fly. + This can create a very unique and immersive experience for players, as it allows them + to feel like they are truly shaping the game world. foo bar two diff --git a/tests/test-project/local-tmpl/template.yaml b/tests/test-project/local-tmpl/template.yaml index ca4e7aab..daf57a15 100644 --- a/tests/test-project/local-tmpl/template.yaml +++ b/tests/test-project/local-tmpl/template.yaml @@ -30,6 +30,7 @@ units: type: printer depends_on: this.create-s3-object outputs: + new_output: "test2" bucket_name: Bucket name is {{ remoteState "this.create-bucket.id" }} test_insert_yaml: {{ insertYAML .variables.list_one }} test_map_output: {{ remoteState "this.test-complex-output-data.map[\"key\"]" }} diff --git a/tests/test-project/local.yaml b/tests/test-project/local.yaml index f734a0a5..c193af7a 100644 --- a/tests/test-project/local.yaml +++ b/tests/test-project/local.yaml @@ -4,4 +4,14 @@ kind: Stack disabled: false backend: aws-backend variables: {} - +--- +name: cdev-local-two +template: ./local-tmpl/ +kind: Stack +disabled: false +backend: aws-backend +variables: + list_one: + - "1" + - "2" + region: {{ .project.variables.region }} From 658606daacb37b0432d98fd8e544e3b44d5e0753 Mon Sep 17 00:00:00 2001 From: romanprog Date: Fri, 15 Dec 2023 14:18:03 +0200 Subject: [PATCH 06/11] github.com/go-jose/go-jose/v3 version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a6a98f64..56796598 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( require ( github.com/Azure/azure-pipeline-go v0.2.3 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/mattn/go-ieproxy v0.0.1 // indirect ) @@ -94,7 +95,6 @@ require ( github.com/fatih/color v1.15.0 // indirect github.com/getsops/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect github.com/go-errors/errors v1.5.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect From d59fa450ceb0cbea61a0eb8c3b8489bf4c0836fc Mon Sep 17 00:00:00 2001 From: romanprog Date: Fri, 15 Dec 2023 14:43:58 +0200 Subject: [PATCH 07/11] fix #191 --- pkg/units/shell/common/state.go | 2 +- tests/test-project/graph-test/template.yaml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/units/shell/common/state.go b/pkg/units/shell/common/state.go index 08f5be15..92a1c1f5 100644 --- a/pkg/units/shell/common/state.go +++ b/pkg/units/shell/common/state.go @@ -14,7 +14,7 @@ import ( type UnitDiffSpec struct { Outputs map[string]string `json:"outputs,omitempty"` // CreateFiles *FilesListT `json:"create_files,omitempty"` - CreateFilesDiff map[string][]string `json:"-"` + CreateFilesDiff map[string][]string `json:"create_files,omitempty"` ApplyConf *OperationConfig `json:"apply,omitempty"` Env map[string]interface{} `json:"env,omitempty"` OutputsConfig *OutputsConfigSpec `json:"outputs_config,omitempty"` diff --git a/tests/test-project/graph-test/template.yaml b/tests/test-project/graph-test/template.yaml index 4bd2c051..7b90befb 100644 --- a/tests/test-project/graph-test/template.yaml +++ b/tests/test-project/graph-test/template.yaml @@ -43,6 +43,14 @@ units: - name: parallelWatcher4 type: shell + create_files: + - file: test.txt + content: | + Some test + multiline + This can create a very unique + and immersive experience for players, as it allows them + new Line depends_on: this.force_apply_unit apply: commands: From f618d6cc8a19223ecdc02223822e04b426b4b00c Mon Sep 17 00:00:00 2001 From: romanprog Date: Fri, 15 Dec 2023 16:51:45 +0200 Subject: [PATCH 08/11] cicd fix --- .github/workflows/pr_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 6afa8a45..a1ef5d53 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -29,7 +29,7 @@ jobs: git fetch --prune --unshallow --tags && make && cp ./bin/linux-amd64/cdev /usr/local/bin/ - name: Run tests - run: cd tests/test-project/ && cdev plan --tf-plan -l debug && cdev apply --force -l debug && cdev destroy --force -l debug + run: cd tests/test-project/ && cdev apply --force -l debug && cdev destroy --force -l debug env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From bb29e0c2e469cdfc38a8e8b8ea867e601ba4c592 Mon Sep 17 00:00:00 2001 From: romanprog Date: Sat, 16 Dec 2023 12:14:29 +0200 Subject: [PATCH 09/11] fix --- pkg/units/shell/common/unit.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/units/shell/common/unit.go b/pkg/units/shell/common/unit.go index 66527460..d3975bee 100644 --- a/pkg/units/shell/common/unit.go +++ b/pkg/units/shell/common/unit.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "strings" "sync" @@ -323,10 +324,9 @@ func (u *Unit) Apply() error { func (u *Unit) MarkTainted(err error) { u.ExecErr = err if u.SavedState != nil { - u.SavedState.(*Unit).Tainted = true + rf := reflect.ValueOf(u.SavedState).Interface().(project.Unit) + rf.SetTainted(true) } - // log.Errorf("MarkTainted: %v, %v", u.Key(), err.Error()) - u.Tainted = true } func (u *Unit) runCommands(commandsCnf OperationConfig, name string) ([]byte, error) { From 2d05860bdcfeda8fc15e7a813f0ea3d6a284c97d Mon Sep 17 00:00:00 2001 From: romanprog Date: Sat, 16 Dec 2023 13:33:44 +0200 Subject: [PATCH 10/11] old code removed --- pkg/project/dependencies.go | 234 ------------------------------------ 1 file changed, 234 deletions(-) diff --git a/pkg/project/dependencies.go b/pkg/project/dependencies.go index bc106be9..282518e0 100644 --- a/pkg/project/dependencies.go +++ b/pkg/project/dependencies.go @@ -5,199 +5,6 @@ import ( "strings" ) -// import ( -// "container/list" -// "fmt" -// "os" -// "os/signal" -// "strings" -// "sync" -// "syscall" - -// "github.com/apex/log" -// "github.com/shalb/cluster.dev/pkg/config" -// ) - -// type unitExecuteResult struct { -// Mod Unit -// Error error -// } - -// type grapher struct { -// finished map[string]unitExecuteResult -// inProgress map[string]Unit -// queue list.List -// units map[string]Unit -// unFinished map[string]Unit -// mux sync.Mutex -// waitForModDone chan unitExecuteResult -// maxParallel int -// hasError bool -// reverse bool -// unitsErrors map[string]error -// sigTrap chan os.Signal -// stopChan chan struct{} -// } - -// func (g *grapher) InitP(planningStatus *ProjectPlanningStatus, maxParallel int, reverse bool) { -// if maxParallel < 1 { -// log.Fatal("Internal error, parallelism < 1.") -// } -// g.units = make(map[string]Unit) -// g.unFinished = make(map[string]Unit) -// g.inProgress = make(map[string]Unit) - -// g.unitsErrors = make(map[string]error) - -// for _, uStatus := range planningStatus.Slice() { -// g.units[uStatus.UnitPtr.Key()] = uStatus.UnitPtr -// g.unFinished[uStatus.UnitPtr.Key()] = uStatus.UnitPtr -// } -// g.maxParallel = maxParallel -// g.queue.Init() - -// g.finished = make(map[string]unitExecuteResult) -// g.waitForModDone = make(chan unitExecuteResult) -// g.hasError = false -// g.reverse = reverse -// g.updateQueue() -// g.stopChan = make(chan struct{}) -// g.listenHupSig() -// } - -// func (g *grapher) HasError() bool { -// return g.hasError -// } - -// func (g *grapher) updateQueue() int { -// if g.reverse { -// return g.updateReverseQueue() -// } -// return g.updateDirectQueue() -// } - -// func (g *grapher) UnitFinished(u Unit) bool { -// return g.unFinished[u.Key()] != nil -// } - -// func (g *grapher) updateDirectQueue() int { -// count := 0 -// for key, mod := range g.units { -// isReady := true -// for _, dep := range mod.Dependencies().Slice() { -// if g.UnitFinished(dep.Unit) { -// isReady = false -// break -// } -// } -// if isReady { -// g.queue.PushBack(mod) -// delete(g.units, key) -// count++ -// } -// } -// return count -// } - -// func (g *grapher) updateReverseQueue() int { -// count := 0 -// for key, mod := range g.units { -// dependedMods := findDependedUnits(g.unFinished, mod) -// if len(dependedMods) > 0 { -// continue -// } -// g.queue.PushBack(mod) -// delete(g.units, key) -// count++ -// } -// return count -// } - -// func (g *grapher) GetNextAsync() (Unit, func(error), error) { -// g.mux.Lock() -// defer g.mux.Unlock() -// for { -// if config.Interrupted { -// g.queue.Init() -// g.units = make(map[string]Unit) -// g.updateQueue() -// return nil, nil, fmt.Errorf("interupted") -// } -// if g.queue.Len() > 0 && len(g.inProgress) < g.maxParallel { -// modElem := g.queue.Front() -// mod := modElem.Value.(Unit) -// finFunc := func(err error) { -// g.waitForModDone <- unitExecuteResult{mod, err} -// } -// g.queue.Remove(modElem) -// g.inProgress[mod.Key()] = mod -// return mod, finFunc, nil -// } -// if g.Len() == 0 { -// return nil, nil, nil -// } -// doneMod := <-g.waitForModDone -// g.setUnitDone(doneMod) -// if doneMod.Error != nil { -// return doneMod.Mod, nil, fmt.Errorf("error while unit running") -// } -// } -// } - -// func (g *grapher) GetNextSync() Unit { -// if g.Len() == 0 { -// return nil -// } -// modElem := g.queue.Front() -// mod := modElem.Value.(Unit) -// g.queue.Remove(modElem) -// g.setUnitDone(unitExecuteResult{mod, nil}) -// return mod -// } - -// func (g *grapher) GetSequenceSet() []Unit { -// res := make([]Unit, g.Len()) -// mCount := g.Len() -// for i := 0; i < mCount; i++ { -// md := g.GetNextSync() -// if md == nil { -// log.Fatal("Building apply units set: getting nil unit, undefined behavior") -// } -// res[i] = md -// log.Infof("GetSequenceSet %v %v", i, md.Key()) -// } -// return res -// } - -// func (g *grapher) setUnitDone(doneMod unitExecuteResult) { -// g.finished[doneMod.Mod.Key()] = doneMod -// delete(g.inProgress, doneMod.Mod.Key()) -// delete(g.unFinished, doneMod.Mod.Key()) -// if doneMod.Error != nil { -// g.unitsErrors[doneMod.Mod.Key()] = doneMod.Error -// g.hasError = true -// } -// g.updateQueue() -// } - -// func (g *grapher) Errors() map[string]error { -// return g.unitsErrors -// } - -// func (g *grapher) Wait() { -// for { -// if len(g.inProgress) == 0 { -// return -// } -// doneMod := <-g.waitForModDone -// g.setUnitDone(doneMod) -// } -// } - -// func (g *grapher) Len() int { -// return len(g.units) + g.queue.Len() + len(g.inProgress) -// } - func checkUnitDependencies(p *Project) error { for _, uniit := range p.Units { if err := checkDependenciesRecursive(uniit); err != nil { @@ -237,44 +44,3 @@ func checkUnitDependenciesCircle(chain []string) error { } return nil } - -func findDependedUnits(modList map[string]Unit, targetMod Unit) map[string]Unit { - res := map[string]Unit{} - for key, mod := range modList { - if mod.Key() == targetMod.Key() { - continue - } - for _, dep := range mod.Dependencies().Slice() { - if dep.Unit.Key() == targetMod.Key() { - res[key] = mod - } - } - } - return res -} - -// func (g *grapher) listenHupSig() { -// signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT} -// g.sigTrap = make(chan os.Signal, 1) -// signal.Notify(g.sigTrap, signals...) -// // log.Warn("Listening signals...") -// go func() { -// for { -// select { -// case <-g.sigTrap: -// config.Interrupted = true -// case <-g.stopChan: -// // log.Warn("Stop listening") -// signal.Stop(g.sigTrap) -// g.sigTrap <- nil -// close(g.sigTrap) -// return -// } -// } -// }() -// } - -// func (g *grapher) Close() error { -// g.stopChan <- struct{}{} -// return nil -// } From 70d482246087f6bb323393230e4732e8cdf07f6e Mon Sep 17 00:00:00 2001 From: Anastasiya Kulyk <56824659+anastyakulyk@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:57:14 +0200 Subject: [PATCH 11/11] Add cdev-tainted screenshot --- docs/images/cdev-tainted.png | Bin 0 -> 56100 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/cdev-tainted.png diff --git a/docs/images/cdev-tainted.png b/docs/images/cdev-tainted.png new file mode 100644 index 0000000000000000000000000000000000000000..4ee4bd002023338fdd9a2cba151b3d6f19d3c1fb GIT binary patch literal 56100 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 aYkp>@^vx=p+WkAgkC>3OV3ELE&;J9u)yf0_ literal 0 HcmV?d00001