package hostactors import ( "context" "errors" "sort" "strings" "github.com/carlosframework/platform/internal/release" ) // Box and group roles (region HA spec 2 ยง7, A11-1). A box's role says // whether placement may choose it: the default role may be chosen, and // legacy and platform never are, for a create, a relocation target or a // move target. The box keeps the instances it has (H25). // // The region's group entry carries a role too. Every box automatic // enrollment admits from the group takes the group's role in the same // roster write, and changing the group's role changes the role of every // box enrolled from it, so a replacement launched after the group was // excluded is never placeable, even for a moment. // // The roster stores the default role as "" so a roster written before // roles existed reads as all-default. const ( RoleDefault = "" RoleLegacy = "legacy" RolePlatform = "platform" ) // ErrRole is a role name a box or group cannot take. var ErrRole = errors.New("hostactors: not a role this box or group can take") // ParseBoxRole reads an operator's box role: default, legacy or platform. func ParseBoxRole(s string) (string, error) { switch strings.TrimSpace(s) { case "default": return RoleDefault, nil case RoleLegacy: return RoleLegacy, nil case RolePlatform: return RolePlatform, nil } return "", ErrRole } // ParseGroupRole reads an operator's group role: default or legacy. A // group is never the platform box. func ParseGroupRole(s string) (string, error) { switch strings.TrimSpace(s) { case "default": return RoleDefault, nil case RoleLegacy: return RoleLegacy, nil } return "", ErrRole } // RoleName is the operator's word for a stored role. func RoleName(role string) string { if role == RoleDefault { return "default" } return role } func validBoxRole(role string) bool { return role == RoleDefault || role == RoleLegacy || role == RolePlatform } func validGroupRole(role string) bool { return role == RoleDefault || role == RoleLegacy } // SetBoxRole sets one box's role in a fresh roster write over o. The box // must be enrolled and not retired. Setting the role it already has // changes nothing and returns o. func (d Directory) SetBoxRole(ctx context.Context, sg release.Signer, o Observed, ref, role string) (Observed, error) { if !validBoxRole(role) { return Observed{}, ErrRole } b, ok := o.Roster.Boxes[ref] if !ok || b.State == "retired" { return Observed{}, ErrRoster } if b.Role == role { return o, nil } r := Clone(o.Roster) b = r.Boxes[ref] b.Role = role r.Boxes[ref] = b return d.Replace(ctx, sg, o, r) } // GroupMembers returns, sorted, the boxes enrolled from region's group // that are not retired: the boxes a group role change applies to. func GroupMembers(r Roster, region string) []string { g, ok := r.Regions[region] if !ok || g.GroupName == "" { return nil } var out []string for ref, b := range r.Boxes { if b.Region == region && b.Group == g.GroupName && b.State != "retired" { out = append(out, ref) } } sort.Strings(out) return out } // SetGroupRole sets region's group role and, in the same roster write, the // role of every box enrolled from that group (GroupMembers). The region // must have a group. It returns the roster and the boxes it set. func (d Directory) SetGroupRole(ctx context.Context, sg release.Signer, o Observed, region, role string) (Observed, []string, error) { if !validGroupRole(role) { return Observed{}, nil, ErrRole } g, ok := o.Roster.Regions[region] if !ok || g.GroupName == "" { return Observed{}, nil, ErrRoster } members := GroupMembers(o.Roster, region) changed := g.Role != role for _, ref := range members { changed = changed || o.Roster.Boxes[ref].Role != role } if !changed { return o, members, nil } r := Clone(o.Roster) g.Role = role r.Regions[region] = g for _, ref := range members { b := r.Boxes[ref] b.Role = role r.Boxes[ref] = b } next, err := d.Replace(ctx, sg, o, r) if err != nil { return Observed{}, nil, err } return next, members, nil }