pkg/dev/docker_opts.go
package dev
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/opts"
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/spf13/pflag"
)
// ContainerOptions is a data object with all the options for creating a container
type ContainerOptions struct {
attach opts.ListOpts
volumes opts.ListOpts
mounts opts.MountOpt
publish opts.ListOpts
expose opts.ListOpts
privileged bool
publishAll bool
stdin bool
tty bool
entrypoint string
netMode opts.NetworkOpt
autoRemove bool
Image string
Args []string
}
// addFlags adds all command line flags that will be used by Parse to the FlagSet
func addFlags(flags *pflag.FlagSet) *ContainerOptions {
copts := &ContainerOptions{
attach: opts.NewListOpts(validateAttach),
expose: opts.NewListOpts(nil),
publish: opts.NewListOpts(nil),
volumes: opts.NewListOpts(nil),
}
// General purpose flags
flags.VarP(&copts.attach, "attach", "a", "Attach to STDIN, STDOUT or STDERR")
_ = flags.MarkHidden("attach")
flags.StringVar(&copts.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image")
flags.BoolVarP(&copts.stdin, "interactive", "i", true, "Keep STDIN open even if not attached")
_ = flags.MarkHidden("interactive")
flags.BoolVarP(&copts.tty, "tty", "t", true, "Allocate a pseudo-TTY")
_ = flags.MarkHidden("tty")
flags.BoolVar(&copts.autoRemove, "rm", true, "Automatically remove the container when it exits")
_ = flags.MarkHidden("rm")
// Security
flags.BoolVar(&copts.privileged, "privileged", true, "Give extended privileges to this container")
flags.Var(&copts.expose, "expose", "Expose a port or a range of ports")
flags.VarP(&copts.publish, "publish", "p", "Publish a container's port(s) to the host")
flags.BoolVarP(&copts.publishAll, "publish-all", "P", false, "Publish all exposed ports to random ports")
// Logging and storage
flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume")
return copts
}
// Parse parses the args for the specified command and generates a Config,
// a HostConfig and returns them with the specified command.
// If the specified args are not valid, it will return an error.
func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*Config, *HostConfig, error) {
var (
attachStdin = copts.attach.Get("stdin")
attachStdout = copts.attach.Get("stdout")
attachStderr = copts.attach.Get("stderr")
)
if copts.stdin {
attachStdin = true
}
// If -a is not set, attach to stdout and stderr
if copts.attach.Len() == 0 {
attachStdout = true
attachStderr = true
}
var err error
mounts := copts.mounts.Value()
var binds []string
volumes := copts.volumes.GetMap()
// add any bind targets to the list of container volumes
for bind := range copts.volumes.GetMap() {
parsed, err := loader.ParseVolume(bind)
if err != nil {
return nil, nil, err
}
if parsed.Source != "" {
toBind := bind
if parsed.Type == string(mounttypes.TypeBind) {
if hostPart, targetPath, ok := strings.Cut(bind, ":"); ok {
if strings.HasPrefix(hostPart, "."+string(filepath.Separator)) || hostPart == "." {
if absHostPart, err := filepath.Abs(hostPart); err == nil {
hostPart = absHostPart
}
}
toBind = hostPart + ":" + targetPath
}
}
// after creating the bind mount we want to delete it from the copts.volumes values because
// we do not want bind mounts being committed to image configs
binds = append(binds, toBind)
// We should delete from the map (`volumes`) here, as deleting from copts.volumes will not work if
// there are duplicates entries.
delete(volumes, bind)
}
}
var (
runCmd strslice.StrSlice
entrypoint strslice.StrSlice
)
if len(copts.Args) > 0 {
runCmd = copts.Args
}
if copts.entrypoint != "" {
entrypoint = strslice.StrSlice{copts.entrypoint}
} else if flags.Changed("entrypoint") {
// if `--entrypoint=` is parsed then Entrypoint is reset
entrypoint = []string{""}
}
publishOpts := copts.publish.GetAll()
var (
ports map[nat.Port]struct{}
portBindings map[nat.Port][]nat.PortBinding
convertedOpts []string
)
convertedOpts, err = convertToStandardNotation(publishOpts)
if err != nil {
return nil, nil, err
}
ports, portBindings, err = nat.ParsePortSpecs(convertedOpts)
if err != nil {
return nil, nil, err
}
// Merge in exposed ports to the map of published ports
for _, e := range copts.expose.GetAll() {
if strings.Contains(e, ":") {
return nil, nil, errors.Errorf("invalid port format for --expose: %s", e)
}
// support two formats for expose, original format <portnum>/[<proto>]
// or <startport-endport>/[<proto>]
proto, port := nat.SplitProtoPort(e)
// Parse the start and end port and create a sequence of ports to expose
// if expose a port, the start and end port are the same
start, end, err := nat.ParsePortRange(port)
if err != nil {
return nil, nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err)
}
for i := start; i <= end; i++ {
p, err := nat.NewPort(proto, strconv.FormatUint(i, 10))
if err != nil {
return nil, nil, err
}
if _, exists := ports[p]; !exists {
ports[p] = struct{}{}
}
}
}
config := &Config{
ExposedPorts: ports,
Tty: copts.tty,
OpenStdin: copts.stdin,
AttachStdin: attachStdin,
AttachStdout: attachStdout,
AttachStderr: attachStderr,
Cmd: runCmd,
Volumes: volumes,
Entrypoint: entrypoint,
}
hostConfig := &HostConfig{
Binds: binds,
AutoRemove: copts.autoRemove,
Privileged: copts.privileged,
PortBindings: portBindings,
PublishAllPorts: copts.publishAll,
Mounts: mounts,
}
return config, hostConfig, nil
}
func convertToStandardNotation(ports []string) ([]string, error) {
optsList := []string{}
for _, publish := range ports {
if strings.Contains(publish, "=") {
params := map[string]string{"protocol": "tcp"}
for _, param := range strings.Split(publish, ",") {
k, v, ok := strings.Cut(param, "=")
if !ok || k == "" {
return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
}
params[k] = v
}
optsList = append(optsList, fmt.Sprintf("%s:%s/%s", params["published"], params["target"], params["protocol"]))
} else {
optsList = append(optsList, publish)
}
}
return optsList, nil
}
// validateAttach validates that the specified string is a valid attach option.
func validateAttach(val string) (string, error) {
s := strings.ToLower(val)
for _, str := range []string{"stdin", "stdout", "stderr"} {
if s == str {
return s, nil
}
}
return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR")
}
type Config struct {
AttachStdin bool // Attach the standard input, makes possible user interaction
AttachStdout bool // Attach the standard output
AttachStderr bool // Attach the standard error
ExposedPorts nat.PortSet `json:",omitempty"` // List of exposed ports
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
OpenStdin bool // Open stdin
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
Cmd strslice.StrSlice // Command to run when starting the container
Volumes map[string]struct{} // List of volumes (mounts) used for the container
Entrypoint strslice.StrSlice // Entrypoint to run when starting the container
}
type HostConfig struct {
Binds []string // List of volume bindings for this container
PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host
AutoRemove bool // Automatically remove container when it exits
Privileged bool // Is the container in privileged mode
PublishAllPorts bool // Should docker publish all exposed port for the container
Mounts []mounttypes.Mount `json:",omitempty"`
}
type RunOptions struct {
SigProxy bool
DetachKeys string
Platform string
Pull string // always, missing, never
}