package apply import ( "errors" "fmt" "sort" "strings" "github.com/novox/mesh-host/internal/declaration" ) // failuresOf is every failure of an apply as the error carries it: the first among them, then the rest, so a module // whose resource failed second is still named (issue 1132, and issue 500, which counted the rest and named none). func failuresOf(failures []*Error) []Failure { out := make([]Failure, 0, len(failures)) for _, f := range failures { out = append(out, Failure{Resource: f.Resource, Err: f.Err}) } return out } // ModuleResults groups an apply's outcome per module the declaration names (novox/hq ADR 0429 ยง4, issue 1132): the // module's resources all applied, or it failed and Detail says which resource failed with its error, which resources // were skipped behind a step that did not complete, or that its resources were not attempted because a step of no // module stopped the apply. A module is named by what the declaration says of each resource (ModuleOfID), so one // whose id would guess another is still its own. // // A failure that is no resource's (an error that is not an Error, or a resource of no module) belongs to no module and // changes none: the node says it in its report. The report is the outcome of an apply that did not fail; an apply that // failed carries its own account in the Error, and that is read in its place. func ModuleResults(d *declaration.Declaration, report Report, err error) map[string]ModuleResult { if d == nil { return nil } out := map[string]ModuleResult{} attempted := map[string]bool{} var failed *Error errors.As(err, &failed) outcomes := report.Outcomes if failed != nil { outcomes = append(append([]Outcome(nil), outcomes...), failed.Done.Outcomes...) } skipped := map[string]string{} for _, o := range outcomes { module, ok := d.ModuleOfID(o.ID) if !ok { continue } if o.Action == "skipped" { skipped[module] = o.Detail continue } attempted[o.ID] = true } var reasons = map[string][]string{} for _, r := range d.Resources { if module, ok := d.ModuleOfID(r.Identity()); ok { out[module] = ModuleResult{} if failed != nil && failed.Stopped && !attempted[r.Identity()] { reasons[module] = append(reasons[module], fmt.Sprintf("%s was not attempted: %s stopped the apply", r.Identity(), failed.Resource)) } } } if failed != nil { for _, f := range failed.Failures { if module, ok := d.ModuleOfID(f.Resource); ok { if _, named := out[module]; named { reasons[module] = append(reasons[module], fmt.Sprintf("%s failed: %v", f.Resource, f.Err)) } } } if len(failed.Failures) == 0 { if module, ok := d.ModuleOfID(failed.Resource); ok { if _, named := out[module]; named { reasons[module] = append(reasons[module], fmt.Sprintf("%s failed: %v", failed.Resource, failed.Err)) } } } for _, module := range failed.Modules { if _, named := out[module]; named { why := skipped[module] if why == "" { why = "a step it declares did not complete" } reasons[module] = append(reasons[module], "gated: "+why) } } } for module, why := range skipped { if _, named := out[module]; named && len(reasons[module]) == 0 { reasons[module] = append(reasons[module], "gated: "+why) } } for module, why := range reasons { sort.Strings(why) out[module] = ModuleResult{Failed: true, Detail: strings.Join(why, "; ")} } return out }