From d28e85741fedb89be48a03d4f05687e970eb71b9 Mon Sep 17 00:00:00 2001 From: cdoern Date: Wed, 14 Jul 2021 16:30:28 -0400 Subject: InfraContainer Rework InfraContainer should go through the same creation process as regular containers. This change was from the cmd level down, involving new container CLI opts and specgen creating functions. What now happens is that both container and pod cli options are populated in cmd and used to create a podSpecgen and a containerSpecgen. The process then goes as follows FillOutSpecGen (infra) -> MapSpec (podOpts -> infraOpts) -> PodCreate -> MakePod -> createPodOptions -> NewPod -> CompleteSpec (infra) -> MakeContainer -> NewContainer -> newContainer -> AddInfra (to pod state) Signed-off-by: cdoern --- pkg/api/handlers/compat/containers_create.go | 3 +- pkg/api/handlers/libpod/containers_create.go | 7 +- pkg/api/handlers/libpod/pods.go | 65 +- pkg/api/handlers/types/types.go | 4 +- pkg/bindings/pods/pods.go | 12 +- pkg/bindings/test/pods_test.go | 7 +- pkg/domain/entities/engine_container.go | 2 +- pkg/domain/entities/pods.go | 183 ++++- pkg/domain/entities/types.go | 38 +- pkg/domain/infra/abi/containers.go | 12 +- pkg/domain/infra/abi/generate.go | 4 +- pkg/domain/infra/abi/play.go | 250 ++++--- pkg/domain/infra/abi/pods.go | 9 +- pkg/domain/infra/tunnel/events.go | 1 + pkg/domain/infra/tunnel/pods.go | 7 +- pkg/specgen/generate/container_create.go | 63 +- pkg/specgen/generate/kube/kube.go | 32 +- pkg/specgen/generate/namespaces.go | 2 +- pkg/specgen/generate/oci.go | 3 +- pkg/specgen/generate/pod_create.go | 214 +++--- pkg/specgen/podspecgen.go | 3 +- pkg/specgenutil/createparse.go | 34 + pkg/specgenutil/ports.go | 22 + pkg/specgenutil/specgen.go | 1004 ++++++++++++++++++++++++++ pkg/specgenutil/util.go | 274 +++++++ pkg/specgenutil/volumes.go | 630 ++++++++++++++++ 26 files changed, 2563 insertions(+), 322 deletions(-) create mode 100644 pkg/specgenutil/createparse.go create mode 100644 pkg/specgenutil/ports.go create mode 100644 pkg/specgenutil/specgen.go create mode 100644 pkg/specgenutil/util.go create mode 100644 pkg/specgenutil/volumes.go (limited to 'pkg') diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 0b5cbd343..9df35697a 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -11,6 +11,7 @@ import ( "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/infra/abi" "github.com/containers/podman/v3/pkg/specgen" + "github.com/containers/podman/v3/pkg/specgenutil" "github.com/containers/storage" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -80,7 +81,7 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { } sg := specgen.NewSpecGenerator(imgNameOrID, cliOpts.RootFS) - if err := common.FillOutSpecGen(sg, cliOpts, args); err != nil { + if err := specgenutil.FillOutSpecGen(sg, cliOpts, args); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "fill out specgen")) return } diff --git a/pkg/api/handlers/libpod/containers_create.go b/pkg/api/handlers/libpod/containers_create.go index 65951861b..0e2163d5c 100644 --- a/pkg/api/handlers/libpod/containers_create.go +++ b/pkg/api/handlers/libpod/containers_create.go @@ -28,7 +28,12 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - ctr, err := generate.MakeContainer(context.Background(), runtime, &sg) + rtSpec, spec, opts, err := generate.MakeContainer(context.Background(), runtime, &sg) + if err != nil { + utils.InternalServerError(w, err) + return + } + ctr, err := generate.ExecuteCreate(context.Background(), runtime, rtSpec, spec, false, opts...) if err != nil { utils.InternalServerError(w, err) return diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index ff105bc48..3d6cf093b 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -1,11 +1,15 @@ package libpod import ( + "context" "encoding/json" "fmt" "net/http" "strings" + "github.com/containers/common/libimage" + "github.com/containers/common/pkg/config" + "github.com/containers/image/v5/transports/alltransports" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/api/handlers" @@ -14,6 +18,7 @@ import ( "github.com/containers/podman/v3/pkg/domain/infra/abi" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/specgen/generate" + "github.com/containers/podman/v3/pkg/specgenutil" "github.com/containers/podman/v3/pkg/util" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -25,24 +30,70 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { runtime = r.Context().Value("runtime").(*libpod.Runtime) err error ) - var psg specgen.PodSpecGenerator + psg := specgen.PodSpecGenerator{InfraContainerSpec: &specgen.SpecGenerator{}} if err := json.NewDecoder(r.Body).Decode(&psg); err != nil { - utils.Error(w, "failed to decode specgen", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) return } - // parse userns so we get the valid default value of userns - psg.Userns, err = specgen.ParseUserNamespace(psg.Userns.String()) if err != nil { - utils.Error(w, "failed to parse userns", http.StatusInternalServerError, errors.Wrap(err, "failed to parse userns")) + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) return } - pod, err := generate.MakePod(&psg, runtime) + if !psg.NoInfra { + infraOptions := &entities.ContainerCreateOptions{ImageVolume: "bind", IsInfra: true, Net: &entities.NetOptions{}} // options for pulling the image and FillOutSpec + err = specgenutil.FillOutSpecGen(psg.InfraContainerSpec, infraOptions, []string{}) // necessary for default values in many cases (userns, idmappings) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error filling out specgen")) + return + } + out, err := json.Marshal(psg) // marshal our spec so the matching options can be unmarshaled into infra + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) + return + } + tempSpec := &specgen.SpecGenerator{} // temporary spec since infra cannot be decoded into + err = json.Unmarshal(out, tempSpec) // unmarhal matching options + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) + return + } + psg.InfraContainerSpec = tempSpec // set infra spec equal to temp + // a few extra that do not have the same json tags + psg.InfraContainerSpec.Name = psg.InfraName + psg.InfraContainerSpec.ConmonPidFile = psg.InfraConmonPidFile + psg.InfraContainerSpec.ContainerCreateCommand = psg.InfraCommand + imageName := psg.InfraImage + rawImageName := psg.InfraImage + if imageName == "" { + imageName = config.DefaultInfraImage + rawImageName = config.DefaultInfraImage + } + curr := infraOptions.Quiet + infraOptions.Quiet = true + pullOptions := &libimage.PullOptions{} + pulledImages, err := runtime.LibimageRuntime().Pull(context.Background(), imageName, config.PullPolicyMissing, pullOptions) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "could not pull image")) + return + } + if _, err := alltransports.ParseImageName(imageName); err == nil { + if len(pulledImages) != 0 { + imageName = pulledImages[0].ID() + } + } + infraOptions.Quiet = curr + psg.InfraImage = imageName + psg.InfraContainerSpec.Image = imageName + psg.InfraContainerSpec.RawImageName = rawImageName + } + podSpecComplete := entities.PodSpec{PodSpecGen: psg} + pod, err := generate.MakePod(&podSpecComplete, runtime) if err != nil { httpCode := http.StatusInternalServerError if errors.Cause(err) == define.ErrPodExists { httpCode = http.StatusConflict } - utils.Error(w, "Something went wrong.", httpCode, err) + utils.Error(w, "Something went wrong.", httpCode, errors.Wrap(err, "failed to make pod")) return } utils.WriteResponse(w, http.StatusCreated, handlers.IDResponse{ID: pod.ID()}) diff --git a/pkg/api/handlers/types/types.go b/pkg/api/handlers/types/types.go index 71165364f..e7920047e 100644 --- a/pkg/api/handlers/types/types.go +++ b/pkg/api/handlers/types/types.go @@ -1,8 +1,6 @@ package types -import ( - "github.com/containers/podman/v3/pkg/domain/entities" -) +import "github.com/containers/podman/v3/pkg/domain/entities" // LibpodImagesRemoveReport is the return type for image removal via the rest // api. diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index 9d3ff322e..a1a431a3b 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -9,27 +9,25 @@ import ( "github.com/containers/podman/v3/pkg/api/handlers" "github.com/containers/podman/v3/pkg/bindings" "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/containers/podman/v3/pkg/specgen" jsoniter "github.com/json-iterator/go" ) -func CreatePodFromSpec(ctx context.Context, s *specgen.PodSpecGenerator, options *CreateOptions) (*entities.PodCreateReport, error) { +func CreatePodFromSpec(ctx context.Context, spec *entities.PodSpec) (*entities.PodCreateReport, error) { var ( pcr entities.PodCreateReport ) - if options == nil { - options = new(CreateOptions) + if spec == nil { + spec = new(entities.PodSpec) } - _ = options conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - specgenString, err := jsoniter.MarshalToString(s) + specString, err := jsoniter.MarshalToString(spec.PodSpecGen) if err != nil { return nil, err } - stringReader := strings.NewReader(specgenString) + stringReader := strings.NewReader(specString) response, err := conn.DoRequest(stringReader, http.MethodPost, "/pods/create", nil, nil) if err != nil { return nil, err diff --git a/pkg/bindings/test/pods_test.go b/pkg/bindings/test/pods_test.go index b06ff31a2..5331cf439 100644 --- a/pkg/bindings/test/pods_test.go +++ b/pkg/bindings/test/pods_test.go @@ -8,6 +8,7 @@ import ( "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/bindings" "github.com/containers/podman/v3/pkg/bindings/pods" + "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/specgen" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -333,9 +334,9 @@ var _ = Describe("Podman pods", func() { }) It("simple create pod", func() { - ps := specgen.PodSpecGenerator{} - ps.Name = "foobar" - _, err := pods.CreatePodFromSpec(bt.conn, &ps, nil) + ps := entities.PodSpec{PodSpecGen: specgen.PodSpecGenerator{InfraContainerSpec: &specgen.SpecGenerator{}}} + ps.PodSpecGen.Name = "foobar" + _, err := pods.CreatePodFromSpec(bt.conn, &ps) Expect(err).To(BeNil()) exists, err := pods.Exists(bt.conn, "foobar", nil) diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 5acf7211c..bd011d309 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -68,7 +68,7 @@ type ContainerEngine interface { NetworkRm(ctx context.Context, namesOrIds []string, options NetworkRmOptions) ([]*NetworkRmReport, error) PlayKube(ctx context.Context, path string, opts PlayKubeOptions) (*PlayKubeReport, error) PlayKubeDown(ctx context.Context, path string, opts PlayKubeDownOptions) (*PlayKubeReport, error) - PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error) + PodCreate(ctx context.Context, specg PodSpec) (*PodCreateReport, error) PodExists(ctx context.Context, nameOrID string) (*BoolReport, error) PodInspect(ctx context.Context, options PodInspectOptions) (*PodInspectReport, error) PodKill(ctx context.Context, namesOrIds []string, options PodKillOptions) ([]*PodKillReport, error) diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index c66bf96fc..10bd7e5ce 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -106,6 +106,14 @@ type PodRmReport struct { Id string //nolint } +// PddSpec is an abstracted version of PodSpecGen designed to eventually accept options +// not meant to be in a specgen +type PodSpec struct { + PodSpecGen specgen.PodSpecGenerator +} + +// PodCreateOptions provides all possible options for creating a pod and its infra container +// swagger:model PodCreateOptions type PodCreateOptions struct { CGroupParent string CreateCommand []string @@ -125,6 +133,123 @@ type PodCreateOptions struct { Userns specgen.Namespace } +type ContainerCreateOptions struct { + Annotation []string + Attach []string + Authfile string + BlkIOWeight string + BlkIOWeightDevice []string + CapAdd []string + CapDrop []string + CgroupNS string + CGroupsMode string + CGroupParent string + CIDFile string + ConmonPIDFile string + CPUPeriod uint64 + CPUQuota int64 + CPURTPeriod uint64 + CPURTRuntime int64 + CPUShares uint64 + CPUS float64 + CPUSetCPUs string + CPUSetMems string + Devices []string + DeviceCGroupRule []string + DeviceReadBPs []string + DeviceReadIOPs []string + DeviceWriteBPs []string + DeviceWriteIOPs []string + Entrypoint *string + Env []string + EnvHost bool + EnvFile []string + Expose []string + GIDMap []string + GroupAdd []string + HealthCmd string + HealthInterval string + HealthRetries uint + HealthStartPeriod string + HealthTimeout string + Hostname string + HTTPProxy bool + ImageVolume string + Init bool + InitContainerType string + InitPath string + Interactive bool + IPC string + KernelMemory string + Label []string + LabelFile []string + LogDriver string + LogOptions []string + Memory string + MemoryReservation string + MemorySwap string + MemorySwappiness int64 + Name string + NoHealthCheck bool + OOMKillDisable bool + OOMScoreAdj int + Arch string + OS string + Variant string + PID string + PIDsLimit *int64 + Platform string + Pod string + PodIDFile string + Personality string + PreserveFDs uint + Privileged bool + PublishAll bool + Pull string + Quiet bool + ReadOnly bool + ReadOnlyTmpFS bool + Restart string + Replace bool + Requires []string + Rm bool + RootFS bool + Secrets []string + SecurityOpt []string + SdNotifyMode string + ShmSize string + SignaturePolicy string + StopSignal string + StopTimeout uint + StorageOpt []string + SubUIDName string + SubGIDName string + Sysctl []string + Systemd string + Timeout uint + TLSVerify bool + TmpFS []string + TTY bool + Timezone string + Umask string + UIDMap []string + Ulimit []string + User string + UserNS string + UTS string + Mount []string + Volume []string + VolumesFrom []string + Workdir string + SeccompPolicy string + PidFile string + IsInfra bool + + Net *NetOptions + + CgroupConf []string +} + type PodCreateReport struct { Id string //nolint } @@ -149,21 +274,15 @@ func (p *PodCreateOptions) CPULimits() *specs.LinuxCPU { return cpu } -func setNamespaces(p *PodCreateOptions) ([4]specgen.Namespace, error) { - allNS := [4]specgen.Namespace{} - if p.Pid != "" { - pid, err := specgen.ParseNamespace(p.Pid) - if err != nil { - return [4]specgen.Namespace{}, err - } - allNS[0] = pid - } - return allNS, nil -} - -func (p *PodCreateOptions) ToPodSpecGen(s *specgen.PodSpecGenerator) error { +func ToPodSpecGen(s specgen.PodSpecGenerator, p *PodCreateOptions) (*specgen.PodSpecGenerator, error) { // Basic Config s.Name = p.Name + s.InfraName = p.InfraName + out, err := specgen.ParseNamespace(p.Pid) + if err != nil { + return nil, err + } + s.Pid = out s.Hostname = p.Hostname s.Labels = p.Labels s.NoInfra = !p.Infra @@ -174,32 +293,26 @@ func (p *PodCreateOptions) ToPodSpecGen(s *specgen.PodSpecGenerator) error { s.InfraConmonPidFile = p.InfraConmonPidFile } s.InfraImage = p.InfraImage - s.InfraName = p.InfraName s.SharedNamespaces = p.Share s.PodCreateCommand = p.CreateCommand // Networking config - s.NetNS = p.Net.Network - s.StaticIP = p.Net.StaticIP - s.StaticMAC = p.Net.StaticMAC - s.PortMappings = p.Net.PublishPorts - s.CNINetworks = p.Net.CNINetworks - s.NetworkOptions = p.Net.NetworkOptions - if p.Net.UseImageResolvConf { - s.NoManageResolvConf = true - } - s.DNSServer = p.Net.DNSServers - s.DNSSearch = p.Net.DNSSearch - s.DNSOption = p.Net.DNSOptions - s.NoManageHosts = p.Net.NoHosts - s.HostAdd = p.Net.AddHosts - namespaces, err := setNamespaces(p) - if err != nil { - return err - } - if !namespaces[0].IsDefault() { - s.Pid = namespaces[0] + if p.Net != nil { + s.NetNS = p.Net.Network + s.StaticIP = p.Net.StaticIP + s.StaticMAC = p.Net.StaticMAC + s.PortMappings = p.Net.PublishPorts + s.CNINetworks = p.Net.CNINetworks + s.NetworkOptions = p.Net.NetworkOptions + if p.Net.UseImageResolvConf { + s.NoManageResolvConf = true + } + s.DNSServer = p.Net.DNSServers + s.DNSSearch = p.Net.DNSSearch + s.DNSOption = p.Net.DNSOptions + s.NoManageHosts = p.Net.NoHosts + s.HostAdd = p.Net.AddHosts } // Cgroup @@ -219,7 +332,7 @@ func (p *PodCreateOptions) ToPodSpecGen(s *specgen.PodSpecGenerator) error { } } s.Userns = p.Userns - return nil + return &s, nil } type PodPruneOptions struct { diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index db4c6bb8a..ec4d4a902 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -31,21 +31,33 @@ type VolumeDeleteReport struct{ Report } // NetOptions reflect the shared network options between // pods and containers +type NetFlags struct { + AddHosts []string `json:"add-host,omitempty"` + DNS []string `json:"dns,omitempty"` + DNSOpt []string `json:"dns-opt,omitempty"` + DNDSearch []string `json:"dns-search,omitempty"` + MacAddr string `json:"mac-address,omitempty"` + Publish []string `json:"publish,omitempty"` + IP string `json:"ip,omitempty"` + NoHosts bool `json:"no-hosts,omitempty"` + Network string `json:"network,omitempty"` + NetworkAlias []string `json:"network-alias,omitempty"` +} type NetOptions struct { - AddHosts []string - Aliases []string - CNINetworks []string - UseImageResolvConf bool - DNSOptions []string - DNSSearch []string - DNSServers []net.IP - Network specgen.Namespace - NoHosts bool - PublishPorts []types.PortMapping - StaticIP *net.IP - StaticMAC *net.HardwareAddr + AddHosts []string `json:"hostadd,omitempty"` + Aliases []string `json:"network_alias,omitempty"` + CNINetworks []string `json:"cni_networks,omitempty"` + UseImageResolvConf bool `json:"no_manage_resolv_conf,omitempty"` + DNSOptions []string `json:"dns_option,omitempty"` + DNSSearch []string `json:"dns_search,omitempty"` + DNSServers []net.IP `json:"dns_server,omitempty"` + Network specgen.Namespace `json:"netns,omitempty"` + NoHosts bool `json:"no_manage_hosts,omitempty"` + PublishPorts []types.PortMapping `json:"portmappings,omitempty"` + StaticIP *net.IP `json:"static_ip,omitempty"` + StaticMAC *net.HardwareAddr `json:"static_mac,omitempty"` // NetworkOptions are additional options for each network - NetworkOptions map[string][]string + NetworkOptions map[string][]string `json:"network_options,omitempty"` } // All CLI inspect commands and inspect sub-commands use the same options diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index a74b65ab9..8b2a5bfae 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -583,7 +583,11 @@ func (ic *ContainerEngine) ContainerCreate(ctx context.Context, s *specgen.SpecG for _, w := range warn { fmt.Fprintf(os.Stderr, "%s\n", w) } - ctr, err := generate.MakeContainer(ctx, ic.Libpod, s) + rtSpec, spec, opts, err := generate.MakeContainer(context.Background(), ic.Libpod, s) + if err != nil { + return nil, err + } + ctr, err := generate.ExecuteCreate(ctx, ic.Libpod, rtSpec, spec, false, opts...) if err != nil { return nil, err } @@ -915,7 +919,11 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta for _, w := range warn { fmt.Fprintf(os.Stderr, "%s\n", w) } - ctr, err := generate.MakeContainer(ctx, ic.Libpod, opts.Spec) + rtSpec, spec, optsN, err := generate.MakeContainer(ctx, ic.Libpod, opts.Spec) + if err != nil { + return nil, err + } + ctr, err := generate.ExecuteCreate(ctx, ic.Libpod, rtSpec, spec, false, optsN...) if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index b0853b554..2d7bc15f5 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -60,9 +60,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, return nil, err } } else { - if len(ctr.Dependencies()) > 0 { - return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies") - } + // now that infra holds NS data, we need to support dependencies. // we cannot deal with ctrs already in a pod. if len(ctr.PodID()) > 0 { return nil, errors.Errorf("container %s is associated with pod %s: use generate on the pod itself", ctr.ID(), ctr.PodID()) diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index f22b2dbbb..2799df794 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "os" "path/filepath" "strconv" @@ -22,6 +23,7 @@ import ( "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/specgen/generate" "github.com/containers/podman/v3/pkg/specgen/generate/kube" + "github.com/containers/podman/v3/pkg/specgenutil" "github.com/containers/podman/v3/pkg/util" "github.com/ghodss/yaml" "github.com/pkg/errors" @@ -179,10 +181,12 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } } - p, err := kube.ToPodGen(ctx, podName, podYAML) + podOpt := entities.PodCreateOptions{Infra: true, Net: &entities.NetOptions{StaticIP: &net.IP{}, StaticMAC: &net.HardwareAddr{}}} + podOpt, err = kube.ToPodOpt(ctx, podName, podOpt, podYAML) if err != nil { return nil, err } + if options.Network != "" { ns, cniNets, netOpts, err := specgen.ParseNetworkString(options.Network) if err != nil { @@ -193,42 +197,37 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY return nil, errors.Errorf("invalid value passed to --network: bridge or host networking must be configured in YAML") } logrus.Debugf("Pod %q joining CNI networks: %v", podName, cniNets) - p.NetNS.NSMode = specgen.Bridge - p.CNINetworks = append(p.CNINetworks, cniNets...) + podOpt.Net.Network.NSMode = specgen.Bridge + podOpt.Net.CNINetworks = append(podOpt.Net.CNINetworks, cniNets...) if len(netOpts) > 0 { - p.NetworkOptions = netOpts + podOpt.Net.NetworkOptions = netOpts } } if len(options.StaticIPs) > *ipIndex { - p.StaticIP = &options.StaticIPs[*ipIndex] + podOpt.Net.StaticIP = &options.StaticIPs[*ipIndex] } else if len(options.StaticIPs) > 0 { // only warn if the user has set at least one ip logrus.Warn("No more static ips left using a random one") } if len(options.StaticMACs) > *ipIndex { - p.StaticMAC = &options.StaticMACs[*ipIndex] + podOpt.Net.StaticMAC = &options.StaticMACs[*ipIndex] } else if len(options.StaticIPs) > 0 { // only warn if the user has set at least one mac logrus.Warn("No more static macs left using a random one") } *ipIndex++ - // Create the Pod - pod, err := generate.MakePod(p, ic.Libpod) + p := specgen.NewPodSpecGenerator() if err != nil { return nil, err } - podInfraID, err := pod.InfraContainerID() + p, err = entities.ToPodSpecGen(*p, &podOpt) if err != nil { return nil, err } - - if !options.Quiet { - writer = os.Stderr - } - + podSpec := entities.PodSpec{PodSpecGen: *p} volumes, err := kube.InitializeVolumes(podYAML.Spec.Volumes) if err != nil { return nil, err @@ -267,112 +266,146 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY configMaps = append(configMaps, cm) } - containers := make([]*libpod.Container, 0, len(podYAML.Spec.Containers)) - cwd, err := os.Getwd() - if err != nil { - return nil, err - } - for _, container := range podYAML.Spec.Containers { - // Contains all labels obtained from kube - labels := make(map[string]string) - var pulledImage *libimage.Image - buildFile, err := getBuildFile(container.Image, cwd) + if podOpt.Infra { + imagePull := config.DefaultInfraImage + if podOpt.InfraImage != config.DefaultInfraImage && podOpt.InfraImage != "" { + imagePull = podOpt.InfraImage + } + + pulledImages, err := pullImage(ic, writer, imagePull, options, config.PullPolicyNewer) if err != nil { return nil, err } - existsLocally, err := ic.Libpod.LibimageRuntime().Exists(container.Image) + infraOptions := entities.ContainerCreateOptions{ImageVolume: "bind"} + + podSpec.PodSpecGen.InfraImage = pulledImages[0].Names()[0] + podSpec.PodSpecGen.NoInfra = false + podSpec.PodSpecGen.InfraContainerSpec = specgen.NewSpecGenerator(pulledImages[0].Names()[0], false) + podSpec.PodSpecGen.InfraContainerSpec.NetworkOptions = p.NetworkOptions + + err = specgenutil.FillOutSpecGen(podSpec.PodSpecGen.InfraContainerSpec, &infraOptions, []string{}) if err != nil { return nil, err } - if (len(buildFile) > 0 && !existsLocally) || (len(buildFile) > 0 && options.Build) { - buildOpts := new(buildahDefine.BuildOptions) - commonOpts := new(buildahDefine.CommonBuildOptions) - buildOpts.ConfigureNetwork = buildahDefine.NetworkDefault - buildOpts.Isolation = buildahDefine.IsolationChroot - buildOpts.CommonBuildOpts = commonOpts - buildOpts.Output = container.Image - if _, _, err := ic.Libpod.Build(ctx, *buildOpts, []string{buildFile}...); err != nil { + } + + // Create the Pod + pod, err := generate.MakePod(&podSpec, ic.Libpod) + if err != nil { + return nil, err + } + + podInfraID, err := pod.InfraContainerID() + if err != nil { + return nil, err + } + + if !options.Quiet { + writer = os.Stderr + } + + containers := make([]*libpod.Container, 0, len(podYAML.Spec.Containers)) + cwd, err := os.Getwd() + if err != nil { + return nil, err + } + for _, container := range podYAML.Spec.Containers { + if !strings.Contains("infra", container.Name) { + // Contains all labels obtained from kube + labels := make(map[string]string) + var pulledImage *libimage.Image + buildFile, err := getBuildFile(container.Image, cwd) + if err != nil { return nil, err } - i, _, err := ic.Libpod.LibimageRuntime().LookupImage(container.Image, new(libimage.LookupImageOptions)) + existsLocally, err := ic.Libpod.LibimageRuntime().Exists(container.Image) if err != nil { return nil, err } - pulledImage = i - } else { - // NOTE: set the pull policy to "newer". This will cover cases - // where the "latest" tag requires a pull and will also - // transparently handle "localhost/" prefixed files which *may* - // refer to a locally built image OR an image running a - // registry on localhost. - pullPolicy := config.PullPolicyNewer - if len(container.ImagePullPolicy) > 0 { - // Make sure to lower the strings since K8s pull policy - // may be capitalized (see bugzilla.redhat.com/show_bug.cgi?id=1985905). - rawPolicy := string(container.ImagePullPolicy) - pullPolicy, err = config.ParsePullPolicy(strings.ToLower(rawPolicy)) + if (len(buildFile) > 0 && !existsLocally) || (len(buildFile) > 0 && options.Build) { + buildOpts := new(buildahDefine.BuildOptions) + commonOpts := new(buildahDefine.CommonBuildOptions) + buildOpts.ConfigureNetwork = buildahDefine.NetworkDefault + buildOpts.Isolation = buildahDefine.IsolationChroot + buildOpts.CommonBuildOpts = commonOpts + buildOpts.Output = container.Image + if _, _, err := ic.Libpod.Build(ctx, *buildOpts, []string{buildFile}...); err != nil { + return nil, err + } + i, _, err := ic.Libpod.LibimageRuntime().LookupImage(container.Image, new(libimage.LookupImageOptions)) + if err != nil { + return nil, err + } + pulledImage = i + } else { + // NOTE: set the pull policy to "newer". This will cover cases + // where the "latest" tag requires a pull and will also + // transparently handle "localhost/" prefixed files which *may* + // refer to a locally built image OR an image running a + // registry on localhost. + pullPolicy := config.PullPolicyNewer + if len(container.ImagePullPolicy) > 0 { + // Make sure to lower the strings since K8s pull policy + // may be capitalized (see bugzilla.redhat.com/show_bug.cgi?id=1985905). + rawPolicy := string(container.ImagePullPolicy) + pullPolicy, err = config.ParsePullPolicy(strings.ToLower(rawPolicy)) + if err != nil { + return nil, err + } + } + pulledImages, err := pullImage(ic, writer, container.Image, options, pullPolicy) if err != nil { return nil, err } + pulledImage = pulledImages[0] + } + + // Handle kube annotations + for k, v := range annotations { + switch k { + // Auto update annotation without container name will apply to + // all containers within the pod + case autoupdate.Label, autoupdate.AuthfileLabel: + labels[k] = v + // Auto update annotation with container name will apply only + // to the specified container + case fmt.Sprintf("%s/%s", autoupdate.Label, container.Name), + fmt.Sprintf("%s/%s", autoupdate.AuthfileLabel, container.Name): + prefixAndCtr := strings.Split(k, "/") + labels[prefixAndCtr[0]] = v + } } - // This ensures the image is the image store - pullOptions := &libimage.PullOptions{} - pullOptions.AuthFilePath = options.Authfile - pullOptions.CertDirPath = options.CertDir - pullOptions.SignaturePolicyPath = options.SignaturePolicy - pullOptions.Writer = writer - pullOptions.Username = options.Username - pullOptions.Password = options.Password - pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify - - pulledImages, err := ic.Libpod.LibimageRuntime().Pull(ctx, container.Image, pullPolicy, pullOptions) + + specgenOpts := kube.CtrSpecGenOptions{ + Container: container, + Image: pulledImage, + Volumes: volumes, + PodID: pod.ID(), + PodName: podName, + PodInfraID: podInfraID, + ConfigMaps: configMaps, + SeccompPaths: seccompPaths, + RestartPolicy: ctrRestartPolicy, + NetNSIsHost: p.NetNS.IsHost(), + SecretsManager: secretsManager, + LogDriver: options.LogDriver, + Labels: labels, + } + specGen, err := kube.ToSpecGen(ctx, &specgenOpts) if err != nil { return nil, err } - pulledImage = pulledImages[0] - } - // Handle kube annotations - for k, v := range annotations { - switch k { - // Auto update annotation without container name will apply to - // all containers within the pod - case autoupdate.Label, autoupdate.AuthfileLabel: - labels[k] = v - // Auto update annotation with container name will apply only - // to the specified container - case fmt.Sprintf("%s/%s", autoupdate.Label, container.Name), - fmt.Sprintf("%s/%s", autoupdate.AuthfileLabel, container.Name): - prefixAndCtr := strings.Split(k, "/") - labels[prefixAndCtr[0]] = v + rtSpec, spec, opts, err := generate.MakeContainer(ctx, ic.Libpod, specGen) + if err != nil { + return nil, err } + ctr, err := generate.ExecuteCreate(ctx, ic.Libpod, rtSpec, spec, false, opts...) + if err != nil { + return nil, err + } + containers = append(containers, ctr) } - - specgenOpts := kube.CtrSpecGenOptions{ - Container: container, - Image: pulledImage, - Volumes: volumes, - PodID: pod.ID(), - PodName: podName, - PodInfraID: podInfraID, - ConfigMaps: configMaps, - SeccompPaths: seccompPaths, - RestartPolicy: ctrRestartPolicy, - NetNSIsHost: p.NetNS.IsHost(), - SecretsManager: secretsManager, - LogDriver: options.LogDriver, - Labels: labels, - } - specGen, err := kube.ToSpecGen(ctx, &specgenOpts) - if err != nil { - return nil, err - } - - ctr, err := generate.MakeContainer(ctx, ic.Libpod, specGen) - if err != nil { - return nil, err - } - containers = append(containers, ctr) } if options.Start != types.OptionalBoolFalse { @@ -383,6 +416,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } for id, err := range podStartErrors { playKubePod.ContainerErrors = append(playKubePod.ContainerErrors, errors.Wrapf(err, "error starting container %s", id).Error()) + fmt.Println(playKubePod.ContainerErrors) } } @@ -656,3 +690,21 @@ func (ic *ContainerEngine) PlayKubeDown(ctx context.Context, path string, _ enti } return reports, nil } + +// pullImage is a helper function to set up the proper pull options and pull the image for certain containers +func pullImage(ic *ContainerEngine, writer io.Writer, imagePull string, options entities.PlayKubeOptions, pullPolicy config.PullPolicy) ([]*libimage.Image, error) { + // This ensures the image is the image store + pullOptions := &libimage.PullOptions{} + pullOptions.AuthFilePath = options.Authfile + pullOptions.CertDirPath = options.CertDir + pullOptions.SignaturePolicyPath = options.SignaturePolicy + pullOptions.Writer = writer + pullOptions.Username = options.Username + pullOptions.Password = options.Password + pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify + pulledImages, err := ic.Libpod.LibimageRuntime().Pull(context.Background(), imagePull, pullPolicy, pullOptions) + if err != nil { + return nil, err + } + return pulledImages, nil +} diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index 055c495d5..98233f60d 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -8,7 +8,6 @@ import ( "github.com/containers/podman/v3/pkg/domain/entities" dfilters "github.com/containers/podman/v3/pkg/domain/filters" "github.com/containers/podman/v3/pkg/signal" - "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/specgen/generate" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -248,12 +247,8 @@ func (ic *ContainerEngine) prunePodHelper(ctx context.Context) ([]*entities.PodP return reports, nil } -func (ic *ContainerEngine) PodCreate(ctx context.Context, opts entities.PodCreateOptions) (*entities.PodCreateReport, error) { - podSpec := specgen.NewPodSpecGenerator() - if err := opts.ToPodSpecGen(podSpec); err != nil { - return nil, err - } - pod, err := generate.MakePod(podSpec, ic.Libpod) +func (ic *ContainerEngine) PodCreate(ctx context.Context, specg entities.PodSpec) (*entities.PodCreateReport, error) { + pod, err := generate.MakePod(&specg, ic.Libpod) if err != nil { return nil, err } diff --git a/pkg/domain/infra/tunnel/events.go b/pkg/domain/infra/tunnel/events.go index 6e2c3f8ba..203550c5d 100644 --- a/pkg/domain/infra/tunnel/events.go +++ b/pkg/domain/infra/tunnel/events.go @@ -7,6 +7,7 @@ import ( "github.com/containers/podman/v3/libpod/events" "github.com/containers/podman/v3/pkg/bindings/system" "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/pkg/errors" ) diff --git a/pkg/domain/infra/tunnel/pods.go b/pkg/domain/infra/tunnel/pods.go index 82f062b2c..480adb88a 100644 --- a/pkg/domain/infra/tunnel/pods.go +++ b/pkg/domain/infra/tunnel/pods.go @@ -6,7 +6,6 @@ import ( "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/bindings/pods" "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" "github.com/pkg/errors" ) @@ -179,10 +178,8 @@ func (ic *ContainerEngine) PodPrune(ctx context.Context, opts entities.PodPruneO return pods.Prune(ic.ClientCtx, nil) } -func (ic *ContainerEngine) PodCreate(ctx context.Context, opts entities.PodCreateOptions) (*entities.PodCreateReport, error) { - podSpec := specgen.NewPodSpecGenerator() - opts.ToPodSpecGen(podSpec) - return pods.CreatePodFromSpec(ic.ClientCtx, podSpec, nil) +func (ic *ContainerEngine) PodCreate(ctx context.Context, specg entities.PodSpec) (*entities.PodCreateReport, error) { + return pods.CreatePodFromSpec(ic.ClientCtx, &specg) } func (ic *ContainerEngine) PodTop(ctx context.Context, opts entities.PodTopOptions) (*entities.StringSliceReport, error) { diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 5101a6ccb..f82b2a3c6 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -22,10 +22,10 @@ import ( // MakeContainer creates a container based on the SpecGenerator. // Returns the created, container and any warnings resulting from creating the // container, or an error. -func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator) (*libpod.Container, error) { +func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator) (*spec.Spec, *specgen.SpecGenerator, []libpod.CtrCreateOption, error) { rtc, err := rt.GetConfig() if err != nil { - return nil, err + return nil, nil, nil, err } // If joining a pod, retrieve the pod for use. @@ -33,7 +33,7 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener if s.Pod != "" { pod, err = rt.LookupPod(s.Pod) if err != nil { - return nil, errors.Wrapf(err, "error retrieving pod %s", s.Pod) + return nil, nil, nil, errors.Wrapf(err, "error retrieving pod %s", s.Pod) } } @@ -41,47 +41,48 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener if s.PidNS.IsDefault() { defaultNS, err := GetDefaultNamespaceMode("pid", rtc, pod) if err != nil { - return nil, err + return nil, nil, nil, err } s.PidNS = defaultNS } if s.IpcNS.IsDefault() { defaultNS, err := GetDefaultNamespaceMode("ipc", rtc, pod) if err != nil { - return nil, err + return nil, nil, nil, err } s.IpcNS = defaultNS } if s.UtsNS.IsDefault() { defaultNS, err := GetDefaultNamespaceMode("uts", rtc, pod) if err != nil { - return nil, err + return nil, nil, nil, err } s.UtsNS = defaultNS } if s.UserNS.IsDefault() { defaultNS, err := GetDefaultNamespaceMode("user", rtc, pod) if err != nil { - return nil, err + return nil, nil, nil, err } s.UserNS = defaultNS } if s.NetNS.IsDefault() { defaultNS, err := GetDefaultNamespaceMode("net", rtc, pod) if err != nil { - return nil, err + return nil, nil, nil, err } s.NetNS = defaultNS } if s.CgroupNS.IsDefault() { defaultNS, err := GetDefaultNamespaceMode("cgroup", rtc, pod) if err != nil { - return nil, err + return nil, nil, nil, err } s.CgroupNS = defaultNS } options := []libpod.CtrCreateOption{} + if s.ContainerCreateCommand != nil { options = append(options, libpod.WithCreateCommand(s.ContainerCreateCommand)) } @@ -94,12 +95,11 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener var resolvedImageName string newImage, resolvedImageName, err = rt.LibimageRuntime().LookupImage(s.Image, nil) if err != nil { - return nil, err + return nil, nil, nil, err } - imageData, err = newImage.Inspect(ctx, false) if err != nil { - return nil, err + return nil, nil, nil, err } // If the input name changed, we could properly resolve the // image. Otherwise, it must have been an ID where we're @@ -115,29 +115,32 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener options = append(options, libpod.WithRootFSFromImage(newImage.ID(), resolvedImageName, s.RawImageName)) } if err := s.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid config provided") + return nil, nil, nil, errors.Wrap(err, "invalid config provided") } finalMounts, finalVolumes, finalOverlays, err := finalizeMounts(ctx, s, rt, rtc, newImage) if err != nil { - return nil, err + return nil, nil, nil, err } command, err := makeCommand(ctx, s, imageData, rtc) if err != nil { - return nil, err + return nil, nil, nil, err } opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command) if err != nil { - return nil, err + return nil, nil, nil, err } options = append(options, opts...) - exitCommandArgs, err := CreateExitCommandArgs(rt.StorageConfig(), rtc, logrus.IsLevelEnabled(logrus.DebugLevel), s.Remove, false) + var exitCommandArgs []string + + exitCommandArgs, err = CreateExitCommandArgs(rt.StorageConfig(), rtc, logrus.IsLevelEnabled(logrus.DebugLevel), s.Remove, false) if err != nil { - return nil, err + return nil, nil, nil, err } + options = append(options, libpod.WithExitCommand(exitCommandArgs)) if len(s.Aliases) > 0 { @@ -147,23 +150,26 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener if containerType := s.InitContainerType; len(containerType) > 0 { options = append(options, libpod.WithInitCtrType(containerType)) } - + if len(s.Name) > 0 { + logrus.Debugf("setting container name %s", s.Name) + options = append(options, libpod.WithName(s.Name)) + } if len(s.Devices) > 0 { opts = extractCDIDevices(s) options = append(options, opts...) } runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts, pod, command) if err != nil { - return nil, err + return nil, nil, nil, err } - - ctr, err := rt.NewContainer(ctx, runtimeSpec, options...) + return runtimeSpec, s, options, err +} +func ExecuteCreate(ctx context.Context, rt *libpod.Runtime, runtimeSpec *spec.Spec, s *specgen.SpecGenerator, infra bool, options ...libpod.CtrCreateOption) (*libpod.Container, error) { + ctr, err := rt.NewContainer(ctx, runtimeSpec, s, infra, options...) if err != nil { return ctr, err } - // Copy the content from the underlying image into the newly created - // volume if configured to do so. return ctr, rt.PrepareVolumeOnCreateContainer(ctx, ctr) } @@ -256,11 +262,6 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if len(s.SdNotifyMode) > 0 { options = append(options, libpod.WithSdNotifyMode(s.SdNotifyMode)) } - - if len(s.Name) > 0 { - logrus.Debugf("setting container name %s", s.Name) - options = append(options, libpod.WithName(s.Name)) - } if pod != nil { logrus.Debugf("adding container to pod %s", pod.Name()) options = append(options, rt.WithPod(pod)) @@ -379,11 +380,11 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. options = append(options, libpod.WithPrivileged(s.Privileged)) // Get namespace related options - namespaceOptions, err := namespaceOptions(ctx, s, rt, pod, imageData) + namespaceOpts, err := namespaceOptions(ctx, s, rt, pod, imageData) if err != nil { return nil, err } - options = append(options, namespaceOptions...) + options = append(options, namespaceOpts...) if len(s.ConmonPidFile) > 0 { options = append(options, libpod.WithConmonPidFile(s.ConmonPidFile)) diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index 04b4e5ab3..5188abc3a 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -14,6 +14,7 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/podman/v3/libpod/network/types" ann "github.com/containers/podman/v3/pkg/annotations" + "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/specgen/generate" "github.com/containers/podman/v3/pkg/util" @@ -23,25 +24,26 @@ import ( "k8s.io/apimachinery/pkg/api/resource" ) -func ToPodGen(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec) (*specgen.PodSpecGenerator, error) { - p := specgen.NewPodSpecGenerator() +func ToPodOpt(ctx context.Context, podName string, p entities.PodCreateOptions, podYAML *v1.PodTemplateSpec) (entities.PodCreateOptions, error) { + // p := specgen.NewPodSpecGenerator() + p.Net = &entities.NetOptions{} p.Name = podName p.Labels = podYAML.ObjectMeta.Labels // Kube pods must share {ipc, net, uts} by default - p.SharedNamespaces = append(p.SharedNamespaces, "ipc") - p.SharedNamespaces = append(p.SharedNamespaces, "net") - p.SharedNamespaces = append(p.SharedNamespaces, "uts") + p.Share = append(p.Share, "ipc") + p.Share = append(p.Share, "net") + p.Share = append(p.Share, "uts") // TODO we only configure Process namespace. We also need to account for Host{IPC,Network,PID} // which is not currently possible with pod create if podYAML.Spec.ShareProcessNamespace != nil && *podYAML.Spec.ShareProcessNamespace { - p.SharedNamespaces = append(p.SharedNamespaces, "pid") + p.Share = append(p.Share, "pid") } p.Hostname = podYAML.Spec.Hostname if p.Hostname == "" { p.Hostname = podName } if podYAML.Spec.HostNetwork { - p.NetNS.NSMode = specgen.Host + p.Net.Network = specgen.Namespace{NSMode: "host"} } if podYAML.Spec.HostAliases != nil { hosts := make([]string, 0, len(podYAML.Spec.HostAliases)) @@ -50,10 +52,10 @@ func ToPodGen(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec) hosts = append(hosts, host+":"+hostAlias.IP) } } - p.HostAdd = hosts + p.Net.AddHosts = hosts } podPorts := getPodPorts(podYAML.Spec.Containers) - p.PortMappings = podPorts + p.Net.PublishPorts = podPorts if dnsConfig := podYAML.Spec.DNSConfig; dnsConfig != nil { // name servers @@ -62,11 +64,11 @@ func ToPodGen(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec) for _, server := range dnsServers { servers = append(servers, net.ParseIP(server)) } - p.DNSServer = servers + p.Net.DNSServers = servers } // search domains if domains := dnsConfig.Searches; len(domains) > 0 { - p.DNSSearch = domains + p.Net.DNSSearch = domains } // dns options if options := dnsConfig.Options; len(options) > 0 { @@ -110,6 +112,8 @@ type CtrSpecGenOptions struct { LogDriver string // Labels define key-value pairs of metadata Labels map[string]string + // + IsInfra bool } func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGenerator, error) { @@ -216,19 +220,19 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener } } // If only the yaml.Command is specified, set it as the entrypoint and drop the image Cmd - if len(opts.Container.Command) != 0 { + if !opts.IsInfra && len(opts.Container.Command) != 0 { s.Entrypoint = opts.Container.Command s.Command = []string{} } // Only override the cmd field if yaml.Args is specified // Keep the image entrypoint, or the yaml.command if specified - if len(opts.Container.Args) != 0 { + if !opts.IsInfra && len(opts.Container.Args) != 0 { s.Command = opts.Container.Args } // FIXME, // we are currently ignoring imageData.Config.ExposedPorts - if opts.Container.WorkingDir != "" { + if !opts.IsInfra && opts.Container.WorkingDir != "" { s.WorkDir = opts.Container.WorkingDir } diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 80790dcc1..5349e224f 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -250,7 +250,7 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. if s.NetNS.Value != "" { val = fmt.Sprintf("slirp4netns:%s", s.NetNS.Value) } - toReturn = append(toReturn, libpod.WithNetNS(portMappings, expose, postConfigureNetNS, val, nil)) + toReturn = append(toReturn, libpod.WithNetNS(portMappings, expose, postConfigureNetNS, val, s.CNINetworks)) case specgen.Private: fallthrough case specgen.Bridge: diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index 1f3f9e832..80c7f112f 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -201,7 +201,8 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt Options: []string{"rprivate", "nosuid", "noexec", "nodev", "rw"}, } g.AddMount(sysMnt) - } else if !canMountSys { + } + if !canMountSys { addCgroup = false g.RemoveMount("/sys") r := "ro" diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 426cf1b6d..e523aef42 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -2,53 +2,82 @@ package generate import ( "context" + "net" + "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod" + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/specgen" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -func MakePod(p *specgen.PodSpecGenerator, rt *libpod.Runtime) (*libpod.Pod, error) { - if err := p.Validate(); err != nil { +func MakePod(p *entities.PodSpec, rt *libpod.Runtime) (*libpod.Pod, error) { + if err := p.PodSpecGen.Validate(); err != nil { return nil, err } - options, err := createPodOptions(p, rt) + if !p.PodSpecGen.NoInfra && p.PodSpecGen.InfraContainerSpec != nil { + var err error + p.PodSpecGen.InfraContainerSpec, err = MapSpec(&p.PodSpecGen) + if err != nil { + return nil, err + } + } + + options, err := createPodOptions(&p.PodSpecGen, rt, p.PodSpecGen.InfraContainerSpec) if err != nil { return nil, err } - return rt.NewPod(context.Background(), options...) + pod, err := rt.NewPod(context.Background(), p.PodSpecGen, options...) + if err != nil { + return nil, err + } + if !p.PodSpecGen.NoInfra && p.PodSpecGen.InfraContainerSpec != nil { + p.PodSpecGen.InfraContainerSpec.ContainerCreateCommand = []string{} // we do NOT want os.Args as the command, will display the pod create cmd + if p.PodSpecGen.InfraContainerSpec.Name == "" { + p.PodSpecGen.InfraContainerSpec.Name = pod.ID()[:12] + "-infra" + } + _, err = CompleteSpec(context.Background(), rt, p.PodSpecGen.InfraContainerSpec) + if err != nil { + return nil, err + } + p.PodSpecGen.InfraContainerSpec.User = "" // infraSpec user will get incorrectly assigned via the container creation process, overwrite here + rtSpec, spec, opts, err := MakeContainer(context.Background(), rt, p.PodSpecGen.InfraContainerSpec) + if err != nil { + return nil, err + } + spec.Pod = pod.ID() + opts = append(opts, rt.WithPod(pod)) + spec.CgroupParent = pod.CgroupParent() + infraCtr, err := ExecuteCreate(context.Background(), rt, rtSpec, spec, true, opts...) + if err != nil { + return nil, err + } + pod, err = rt.AddInfra(context.Background(), pod, infraCtr) + if err != nil { + return nil, err + } + } + return pod, nil } -func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime) ([]libpod.PodCreateOption, error) { +func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime, infraSpec *specgen.SpecGenerator) ([]libpod.PodCreateOption, error) { var ( options []libpod.PodCreateOption ) - if !p.NoInfra { + if !p.NoInfra { //&& infraSpec != nil { options = append(options, libpod.WithInfraContainer()) - nsOptions, err := GetNamespaceOptions(p.SharedNamespaces, p.NetNS.IsHost()) + nsOptions, err := GetNamespaceOptions(p.SharedNamespaces, p.InfraContainerSpec.NetNS.IsHost()) if err != nil { return nil, err } options = append(options, nsOptions...) // Use pod user and infra userns only when --userns is not set to host - if !p.Userns.IsHost() { + if !p.InfraContainerSpec.UserNS.IsHost() && !p.InfraContainerSpec.UserNS.IsDefault() { options = append(options, libpod.WithPodUser()) - options = append(options, libpod.WithPodUserns(p.Userns)) } - - // Make our exit command - storageConfig := rt.StorageConfig() - runtimeConfig, err := rt.GetConfig() - if err != nil { - return nil, err - } - exitCommand, err := CreateExitCommandArgs(storageConfig, runtimeConfig, logrus.IsLevelEnabled(logrus.DebugLevel), false, false) - if err != nil { - return nil, errors.Wrapf(err, "error creating infra container exit command") - } - options = append(options, libpod.WithPodInfraExitCommand(exitCommand)) } if len(p.CgroupParent) > 0 { options = append(options, libpod.WithPodCgroupParent(p.CgroupParent)) @@ -59,62 +88,27 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime) ([]libpod if len(p.Name) > 0 { options = append(options, libpod.WithPodName(p.Name)) } - if p.ResourceLimits != nil && p.ResourceLimits.CPU != nil && p.ResourceLimits.CPU.Period != nil && p.ResourceLimits.CPU.Quota != nil { - if *p.ResourceLimits.CPU.Period != 0 || *p.ResourceLimits.CPU.Quota != 0 { - options = append(options, libpod.WithPodCPUPAQ((*p.ResourceLimits.CPU.Period), (*p.ResourceLimits.CPU.Quota))) - } - } - if p.ResourceLimits != nil && p.ResourceLimits.CPU != nil && p.ResourceLimits.CPU.Cpus != "" { - options = append(options, libpod.WithPodCPUSetCPUs(p.ResourceLimits.CPU.Cpus)) + if p.PodCreateCommand != nil { + options = append(options, libpod.WithPodCreateCommand(p.PodCreateCommand)) } + if len(p.Hostname) > 0 { options = append(options, libpod.WithPodHostname(p.Hostname)) } - if len(p.HostAdd) > 0 { - options = append(options, libpod.WithPodHosts(p.HostAdd)) - } - if len(p.DNSServer) > 0 { - var dnsServers []string - for _, d := range p.DNSServer { - dnsServers = append(dnsServers, d.String()) - } - options = append(options, libpod.WithPodDNS(dnsServers)) - } - if len(p.DNSOption) > 0 { - options = append(options, libpod.WithPodDNSOption(p.DNSOption)) - } - if len(p.DNSSearch) > 0 { - options = append(options, libpod.WithPodDNSSearch(p.DNSSearch)) - } - if p.StaticIP != nil { - options = append(options, libpod.WithPodStaticIP(*p.StaticIP)) - } - if p.StaticMAC != nil { - options = append(options, libpod.WithPodStaticMAC(*p.StaticMAC)) - } - if p.NoManageResolvConf { - options = append(options, libpod.WithPodUseImageResolvConf()) - } - if len(p.CNINetworks) > 0 { - options = append(options, libpod.WithPodNetworks(p.CNINetworks)) - } - - if len(p.InfraImage) > 0 { - options = append(options, libpod.WithInfraImage(p.InfraImage)) - } - if len(p.InfraName) > 0 { - options = append(options, libpod.WithInfraName(p.InfraName)) - } - - if len(p.InfraCommand) > 0 { - options = append(options, libpod.WithInfraCommand(p.InfraCommand)) - } + return options, nil +} - if !p.Pid.IsDefault() { - options = append(options, libpod.WithPodPidNS(p.Pid)) +// MapSpec modifies the already filled Infra specgenerator, +// replacing necessary values with those specified in pod creation +func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) { + if len(p.PortMappings) > 0 { + ports, _, _, err := ParsePortMapping(p.PortMappings) + if err != nil { + return nil, err + } + p.InfraContainerSpec.PortMappings = libpod.WithInfraContainerPorts(ports, p.InfraContainerSpec) } - switch p.NetNS.NSMode { case specgen.Default, "": if p.NoInfra { @@ -123,42 +117,88 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime) ([]libpod } if rootless.IsRootless() { logrus.Debugf("Pod will use slirp4netns") - options = append(options, libpod.WithPodSlirp4netns(p.NetworkOptions)) + if p.InfraContainerSpec.NetNS.NSMode != "host" { + p.InfraContainerSpec.NetworkOptions = p.NetworkOptions + p.InfraContainerSpec.NetNS.NSMode = specgen.NamespaceMode("slirp4netns") + } } else { logrus.Debugf("Pod using bridge network mode") } case specgen.Bridge: + p.InfraContainerSpec.NetNS.NSMode = specgen.Bridge logrus.Debugf("Pod using bridge network mode") case specgen.Host: logrus.Debugf("Pod will use host networking") - options = append(options, libpod.WithPodHostNetwork()) + if len(p.InfraContainerSpec.PortMappings) > 0 || + p.InfraContainerSpec.StaticIP != nil || + p.InfraContainerSpec.StaticMAC != nil || + len(p.InfraContainerSpec.CNINetworks) > 0 || + p.InfraContainerSpec.NetNS.NSMode == specgen.NoNetwork { + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot set host network if network-related configuration is specified") + } + p.InfraContainerSpec.NetNS.NSMode = specgen.Host case specgen.Slirp: logrus.Debugf("Pod will use slirp4netns") - options = append(options, libpod.WithPodSlirp4netns(p.NetworkOptions)) + if p.InfraContainerSpec.NetNS.NSMode != "host" { + p.InfraContainerSpec.NetworkOptions = p.NetworkOptions + p.InfraContainerSpec.NetNS.NSMode = specgen.NamespaceMode("slirp4netns") + } case specgen.NoNetwork: logrus.Debugf("Pod will not use networking") - options = append(options, libpod.WithPodNoNetwork()) + if len(p.InfraContainerSpec.PortMappings) > 0 || + p.InfraContainerSpec.StaticIP != nil || + p.InfraContainerSpec.StaticMAC != nil || + len(p.InfraContainerSpec.CNINetworks) > 0 || + p.InfraContainerSpec.NetNS.NSMode == "host" { + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot disable pod network if network-related configuration is specified") + } + p.InfraContainerSpec.NetNS.NSMode = specgen.NoNetwork default: return nil, errors.Errorf("pods presently do not support network mode %s", p.NetNS.NSMode) } - if p.NoManageHosts { - options = append(options, libpod.WithPodUseImageHosts()) + libpod.WithPodCgroups() + if len(p.InfraCommand) > 0 { + p.InfraContainerSpec.Entrypoint = p.InfraCommand } - if len(p.PortMappings) > 0 { - ports, _, _, err := ParsePortMapping(p.PortMappings) - if err != nil { - return nil, err - } - options = append(options, libpod.WithInfraContainerPorts(ports)) + + if len(p.HostAdd) > 0 { + p.InfraContainerSpec.HostAdd = p.HostAdd } - options = append(options, libpod.WithPodCgroups()) - if p.PodCreateCommand != nil { - options = append(options, libpod.WithPodCreateCommand(p.PodCreateCommand)) + if len(p.DNSServer) > 0 { + var dnsServers []net.IP + dnsServers = append(dnsServers, p.DNSServer...) + + p.InfraContainerSpec.DNSServers = dnsServers + } + if len(p.DNSOption) > 0 { + p.InfraContainerSpec.DNSOptions = p.DNSOption + } + if len(p.DNSSearch) > 0 { + p.InfraContainerSpec.DNSSearch = p.DNSSearch + } + if p.StaticIP != nil { + p.InfraContainerSpec.StaticIP = p.StaticIP + } + if p.StaticMAC != nil { + p.InfraContainerSpec.StaticMAC = p.StaticMAC + } + if p.NoManageResolvConf { + p.InfraContainerSpec.UseImageResolvConf = true + } + if len(p.CNINetworks) > 0 { + p.InfraContainerSpec.CNINetworks = p.CNINetworks + } + if p.NoManageHosts { + p.InfraContainerSpec.UseImageHosts = p.NoManageHosts } + if len(p.InfraConmonPidFile) > 0 { - options = append(options, libpod.WithInfraConmonPidFile(p.InfraConmonPidFile)) + p.InfraContainerSpec.ConmonPidFile = p.InfraConmonPidFile } - return options, nil + if p.InfraImage != config.DefaultInfraImage { + p.InfraContainerSpec.Image = p.InfraImage + } + return p.InfraContainerSpec, nil } diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index 386571d11..8872a1321 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -67,7 +67,7 @@ type PodBasicConfig struct { // Pid sets the process id namespace of the pod // Optional (defaults to private if unset). This sets the PID namespace of the infra container // This configuration will then be shared with the entire pod if PID namespace sharing is enabled via --share - Pid Namespace `json:"pid,omitempty:"` + Pid Namespace `json:"pidns,omitempty"` // Userns is used to indicate which kind of Usernamespace to enter. // Any containers created within the pod will inherit the pod's userns settings. // Optional @@ -173,6 +173,7 @@ type PodSpecGenerator struct { PodNetworkConfig PodCgroupConfig PodResourceConfig + InfraContainerSpec *SpecGenerator `json:"-"` } type PodResourceConfig struct { diff --git a/pkg/specgenutil/createparse.go b/pkg/specgenutil/createparse.go new file mode 100644 index 000000000..b46d8fbc6 --- /dev/null +++ b/pkg/specgenutil/createparse.go @@ -0,0 +1,34 @@ +package specgenutil + +import ( + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/pkg/errors" +) + +// validate determines if the flags and values given by the user are valid. things checked +// by validate must not need any state information on the flag (i.e. changed) +func validate(c *entities.ContainerCreateOptions) error { + var () + if c.Rm && (c.Restart != "" && c.Restart != "no" && c.Restart != "on-failure") { + return errors.Errorf(`the --rm option conflicts with --restart, when the restartPolicy is not "" and "no"`) + } + + if _, err := config.ParsePullPolicy(c.Pull); err != nil { + return err + } + + var imageVolType = map[string]string{ + "bind": "", + "tmpfs": "", + "ignore": "", + } + if _, ok := imageVolType[c.ImageVolume]; !ok { + if c.IsInfra { + c.ImageVolume = "bind" + } else { + return errors.Errorf("invalid image-volume type %q. Pick one of bind, tmpfs, or ignore", c.ImageVolume) + } + } + return nil +} diff --git a/pkg/specgenutil/ports.go b/pkg/specgenutil/ports.go new file mode 100644 index 000000000..6cc4de1ed --- /dev/null +++ b/pkg/specgenutil/ports.go @@ -0,0 +1,22 @@ +package specgenutil + +import ( + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" +) + +func verifyExpose(expose []string) error { + // add the expose ports from the user (--expose) + // can be single or a range + for _, expose := range expose { + // support two formats for expose, original format /[] or /[] + _, port := nat.SplitProtoPort(expose) + // 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 + _, _, err := nat.ParsePortRange(port) + if err != nil { + return errors.Wrapf(err, "invalid range format for --expose: %s", expose) + } + } + return nil +} diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go new file mode 100644 index 000000000..9f676db1b --- /dev/null +++ b/pkg/specgenutil/specgen.go @@ -0,0 +1,1004 @@ +package specgenutil + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/containers/image/v5/manifest" + "github.com/containers/podman/v3/cmd/podman/parse" + "github.com/containers/podman/v3/libpod/define" + ann "github.com/containers/podman/v3/pkg/annotations" + "github.com/containers/podman/v3/pkg/domain/entities" + envLib "github.com/containers/podman/v3/pkg/env" + "github.com/containers/podman/v3/pkg/namespaces" + "github.com/containers/podman/v3/pkg/specgen" + systemdDefine "github.com/containers/podman/v3/pkg/systemd/define" + "github.com/containers/podman/v3/pkg/util" + "github.com/docker/go-units" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +func getCPULimits(c *entities.ContainerCreateOptions) *specs.LinuxCPU { + cpu := &specs.LinuxCPU{} + hasLimits := false + + if c.CPUS > 0 { + period, quota := util.CoresToPeriodAndQuota(c.CPUS) + + cpu.Period = &period + cpu.Quota = "a + hasLimits = true + } + if c.CPUShares > 0 { + cpu.Shares = &c.CPUShares + hasLimits = true + } + if c.CPUPeriod > 0 { + cpu.Period = &c.CPUPeriod + hasLimits = true + } + if c.CPUSetCPUs != "" { + cpu.Cpus = c.CPUSetCPUs + hasLimits = true + } + if c.CPUSetMems != "" { + cpu.Mems = c.CPUSetMems + hasLimits = true + } + if c.CPUQuota > 0 { + cpu.Quota = &c.CPUQuota + hasLimits = true + } + if c.CPURTPeriod > 0 { + cpu.RealtimePeriod = &c.CPURTPeriod + hasLimits = true + } + if c.CPURTRuntime > 0 { + cpu.RealtimeRuntime = &c.CPURTRuntime + hasLimits = true + } + + if !hasLimits { + return nil + } + return cpu +} + +func getIOLimits(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) (*specs.LinuxBlockIO, error) { + var err error + io := &specs.LinuxBlockIO{} + hasLimits := false + if b := c.BlkIOWeight; len(b) > 0 { + u, err := strconv.ParseUint(b, 10, 16) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for blkio-weight") + } + nu := uint16(u) + io.Weight = &nu + hasLimits = true + } + + if len(c.BlkIOWeightDevice) > 0 { + if err := parseWeightDevices(s, c.BlkIOWeightDevice); err != nil { + return nil, err + } + hasLimits = true + } + + if bps := c.DeviceReadBPs; len(bps) > 0 { + if s.ThrottleReadBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { + return nil, err + } + hasLimits = true + } + + if bps := c.DeviceWriteBPs; len(bps) > 0 { + if s.ThrottleWriteBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { + return nil, err + } + hasLimits = true + } + + if iops := c.DeviceReadIOPs; len(iops) > 0 { + if s.ThrottleReadIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { + return nil, err + } + hasLimits = true + } + + if iops := c.DeviceWriteIOPs; len(iops) > 0 { + if s.ThrottleWriteIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { + return nil, err + } + hasLimits = true + } + + if !hasLimits { + return nil, nil + } + return io, nil +} + +func getMemoryLimits(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) (*specs.LinuxMemory, error) { + var err error + memory := &specs.LinuxMemory{} + hasLimits := false + if m := c.Memory; len(m) > 0 { + ml, err := units.RAMInBytes(m) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for memory") + } + memory.Limit = &ml + if c.MemorySwap == "" { + limit := 2 * ml + memory.Swap = &(limit) + } + hasLimits = true + } + if m := c.MemoryReservation; len(m) > 0 { + mr, err := units.RAMInBytes(m) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for memory") + } + memory.Reservation = &mr + hasLimits = true + } + if m := c.MemorySwap; len(m) > 0 { + var ms int64 + // only set memory swap if it was set + // -1 indicates unlimited + if m != "-1" { + ms, err = units.RAMInBytes(m) + memory.Swap = &ms + if err != nil { + return nil, errors.Wrapf(err, "invalid value for memory") + } + hasLimits = true + } + } + if m := c.KernelMemory; len(m) > 0 { + mk, err := units.RAMInBytes(m) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for kernel-memory") + } + memory.Kernel = &mk + hasLimits = true + } + if c.MemorySwappiness > 0 { + swappiness := uint64(c.MemorySwappiness) + memory.Swappiness = &swappiness + hasLimits = true + } + if c.OOMKillDisable { + memory.DisableOOMKiller = &c.OOMKillDisable + hasLimits = true + } + if !hasLimits { + return nil, nil + } + return memory, nil +} + +func setNamespaces(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) error { + var err error + + if c.PID != "" { + s.PidNS, err = specgen.ParseNamespace(c.PID) + if err != nil { + return err + } + } + if c.IPC != "" { + s.IpcNS, err = specgen.ParseNamespace(c.IPC) + if err != nil { + return err + } + } + if c.UTS != "" { + s.UtsNS, err = specgen.ParseNamespace(c.UTS) + if err != nil { + return err + } + } + if c.CgroupNS != "" { + s.CgroupNS, err = specgen.ParseNamespace(c.CgroupNS) + if err != nil { + return err + } + } + // userns must be treated differently + if c.UserNS != "" { + s.UserNS, err = specgen.ParseUserNamespace(c.UserNS) + if err != nil { + return err + } + } + if c.Net != nil { + s.NetNS = c.Net.Network + } + return nil +} + +func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions, args []string) error { + var ( + err error + ) + // validate flags as needed + if err := validate(c); err != nil { + return err + } + s.User = c.User + var inputCommand []string + if !c.IsInfra { + if len(args) > 1 { + inputCommand = args[1:] + } + } + + if len(c.HealthCmd) > 0 { + if c.NoHealthCheck { + return errors.New("Cannot specify both --no-healthcheck and --health-cmd") + } + s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod) + if err != nil { + return err + } + } else if c.NoHealthCheck { + s.HealthConfig = &manifest.Schema2HealthConfig{ + Test: []string{"NONE"}, + } + } + if err := setNamespaces(s, c); err != nil { + return err + } + userNS := namespaces.UsernsMode(s.UserNS.NSMode) + tempIDMap, err := util.ParseIDMapping(namespaces.UsernsMode(c.UserNS), []string{}, []string{}, "", "") + if err != nil { + return err + } + s.IDMappings, err = util.ParseIDMapping(userNS, c.UIDMap, c.GIDMap, c.SubUIDName, c.SubGIDName) + if err != nil { + return err + } + if len(s.IDMappings.GIDMap) == 0 { + s.IDMappings.AutoUserNsOpts.AdditionalGIDMappings = tempIDMap.AutoUserNsOpts.AdditionalGIDMappings + if s.UserNS.NSMode == specgen.NamespaceMode("auto") { + s.IDMappings.AutoUserNs = true + } + } + if len(s.IDMappings.UIDMap) == 0 { + s.IDMappings.AutoUserNsOpts.AdditionalUIDMappings = tempIDMap.AutoUserNsOpts.AdditionalUIDMappings + if s.UserNS.NSMode == specgen.NamespaceMode("auto") { + s.IDMappings.AutoUserNs = true + } + } + if tempIDMap.AutoUserNsOpts.Size != 0 { + s.IDMappings.AutoUserNsOpts.Size = tempIDMap.AutoUserNsOpts.Size + } + // If some mappings are specified, assume a private user namespace + if userNS.IsDefaultValue() && (!s.IDMappings.HostUIDMapping || !s.IDMappings.HostGIDMapping) { + s.UserNS.NSMode = specgen.Private + } else { + s.UserNS.NSMode = specgen.NamespaceMode(userNS) + } + + s.Terminal = c.TTY + + if err := verifyExpose(c.Expose); err != nil { + return err + } + // We are not handling the Expose flag yet. + // s.PortsExpose = c.Expose + if c.Net != nil { + s.PortMappings = c.Net.PublishPorts + } + s.PublishExposedPorts = c.PublishAll + s.Pod = c.Pod + + if len(c.PodIDFile) > 0 { + if len(s.Pod) > 0 { + return errors.New("Cannot specify both --pod and --pod-id-file") + } + podID, err := ReadPodIDFile(c.PodIDFile) + if err != nil { + return err + } + s.Pod = podID + } + + expose, err := createExpose(c.Expose) + if err != nil { + return err + } + s.Expose = expose + + if sig := c.StopSignal; len(sig) > 0 { + stopSignal, err := util.ParseSignal(sig) + if err != nil { + return err + } + s.StopSignal = &stopSignal + } + + // ENVIRONMENT VARIABLES + // + // Precedence order (higher index wins): + // 1) containers.conf (EnvHost, EnvHTTP, Env) 2) image data, 3 User EnvHost/EnvHTTP, 4) env-file, 5) env + // containers.conf handled and image data handled on the server side + // user specified EnvHost and EnvHTTP handled on Server Side relative to Server + // env-file and env handled on client side + var env map[string]string + + // First transform the os env into a map. We need it for the labels later in + // any case. + osEnv, err := envLib.ParseSlice(os.Environ()) + if err != nil { + return errors.Wrap(err, "error parsing host environment variables") + } + + s.EnvHost = c.EnvHost + s.HTTPProxy = c.HTTPProxy + + // env-file overrides any previous variables + for _, f := range c.EnvFile { + fileEnv, err := envLib.ParseFile(f) + if err != nil { + return err + } + // File env is overridden by env. + env = envLib.Join(env, fileEnv) + } + + parsedEnv, err := envLib.ParseSlice(c.Env) + if err != nil { + return err + } + + s.Env = envLib.Join(env, parsedEnv) + + // LABEL VARIABLES + labels, err := parse.GetAllLabels(c.LabelFile, c.Label) + if err != nil { + return errors.Wrapf(err, "unable to process labels") + } + + if systemdUnit, exists := osEnv[systemdDefine.EnvVariable]; exists { + labels[systemdDefine.EnvVariable] = systemdUnit + } + + s.Labels = labels + + // ANNOTATIONS + annotations := make(map[string]string) + + // First, add our default annotations + annotations[ann.TTY] = "false" + if c.TTY { + annotations[ann.TTY] = "true" + } + + // Last, add user annotations + for _, annotation := range c.Annotation { + splitAnnotation := strings.SplitN(annotation, "=", 2) + if len(splitAnnotation) < 2 { + return errors.Errorf("Annotations must be formatted KEY=VALUE") + } + annotations[splitAnnotation[0]] = splitAnnotation[1] + } + s.Annotations = annotations + + s.WorkDir = c.Workdir + if c.Entrypoint != nil { + entrypoint := []string{} + if ep := *c.Entrypoint; len(ep) > 0 { + // Check if entrypoint specified is json + if err := json.Unmarshal([]byte(*c.Entrypoint), &entrypoint); err != nil { + entrypoint = append(entrypoint, ep) + } + } + s.Entrypoint = entrypoint + } + + // Include the command used to create the container. + + s.ContainerCreateCommand = os.Args + + if len(inputCommand) > 0 { + s.Command = inputCommand + } + + // SHM Size + if c.ShmSize != "" { + shmSize, err := units.FromHumanSize(c.ShmSize) + if err != nil { + return errors.Wrapf(err, "unable to translate --shm-size") + } + s.ShmSize = &shmSize + } + + if c.Net != nil { + s.CNINetworks = c.Net.CNINetworks + } + + // Network aliases + if c.Net != nil { + if len(c.Net.Aliases) > 0 { + // build a map of aliases where key=cniName + aliases := make(map[string][]string, len(s.CNINetworks)) + for _, cniNetwork := range s.CNINetworks { + aliases[cniNetwork] = c.Net.Aliases + } + s.Aliases = aliases + } + } + + if c.Net != nil { + s.HostAdd = c.Net.AddHosts + s.UseImageResolvConf = c.Net.UseImageResolvConf + s.DNSServers = c.Net.DNSServers + s.DNSSearch = c.Net.DNSSearch + s.DNSOptions = c.Net.DNSOptions + s.StaticIP = c.Net.StaticIP + s.StaticMAC = c.Net.StaticMAC + s.NetworkOptions = c.Net.NetworkOptions + s.UseImageHosts = c.Net.NoHosts + } + s.ImageVolumeMode = c.ImageVolume + if s.ImageVolumeMode == "bind" { + s.ImageVolumeMode = "anonymous" + } + + s.Systemd = c.Systemd + s.SdNotifyMode = c.SdNotifyMode + if s.ResourceLimits == nil { + s.ResourceLimits = &specs.LinuxResources{} + } + s.ResourceLimits.Memory, err = getMemoryLimits(s, c) + if err != nil { + return err + } + s.ResourceLimits.BlockIO, err = getIOLimits(s, c) + if err != nil { + return err + } + if c.PIDsLimit != nil { + pids := specs.LinuxPids{ + Limit: *c.PIDsLimit, + } + + s.ResourceLimits.Pids = &pids + } + s.ResourceLimits.CPU = getCPULimits(c) + + unifieds := make(map[string]string) + for _, unified := range c.CgroupConf { + splitUnified := strings.SplitN(unified, "=", 2) + if len(splitUnified) < 2 { + return errors.Errorf("--cgroup-conf must be formatted KEY=VALUE") + } + unifieds[splitUnified[0]] = splitUnified[1] + } + if len(unifieds) > 0 { + s.ResourceLimits.Unified = unifieds + } + + if s.ResourceLimits.CPU == nil && s.ResourceLimits.Pids == nil && s.ResourceLimits.BlockIO == nil && s.ResourceLimits.Memory == nil && s.ResourceLimits.Unified == nil { + s.ResourceLimits = nil + } + + if s.LogConfiguration == nil { + s.LogConfiguration = &specgen.LogConfig{} + } + + if ld := c.LogDriver; len(ld) > 0 { + s.LogConfiguration.Driver = ld + } + s.CgroupParent = c.CGroupParent + s.CgroupsMode = c.CGroupsMode + s.Groups = c.GroupAdd + + s.Hostname = c.Hostname + sysctl := map[string]string{} + if ctl := c.Sysctl; len(ctl) > 0 { + sysctl, err = util.ValidateSysctls(ctl) + if err != nil { + return err + } + } + s.Sysctl = sysctl + + s.CapAdd = c.CapAdd + s.CapDrop = c.CapDrop + s.Privileged = c.Privileged + s.ReadOnlyFilesystem = c.ReadOnly + s.ConmonPidFile = c.ConmonPIDFile + + s.DependencyContainers = c.Requires + + // TODO + // outside of specgen and oci though + // defaults to true, check spec/storage + // s.readonly = c.ReadOnlyTmpFS + // TODO convert to map? + // check if key=value and convert + sysmap := make(map[string]string) + for _, ctl := range c.Sysctl { + splitCtl := strings.SplitN(ctl, "=", 2) + if len(splitCtl) < 2 { + return errors.Errorf("invalid sysctl value %q", ctl) + } + sysmap[splitCtl[0]] = splitCtl[1] + } + s.Sysctl = sysmap + + if c.CIDFile != "" { + s.Annotations[define.InspectAnnotationCIDFile] = c.CIDFile + } + + for _, opt := range c.SecurityOpt { + if opt == "no-new-privileges" { + s.ContainerSecurityConfig.NoNewPrivileges = true + } else { + con := strings.SplitN(opt, "=", 2) + if len(con) != 2 { + return fmt.Errorf("invalid --security-opt 1: %q", opt) + } + switch con[0] { + case "apparmor": + s.ContainerSecurityConfig.ApparmorProfile = con[1] + s.Annotations[define.InspectAnnotationApparmor] = con[1] + case "label": + // TODO selinux opts and label opts are the same thing + s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1]) + s.Annotations[define.InspectAnnotationLabel] = strings.Join(s.ContainerSecurityConfig.SelinuxOpts, ",label=") + case "mask": + s.ContainerSecurityConfig.Mask = append(s.ContainerSecurityConfig.Mask, strings.Split(con[1], ":")...) + case "proc-opts": + s.ProcOpts = strings.Split(con[1], ",") + case "seccomp": + s.SeccompProfilePath = con[1] + s.Annotations[define.InspectAnnotationSeccomp] = con[1] + // this option is for docker compatibility, it is the same as unmask=ALL + case "systempaths": + if con[1] == "unconfined" { + s.ContainerSecurityConfig.Unmask = append(s.ContainerSecurityConfig.Unmask, []string{"ALL"}...) + } else { + return fmt.Errorf("invalid systempaths option %q, only `unconfined` is supported", con[1]) + } + case "unmask": + s.ContainerSecurityConfig.Unmask = append(s.ContainerSecurityConfig.Unmask, con[1:]...) + default: + return fmt.Errorf("invalid --security-opt 2: %q", opt) + } + } + } + + s.SeccompPolicy = c.SeccompPolicy + + s.VolumesFrom = c.VolumesFrom + + // Only add read-only tmpfs mounts in case that we are read-only and the + // read-only tmpfs flag has been set. + mounts, volumes, overlayVolumes, imageVolumes, err := parseVolumes(c.Volume, c.Mount, c.TmpFS, c.ReadOnlyTmpFS && c.ReadOnly) + if err != nil { + return err + } + s.Mounts = mounts + s.Volumes = volumes + s.OverlayVolumes = overlayVolumes + s.ImageVolumes = imageVolumes + + for _, dev := range c.Devices { + s.Devices = append(s.Devices, specs.LinuxDevice{Path: dev}) + } + + for _, rule := range c.DeviceCGroupRule { + dev, err := parseLinuxResourcesDeviceAccess(rule) + if err != nil { + return err + } + s.DeviceCGroupRule = append(s.DeviceCGroupRule, dev) + } + + s.Init = c.Init + s.InitPath = c.InitPath + s.Stdin = c.Interactive + // quiet + // DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), + + // Rlimits/Ulimits + for _, u := range c.Ulimit { + if u == "host" { + s.Rlimits = nil + break + } + ul, err := units.ParseUlimit(u) + if err != nil { + return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u) + } + rl := specs.POSIXRlimit{ + Type: ul.Name, + Hard: uint64(ul.Hard), + Soft: uint64(ul.Soft), + } + s.Rlimits = append(s.Rlimits, rl) + } + + logOpts := make(map[string]string) + for _, o := range c.LogOptions { + split := strings.SplitN(o, "=", 2) + if len(split) < 2 { + return errors.Errorf("invalid log option %q", o) + } + switch strings.ToLower(split[0]) { + case "driver": + s.LogConfiguration.Driver = split[1] + case "path": + s.LogConfiguration.Path = split[1] + case "max-size": + logSize, err := units.FromHumanSize(split[1]) + if err != nil { + return err + } + s.LogConfiguration.Size = logSize + default: + logOpts[split[0]] = split[1] + } + } + s.LogConfiguration.Options = logOpts + s.Name = c.Name + s.PreserveFDs = c.PreserveFDs + + s.OOMScoreAdj = &c.OOMScoreAdj + if c.Restart != "" { + splitRestart := strings.Split(c.Restart, ":") + switch len(splitRestart) { + case 1: + // No retries specified + case 2: + if strings.ToLower(splitRestart[0]) != "on-failure" { + return errors.Errorf("restart policy retries can only be specified with on-failure restart policy") + } + retries, err := strconv.Atoi(splitRestart[1]) + if err != nil { + return errors.Wrapf(err, "error parsing restart policy retry count") + } + if retries < 0 { + return errors.Errorf("must specify restart policy retry count as a number greater than 0") + } + var retriesUint = uint(retries) + s.RestartRetries = &retriesUint + default: + return errors.Errorf("invalid restart policy: may specify retries at most once") + } + s.RestartPolicy = splitRestart[0] + } + + s.Secrets, s.EnvSecrets, err = parseSecrets(c.Secrets) + if err != nil { + return err + } + + if c.Personality != "" { + s.Personality = &specs.LinuxPersonality{} + s.Personality.Domain = specs.LinuxPersonalityDomain(c.Personality) + } + + s.Remove = c.Rm + s.StopTimeout = &c.StopTimeout + s.Timeout = c.Timeout + s.Timezone = c.Timezone + s.Umask = c.Umask + s.PidFile = c.PidFile + s.Volatile = c.Rm + + // Initcontainers + s.InitContainerType = c.InitContainerType + return nil +} + +func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string) (*manifest.Schema2HealthConfig, error) { + cmdArr := []string{} + isArr := true + err := json.Unmarshal([]byte(inCmd), &cmdArr) // array unmarshalling + if err != nil { + cmdArr = strings.SplitN(inCmd, " ", 2) // default for compat + isArr = false + } + // Every healthcheck requires a command + if len(cmdArr) == 0 { + return nil, errors.New("Must define a healthcheck command for all healthchecks") + } + concat := "" + if cmdArr[0] == "CMD" || cmdArr[0] == "none" { // this is for compat, we are already split properly for most compat cases + cmdArr = strings.Fields(inCmd) + } else if cmdArr[0] != "CMD-SHELL" { // this is for podman side of things, won't contain the keywords + if isArr && len(cmdArr) > 1 { // an array of consecutive commands + cmdArr = append([]string{"CMD"}, cmdArr...) + } else { // one singular command + if len(cmdArr) == 1 { + concat = cmdArr[0] + } else { + concat = strings.Join(cmdArr[0:], " ") + } + cmdArr = append([]string{"CMD-SHELL"}, concat) + } + } + + if cmdArr[0] == "none" { // if specified to remove healtcheck + cmdArr = []string{"NONE"} + } + + // healthcheck is by default an array, so we simply pass the user input + hc := manifest.Schema2HealthConfig{ + Test: cmdArr, + } + + if interval == "disable" { + interval = "0" + } + intervalDuration, err := time.ParseDuration(interval) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-interval") + } + + hc.Interval = intervalDuration + + if retries < 1 { + return nil, errors.New("healthcheck-retries must be greater than 0") + } + hc.Retries = int(retries) + timeoutDuration, err := time.ParseDuration(timeout) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-timeout") + } + if timeoutDuration < time.Duration(1) { + return nil, errors.New("healthcheck-timeout must be at least 1 second") + } + hc.Timeout = timeoutDuration + + startPeriodDuration, err := time.ParseDuration(startPeriod) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-start-period") + } + if startPeriodDuration < time.Duration(0) { + return nil, errors.New("healthcheck-start-period must be 0 seconds or greater") + } + hc.StartPeriod = startPeriodDuration + + return &hc, nil +} + +func parseWeightDevices(s *specgen.SpecGenerator, weightDevs []string) error { + for _, val := range weightDevs { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return fmt.Errorf("bad format for device path: %s", val) + } + weight, err := strconv.ParseUint(split[1], 10, 0) + if err != nil { + return fmt.Errorf("invalid weight for device: %s", val) + } + if weight > 0 && (weight < 10 || weight > 1000) { + return fmt.Errorf("invalid weight for device: %s", val) + } + w := uint16(weight) + s.WeightDevice[split[0]] = specs.LinuxWeightDevice{ + Weight: &w, + LeafWeight: nil, + } + } + return nil +} + +func parseThrottleBPSDevices(bpsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { + td := make(map[string]specs.LinuxThrottleDevice) + for _, val := range bpsDevices { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := units.RAMInBytes(split[1]) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :[]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) + } + if rate < 0 { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :[]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: uint64(rate)} + } + return td, nil +} + +func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { + td := make(map[string]specs.LinuxThrottleDevice) + for _, val := range iopsDevices { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := strconv.ParseUint(split[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :. Number must be a positive integer", val) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: rate} + } + return td, nil +} + +func parseSecrets(secrets []string) ([]specgen.Secret, map[string]string, error) { + secretParseError := errors.New("error parsing secret") + var mount []specgen.Secret + envs := make(map[string]string) + for _, val := range secrets { + // mount only tells if user has set an option that can only be used with mount secret type + mountOnly := false + source := "" + secretType := "" + target := "" + var uid, gid uint32 + // default mode 444 octal = 292 decimal + var mode uint32 = 292 + split := strings.Split(val, ",") + + // --secret mysecret + if len(split) == 1 { + mountSecret := specgen.Secret{ + Source: val, + UID: uid, + GID: gid, + Mode: mode, + } + mount = append(mount, mountSecret) + continue + } + // --secret mysecret,opt=opt + if !strings.Contains(split[0], "=") { + source = split[0] + split = split[1:] + } + + for _, val := range split { + kv := strings.SplitN(val, "=", 2) + if len(kv) < 2 { + return nil, nil, errors.Wrapf(secretParseError, "option %s must be in form option=value", val) + } + switch kv[0] { + case "source": + source = kv[1] + case "type": + if secretType != "" { + return nil, nil, errors.Wrap(secretParseError, "cannot set more tha one secret type") + } + if kv[1] != "mount" && kv[1] != "env" { + return nil, nil, errors.Wrapf(secretParseError, "type %s is invalid", kv[1]) + } + secretType = kv[1] + case "target": + target = kv[1] + case "mode": + mountOnly = true + mode64, err := strconv.ParseUint(kv[1], 8, 32) + if err != nil { + return nil, nil, errors.Wrapf(secretParseError, "mode %s invalid", kv[1]) + } + mode = uint32(mode64) + case "uid", "UID": + mountOnly = true + uid64, err := strconv.ParseUint(kv[1], 10, 32) + if err != nil { + return nil, nil, errors.Wrapf(secretParseError, "UID %s invalid", kv[1]) + } + uid = uint32(uid64) + case "gid", "GID": + mountOnly = true + gid64, err := strconv.ParseUint(kv[1], 10, 32) + if err != nil { + return nil, nil, errors.Wrapf(secretParseError, "GID %s invalid", kv[1]) + } + gid = uint32(gid64) + + default: + return nil, nil, errors.Wrapf(secretParseError, "option %s invalid", val) + } + } + + if secretType == "" { + secretType = "mount" + } + if source == "" { + return nil, nil, errors.Wrapf(secretParseError, "no source found %s", val) + } + if secretType == "mount" { + if target != "" { + return nil, nil, errors.Wrapf(secretParseError, "target option is invalid for mounted secrets") + } + mountSecret := specgen.Secret{ + Source: source, + UID: uid, + GID: gid, + Mode: mode, + } + mount = append(mount, mountSecret) + } + if secretType == "env" { + if mountOnly { + return nil, nil, errors.Wrap(secretParseError, "UID, GID, Mode options cannot be set with secret type env") + } + if target == "" { + target = source + } + envs[target] = source + } + } + return mount, envs, nil +} + +var cgroupDeviceType = map[string]bool{ + "a": true, // all + "b": true, // block device + "c": true, // character device +} + +var cgroupDeviceAccess = map[string]bool{ + "r": true, //read + "w": true, //write + "m": true, //mknod +} + +// parseLinuxResourcesDeviceAccess parses the raw string passed with the --device-access-add flag +func parseLinuxResourcesDeviceAccess(device string) (specs.LinuxDeviceCgroup, error) { + var devType, access string + var major, minor *int64 + + value := strings.Split(device, " ") + if len(value) != 3 { + return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device cgroup rule requires type, major:Minor, and access rules: %q", device) + } + + devType = value[0] + if !cgroupDeviceType[devType] { + return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device type in device-access-add: %s", devType) + } + + number := strings.SplitN(value[1], ":", 2) + i, err := strconv.ParseInt(number[0], 10, 64) + if err != nil { + return specs.LinuxDeviceCgroup{}, err + } + major = &i + if len(number) == 2 && number[1] != "*" { + i, err := strconv.ParseInt(number[1], 10, 64) + if err != nil { + return specs.LinuxDeviceCgroup{}, err + } + minor = &i + } + access = value[2] + for _, c := range strings.Split(access, "") { + if !cgroupDeviceAccess[c] { + return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device access in device-access-add: %s", c) + } + } + return specs.LinuxDeviceCgroup{ + Allow: true, + Type: devType, + Major: major, + Minor: minor, + Access: access, + }, nil +} diff --git a/pkg/specgenutil/util.go b/pkg/specgenutil/util.go new file mode 100644 index 000000000..15676d086 --- /dev/null +++ b/pkg/specgenutil/util.go @@ -0,0 +1,274 @@ +package specgenutil + +import ( + "io/ioutil" + "net" + "strconv" + "strings" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ReadPodIDFile reads the specified file and returns its content (i.e., first +// line). +func ReadPodIDFile(path string) (string, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return "", errors.Wrap(err, "error reading pod ID file") + } + return strings.Split(string(content), "\n")[0], nil +} + +// ReadPodIDFiles reads the specified files and returns their content (i.e., +// first line). +func ReadPodIDFiles(files []string) ([]string, error) { + ids := []string{} + for _, file := range files { + id, err := ReadPodIDFile(file) + if err != nil { + return nil, err + } + ids = append(ids, id) + } + return ids, nil +} + +// ParseFilters transforms one filter format to another and validates input +func ParseFilters(filter []string) (map[string][]string, error) { + // TODO Remove once filter refactor is finished and url.Values done. + filters := map[string][]string{} + for _, f := range filter { + t := strings.SplitN(f, "=", 2) + filters = make(map[string][]string) + if len(t) < 2 { + return map[string][]string{}, errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) + } + filters[t[0]] = append(filters[t[0]], t[1]) + } + return filters, nil +} + +// createExpose parses user-provided exposed port definitions and converts them +// into SpecGen format. +// TODO: The SpecGen format should really handle ranges more sanely - we could +// be massively inflating what is sent over the wire with a large range. +func createExpose(expose []string) (map[uint16]string, error) { + toReturn := make(map[uint16]string) + + for _, e := range expose { + // Check for protocol + proto := "tcp" + splitProto := strings.Split(e, "/") + if len(splitProto) > 2 { + return nil, errors.Errorf("invalid expose format - protocol can only be specified once") + } else if len(splitProto) == 2 { + proto = splitProto[1] + } + + // Check for a range + start, len, err := parseAndValidateRange(splitProto[0]) + if err != nil { + return nil, err + } + + var index uint16 + for index = 0; index < len; index++ { + portNum := start + index + protocols, ok := toReturn[portNum] + if !ok { + toReturn[portNum] = proto + } else { + newProto := strings.Join(append(strings.Split(protocols, ","), strings.Split(proto, ",")...), ",") + toReturn[portNum] = newProto + } + } + } + + return toReturn, nil +} + +// CreatePortBindings iterates ports mappings into SpecGen format. +func CreatePortBindings(ports []string) ([]types.PortMapping, error) { + // --publish is formatted as follows: + // [[hostip:]hostport[-endPort]:]containerport[-endPort][/protocol] + toReturn := make([]types.PortMapping, 0, len(ports)) + + for _, p := range ports { + var ( + ctrPort string + proto, hostIP, hostPort *string + ) + + splitProto := strings.Split(p, "/") + switch len(splitProto) { + case 1: + // No protocol was provided + case 2: + proto = &(splitProto[1]) + default: + return nil, errors.Errorf("invalid port format - protocol can only be specified once") + } + + remainder := splitProto[0] + haveV6 := false + + // Check for an IPv6 address in brackets + splitV6 := strings.Split(remainder, "]") + switch len(splitV6) { + case 1: + // Do nothing, proceed as before + case 2: + // We potentially have an IPv6 address + haveV6 = true + if !strings.HasPrefix(splitV6[0], "[") { + return nil, errors.Errorf("invalid port format - IPv6 addresses must be enclosed by []") + } + if !strings.HasPrefix(splitV6[1], ":") { + return nil, errors.Errorf("invalid port format - IPv6 address must be followed by a colon (':')") + } + ipNoPrefix := strings.TrimPrefix(splitV6[0], "[") + hostIP = &ipNoPrefix + remainder = strings.TrimPrefix(splitV6[1], ":") + default: + return nil, errors.Errorf("invalid port format - at most one IPv6 address can be specified in a --publish") + } + + splitPort := strings.Split(remainder, ":") + switch len(splitPort) { + case 1: + if haveV6 { + return nil, errors.Errorf("invalid port format - must provide host and destination port if specifying an IP") + } + ctrPort = splitPort[0] + case 2: + hostPort = &(splitPort[0]) + ctrPort = splitPort[1] + case 3: + if haveV6 { + return nil, errors.Errorf("invalid port format - when v6 address specified, must be [ipv6]:hostPort:ctrPort") + } + hostIP = &(splitPort[0]) + hostPort = &(splitPort[1]) + ctrPort = splitPort[2] + default: + return nil, errors.Errorf("invalid port format - format is [[hostIP:]hostPort:]containerPort") + } + + newPort, err := parseSplitPort(hostIP, hostPort, ctrPort, proto) + if err != nil { + return nil, err + } + + toReturn = append(toReturn, newPort) + } + + return toReturn, nil +} + +// parseSplitPort parses individual components of the --publish flag to produce +// a single port mapping in SpecGen format. +func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (types.PortMapping, error) { + newPort := types.PortMapping{} + if ctrPort == "" { + return newPort, errors.Errorf("must provide a non-empty container port to publish") + } + ctrStart, ctrLen, err := parseAndValidateRange(ctrPort) + if err != nil { + return newPort, errors.Wrapf(err, "error parsing container port") + } + newPort.ContainerPort = ctrStart + newPort.Range = ctrLen + + if protocol != nil { + if *protocol == "" { + return newPort, errors.Errorf("must provide a non-empty protocol to publish") + } + newPort.Protocol = *protocol + } + if hostIP != nil { + if *hostIP == "" { + return newPort, errors.Errorf("must provide a non-empty container host IP to publish") + } else if *hostIP != "0.0.0.0" { + // If hostIP is 0.0.0.0, leave it unset - CNI treats + // 0.0.0.0 and empty differently, Docker does not. + testIP := net.ParseIP(*hostIP) + if testIP == nil { + return newPort, errors.Errorf("cannot parse %q as an IP address", *hostIP) + } + newPort.HostIP = testIP.String() + } + } + if hostPort != nil { + if *hostPort == "" { + // Set 0 as a placeholder. The server side of Specgen + // will find a random, open, unused port to use. + newPort.HostPort = 0 + } else { + hostStart, hostLen, err := parseAndValidateRange(*hostPort) + if err != nil { + return newPort, errors.Wrapf(err, "error parsing host port") + } + if hostLen != ctrLen { + return newPort, errors.Errorf("host and container port ranges have different lengths: %d vs %d", hostLen, ctrLen) + } + newPort.HostPort = hostStart + } + } + + hport := newPort.HostPort + logrus.Debugf("Adding port mapping from %d to %d length %d protocol %q", hport, newPort.ContainerPort, newPort.Range, newPort.Protocol) + + return newPort, nil +} + +// Parse and validate a port range. +// Returns start port, length of range, error. +func parseAndValidateRange(portRange string) (uint16, uint16, error) { + splitRange := strings.Split(portRange, "-") + if len(splitRange) > 2 { + return 0, 0, errors.Errorf("invalid port format - port ranges are formatted as startPort-stopPort") + } + + if splitRange[0] == "" { + return 0, 0, errors.Errorf("port numbers cannot be negative") + } + + startPort, err := parseAndValidatePort(splitRange[0]) + if err != nil { + return 0, 0, err + } + + var rangeLen uint16 = 1 + if len(splitRange) == 2 { + if splitRange[1] == "" { + return 0, 0, errors.Errorf("must provide ending number for port range") + } + endPort, err := parseAndValidatePort(splitRange[1]) + if err != nil { + return 0, 0, err + } + if endPort <= startPort { + return 0, 0, errors.Errorf("the end port of a range must be higher than the start port - %d is not higher than %d", endPort, startPort) + } + // Our range is the total number of ports + // involved, so we need to add 1 (8080:8081 is + // 2 ports, for example, not 1) + rangeLen = endPort - startPort + 1 + } + + return startPort, rangeLen, nil +} + +// Turn a single string into a valid U16 port. +func parseAndValidatePort(port string) (uint16, error) { + num, err := strconv.Atoi(port) + if err != nil { + return 0, errors.Wrapf(err, "invalid port number") + } + if num < 1 || num > 65535 { + return 0, errors.Errorf("port numbers must be between 1 and 65535 (inclusive), got %d", num) + } + return uint16(num), nil +} diff --git a/pkg/specgenutil/volumes.go b/pkg/specgenutil/volumes.go new file mode 100644 index 000000000..e9f70fc9d --- /dev/null +++ b/pkg/specgenutil/volumes.go @@ -0,0 +1,630 @@ +package specgenutil + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/containers/common/pkg/parse" + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/pkg/specgen" + "github.com/containers/podman/v3/pkg/util" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +var ( + errDuplicateDest = errors.Errorf("duplicate mount destination") + optionArgError = errors.Errorf("must provide an argument for option") + noDestError = errors.Errorf("must set volume destination") + errInvalidSyntax = errors.Errorf("incorrect mount format: should be --mount type=,[src=,]target=[,options]") +) + +// Parse all volume-related options in the create config into a set of mounts +// and named volumes to add to the container. +// Handles --volumes, --mount, and --tmpfs flags. +// Does not handle image volumes, init, and --volumes-from flags. +// Can also add tmpfs mounts from read-only tmpfs. +// TODO: handle options parsing/processing via containers/storage/pkg/mount +func parseVolumes(volumeFlag, mountFlag, tmpfsFlag []string, addReadOnlyTmpfs bool) ([]spec.Mount, []*specgen.NamedVolume, []*specgen.OverlayVolume, []*specgen.ImageVolume, error) { + // Get mounts from the --mounts flag. + unifiedMounts, unifiedVolumes, unifiedImageVolumes, err := getMounts(mountFlag) + if err != nil { + return nil, nil, nil, nil, err + } + + // Next --volumes flag. + volumeMounts, volumeVolumes, overlayVolumes, err := specgen.GenVolumeMounts(volumeFlag) + if err != nil { + return nil, nil, nil, nil, err + } + + // Next --tmpfs flag. + tmpfsMounts, err := getTmpfsMounts(tmpfsFlag) + if err != nil { + return nil, nil, nil, nil, err + } + + // Unify mounts from --mount, --volume, --tmpfs. + // Start with --volume. + for dest, mount := range volumeMounts { + if _, ok := unifiedMounts[dest]; ok { + return nil, nil, nil, nil, errors.Wrapf(errDuplicateDest, dest) + } + unifiedMounts[dest] = mount + } + for dest, volume := range volumeVolumes { + if _, ok := unifiedVolumes[dest]; ok { + return nil, nil, nil, nil, errors.Wrapf(errDuplicateDest, dest) + } + unifiedVolumes[dest] = volume + } + // Now --tmpfs + for dest, tmpfs := range tmpfsMounts { + if _, ok := unifiedMounts[dest]; ok { + return nil, nil, nil, nil, errors.Wrapf(errDuplicateDest, dest) + } + unifiedMounts[dest] = tmpfs + } + + // If requested, add tmpfs filesystems for read-only containers. + if addReadOnlyTmpfs { + readonlyTmpfs := []string{"/tmp", "/var/tmp", "/run"} + options := []string{"rw", "rprivate", "nosuid", "nodev", "tmpcopyup"} + for _, dest := range readonlyTmpfs { + if _, ok := unifiedMounts[dest]; ok { + continue + } + if _, ok := unifiedVolumes[dest]; ok { + continue + } + unifiedMounts[dest] = spec.Mount{ + Destination: dest, + Type: define.TypeTmpfs, + Source: "tmpfs", + Options: options, + } + } + } + + // Check for conflicts between named volumes, overlay & image volumes, + // and mounts + allMounts := make(map[string]bool) + testAndSet := func(dest string) error { + if _, ok := allMounts[dest]; ok { + return errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) + } + allMounts[dest] = true + return nil + } + for dest := range unifiedMounts { + if err := testAndSet(dest); err != nil { + return nil, nil, nil, nil, err + } + } + for dest := range unifiedVolumes { + if err := testAndSet(dest); err != nil { + return nil, nil, nil, nil, err + } + } + for dest := range overlayVolumes { + if err := testAndSet(dest); err != nil { + return nil, nil, nil, nil, err + } + } + for dest := range unifiedImageVolumes { + if err := testAndSet(dest); err != nil { + return nil, nil, nil, nil, err + } + } + + // Final step: maps to arrays + finalMounts := make([]spec.Mount, 0, len(unifiedMounts)) + for _, mount := range unifiedMounts { + if mount.Type == define.TypeBind { + absSrc, err := filepath.Abs(mount.Source) + if err != nil { + return nil, nil, nil, nil, errors.Wrapf(err, "error getting absolute path of %s", mount.Source) + } + mount.Source = absSrc + } + finalMounts = append(finalMounts, mount) + } + finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedVolumes)) + for _, volume := range unifiedVolumes { + finalVolumes = append(finalVolumes, volume) + } + finalOverlayVolume := make([]*specgen.OverlayVolume, 0) + for _, volume := range overlayVolumes { + finalOverlayVolume = append(finalOverlayVolume, volume) + } + finalImageVolumes := make([]*specgen.ImageVolume, 0, len(unifiedImageVolumes)) + for _, volume := range unifiedImageVolumes { + finalImageVolumes = append(finalImageVolumes, volume) + } + + return finalMounts, finalVolumes, finalOverlayVolume, finalImageVolumes, nil +} + +// findMountType parses the input and extracts the type of the mount type and +// the remaining non-type tokens. +func findMountType(input string) (mountType string, tokens []string, err error) { + // Split by comma, iterate over the slice and look for + // "type=$mountType". Everything else is appended to tokens. + found := false + for _, s := range strings.Split(input, ",") { + kv := strings.Split(s, "=") + if found || !(len(kv) == 2 && kv[0] == "type") { + tokens = append(tokens, s) + continue + } + mountType = kv[1] + found = true + } + if !found { + err = errInvalidSyntax + } + return +} + +// getMounts takes user-provided input from the --mount flag and creates OCI +// spec mounts and Libpod named volumes. +// podman run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ... +// podman run --mount type=tmpfs,target=/dev/shm ... +// podman run --mount type=volume,source=test-volume, ... +func getMounts(mountFlag []string) (map[string]spec.Mount, map[string]*specgen.NamedVolume, map[string]*specgen.ImageVolume, error) { + finalMounts := make(map[string]spec.Mount) + finalNamedVolumes := make(map[string]*specgen.NamedVolume) + finalImageVolumes := make(map[string]*specgen.ImageVolume) + + for _, mount := range mountFlag { + // TODO: Docker defaults to "volume" if no mount type is specified. + mountType, tokens, err := findMountType(mount) + if err != nil { + return nil, nil, nil, err + } + switch mountType { + case define.TypeBind: + mount, err := getBindMount(tokens) + if err != nil { + return nil, nil, nil, err + } + if _, ok := finalMounts[mount.Destination]; ok { + return nil, nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) + } + finalMounts[mount.Destination] = mount + case define.TypeTmpfs: + mount, err := getTmpfsMount(tokens) + if err != nil { + return nil, nil, nil, err + } + if _, ok := finalMounts[mount.Destination]; ok { + return nil, nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) + } + finalMounts[mount.Destination] = mount + case define.TypeDevpts: + mount, err := getDevptsMount(tokens) + if err != nil { + return nil, nil, nil, err + } + if _, ok := finalMounts[mount.Destination]; ok { + return nil, nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) + } + finalMounts[mount.Destination] = mount + case "image": + volume, err := getImageVolume(tokens) + if err != nil { + return nil, nil, nil, err + } + if _, ok := finalImageVolumes[volume.Destination]; ok { + return nil, nil, nil, errors.Wrapf(errDuplicateDest, volume.Destination) + } + finalImageVolumes[volume.Destination] = volume + case "volume": + volume, err := getNamedVolume(tokens) + if err != nil { + return nil, nil, nil, err + } + if _, ok := finalNamedVolumes[volume.Dest]; ok { + return nil, nil, nil, errors.Wrapf(errDuplicateDest, volume.Dest) + } + finalNamedVolumes[volume.Dest] = volume + default: + return nil, nil, nil, errors.Errorf("invalid filesystem type %q", mountType) + } + } + + return finalMounts, finalNamedVolumes, finalImageVolumes, nil +} + +// Parse a single bind mount entry from the --mount flag. +func getBindMount(args []string) (spec.Mount, error) { + newMount := spec.Mount{ + Type: define.TypeBind, + } + + var setSource, setDest, setRORW, setSuid, setDev, setExec, setRelabel bool + + for _, val := range args { + kv := strings.SplitN(val, "=", 2) + switch kv[0] { + case "bind-nonrecursive": + newMount.Options = append(newMount.Options, "bind") + case "readonly", "ro", "rw": + if setRORW { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'readonly', 'ro', or 'rw' options more than once") + } + setRORW = true + // Can be formatted as one of: + // readonly + // readonly=[true|false] + // ro + // ro=[true|false] + // rw + // rw=[true|false] + if kv[0] == "readonly" { + kv[0] = "ro" + } + switch len(kv) { + case 1: + newMount.Options = append(newMount.Options, kv[0]) + case 2: + switch strings.ToLower(kv[1]) { + case "true": + newMount.Options = append(newMount.Options, kv[0]) + case "false": + // Set the opposite only for rw + // ro's opposite is the default + if kv[0] == "rw" { + newMount.Options = append(newMount.Options, "ro") + } + default: + return newMount, errors.Wrapf(optionArgError, "'readonly', 'ro', or 'rw' must be set to true or false, instead received %q", kv[1]) + } + default: + return newMount, errors.Wrapf(optionArgError, "badly formatted option %q", val) + } + case "nosuid", "suid": + if setSuid { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newMount.Options = append(newMount.Options, kv[0]) + case "nodev", "dev": + if setDev { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newMount.Options = append(newMount.Options, kv[0]) + case "noexec", "exec": + if setExec { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newMount.Options = append(newMount.Options, kv[0]) + case "shared", "rshared", "private", "rprivate", "slave", "rslave", "unbindable", "runbindable", "Z", "z": + newMount.Options = append(newMount.Options, kv[0]) + case "bind-propagation": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, kv[1]) + case "src", "source": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if len(kv[1]) == 0 { + return newMount, errors.Wrapf(optionArgError, "host directory cannot be empty") + } + newMount.Source = kv[1] + setSource = true + case "target", "dst", "destination": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return newMount, err + } + newMount.Destination = filepath.Clean(kv[1]) + setDest = true + case "relabel": + if setRelabel { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'relabel' option more than once") + } + setRelabel = true + if len(kv) != 2 { + return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0]) + } + switch kv[1] { + case "private": + newMount.Options = append(newMount.Options, "Z") + case "shared": + newMount.Options = append(newMount.Options, "z") + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0]) + } + case "consistency": + // Often used on MACs and mistakenly on Linux platforms. + // Since Docker ignores this option so shall we. + continue + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setDest { + return newMount, noDestError + } + + if !setSource { + newMount.Source = newMount.Destination + } + + options, err := parse.ValidateVolumeOpts(newMount.Options) + if err != nil { + return newMount, err + } + newMount.Options = options + return newMount, nil +} + +// Parse a single tmpfs mount entry from the --mount flag +func getTmpfsMount(args []string) (spec.Mount, error) { + newMount := spec.Mount{ + Type: define.TypeTmpfs, + Source: define.TypeTmpfs, + } + + var setDest, setRORW, setSuid, setDev, setExec, setTmpcopyup bool + + for _, val := range args { + kv := strings.SplitN(val, "=", 2) + switch kv[0] { + case "tmpcopyup", "notmpcopyup": + if setTmpcopyup { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'tmpcopyup' and 'notmpcopyup' options more than once") + } + setTmpcopyup = true + newMount.Options = append(newMount.Options, kv[0]) + case "ro", "rw": + if setRORW { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once") + } + setRORW = true + newMount.Options = append(newMount.Options, kv[0]) + case "nosuid", "suid": + if setSuid { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newMount.Options = append(newMount.Options, kv[0]) + case "nodev", "dev": + if setDev { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newMount.Options = append(newMount.Options, kv[0]) + case "noexec", "exec": + if setExec { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newMount.Options = append(newMount.Options, kv[0]) + case "tmpfs-mode": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1])) + case "tmpfs-size": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1])) + case "src", "source": + return newMount, errors.Errorf("source is not supported with tmpfs mounts") + case "target", "dst", "destination": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return newMount, err + } + newMount.Destination = filepath.Clean(kv[1]) + setDest = true + case "consistency": + // Often used on MACs and mistakenly on Linux platforms. + // Since Docker ignores this option so shall we. + continue + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setDest { + return newMount, noDestError + } + + return newMount, nil +} + +// Parse a single devpts mount entry from the --mount flag +func getDevptsMount(args []string) (spec.Mount, error) { + newMount := spec.Mount{ + Type: define.TypeDevpts, + Source: define.TypeDevpts, + } + + var setDest bool + + for _, val := range args { + kv := strings.SplitN(val, "=", 2) + switch kv[0] { + case "target", "dst", "destination": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return newMount, err + } + newMount.Destination = filepath.Clean(kv[1]) + setDest = true + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setDest { + return newMount, noDestError + } + + return newMount, nil +} + +// Parse a single volume mount entry from the --mount flag. +// Note that the volume-label option for named volumes is currently NOT supported. +// TODO: add support for --volume-label +func getNamedVolume(args []string) (*specgen.NamedVolume, error) { + newVolume := new(specgen.NamedVolume) + + var setSource, setDest, setRORW, setSuid, setDev, setExec bool + + for _, val := range args { + kv := strings.SplitN(val, "=", 2) + switch kv[0] { + case "ro", "rw": + if setRORW { + return nil, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once") + } + setRORW = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "nosuid", "suid": + if setSuid { + return nil, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "nodev", "dev": + if setDev { + return nil, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "noexec", "exec": + if setExec { + return nil, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "volume-label": + return nil, errors.Errorf("the --volume-label option is not presently implemented") + case "src", "source": + if len(kv) == 1 { + return nil, errors.Wrapf(optionArgError, kv[0]) + } + newVolume.Name = kv[1] + setSource = true + case "target", "dst", "destination": + if len(kv) == 1 { + return nil, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return nil, err + } + newVolume.Dest = filepath.Clean(kv[1]) + setDest = true + case "consistency": + // Often used on MACs and mistakenly on Linux platforms. + // Since Docker ignores this option so shall we. + continue + default: + return nil, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setSource { + return nil, errors.Errorf("must set source volume") + } + if !setDest { + return nil, noDestError + } + + return newVolume, nil +} + +// Parse the arguments into an image volume. An image volume is a volume based +// on a container image. The container image is first mounted on the host and +// is then bind-mounted into the container. An ImageVolume is always mounted +// read only. +func getImageVolume(args []string) (*specgen.ImageVolume, error) { + newVolume := new(specgen.ImageVolume) + + for _, val := range args { + kv := strings.SplitN(val, "=", 2) + switch kv[0] { + case "src", "source": + if len(kv) == 1 { + return nil, errors.Wrapf(optionArgError, kv[0]) + } + newVolume.Source = kv[1] + case "target", "dst", "destination": + if len(kv) == 1 { + return nil, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return nil, err + } + newVolume.Destination = filepath.Clean(kv[1]) + case "rw", "readwrite": + switch kv[1] { + case "true": + newVolume.ReadWrite = true + case "false": + // Nothing to do. RO is default. + default: + return nil, errors.Wrapf(util.ErrBadMntOption, "invalid rw value %q", kv[1]) + } + case "consistency": + // Often used on MACs and mistakenly on Linux platforms. + // Since Docker ignores this option so shall we. + continue + default: + return nil, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if len(newVolume.Source)*len(newVolume.Destination) == 0 { + return nil, errors.Errorf("must set source and destination for image volume") + } + + return newVolume, nil +} + +// GetTmpfsMounts creates spec.Mount structs for user-requested tmpfs mounts +func getTmpfsMounts(tmpfsFlag []string) (map[string]spec.Mount, error) { + m := make(map[string]spec.Mount) + for _, i := range tmpfsFlag { + // Default options if nothing passed + var options []string + spliti := strings.Split(i, ":") + destPath := spliti[0] + if err := parse.ValidateVolumeCtrDir(spliti[0]); err != nil { + return nil, err + } + if len(spliti) > 1 { + options = strings.Split(spliti[1], ",") + } + + if _, ok := m[destPath]; ok { + return nil, errors.Wrapf(errDuplicateDest, destPath) + } + + mount := spec.Mount{ + Destination: filepath.Clean(destPath), + Type: string(define.TypeTmpfs), + Options: options, + Source: string(define.TypeTmpfs), + } + m[destPath] = mount + } + return m, nil +} -- cgit v1.2.3-54-g00ecf