Initial push
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ShellQuote wraps s so that a POSIX shell passes it through as a single
|
||||
// literal argument. Single quotes inside are escaped the usual way.
|
||||
func ShellQuote(s string) string {
|
||||
if s == "" {
|
||||
return "''"
|
||||
}
|
||||
if safeArg.MatchString(s) {
|
||||
return s
|
||||
}
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
var safeArg = regexp.MustCompile(`^[A-Za-z0-9_@%+=:,./-]+$`)
|
||||
|
||||
// ShellQuoteAll quotes every argument and joins them with spaces.
|
||||
func ShellQuoteAll(args []string) string {
|
||||
parts := make([]string, len(args))
|
||||
for i, a := range args {
|
||||
parts[i] = ShellQuote(a)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// RenderOptions tunes how a container spec is turned into a create command.
|
||||
type RenderOptions struct {
|
||||
// NameOverride replaces the container name on the target (empty = keep).
|
||||
NameOverride string
|
||||
// KeepStaticIPs re-applies the source IP addresses. Off by default because
|
||||
// the target subnets are often different.
|
||||
KeepStaticIPs bool
|
||||
// SkipNetworks drops all network flags, leaving the container on the
|
||||
// default bridge. Used when the user opts out of network migration.
|
||||
SkipNetworks bool
|
||||
// SkipPorts drops published port flags (useful when the target already
|
||||
// runs something on those ports).
|
||||
SkipPorts bool
|
||||
// DropMounts omits mount flags for destinations listed here, so a
|
||||
// container can be migrated without one of its data locations.
|
||||
DropMounts map[string]bool
|
||||
// AnonymousVolumesAsAnonymous recreates generated-name volumes as fresh
|
||||
// anonymous volumes instead of pinning the source name.
|
||||
AnonymousVolumesAsAnonymous bool
|
||||
}
|
||||
|
||||
// CreateArgs renders the full `docker create ...` argument list for a
|
||||
// container, excluding the leading "docker". The first attached network is
|
||||
// applied here; any additional networks need NetworkConnectArgs afterwards
|
||||
// because `docker create` accepts only one --network.
|
||||
func (c *Container) CreateArgs(o RenderOptions) []string {
|
||||
name := c.Name
|
||||
if o.NameOverride != "" {
|
||||
name = o.NameOverride
|
||||
}
|
||||
|
||||
a := []string{"create", "--name", name}
|
||||
|
||||
add := func(v ...string) { a = append(a, v...) }
|
||||
flag := func(f, v string) {
|
||||
if v != "" {
|
||||
add(f, v)
|
||||
}
|
||||
}
|
||||
|
||||
flag("--hostname", c.Hostname)
|
||||
flag("--domainname", c.Domainname)
|
||||
flag("--user", c.User)
|
||||
flag("--workdir", c.WorkingDir)
|
||||
|
||||
for _, e := range c.Env {
|
||||
add("--env", e)
|
||||
}
|
||||
for _, k := range sortedKeys(c.Labels) {
|
||||
if isManagedLabel(k) {
|
||||
continue
|
||||
}
|
||||
add("--label", k+"="+c.Labels[k])
|
||||
}
|
||||
|
||||
if c.Tty {
|
||||
add("--tty")
|
||||
}
|
||||
if c.OpenStdin {
|
||||
add("--interactive")
|
||||
}
|
||||
flag("--stop-signal", c.StopSignal)
|
||||
if c.StopTimeout != nil {
|
||||
add("--stop-timeout", strconv.Itoa(*c.StopTimeout))
|
||||
}
|
||||
if c.Init != nil && *c.Init {
|
||||
add("--init")
|
||||
}
|
||||
|
||||
if c.RestartPolicy != "" && c.RestartPolicy != "no" {
|
||||
if c.RestartPolicy == "on-failure" && c.RestartMaxRetries > 0 {
|
||||
add("--restart", fmt.Sprintf("on-failure:%d", c.RestartMaxRetries))
|
||||
} else {
|
||||
add("--restart", c.RestartPolicy)
|
||||
}
|
||||
}
|
||||
// --rm is intentionally never re-applied: an auto-removing container would
|
||||
// vanish before the operator can verify the migration.
|
||||
|
||||
if c.Privileged {
|
||||
add("--privileged")
|
||||
}
|
||||
if c.ReadonlyRootfs {
|
||||
add("--read-only")
|
||||
}
|
||||
for _, v := range c.CapAdd {
|
||||
add("--cap-add", v)
|
||||
}
|
||||
for _, v := range c.CapDrop {
|
||||
add("--cap-drop", v)
|
||||
}
|
||||
for _, v := range c.SecurityOpt {
|
||||
add("--security-opt", v)
|
||||
}
|
||||
for _, v := range c.GroupAdd {
|
||||
add("--group-add", v)
|
||||
}
|
||||
for _, k := range sortedKeys(c.Sysctls) {
|
||||
add("--sysctl", k+"="+c.Sysctls[k])
|
||||
}
|
||||
for _, d := range c.Devices {
|
||||
v := d.PathOnHost
|
||||
if d.PathInContainer != "" && d.PathInContainer != d.PathOnHost {
|
||||
v += ":" + d.PathInContainer
|
||||
}
|
||||
if p := d.CgroupPermissions; p != "" && p != "rwm" {
|
||||
if !strings.Contains(v, ":") {
|
||||
v += ":" + d.PathOnHost
|
||||
}
|
||||
v += ":" + p
|
||||
}
|
||||
add("--device", v)
|
||||
}
|
||||
for _, u := range c.Ulimits {
|
||||
add("--ulimit", fmt.Sprintf("%s=%d:%d", u.Name, u.Soft, u.Hard))
|
||||
}
|
||||
if c.Runtime != "" && c.Runtime != "runc" {
|
||||
add("--runtime", c.Runtime)
|
||||
}
|
||||
|
||||
flag("--pid", nonDefault(c.PidMode, ""))
|
||||
flag("--ipc", nonDefault(c.IpcMode, "private", "shareable"))
|
||||
flag("--uts", nonDefault(c.UtsMode, ""))
|
||||
flag("--userns", nonDefault(c.UsernsMode, ""))
|
||||
// "private" is what a cgroup v2 host reports by default, and passing it
|
||||
// explicitly breaks on a target whose kernel only has cgroup v1. Only the
|
||||
// deliberate "host" override is worth carrying across.
|
||||
flag("--cgroupns", nonDefault(c.CgroupnsMode, "private"))
|
||||
|
||||
for _, v := range c.DNS {
|
||||
add("--dns", v)
|
||||
}
|
||||
for _, v := range c.DNSSearch {
|
||||
add("--dns-search", v)
|
||||
}
|
||||
for _, v := range c.DNSOptions {
|
||||
add("--dns-option", v)
|
||||
}
|
||||
for _, v := range c.ExtraHosts {
|
||||
add("--add-host", v)
|
||||
}
|
||||
|
||||
// Networking. Only the first endpoint can be expressed here.
|
||||
if !o.SkipNetworks {
|
||||
switch {
|
||||
case strings.HasPrefix(c.NetworkMode, "container:"):
|
||||
add("--network", c.NetworkMode)
|
||||
case c.NetworkMode == "host" || c.NetworkMode == "none":
|
||||
add("--network", c.NetworkMode)
|
||||
case len(c.Endpoints) > 0:
|
||||
ep := c.Endpoints[0]
|
||||
add("--network", ep.Network)
|
||||
for _, al := range ep.Aliases {
|
||||
add("--network-alias", al)
|
||||
}
|
||||
if o.KeepStaticIPs {
|
||||
flag("--ip", ep.IPv4Address)
|
||||
flag("--ip6", ep.IPv6Address)
|
||||
}
|
||||
flag("--mac-address", ep.MacAddress)
|
||||
case c.NetworkMode != "" && c.NetworkMode != "default":
|
||||
add("--network", c.NetworkMode)
|
||||
}
|
||||
}
|
||||
|
||||
if !o.SkipPorts && c.NetworkMode != "host" {
|
||||
for _, p := range c.Ports {
|
||||
add("--publish", p.String())
|
||||
}
|
||||
if c.PublishAll {
|
||||
add("--publish-all")
|
||||
}
|
||||
}
|
||||
for _, e := range c.ExposedPorts {
|
||||
if !c.isPublished(e) {
|
||||
add("--expose", e)
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range c.Mounts {
|
||||
if o.DropMounts[m.Destination] {
|
||||
continue
|
||||
}
|
||||
switch m.Kind {
|
||||
case MountTmpfs:
|
||||
if m.TmpfsOpts != "" {
|
||||
add("--tmpfs", m.Destination+":"+m.TmpfsOpts)
|
||||
} else {
|
||||
add("--tmpfs", m.Destination)
|
||||
}
|
||||
case MountAnonymous:
|
||||
if o.AnonymousVolumesAsAnonymous || m.Name == "" {
|
||||
add("--volume", m.Destination+roSuffix(m))
|
||||
} else {
|
||||
add("--volume", m.Name+":"+m.Destination+roSuffix(m))
|
||||
}
|
||||
case MountVolume:
|
||||
add("--volume", m.Name+":"+m.Destination+roSuffix(m))
|
||||
case MountBind:
|
||||
v := m.Source + ":" + m.Destination + roSuffix(m)
|
||||
if m.Propagation != "" && m.Propagation != "rprivate" {
|
||||
if roSuffix(m) == "" {
|
||||
v += ":" + m.Propagation
|
||||
} else {
|
||||
v += "," + m.Propagation
|
||||
}
|
||||
}
|
||||
add("--volume", v)
|
||||
}
|
||||
}
|
||||
|
||||
if c.LogDriver != "" && c.LogDriver != "json-file" {
|
||||
add("--log-driver", c.LogDriver)
|
||||
}
|
||||
for _, k := range sortedKeys(c.LogOptions) {
|
||||
add("--log-opt", k+"="+c.LogOptions[k])
|
||||
}
|
||||
|
||||
if h := c.Healthcheck; h != nil && len(h.Test) > 0 {
|
||||
switch h.Test[0] {
|
||||
case "NONE":
|
||||
add("--no-healthcheck")
|
||||
case "CMD":
|
||||
add("--health-cmd", ShellQuoteAll(h.Test[1:]))
|
||||
case "CMD-SHELL":
|
||||
if len(h.Test) > 1 {
|
||||
add("--health-cmd", h.Test[1])
|
||||
}
|
||||
}
|
||||
if h.Interval > 0 {
|
||||
add("--health-interval", durStr(h.Interval))
|
||||
}
|
||||
if h.Timeout > 0 {
|
||||
add("--health-timeout", durStr(h.Timeout))
|
||||
}
|
||||
if h.StartPeriod > 0 {
|
||||
add("--health-start-period", durStr(h.StartPeriod))
|
||||
}
|
||||
if h.Retries > 0 {
|
||||
add("--health-retries", strconv.Itoa(h.Retries))
|
||||
}
|
||||
}
|
||||
|
||||
r := c.Resources
|
||||
if r.Memory > 0 {
|
||||
add("--memory", strconv.FormatInt(r.Memory, 10))
|
||||
}
|
||||
if r.MemoryReservation > 0 {
|
||||
add("--memory-reservation", strconv.FormatInt(r.MemoryReservation, 10))
|
||||
}
|
||||
if r.MemorySwap != 0 {
|
||||
add("--memory-swap", strconv.FormatInt(r.MemorySwap, 10))
|
||||
}
|
||||
if r.MemorySwappiness != nil && *r.MemorySwappiness >= 0 {
|
||||
add("--memory-swappiness", strconv.FormatInt(*r.MemorySwappiness, 10))
|
||||
}
|
||||
if r.NanoCPUs > 0 {
|
||||
add("--cpus", strconv.FormatFloat(float64(r.NanoCPUs)/1e9, 'f', -1, 64))
|
||||
}
|
||||
if r.CPUShares > 0 {
|
||||
add("--cpu-shares", strconv.FormatInt(r.CPUShares, 10))
|
||||
}
|
||||
if r.CPUPeriod > 0 {
|
||||
add("--cpu-period", strconv.FormatInt(r.CPUPeriod, 10))
|
||||
}
|
||||
if r.CPUQuota > 0 {
|
||||
add("--cpu-quota", strconv.FormatInt(r.CPUQuota, 10))
|
||||
}
|
||||
flag("--cpuset-cpus", r.CpusetCpus)
|
||||
flag("--cpuset-mems", r.CpusetMems)
|
||||
if r.PidsLimit != nil && *r.PidsLimit > 0 {
|
||||
add("--pids-limit", strconv.FormatInt(*r.PidsLimit, 10))
|
||||
}
|
||||
if r.OomKillDisable != nil && *r.OomKillDisable {
|
||||
add("--oom-kill-disable")
|
||||
}
|
||||
if r.OomScoreAdj != 0 {
|
||||
add("--oom-score-adj", strconv.Itoa(r.OomScoreAdj))
|
||||
}
|
||||
if r.ShmSize > 0 && r.ShmSize != 67108864 {
|
||||
add("--shm-size", strconv.FormatInt(r.ShmSize, 10))
|
||||
}
|
||||
|
||||
if c.EntrypointSet && len(c.Entrypoint) > 0 {
|
||||
// docker only accepts a single --entrypoint token; extra words are
|
||||
// prepended to the command instead.
|
||||
add("--entrypoint", c.Entrypoint[0])
|
||||
}
|
||||
|
||||
add(c.Image)
|
||||
|
||||
if c.EntrypointSet && len(c.Entrypoint) > 1 {
|
||||
add(c.Entrypoint[1:]...)
|
||||
}
|
||||
if c.CmdSet {
|
||||
add(c.Cmd...)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// NetworkConnectArgs renders `docker network connect ...` for every endpoint
|
||||
// beyond the first, which `docker create` could not express.
|
||||
func (c *Container) NetworkConnectArgs(o RenderOptions) [][]string {
|
||||
if o.SkipNetworks || len(c.Endpoints) < 2 {
|
||||
return nil
|
||||
}
|
||||
name := c.Name
|
||||
if o.NameOverride != "" {
|
||||
name = o.NameOverride
|
||||
}
|
||||
var out [][]string
|
||||
for _, ep := range c.Endpoints[1:] {
|
||||
a := []string{"network", "connect"}
|
||||
for _, al := range ep.Aliases {
|
||||
a = append(a, "--alias", al)
|
||||
}
|
||||
if o.KeepStaticIPs {
|
||||
if ep.IPv4Address != "" {
|
||||
a = append(a, "--ip", ep.IPv4Address)
|
||||
}
|
||||
if ep.IPv6Address != "" {
|
||||
a = append(a, "--ip6", ep.IPv6Address)
|
||||
}
|
||||
}
|
||||
for _, l := range ep.Links {
|
||||
a = append(a, "--link", l)
|
||||
}
|
||||
out = append(out, append(a, ep.Network, name))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CreateArgs renders `docker volume create ...` for a named volume.
|
||||
func (v Volume) CreateArgs() []string {
|
||||
a := []string{"volume", "create"}
|
||||
if v.Driver != "" && v.Driver != "local" {
|
||||
a = append(a, "--driver", v.Driver)
|
||||
}
|
||||
for _, k := range sortedKeys(v.DriverOpts) {
|
||||
a = append(a, "--opt", k+"="+v.DriverOpts[k])
|
||||
}
|
||||
for _, k := range sortedKeys(v.Labels) {
|
||||
if isManagedLabel(k) {
|
||||
continue
|
||||
}
|
||||
a = append(a, "--label", k+"="+v.Labels[k])
|
||||
}
|
||||
return append(a, v.Name)
|
||||
}
|
||||
|
||||
// CreateArgs renders `docker network create ...` for a user-defined network.
|
||||
func (n Network) CreateArgs() []string {
|
||||
a := []string{"network", "create"}
|
||||
if n.Driver != "" {
|
||||
a = append(a, "--driver", n.Driver)
|
||||
}
|
||||
if n.EnableIPv6 {
|
||||
a = append(a, "--ipv6")
|
||||
}
|
||||
if n.Internal {
|
||||
a = append(a, "--internal")
|
||||
}
|
||||
if n.Attachable {
|
||||
a = append(a, "--attachable")
|
||||
}
|
||||
if n.IPAMDriver != "" && n.IPAMDriver != "default" {
|
||||
a = append(a, "--ipam-driver", n.IPAMDriver)
|
||||
}
|
||||
for _, p := range n.IPAMPools {
|
||||
if p.Subnet != "" {
|
||||
a = append(a, "--subnet", p.Subnet)
|
||||
}
|
||||
if p.IPRange != "" {
|
||||
a = append(a, "--ip-range", p.IPRange)
|
||||
}
|
||||
if p.Gateway != "" {
|
||||
a = append(a, "--gateway", p.Gateway)
|
||||
}
|
||||
for _, k := range sortedKeys(p.AuxAddress) {
|
||||
a = append(a, "--aux-address", k+"="+p.AuxAddress[k])
|
||||
}
|
||||
}
|
||||
for _, k := range sortedKeys(n.Options) {
|
||||
a = append(a, "--opt", k+"="+n.Options[k])
|
||||
}
|
||||
for _, k := range sortedKeys(n.Labels) {
|
||||
if isManagedLabel(k) {
|
||||
continue
|
||||
}
|
||||
a = append(a, "--label", k+"="+n.Labels[k])
|
||||
}
|
||||
return append(a, n.Name)
|
||||
}
|
||||
|
||||
// String renders a port binding in `docker publish` syntax.
|
||||
func (p PortBinding) String() string {
|
||||
port := p.ContainerPort
|
||||
proto := "tcp"
|
||||
if i := strings.LastIndex(port, "/"); i >= 0 {
|
||||
proto, port = port[i+1:], port[:i]
|
||||
}
|
||||
var b strings.Builder
|
||||
if p.HostIP != "" && p.HostIP != "0.0.0.0" {
|
||||
b.WriteString(p.HostIP + ":")
|
||||
}
|
||||
b.WriteString(p.HostPort + ":" + port)
|
||||
if proto != "tcp" {
|
||||
b.WriteString("/" + proto)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (c *Container) isPublished(exposed string) bool {
|
||||
for _, p := range c.Ports {
|
||||
if p.ContainerPort == exposed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func roSuffix(m Mount) string {
|
||||
if m.ReadOnly {
|
||||
return ":ro"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// nonDefault returns v unless it is one of the values Docker would have chosen
|
||||
// anyway, in which case emitting a flag adds noise without changing behaviour.
|
||||
func nonDefault(v string, defaults ...string) string {
|
||||
if v == "" || v == "default" {
|
||||
return ""
|
||||
}
|
||||
for _, d := range defaults {
|
||||
if v == d {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// isManagedLabel filters out labels Docker or compose maintain themselves;
|
||||
// re-applying them would make the target look like it belongs to a compose
|
||||
// project that is not actually there.
|
||||
func isManagedLabel(k string) bool {
|
||||
return strings.HasPrefix(k, "com.docker.compose.") ||
|
||||
strings.HasPrefix(k, "com.docker.swarm.") ||
|
||||
strings.HasPrefix(k, "desktop.docker.io/")
|
||||
}
|
||||
|
||||
func durStr(ns int64) string {
|
||||
if ns%1e9 == 0 {
|
||||
return strconv.FormatInt(ns/1e9, 10) + "s"
|
||||
}
|
||||
return strconv.FormatInt(ns/1e6, 10) + "ms"
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user