107 lines
4.0 KiB
Go
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
|
|
}
|