// Package planner builds offline previews. It does not inspect or change a host. package planner import ( "errors" "net" "regexp" "strings" ) const ProtocolVersion = 1 var ( idPattern = regexp.MustCompile(`^[a-z][a-z0-9-]{0,47}$`) digestPattern = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`) labelPattern = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`) ) // Intent contains no secrets. ObservedStateDigest is caller-supplied in offline // mode; a future executor must obtain it independently from the target host. type Intent struct { ProtocolVersion int `json:"protocolVersion"` HostID string `json:"hostId"` InstanceID string `json:"instanceId"` AppID string `json:"appId"` PackageDigest string `json:"packageDigest"` ImageDigest string `json:"imageDigest"` Domain string `json:"domain"` ObservedStateDigest string `json:"observedStateDigest"` } func (i Intent) Validate() error { if i.ProtocolVersion != ProtocolVersion { return errors.New("unsupported protocol version") } for _, id := range []string{i.HostID, i.InstanceID, i.AppID} { if !idPattern.MatchString(id) { return errors.New("invalid resource identifier") } } for _, digest := range []string{i.PackageDigest, i.ImageDigest, i.ObservedStateDigest} { if !digestPattern.MatchString(digest) { return errors.New("expected a SHA-256 digest") } } labels := strings.Split(i.Domain, ".") if len(i.Domain) > 253 || len(labels) < 2 || net.ParseIP(i.Domain) != nil { return errors.New("expected an ASCII DNS hostname") } for _, label := range labels { if !labelPattern.MatchString(label) { return errors.New("invalid DNS hostname label") } } return nil }