175 lines
6.6 KiB
Go
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"`
|
|
}
|