209 lines
9.5 KiB
Go
209 lines
9.5 KiB
Go
package assign
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
"git.novox.be/novox/mesh-sdk/go/provisions"
|
|
)
|
|
|
|
// A person's upgrade policy is the assigner's (novox/hq ADR 0565 §7, §8, issue 1953 D4, D5): kept in its state `policies`, one
|
|
// key per module, set and taken back through `mesh-wants.upgrade`. Every reader takes one derivation, mesh-sdk's
|
|
// catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus never rolled.
|
|
// Until the lease write the controller still writes a policy beside each composition's digest; where it differs from the
|
|
// assigner's, the assigner says so as its finding `policy-split`, naming mesh-wants.upgrade as the remedy.
|
|
|
|
// PoliciesBucket is the assigner's state `policies`.
|
|
var PoliciesBucket = delivery.AssignerModule + "_" + delivery.AssignerPoliciesState
|
|
|
|
// DecodePolicies reads the policies state's values by key (the module); a value that cannot be read is left out, named.
|
|
func DecodePolicies(values map[string][]byte) (map[string]catalogue.PersonPolicy, []string) {
|
|
out := map[string]catalogue.PersonPolicy{}
|
|
var bad []string
|
|
for key, raw := range values {
|
|
var p catalogue.PersonPolicy
|
|
if err := json.Unmarshal(raw, &p); err != nil || p.V != catalogue.PersonPolicyVersion || p.Check() != nil || p.Module != key {
|
|
bad = append(bad, key)
|
|
continue
|
|
}
|
|
out[key] = p
|
|
}
|
|
sort.Strings(bad)
|
|
return out, bad
|
|
}
|
|
|
|
// PolicyOf is a module's policy and where it comes from, with the person's choice for it when there is one.
|
|
func PolicyOf(m catalogue.Manifest, chosen map[string]catalogue.PersonPolicy) (policy, from, why, by string) {
|
|
var c *catalogue.PersonPolicy
|
|
if p, ok := chosen[m.Module]; ok {
|
|
c = &p
|
|
}
|
|
return catalogue.Upgrade(m, c)
|
|
}
|
|
|
|
// ControllerPolicies is, by module, every policy the controller writes beside a composition's digest of it.
|
|
func ControllerPolicies(composed map[string][]byte) map[string][]string {
|
|
out := map[string][]string{}
|
|
for key, raw := range composed {
|
|
_, module, _, isNode, err := delivery.ParseComposedKey(key)
|
|
if err != nil || isNode {
|
|
continue
|
|
}
|
|
d, err := delivery.DecodeComposedDigest(raw)
|
|
if err != nil || d.Policy == "" {
|
|
continue
|
|
}
|
|
if !contains(out[module], d.Policy) {
|
|
out[module] = append(out[module], d.Policy)
|
|
sort.Strings(out[module])
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func contains(list []string, s string) bool {
|
|
for _, v := range list {
|
|
if v == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Split is a module whose policy at the assigner differs from one the controller writes beside a composition of it.
|
|
type Split struct {
|
|
Module string `json:"module"`
|
|
Policy string `json:"policy"`
|
|
From string `json:"from"`
|
|
Controller []string `json:"controller"`
|
|
}
|
|
|
|
// Splits is every module of manifests the controller writes a policy for that differs from the assigner's, sorted.
|
|
func Splits(manifests map[string]catalogue.Manifest, chosen map[string]catalogue.PersonPolicy, controller map[string][]string) []Split {
|
|
var out []Split
|
|
for module, theirs := range controller {
|
|
m, ok := manifests[module]
|
|
if !ok {
|
|
continue
|
|
}
|
|
policy, from, _, _ := PolicyOf(m, chosen)
|
|
for _, p := range theirs {
|
|
if p != policy {
|
|
out = append(out, Split{Module: module, Policy: policy, From: from, Controller: theirs})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Module < out[j].Module })
|
|
return out
|
|
}
|
|
|
|
// KindPolicySplit is the assigner's finding of a split policy (ADR 0565 §8).
|
|
const KindPolicySplit = "policy-split"
|
|
|
|
// PolicySplitKey is a module's policy-split finding's key.
|
|
func PolicySplitKey(module string) string {
|
|
return health.FindingKey(health.ScopeModule, module, KindPolicySplit)
|
|
}
|
|
|
|
// PolicySplitFinding is the finding of one split, in mesh-sdk's layout.
|
|
func PolicySplitFinding(s Split, at time.Time) ([]byte, error) {
|
|
summary := fmt.Sprintf("%s's upgrade policy is %s at the assigner (%s) and %s at the controller", s.Module, s.Policy, s.From,
|
|
strings.Join(s.Controller, " and "))
|
|
return health.EncodeFinding(health.Finding{Key: PolicySplitKey(s.Module), Kind: KindPolicySplit,
|
|
Subject: health.Subject{Scope: health.ScopeModule, ID: s.Module}, Severity: health.Warning, Summary: summary,
|
|
Headline: clipHeadline(s.Module + "'s upgrade policy is split"),
|
|
Needs: "set it with mesh-wants.upgrade, so the assigner and the controller agree",
|
|
Explanation: asks.NeedsYou + "set it with mesh-wants.upgrade, so the assigner and the controller agree." + " The controller still " +
|
|
"writes a policy beside each composition until it hands its lease away; a choice made at its upgrade verb after the copy " +
|
|
"is not the assigner's.",
|
|
Resolved: s.Module + "'s upgrade policy agrees again", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}},
|
|
Source: "assigner." + KindPolicySplit, Observer: delivery.AssignerModule, At: at.UTC()})
|
|
}
|
|
|
|
func clipHeadline(s string) string {
|
|
if len([]rune(s)) <= 60 {
|
|
return s
|
|
}
|
|
return string([]rune(s)[:60])
|
|
}
|
|
|
|
// KindProvisionFailed is the assigner's finding of a provision its module's want waits for that failed (ADR 0570 §4).
|
|
const KindProvisionFailed = "provision-failed"
|
|
|
|
// ProvisionFailedKey is a failed provision's finding's key, `provider.<node>.<consumer>.<provision>[.<local>].provision-failed`.
|
|
func ProvisionFailedKey(key string) string {
|
|
return health.FindingKey(health.ScopeProvider, key, KindProvisionFailed)
|
|
}
|
|
|
|
// ProvisionFailedFinding is the finding of a failed provision, naming its consumer, its provider and why.
|
|
func ProvisionFailedFinding(key string, p provisions.Provision, at time.Time) ([]byte, error) {
|
|
summary := fmt.Sprintf("%s for %s on %s failed at %s on %s: %s, so %s is not moved there until it is provided", p.Provision,
|
|
p.Consumer.Module, p.Consumer.Node, p.Provider.Module, p.Provider.Node, p.Why, p.Consumer.Module)
|
|
return health.EncodeFinding(health.Finding{Key: ProvisionFailedKey(key), Kind: KindProvisionFailed,
|
|
Subject: health.Subject{Scope: health.ScopeProvider, ID: key, Machine: p.Consumer.Node, Also: also(p.Provider.Node, p.Consumer.Node)},
|
|
Severity: health.Warning, Summary: summary, Headline: clipHeadline(p.Consumer.Module + " waits for a provision that failed"),
|
|
Explanation: asks.NothingToDo + " " + summary + ". It clears once the provider provides it.",
|
|
Resolved: p.Provision + " for " + p.Consumer.Module + " is provided", Evidence: []health.Evidence{{At: at.UTC(), Said: p.Why}},
|
|
Source: "assigner." + KindProvisionFailed, Observer: delivery.AssignerModule, At: at.UTC()})
|
|
}
|
|
|
|
func also(provider, consumer string) []string {
|
|
if provider == "" || provider == consumer {
|
|
return nil
|
|
}
|
|
return []string{provider}
|
|
}
|
|
|
|
// KindProvisionMissing is the assigner's finding of a required provision that has had no key for longer than its bound.
|
|
const KindProvisionMissing = "provision-missing"
|
|
|
|
// ProvisionMissingKey is its key, `provider.<node>.<module>.<provision>.provision-missing`.
|
|
func ProvisionMissingKey(key string) string {
|
|
return health.FindingKey(health.ScopeProvider, key, KindProvisionMissing)
|
|
}
|
|
|
|
// ProvisionMissingFinding is the finding of a required provision with no key past bound (key `<node>.<module>.<provision>`).
|
|
func ProvisionMissingFinding(key string, bound time.Duration, at time.Time) ([]byte, error) {
|
|
parts := strings.SplitN(key, ".", 3)
|
|
if len(parts) != 3 {
|
|
return nil, fmt.Errorf("%q is not <node>.<module>.<provision>", key)
|
|
}
|
|
node, module, provision := parts[0], parts[1], parts[2]
|
|
summary := fmt.Sprintf("%s on %s requires %s and mesh-provisioner has composed no key for it in %s, so %s is not moved there: "+
|
|
"no provider of it may be placed", module, node, provision, bound, module)
|
|
return health.EncodeFinding(health.Finding{Key: ProvisionMissingKey(key), Kind: KindProvisionMissing,
|
|
Subject: health.Subject{Scope: health.ScopeProvider, ID: key, Machine: node},
|
|
Severity: health.Warning, Summary: summary, Headline: clipHeadline(module + " on " + node + " lacks " + provision),
|
|
Explanation: asks.NothingToDo + " " + summary + ". It clears once the provision has its key.",
|
|
Resolved: provision + " for " + module + " on " + node + " has its key", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}},
|
|
Source: "assigner." + KindProvisionMissing, Observer: delivery.AssignerModule, At: at.UTC()})
|
|
}
|
|
|
|
// KindUnplaced is, from the lease write, a node that runs but no placement names (ADR 0571 §6).
|
|
const KindUnplaced = "unplaced"
|
|
|
|
// UnplacedKey is its finding's key, `machine.<node>.unplaced`.
|
|
func UnplacedKey(node string) string {
|
|
return health.FindingKey(health.ScopeMachine, node, KindUnplaced)
|
|
}
|
|
|
|
// UnplacedFinding is the finding of a node no placement names since the lease write.
|
|
func UnplacedFinding(node string, at time.Time) ([]byte, error) {
|
|
summary := fmt.Sprintf("%s runs, but no placement names it: since the controller handed its lease away nothing composes it, "+
|
|
"so the assigner writes it no want", node)
|
|
return health.EncodeFinding(health.Finding{Key: UnplacedKey(node), Kind: KindUnplaced,
|
|
Subject: health.Subject{Scope: health.ScopeMachine, ID: node, Machine: node}, Severity: health.Warning, Summary: summary,
|
|
Headline: clipHeadline(node + " has no placement"), Needs: "place it with mesh-placement.propose.",
|
|
Explanation: asks.NeedsYou + "place it with mesh-placement.propose. " + summary + ".",
|
|
Resolved: node + " is placed", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}},
|
|
Source: "assigner." + KindUnplaced, Observer: delivery.AssignerModule, At: at.UTC()})
|
|
}
|