Files
DoGaMa-serv/internal/agent/agent_plan.go
T

107 lines
4.0 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 || snapshot.Digest != plan.TemplateDigest {
return nil, errors.New("unknown template snapshot")
}
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 || snapshot.Digest != plan.TemplateDigest {
return errors.New("unknown template snapshot")
}
template := snapshot.Template
imagePrefix := template.Container.Image + ":"
if !strings.HasPrefix(plan.Image, imagePrefix) || !reflect.DeepEqual(plan.Entrypoint, template.Container.Entrypoint) || !reflect.DeepEqual(plan.Arguments, template.Container.Arguments) || plan.StopTimeoutSeconds != template.Container.StopTimeoutSeconds {
return errors.New("container plan 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
}