feat: parse tag filters and resolve targets as ids union tag selector
This commit is contained in:
@@ -53,3 +53,26 @@ func ValidateTags(tags map[string]string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseTagFilters turns repeated ?tag=key:value query values into a map.
|
||||
//
|
||||
// A malformed filter is an error rather than a silently ignored value: a
|
||||
// filter that matches nothing and a filter that is nonsense look identical in
|
||||
// a list, and only one of them is the caller's fault.
|
||||
func ParseTagFilters(raw []string) (map[string]string, error) {
|
||||
out := make(map[string]string, len(raw))
|
||||
for _, r := range raw {
|
||||
k, v, found := strings.Cut(r, ":")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w: filter %q must be key:value", ErrInvalidTag, r)
|
||||
}
|
||||
if strings.Contains(v, ":") {
|
||||
return nil, fmt.Errorf("%w: filter %q has more than one colon", ErrInvalidTag, r)
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
if err := ValidateTags(out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
|
||||
)
|
||||
|
||||
// ErrNoTargets means a workflow named no servers and matched none. Handlers
|
||||
// map it to 400: a workflow that matches nothing must say so rather than
|
||||
// report success over zero servers.
|
||||
var ErrNoTargets = errors.New("workflow has no target servers")
|
||||
|
||||
// MatchesTags reports whether srv carries every pair in sel — AND across keys.
|
||||
// An empty selector matches nothing. That is deliberate: the alternative,
|
||||
// "matches everything", turns a cleared field in the workflow designer into a
|
||||
// fleet-wide run.
|
||||
func MatchesTags(srv models.Server, sel map[string]string) bool {
|
||||
if len(sel) == 0 {
|
||||
return false
|
||||
}
|
||||
for k, v := range sel {
|
||||
if srv.Tags[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UnionTargets returns the distinct union of the servers named by ids and
|
||||
// those matching sel, in the order they appear in all.
|
||||
//
|
||||
// Order comes from the fleet rather than the arguments so that two workflows
|
||||
// naming the same servers in a different order still run them in the same
|
||||
// order, which makes two runs comparable line by line.
|
||||
//
|
||||
// Offline servers are NOT filtered out. The dispatcher already answers 503 per
|
||||
// server, and a patch run that silently omits an unreachable machine is worse
|
||||
// than one that visibly fails on it.
|
||||
func UnionTargets(all []models.Server, ids []string, sel map[string]string) []models.Server {
|
||||
named := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
named[id] = true
|
||||
}
|
||||
|
||||
out := make([]models.Server, 0, len(ids)+len(all))
|
||||
for _, s := range all {
|
||||
if named[s.ServerID] || MatchesTags(s, sel) {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user