Files
DoGaMa-serv/internal/instance/configuration_resolver.go
T
codex 35c11aff6d
CI / validate (pull_request) Successful in 26m22s
feat(vrising): support template runtime requirements
2026-08-25 22:32:15 +02:00

123 lines
4.2 KiB
Go

package instance
import (
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
)
// ResolvedConfiguration is the safe, non-secret part of a template's runtime
// configuration. It is persisted with the preview so a recreated container is
// built identically. Secret mutations retain only their field identifier.
type ResolvedConfiguration struct {
Environment map[string]string `json:"environment,omitempty"`
Arguments []string `json:"arguments,omitempty"`
INI []INIMutation `json:"ini,omitempty"`
SecretEnvironment map[string]string `json:"secret_environment,omitempty"`
SecretArguments []SecretArgument `json:"secret_arguments,omitempty"`
}
type SecretArgument struct {
Name string `json:"name"`
ID string `json:"id"`
Type string `json:"type"`
}
type INIMutation struct {
Mount string `json:"mount"`
File string `json:"file"`
Section string `json:"section"`
Key string `json:"key"`
Value string `json:"value,omitempty"`
SecretID string `json:"secret_id,omitempty"`
}
var protectedEnvironment = map[string]bool{"PATH": true, "HOME": true, "HOSTNAME": true, "DOCKER_HOST": true, "DOGAMA_INSTANCE_ID": true}
func ResolveConfiguration(template catalog.Template, values map[string]string) (ResolvedConfiguration, error) {
result := ResolvedConfiguration{Environment: make(map[string]string), SecretEnvironment: make(map[string]string)}
for key, value := range template.Container.Environment {
result.Environment[key] = value
}
for _, field := range template.Configuration.Fields {
value, configured := values[field.ID]
secret := field.Type == "secret"
if !configured {
continue
}
target := field.Target
switch target.Kind {
case "environment":
if value == "" && !field.Required {
continue
}
if protectedEnvironment[target.Name] || strings.HasPrefix(target.Name, "DOGAMA_") {
return ResolvedConfiguration{}, fmt.Errorf("%s targets a protected environment variable", field.ID)
}
if secret {
result.SecretEnvironment[target.Name] = field.ID
continue
}
// A field may replace only the explicitly named template variable.
result.Environment[target.Name] = value
case "argument":
if secret {
result.SecretArguments = append(result.SecretArguments, SecretArgument{Name: target.Name, ID: field.ID, Type: field.Type})
continue
}
argument, include, err := resolveArgument(target.Name, field.Type, value)
if err != nil {
return ResolvedConfiguration{}, fmt.Errorf("%s has an invalid argument target", field.ID)
}
if include {
result.Arguments = append(result.Arguments, argument)
}
case "ini":
mutation := INIMutation{Mount: target.Mount, File: target.File, Section: target.Section, Key: target.Key, Value: value}
if secret {
mutation.SecretID = field.ID
mutation.Value = ""
}
if err := validateINIMutation(mutation); err != nil {
return ResolvedConfiguration{}, fmt.Errorf("%s has an invalid INI target", field.ID)
}
result.INI = append(result.INI, mutation)
default:
return ResolvedConfiguration{}, fmt.Errorf("%s has an unknown configuration target", field.ID)
}
}
sort.Slice(result.INI, func(i, j int) bool {
return result.INI[i].Mount+result.INI[i].File+result.INI[i].Section+result.INI[i].Key < result.INI[j].Mount+result.INI[j].File+result.INI[j].Section+result.INI[j].Key
})
return result, nil
}
func resolveArgument(name, typ, value string) (string, bool, error) {
if strings.TrimSpace(name) == "" || strings.ContainsAny(name, "\x00\n\r") {
return "", false, errors.New("invalid")
}
if typ == "boolean" {
if value == "false" && !strings.Contains(name, "{{value}}") {
return "", false, nil
}
}
if strings.Contains(name, "{{value}}") {
return strings.ReplaceAll(name, "{{value}}", value), true, nil
}
if typ == "boolean" {
return name, true, nil
}
return name + "=" + value, true, nil
}
func validateINIMutation(m INIMutation) error {
if m.Mount == "" || m.File == "" || m.Key == "" || filepath.IsAbs(m.File) || filepath.Clean(m.File) != m.File || strings.HasPrefix(m.File, ".."+string(filepath.Separator)) || strings.ContainsAny(m.File+m.Section+m.Key, "\x00\r\n") {
return errors.New("invalid ini path")
}
return nil
}