155 lines
5.7 KiB
Go
155 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
|
|
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, 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) || !reflect.DeepEqual(plan.CapAdd, template.Container.Capabilities.Add) || 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
|
|
}
|