Files
DoGaMa-serv/internal/agent/agent_plan.go
T
codex b575b5260d
CI / validate (pull_request) Failing after 8m3s
fix(deploy): start current Palworld template
2026-08-15 21:43:48 +02:00

156 lines
5.7 KiB
Go

package agent
import (
"errors"
"fmt"
"io/fs"
"path"
"reflect"
"strings"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/agentwire"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
)
// PlanPolicy independently binds privileged deployment fields to validated,
// embedded template snapshots. The agent never trusts a caller-supplied image,
// container port, mount destination or security-sensitive command by itself.
type PlanPolicy struct {
snapshots map[string]catalog.Snapshot
assets fs.FS
}
func NewPlanPolicy(snapshots []catalog.Snapshot, assets fs.FS) (*PlanPolicy, error) {
if len(snapshots) == 0 {
return nil, errors.New("agent plan policy requires validated templates")
}
if assets == nil {
return nil, errors.New("agent plan policy requires embedded assets")
}
policy := &PlanPolicy{snapshots: make(map[string]catalog.Snapshot, len(snapshots)), assets: assets}
for _, snapshot := range snapshots {
key := snapshot.Template.ID + "@" + snapshot.Template.Version
if _, exists := policy.snapshots[key]; exists {
return nil, errors.New("duplicate agent template snapshot")
}
policy.snapshots[key] = snapshot
}
return policy, nil
}
type ApprovedAsset struct {
Destination string
SHA256 string
Content []byte
}
func (p *PlanPolicy) Assets(plan agentwire.DeploymentPlan) ([]ApprovedAsset, error) {
snapshot, ok := p.snapshots[plan.TemplateID+"@"+plan.TemplateVersion]
if !ok {
return nil, fmt.Errorf("template snapshot not registered: %s@%s", plan.TemplateID, plan.TemplateVersion)
}
if snapshot.Digest != plan.TemplateDigest {
return nil, fmt.Errorf("template snapshot digest mismatch: received=%s expected=%s", plan.TemplateDigest, snapshot.Digest)
}
result := make([]ApprovedAsset, 0, len(snapshot.Template.Container.Assets))
for _, asset := range snapshot.Template.Container.Assets {
if !asset.ReadOnly {
return nil, errors.New("writable template asset is not allowed")
}
content, err := fs.ReadFile(p.assets, path.Join(snapshot.AssetRoot, asset.Source))
if err != nil {
return nil, fmt.Errorf("read approved template asset: %w", err)
}
result = append(result, ApprovedAsset{Destination: asset.Destination, SHA256: asset.SHA256, Content: content})
}
return result, nil
}
func (p *PlanPolicy) Validate(plan agentwire.DeploymentPlan) error {
if p == nil || plan.Validate() != nil {
return errors.New("invalid deployment plan")
}
snapshot, ok := p.snapshots[plan.TemplateID+"@"+plan.TemplateVersion]
if !ok {
return fmt.Errorf("template snapshot not registered: %s@%s", plan.TemplateID, plan.TemplateVersion)
}
if snapshot.Digest != plan.TemplateDigest {
return fmt.Errorf("template snapshot digest mismatch: received=%s expected=%s", plan.TemplateDigest, snapshot.Digest)
}
template := snapshot.Template
imagePrefix := template.Container.Image + ":"
if !strings.HasPrefix(plan.Image, imagePrefix) || !reflect.DeepEqual(plan.Entrypoint, template.Container.Entrypoint) || plan.StopTimeoutSeconds != template.Container.StopTimeoutSeconds {
return errors.New("container plan differs from template")
}
if len(plan.Arguments) < len(template.Container.Arguments) || !reflect.DeepEqual(plan.Arguments[:len(template.Container.Arguments)], template.Container.Arguments) || !allowedArguments(template, plan.Arguments[len(template.Container.Arguments):]) || !allowedEnvironment(template, plan.Environment) {
return errors.New("container configuration differs from template")
}
if plan.Resources.CPUCores < template.Requirements.Minimum.CPUCores || plan.Resources.MemoryMB < template.Requirements.Minimum.MemoryMB || plan.Resources.StorageGB < template.Requirements.Minimum.StorageGB {
return errors.New("container resources are below template minimum")
}
if len(plan.Ports) != len(template.Container.Ports) || len(plan.Mounts) != len(template.Storage.Mounts) {
return errors.New("container plan shape differs from template")
}
ports := make(map[string]agentwire.PlanPort, len(plan.Ports))
for _, port := range plan.Ports {
ports[port.ID] = port
}
for _, expected := range template.Container.Ports {
actual, ok := ports[expected.ID]
if !ok || actual.Protocol != expected.Protocol || actual.ContainerPort != expected.ContainerPort || actual.Publish != expected.Publish {
return errors.New("container port differs from template")
}
}
mounts := make(map[string]agentwire.PlanMount, len(plan.Mounts))
for _, mount := range plan.Mounts {
mounts[mount.ID] = mount
}
for _, expected := range template.Storage.Mounts {
actual, ok := mounts[expected.ID]
if !ok || actual.ContainerPath != expected.ContainerPath || actual.ReadOnly != expected.ReadOnly {
return errors.New("container mount differs from template")
}
}
return nil
}
func allowedEnvironment(template catalog.Template, environment map[string]string) bool {
allowed := map[string]bool{}
for key := range template.Container.Environment {
allowed[key] = true
}
for _, field := range template.Configuration.Fields {
if field.Target.Kind == "environment" {
allowed[field.Target.Name] = true
}
}
for key := range environment {
if !allowed[key] {
return false
}
}
return true
}
func allowedArguments(template catalog.Template, arguments []string) bool {
allowed := make([]string, 0)
for _, field := range template.Configuration.Fields {
if field.Target.Kind == "argument" {
allowed = append(allowed, strings.ReplaceAll(field.Target.Name, "{{value}}", ""))
}
}
for _, argument := range arguments {
ok := false
for _, prefix := range allowed {
if argument == prefix || strings.HasPrefix(argument, prefix+"=") || (strings.HasSuffix(prefix, "=") && strings.HasPrefix(argument, prefix)) {
ok = true
break
}
}
if !ok {
return false
}
}
return true
}