feat: validate server tags and add the model field

This commit is contained in:
2026-08-04 13:30:06 +01:00
parent 3530ce6cb7
commit 13cd41d202
2 changed files with 76 additions and 20 deletions
+55
View File
@@ -0,0 +1,55 @@
package services
import (
"errors"
"fmt"
"strings"
)
// ErrInvalidTag is returned for any tag the rules below reject. Handlers map
// it to 400 — a malformed tag is the caller's mistake, not a server fault.
var ErrInvalidTag = errors.New("invalid tag")
const (
maxTagKeyLen = 32
maxTagValueLen = 64
maxTagsPerHost = 20
// Reserved for tags the agent may derive from inventory later. Refusing
// it now means a user tag written today can never collide with a system
// tag invented tomorrow.
sysTagPrefix = "sys:"
)
func validTagRunes(s string) bool {
for _, r := range s {
switch {
case r >= 'a' && r <= 'z':
case r >= '0' && r <= '9':
case r == '-' || r == '_':
default:
return false
}
}
return true
}
// ValidateTags enforces the shape of a whole tag map. It lives in the service
// layer rather than a handler so that every write path — the tags endpoint,
// server create, anything added later — agrees on what a valid tag is.
func ValidateTags(tags map[string]string) error {
if len(tags) > maxTagsPerHost {
return fmt.Errorf("%w: at most %d tags per server", ErrInvalidTag, maxTagsPerHost)
}
for k, v := range tags {
if strings.HasPrefix(k, sysTagPrefix) {
return fmt.Errorf("%w: keys beginning %q are reserved", ErrInvalidTag, sysTagPrefix)
}
if k == "" || len(k) > maxTagKeyLen || !validTagRunes(k) {
return fmt.Errorf("%w: key %q must be 1-%d chars of a-z, 0-9, - or _", ErrInvalidTag, k, maxTagKeyLen)
}
if v == "" || len(v) > maxTagValueLen || !validTagRunes(v) {
return fmt.Errorf("%w: value for %q must be 1-%d chars of a-z, 0-9, - or _", ErrInvalidTag, k, maxTagValueLen)
}
}
return nil
}