217 lines
6.0 KiB
Go
217 lines
6.0 KiB
Go
package instance
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
const (
|
|
DockerUserDoGaMa = "dogama"
|
|
DockerUserCustom = "custom"
|
|
DockerUserImage = "image"
|
|
ImageTagTracked = "tracked"
|
|
ImageTagPinned = "pinned"
|
|
)
|
|
|
|
var (
|
|
labelKeyPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]*(?:/[A-Za-z0-9][A-Za-z0-9_.-]*)?$`)
|
|
tagPattern = regexp.MustCompile(`^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$`)
|
|
variablePattern = regexp.MustCompile(`\{\{([^{}]+)\}\}`)
|
|
)
|
|
|
|
var AllowedLabelVariables = []string{
|
|
"game.name", "game.id", "game.icon_url", "instance.name", "instance.id", "instance.slug", "server.name",
|
|
}
|
|
|
|
type DockerUser struct {
|
|
Mode string `json:"mode"`
|
|
UID *uint32 `json:"uid,omitempty"`
|
|
GID *uint32 `json:"gid,omitempty"`
|
|
}
|
|
|
|
type ImageTag struct {
|
|
Mode string `json:"mode"`
|
|
Tag string `json:"tag"`
|
|
}
|
|
|
|
type ContainerConfiguration struct {
|
|
Labels map[string]string `json:"labels"`
|
|
DockerUser DockerUser `json:"docker_user"`
|
|
ImageTag ImageTag `json:"image_tag"`
|
|
}
|
|
|
|
type LabelContext struct {
|
|
GameName, GameID, GameIconURL string
|
|
InstanceName, InstanceID, InstanceSlug string
|
|
ServerName string
|
|
}
|
|
|
|
func ParseLabels(input string) (map[string]string, error) {
|
|
labels := make(map[string]string)
|
|
for index, raw := range strings.Split(strings.ReplaceAll(input, "\r\n", "\n"), "\n") {
|
|
line := strings.TrimSpace(raw)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
separator := strings.IndexByte(line, '=')
|
|
if separator < 1 {
|
|
return nil, fmt.Errorf("label line %d must use key=value", index+1)
|
|
}
|
|
key := strings.TrimSpace(line[:separator])
|
|
if !labelKeyPattern.MatchString(key) {
|
|
return nil, fmt.Errorf("label line %d has an invalid key", index+1)
|
|
}
|
|
lower := strings.ToLower(key)
|
|
if strings.HasPrefix(lower, "dogama.") || strings.HasPrefix(lower, "io.dogama.") {
|
|
return nil, fmt.Errorf("label line %d uses a reserved DoGaMa key", index+1)
|
|
}
|
|
value := line[separator+1:]
|
|
if err := ValidateLabelTemplate(value); err != nil {
|
|
return nil, fmt.Errorf("label line %d: %w", index+1, err)
|
|
}
|
|
labels[key] = value
|
|
}
|
|
return labels, nil
|
|
}
|
|
|
|
func ValidateLabelTemplate(value string) error {
|
|
allowed := make(map[string]bool, len(AllowedLabelVariables))
|
|
for _, variable := range AllowedLabelVariables {
|
|
allowed[variable] = true
|
|
}
|
|
for _, match := range variablePattern.FindAllStringSubmatch(value, -1) {
|
|
if !allowed[match[1]] {
|
|
return fmt.Errorf("unknown label variable %q", match[1])
|
|
}
|
|
}
|
|
withoutKnown := variablePattern.ReplaceAllString(value, "")
|
|
if strings.Contains(withoutKnown, "{{") || strings.Contains(withoutKnown, "}}") {
|
|
return errors.New("invalid label variable syntax")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ResolveLabels(labels map[string]string, context LabelContext) (map[string]string, error) {
|
|
values := map[string]string{
|
|
"game.name": context.GameName, "game.id": context.GameID, "game.icon_url": context.GameIconURL,
|
|
"instance.name": context.InstanceName, "instance.id": context.InstanceID, "instance.slug": context.InstanceSlug,
|
|
"server.name": context.ServerName,
|
|
}
|
|
result := make(map[string]string, len(labels))
|
|
for key, value := range labels {
|
|
if err := ValidateLabelTemplate(value); err != nil {
|
|
return nil, err
|
|
}
|
|
result[key] = variablePattern.ReplaceAllStringFunc(value, func(token string) string {
|
|
name := token[2 : len(token)-2]
|
|
return values[name]
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func MergeLabels(technical, global, local map[string]string) map[string]string {
|
|
result := make(map[string]string, len(technical)+len(global)+len(local))
|
|
for key, value := range global {
|
|
result[key] = value
|
|
}
|
|
for key, value := range local {
|
|
result[key] = value
|
|
}
|
|
for key, value := range technical {
|
|
result[key] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
func FormatLabels(labels map[string]string) string {
|
|
keys := make([]string, 0, len(labels))
|
|
for key := range labels {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
lines := make([]string, 0, len(keys))
|
|
for _, key := range keys {
|
|
lines = append(lines, key+"="+labels[key])
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func Slugify(value string) string {
|
|
decomposed := norm.NFD.String(strings.ToLower(strings.TrimSpace(value)))
|
|
var builder strings.Builder
|
|
separator := false
|
|
for _, r := range decomposed {
|
|
if unicode.Is(unicode.Mn, r) {
|
|
continue
|
|
}
|
|
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
|
|
if separator && builder.Len() > 0 {
|
|
builder.WriteByte('-')
|
|
}
|
|
builder.WriteRune(r)
|
|
separator = false
|
|
} else {
|
|
separator = true
|
|
}
|
|
}
|
|
return strings.Trim(builder.String(), "-")
|
|
}
|
|
|
|
func ValidateDockerUser(user DockerUser) error {
|
|
switch user.Mode {
|
|
case DockerUserDoGaMa, DockerUserImage:
|
|
if user.UID != nil || user.GID != nil {
|
|
return errors.New("UID and GID are only valid in custom mode")
|
|
}
|
|
case DockerUserCustom:
|
|
if user.UID == nil || user.GID == nil {
|
|
return errors.New("custom Docker user requires UID and GID")
|
|
}
|
|
default:
|
|
return errors.New("invalid Docker user mode")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DockerUserValue(user DockerUser, processUID, processGID uint32) (string, error) {
|
|
if err := ValidateDockerUser(user); err != nil {
|
|
return "", err
|
|
}
|
|
switch user.Mode {
|
|
case DockerUserImage:
|
|
return "", nil
|
|
case DockerUserDoGaMa:
|
|
return strconv.FormatUint(uint64(processUID), 10) + ":" + strconv.FormatUint(uint64(processGID), 10), nil
|
|
default:
|
|
return strconv.FormatUint(uint64(*user.UID), 10) + ":" + strconv.FormatUint(uint64(*user.GID), 10), nil
|
|
}
|
|
}
|
|
|
|
func ValidateImageTag(value ImageTag, defaultTag string) (ImageTag, error) {
|
|
if value.Mode == "" {
|
|
value.Mode = ImageTagTracked
|
|
}
|
|
switch value.Mode {
|
|
case ImageTagTracked:
|
|
value.Tag = defaultTag
|
|
case ImageTagPinned:
|
|
if !tagPattern.MatchString(value.Tag) {
|
|
return ImageTag{}, errors.New("invalid pinned Docker image tag")
|
|
}
|
|
default:
|
|
return ImageTag{}, errors.New("invalid Docker image tag mode")
|
|
}
|
|
if !tagPattern.MatchString(value.Tag) {
|
|
return ImageTag{}, errors.New("invalid Docker image tag")
|
|
}
|
|
return value, nil
|
|
}
|