Files
DoGaMa-serv/internal/agentwire/plan.go
T

175 lines
6.6 KiB
Go

package agentwire
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
const DeploymentPlanVersion = 1
var (
instanceIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{20,128}$`)
templateIDPattern = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
componentIDPattern = regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`)
digestPattern = regexp.MustCompile(`^[a-f0-9]{64}$`)
)
type DeploymentPlan struct {
SchemaVersion int `json:"schema_version"`
InstanceID string `json:"instance_id"`
TemplateID string `json:"template_id"`
TemplateVersion string `json:"template_version"`
TemplateDigest string `json:"template_digest"`
Image string `json:"image"`
Entrypoint []string `json:"entrypoint,omitempty"`
Arguments []string `json:"arguments,omitempty"`
Ports []PlanPort `json:"ports"`
Mounts []PlanMount `json:"mounts"`
Resources PlanResource `json:"resources"`
StopTimeoutSeconds int `json:"stop_timeout_seconds"`
Labels map[string]string `json:"labels,omitempty"`
User string `json:"user,omitempty"`
PlanDigest string `json:"plan_digest"`
}
type PlanPort struct {
ID string `json:"id"`
Protocol string `json:"protocol"`
ContainerPort int `json:"container_port"`
HostPort int `json:"host_port"`
Publish bool `json:"publish"`
}
type PlanMount struct {
ID string `json:"id"`
HostPath string `json:"host_path"`
ContainerPath string `json:"container_path"`
ReadOnly bool `json:"read_only"`
}
type PlanResource struct {
CPUCores float64 `json:"cpu_cores"`
MemoryMB int `json:"memory_mb"`
StorageGB int `json:"storage_gb"`
}
func (p DeploymentPlan) CanonicalDigest() (string, error) {
copyPlan := p
copyPlan.PlanDigest = ""
copyPlan.Entrypoint = append([]string(nil), p.Entrypoint...)
copyPlan.Arguments = append([]string(nil), p.Arguments...)
copyPlan.Ports = append([]PlanPort(nil), p.Ports...)
copyPlan.Mounts = append([]PlanMount(nil), p.Mounts...)
sort.Slice(copyPlan.Ports, func(i, j int) bool { return copyPlan.Ports[i].ID < copyPlan.Ports[j].ID })
sort.Slice(copyPlan.Mounts, func(i, j int) bool { return copyPlan.Mounts[i].ID < copyPlan.Mounts[j].ID })
body, err := json.Marshal(copyPlan)
if err != nil {
return "", fmt.Errorf("encode deployment plan: %w", err)
}
digest := sha256.Sum256(body)
return hex.EncodeToString(digest[:]), nil
}
func (p DeploymentPlan) Validate() error {
if p.SchemaVersion != DeploymentPlanVersion || !instanceIDPattern.MatchString(p.InstanceID) || !templateIDPattern.MatchString(p.TemplateID) {
return errors.New("invalid deployment plan identity")
}
if strings.TrimSpace(p.TemplateVersion) == "" || !digestPattern.MatchString(p.TemplateDigest) || strings.TrimSpace(p.Image) == "" {
return errors.New("invalid deployment plan template")
}
if p.Resources.CPUCores <= 0 || p.Resources.CPUCores > 256 || p.Resources.MemoryMB < 128 || p.Resources.MemoryMB > 4*1024*1024 || p.Resources.StorageGB < 1 || p.Resources.StorageGB > 100000 {
return errors.New("invalid deployment plan resources")
}
if p.StopTimeoutSeconds < 5 || p.StopTimeoutSeconds > 900 || len(p.Ports) > 32 || len(p.Mounts) == 0 || len(p.Mounts) > 16 {
return errors.New("invalid deployment plan limits")
}
if len(p.Labels) > 64 || len(p.User) > 32 {
return errors.New("invalid deployment plan container configuration")
}
for key, value := range p.Labels {
lower := strings.ToLower(key)
if key == "" || len(key) > 255 || len(value) > 4096 || strings.HasPrefix(lower, "dogama.") || strings.HasPrefix(lower, "io.dogama.") {
return errors.New("invalid deployment plan label")
}
}
if p.User != "" {
parts := strings.Split(p.User, ":")
if len(parts) != 2 {
return errors.New("invalid deployment plan user")
}
for _, part := range parts {
if _, err := strconv.ParseUint(part, 10, 32); err != nil {
return errors.New("invalid deployment plan user")
}
}
}
portIDs := make(map[string]struct{}, len(p.Ports))
hostPorts := make(map[string]struct{})
for _, port := range p.Ports {
if !componentIDPattern.MatchString(port.ID) || (port.Protocol != "tcp" && port.Protocol != "udp") || port.ContainerPort < 1 || port.ContainerPort > 65535 {
return errors.New("invalid deployment plan port")
}
if _, exists := portIDs[port.ID]; exists {
return errors.New("duplicate deployment plan port")
}
portIDs[port.ID] = struct{}{}
if port.Publish {
if port.HostPort < 1 || port.HostPort > 65535 {
return errors.New("invalid published host port")
}
key := fmt.Sprintf("%s/%d", port.Protocol, port.HostPort)
if _, exists := hostPorts[key]; exists {
return errors.New("duplicate published host port")
}
hostPorts[key] = struct{}{}
} else if port.HostPort != 0 {
return errors.New("private port cannot have a host port")
}
}
mountIDs := make(map[string]struct{}, len(p.Mounts))
destinations := make(map[string]struct{}, len(p.Mounts))
for _, mount := range p.Mounts {
if !componentIDPattern.MatchString(mount.ID) || !filepath.IsAbs(mount.HostPath) || filepath.Clean(mount.HostPath) != mount.HostPath || !strings.HasPrefix(mount.ContainerPath, "/") || filepath.Clean(mount.ContainerPath) != mount.ContainerPath || mount.ContainerPath == "/" || strings.ContainsRune(mount.ContainerPath, '\x00') {
return errors.New("invalid deployment plan mount")
}
if _, exists := mountIDs[mount.ID]; exists {
return errors.New("duplicate deployment plan mount")
}
if _, exists := destinations[mount.ContainerPath]; exists {
return errors.New("duplicate deployment plan mount destination")
}
mountIDs[mount.ID] = struct{}{}
destinations[mount.ContainerPath] = struct{}{}
}
expected, err := p.CanonicalDigest()
if err != nil || !digestPattern.MatchString(p.PlanDigest) || p.PlanDigest != expected {
return errors.New("deployment plan digest mismatch")
}
return nil
}
type InstanceState struct {
InstanceID string `json:"instance_id"`
ContainerID string `json:"container_id"`
PlanDigest string `json:"plan_digest"`
Running bool `json:"running"`
Ready bool `json:"ready"`
Health string `json:"health"`
ExitCode int `json:"exit_code,omitempty"`
}
type InstanceStats struct {
InstanceID string `json:"instance_id"`
CPUPercentage float64 `json:"cpu_percentage"`
MemoryBytes uint64 `json:"memory_bytes"`
MemoryLimit uint64 `json:"memory_limit"`
}