diff options
Diffstat (limited to 'pkg')
44 files changed, 2064 insertions, 299 deletions
diff --git a/pkg/adapter/pods.go b/pkg/adapter/pods.go index 49f086ef3..0d9fa7210 100644 --- a/pkg/adapter/pods.go +++ b/pkg/adapter/pods.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "os" "path/filepath" "strings" @@ -248,6 +249,17 @@ func (r *LocalRuntime) CreatePod(ctx context.Context, cli *cliconfig.PodCreateVa err error ) + // This needs to be first, as a lot of options depend on + // WithInfraContainer() + if cli.Infra { + options = append(options, libpod.WithInfraContainer()) + nsOptions, err := shared.GetNamespaceOptions(strings.Split(cli.Share, ",")) + if err != nil { + return "", err + } + options = append(options, nsOptions...) + } + if cli.Flag("cgroup-parent").Changed { options = append(options, libpod.WithPodCgroupParent(cli.CgroupParent)) } @@ -264,17 +276,78 @@ func (r *LocalRuntime) CreatePod(ctx context.Context, cli *cliconfig.PodCreateVa options = append(options, libpod.WithPodHostname(cli.Hostname)) } - if cli.Infra { - options = append(options, libpod.WithInfraContainer()) - nsOptions, err := shared.GetNamespaceOptions(strings.Split(cli.Share, ",")) + if cli.Flag("add-host").Changed { + options = append(options, libpod.WithPodHosts(cli.StringSlice("add-host"))) + } + if cli.Flag("dns").Changed { + dns := cli.StringSlice("dns") + foundHost := false + for _, entry := range dns { + if entry == "host" { + foundHost = true + } + } + if foundHost && len(dns) > 1 { + return "", errors.Errorf("cannot set dns=host and still provide other DNS servers") + } + if foundHost { + options = append(options, libpod.WithPodUseImageResolvConf()) + } else { + options = append(options, libpod.WithPodDNS(cli.StringSlice("dns"))) + } + } + if cli.Flag("dns-opt").Changed { + options = append(options, libpod.WithPodDNSOption(cli.StringSlice("dns-opt"))) + } + if cli.Flag("dns-search").Changed { + options = append(options, libpod.WithPodDNSSearch(cli.StringSlice("dns-search"))) + } + if cli.Flag("ip").Changed { + ip := net.ParseIP(cli.String("ip")) + if ip == nil { + return "", errors.Errorf("invalid IP address %q passed to --ip", cli.String("ip")) + } + + options = append(options, libpod.WithPodStaticIP(ip)) + } + if cli.Flag("mac-address").Changed { + mac, err := net.ParseMAC(cli.String("mac-address")) if err != nil { - return "", err + return "", errors.Wrapf(err, "invalid MAC address %q passed to --mac-address", cli.String("mac-address")) + } + + options = append(options, libpod.WithPodStaticMAC(mac)) + } + if cli.Flag("network").Changed { + netValue := cli.String("network") + switch strings.ToLower(netValue) { + case "bridge": + // Do nothing. + // TODO: Maybe this should be split between slirp and + // bridge? Better to wait until someone asks... + logrus.Debugf("Pod using default network mode") + case "host": + logrus.Debugf("Pod will use host networking") + options = append(options, libpod.WithPodHostNetwork()) + case "": + return "", errors.Errorf("invalid value passed to --net: must provide a comma-separated list of CNI networks or host") + default: + // We'll assume this is a comma-separated list of CNI + // networks. + networks := strings.Split(netValue, ",") + logrus.Debugf("Pod joining CNI networks: %v", networks) + options = append(options, libpod.WithPodNetworks(networks)) + } + } + if cli.Flag("no-hosts").Changed { + if cli.Bool("no-hosts") { + options = append(options, libpod.WithPodUseImageHosts()) } - options = append(options, nsOptions...) } - if len(cli.Publish) > 0 { - portBindings, err := shared.CreatePortBindings(cli.Publish) + publish := cli.StringSlice("publish") + if len(publish) > 0 { + portBindings, err := shared.CreatePortBindings(publish) if err != nil { return "", err } @@ -497,6 +570,10 @@ func (r *LocalRuntime) PlayKubeYAML(ctx context.Context, c *cliconfig.KubePlayVa } podOptions = append(podOptions, libpod.WithPodHostname(hostname)) + if podYAML.Spec.HostNetwork { + podOptions = append(podOptions, libpod.WithPodHostNetwork()) + } + nsOptions, err := shared.GetNamespaceOptions(strings.Split(shared.DefaultKernelNamespaces, ",")) if err != nil { return nil, err diff --git a/pkg/adapter/pods_remote.go b/pkg/adapter/pods_remote.go index 5ef1a9216..20f089628 100644 --- a/pkg/adapter/pods_remote.go +++ b/pkg/adapter/pods_remote.go @@ -185,7 +185,7 @@ func (r *LocalRuntime) CreatePod(ctx context.Context, cli *cliconfig.PodCreateVa Infra: cli.Infra, InfraCommand: cli.InfraCommand, InfraImage: cli.InfraCommand, - Publish: cli.Publish, + Publish: cli.StringSlice("publish"), } return iopodman.CreatePod().Call(r.Conn, pc) diff --git a/pkg/api/handlers/decoder.go b/pkg/api/handlers/decoder.go index 890d77ecc..03b86275d 100644 --- a/pkg/api/handlers/decoder.go +++ b/pkg/api/handlers/decoder.go @@ -3,8 +3,10 @@ package handlers import ( "encoding/json" "reflect" + "syscall" "time" + "github.com/containers/libpod/pkg/util" "github.com/gorilla/schema" "github.com/sirupsen/logrus" ) @@ -17,6 +19,9 @@ func NewAPIDecoder() *schema.Decoder { d.IgnoreUnknownKeys(true) d.RegisterConverter(map[string][]string{}, convertUrlValuesString) d.RegisterConverter(time.Time{}, convertTimeString) + + var Signal syscall.Signal + d.RegisterConverter(Signal, convertSignal) return d } @@ -89,3 +94,11 @@ func convertTimeString(query string) reflect.Value { func ParseDateTime(query string) time.Time { return convertTimeString(query).Interface().(time.Time) } + +func convertSignal(query string) reflect.Value { + signal, err := util.ParseSignal(query) + if err != nil { + logrus.Infof("convertSignal: Failed to parse %s: %s", query, err.Error()) + } + return reflect.ValueOf(signal) +} diff --git a/pkg/api/handlers/containers_create.go b/pkg/api/handlers/generic/containers_create.go index 48f0de94d..7e542752f 100644 --- a/pkg/api/handlers/containers_create.go +++ b/pkg/api/handlers/generic/containers_create.go @@ -1,4 +1,4 @@ -package handlers +package generic import ( "encoding/json" @@ -6,10 +6,10 @@ import ( "net/http" "strings" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" image2 "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/namespaces" "github.com/containers/libpod/pkg/signal" @@ -17,14 +17,13 @@ import ( "github.com/containers/storage" "github.com/gorilla/schema" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) func CreateContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) - input := CreateContainerConfig{} + input := handlers.CreateContainerConfig{} query := struct { Name string `schema:"name"` }{ @@ -52,34 +51,11 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "makeCreatConfig()")) return } - cc.Name = query.Name - var pod *libpod.Pod - ctr, err := shared.CreateContainerFromCreateConfig(runtime, &cc, r.Context(), pod) - if err != nil { - if strings.Contains(err.Error(), "invalid log driver") { - // this does not quite work yet and needs a little more massaging - w.Header().Set("Content-Type", "text/plain; charset=us-ascii") - w.WriteHeader(http.StatusInternalServerError) - msg := fmt.Sprintf("logger: no log driver named '%s' is registered", input.HostConfig.LogConfig.Type) - if _, err := fmt.Fprintln(w, msg); err != nil { - log.Errorf("%s: %q", msg, err) - } - //s.WriteResponse(w, http.StatusInternalServerError, fmt.Sprintf("logger: no log driver named '%s' is registered", input.HostConfig.LogConfig.Type)) - return - } - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "CreateContainerFromCreateConfig()")) - return - } - - response := ContainerCreateResponse{ - ID: ctr.ID(), - Warnings: []string{}} - - utils.WriteResponse(w, http.StatusCreated, response) + utils.CreateContainer(r.Context(), w, runtime, &cc) } -func makeCreateConfig(input CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { +func makeCreateConfig(input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { var ( err error init bool diff --git a/pkg/api/handlers/generic/containers_stats.go b/pkg/api/handlers/generic/containers_stats.go index 19e2cc882..977979741 100644 --- a/pkg/api/handlers/generic/containers_stats.go +++ b/pkg/api/handlers/generic/containers_stats.go @@ -7,7 +7,6 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/cgroups" docker "github.com/docker/docker/api/types" @@ -58,17 +57,18 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { } var preRead time.Time - var preCPUStats docker.CPUStats + var preCPUStats CPUStats if query.Stream { preRead = time.Now() systemUsage, _ := cgroups.GetSystemCPUUsage() - preCPUStats = docker.CPUStats{ + preCPUStats = CPUStats{ CPUUsage: docker.CPUUsage{ TotalUsage: stats.CPUNano, PercpuUsage: stats.PerCPU, UsageInKernelmode: stats.CPUSystemNano, UsageInUsermode: stats.CPUNano - stats.CPUSystemNano, }, + CPU: stats.CPU, SystemUsage: systemUsage, OnlineCPUs: 0, ThrottlingData: docker.ThrottlingData{}, @@ -124,9 +124,8 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { } systemUsage, _ := cgroups.GetSystemCPUUsage() - - s := handlers.Stats{StatsJSON: docker.StatsJSON{ - Stats: docker.Stats{ + s := StatsJSON{ + Stats: Stats{ Read: time.Now(), PreRead: preRead, PidsStats: docker.PidsStats{ @@ -143,13 +142,14 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { IoTimeRecursive: nil, SectorsRecursive: nil, }, - CPUStats: docker.CPUStats{ + CPUStats: CPUStats{ CPUUsage: docker.CPUUsage{ TotalUsage: cgroupStat.CPU.Usage.Total, PercpuUsage: cgroupStat.CPU.Usage.PerCPU, UsageInKernelmode: cgroupStat.CPU.Usage.Kernel, UsageInUsermode: cgroupStat.CPU.Usage.Total - cgroupStat.CPU.Usage.Kernel, }, + CPU: stats.CPU, SystemUsage: systemUsage, OnlineCPUs: uint32(len(cgroupStat.CPU.Usage.PerCPU)), ThrottlingData: docker.ThrottlingData{ @@ -173,7 +173,7 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { Name: stats.Name, ID: stats.ContainerID, Networks: net, - }} + } utils.WriteJSON(w, http.StatusOK, s) if flusher, ok := w.(http.Flusher); ok { diff --git a/pkg/api/handlers/generic/images.go b/pkg/api/handlers/generic/images.go index afd107207..1ced499d9 100644 --- a/pkg/api/handlers/generic/images.go +++ b/pkg/api/handlers/generic/images.go @@ -255,7 +255,7 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { tag – Tag or digest. If empty when pulling an image, this causes all tags for the given image to be pulled. */ fromImage := query.FromImage - if len(query.Tag) < 1 { + if len(query.Tag) >= 1 { fromImage = fmt.Sprintf("%s:%s", fromImage, query.Tag) } diff --git a/pkg/api/handlers/generic/swagger.go b/pkg/api/handlers/generic/swagger.go index bfe527c41..c9c9610bb 100644 --- a/pkg/api/handlers/generic/swagger.go +++ b/pkg/api/handlers/generic/swagger.go @@ -1,13 +1,15 @@ package generic -import "github.com/containers/libpod/pkg/api/handlers" +import ( + "github.com/containers/libpod/pkg/api/handlers/utils" +) // Create container // swagger:response ContainerCreateResponse type swagCtrCreateResponse struct { // in:body Body struct { - handlers.ContainerCreateResponse + utils.ContainerCreateResponse } } diff --git a/pkg/api/handlers/generic/types.go b/pkg/api/handlers/generic/types.go new file mode 100644 index 000000000..f068ac011 --- /dev/null +++ b/pkg/api/handlers/generic/types.go @@ -0,0 +1,55 @@ +package generic + +import ( + "time" + + docker "github.com/docker/docker/api/types" +) + +// CPUStats aggregates and wraps all CPU related info of container +type CPUStats struct { + // CPU Usage. Linux and Windows. + CPUUsage docker.CPUUsage `json:"cpu_usage"` + + // System Usage. Linux only. + SystemUsage uint64 `json:"system_cpu_usage,omitempty"` + + // Online CPUs. Linux only. + OnlineCPUs uint32 `json:"online_cpus,omitempty"` + + // Usage of CPU in %. Linux only. + CPU float64 `json:"cpu"` + + // Throttling Data. Linux only. + ThrottlingData docker.ThrottlingData `json:"throttling_data,omitempty"` +} + +// Stats is Ultimate struct aggregating all types of stats of one container +type Stats struct { + // Common stats + Read time.Time `json:"read"` + PreRead time.Time `json:"preread"` + + // Linux specific stats, not populated on Windows. + PidsStats docker.PidsStats `json:"pids_stats,omitempty"` + BlkioStats docker.BlkioStats `json:"blkio_stats,omitempty"` + + // Windows specific stats, not populated on Linux. + NumProcs uint32 `json:"num_procs"` + StorageStats docker.StorageStats `json:"storage_stats,omitempty"` + + // Shared stats + CPUStats CPUStats `json:"cpu_stats,omitempty"` + PreCPUStats CPUStats `json:"precpu_stats,omitempty"` // "Pre"="Previous" + MemoryStats docker.MemoryStats `json:"memory_stats,omitempty"` +} + +type StatsJSON struct { + Stats + + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + + // Networks request version >=1.21 + Networks map[string]docker.NetworkStats `json:"networks,omitempty"` +} diff --git a/pkg/api/handlers/libpod/containers_create.go b/pkg/api/handlers/libpod/containers_create.go new file mode 100644 index 000000000..ebca41151 --- /dev/null +++ b/pkg/api/handlers/libpod/containers_create.go @@ -0,0 +1,29 @@ +package libpod + +import ( + "encoding/json" + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/specgen" + "github.com/pkg/errors" +) + +// CreateContainer takes a specgenerator and makes a container. It returns +// the new container ID on success along with any warnings. +func CreateContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + var sg specgen.SpecGenerator + if err := json.NewDecoder(r.Body).Decode(&sg); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + ctr, err := sg.MakeContainer(runtime) + if err != nil { + utils.InternalServerError(w, err) + return + } + response := utils.ContainerCreateResponse{ID: ctr.ID()} + utils.WriteJSON(w, http.StatusCreated, response) +} diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index d043b1204..f5700579b 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -99,12 +99,10 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http_code, err) return } - utils.WriteResponse(w, http.StatusCreated, handlers.IDResponse{ID: pod.CgroupParent()}) + utils.WriteResponse(w, http.StatusCreated, handlers.IDResponse{ID: pod.ID()}) } func Pods(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 500 internal var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) podInspectData []*libpod.PodInspect @@ -121,7 +119,7 @@ func Pods(w http.ResponseWriter, r *http.Request) { return } - if _, found := r.URL.Query()["filters"]; found { + if len(query.Filters) > 0 { utils.Error(w, "filters are not implemented yet", http.StatusInternalServerError, define.ErrNotImplemented) return } diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index a50f183f7..c72b0f817 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -78,10 +78,6 @@ type Container struct { docker.ContainerCreateConfig } -type ContainerStats struct { - docker.ContainerStats -} - type Version struct { docker.Version } @@ -143,14 +139,11 @@ type IDResponse struct { ID string `json:"id"` } -type Stats struct { - docker.StatsJSON -} - type ContainerTopOKBody struct { dockerContainer.ContainerTopOKBody } +// swagger:model PodCreateConfig type PodCreateConfig struct { Name string `json:"name"` CGroupParent string `json:"cgroup-parent"` @@ -548,11 +541,3 @@ func portsToPortSet(input map[string]struct{}) (nat.PortSet, error) { } return ports, nil } - -// ContainerCreateResponse is the response struct for creating a container -type ContainerCreateResponse struct { - // ID of the container created - ID string `json:"id"` - // Warnings during container creation - Warnings []string `json:"Warnings"` -} diff --git a/pkg/api/handlers/utils/containers.go b/pkg/api/handlers/utils/containers.go index c9bb9cf09..402005581 100644 --- a/pkg/api/handlers/utils/containers.go +++ b/pkg/api/handlers/utils/containers.go @@ -1,6 +1,7 @@ package utils import ( + "context" "fmt" "net/http" "syscall" @@ -9,10 +10,19 @@ import ( "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" + createconfig "github.com/containers/libpod/pkg/spec" "github.com/gorilla/schema" "github.com/pkg/errors" ) +// ContainerCreateResponse is the response struct for creating a container +type ContainerCreateResponse struct { + // ID of the container created + ID string `json:"id"` + // Warnings during container creation + Warnings []string `json:"Warnings"` +} + func KillContainer(w http.ResponseWriter, r *http.Request) (*libpod.Container, error) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) @@ -119,3 +129,18 @@ func GenerateFilterFuncsFromMap(r *libpod.Runtime, filters map[string][]string) } return filterFuncs, nil } + +func CreateContainer(ctx context.Context, w http.ResponseWriter, runtime *libpod.Runtime, cc *createconfig.CreateConfig) { + var pod *libpod.Pod + ctr, err := shared.CreateContainerFromCreateConfig(runtime, cc, ctx, pod) + if err != nil { + Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "CreateContainerFromCreateConfig()")) + return + } + + response := ContainerCreateResponse{ + ID: ctr.ID(), + Warnings: []string{}} + + WriteResponse(w, http.StatusCreated, response) +} diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 06877863c..6a2ba4b1e 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -33,7 +33,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/ConflictError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/create"), s.APIHandler(handlers.CreateContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/create"), s.APIHandler(generic.CreateContainer)).Methods(http.MethodPost) // swagger:operation GET /containers/json compat listContainers // --- // tags: @@ -550,7 +550,31 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { libpod endpoints */ - r.HandleFunc(VersionedPath("/libpod/containers/create"), s.APIHandler(handlers.CreateContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/containers/create libpod libpodCreateContainer + // --- + // summary: Create a container + // tags: + // - containers + // produces: + // - application/json + // parameters: + // - in: body + // name: create + // description: attributes for creating a container + // schema: + // $ref: "#/definitions/SpecGenerator" + // responses: + // 201: + // $ref: "#/responses/ContainerCreateResponse" + // 400: + // $ref: "#/responses/BadParamError" + // 404: + // $ref: "#/responses/NoSuchContainer" + // 409: + // $ref: "#/responses/ConflictError" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/create"), s.APIHandler(libpod.CreateContainer)).Methods(http.MethodPost) // swagger:operation GET /libpod/containers/json libpod libpodListContainers // --- // tags: @@ -591,21 +615,21 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // name: filters // type: string // description: | - // Returns a list of containers. - // - ancestor=(<image-name>[:<tag>], <image id>, or <image@digest>) - // - before=(<container id> or <container name>) - // - expose=(<port>[/<proto>]|<startport-endport>/[<proto>]) - // - exited=<int> containers with exit code of <int> - // - health=(starting|healthy|unhealthy|none) - // - id=<ID> a container's ID - // - is-task=(true|false) - // - label=key or label="key=value" of a container label - // - name=<name> a container's name - // - network=(<network id> or <network name>) - // - publish=(<port>[/<proto>]|<startport-endport>/[<proto>]) - // - since=(<container id> or <container name>) - // - status=(created|restarting|running|removing|paused|exited|dead) - // - volume=(<volume name> or <mount point destination>) + // A JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: + // - `ancestor`=(`<image-name>[:<tag>]`, `<image id>`, or `<image@digest>`) + // - `before`=(`<container id>` or `<container name>`) + // - `expose`=(`<port>[/<proto>]` or `<startport-endport>/[<proto>]`) + // - `exited=<int>` containers with exit code of `<int>` + // - `health`=(`starting`, `healthy`, `unhealthy` or `none`) + // - `id=<ID>` a container's ID + // - `is-task`=(`true` or `false`) + // - `label`=(`key` or `"key=value"`) of an container label + // - `name=<name>` a container's name + // - `network`=(`<network id>` or `<network name>`) + // - `publish`=(`<port>[/<proto>]` or `<startport-endport>/[<proto>]`) + // - `since`=(`<container id>` or `<container name>`) + // - `status`=(`created`, `restarting`, `running`, `removing`, `paused`, `exited` or `dead`) + // - `volume`=(`<volume name>` or `<mount point destination>`) // produces: // - application/json // responses: @@ -638,7 +662,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/prune"), s.APIHandler(handlers.PruneContainers)).Methods(http.MethodPost) - // swagger:operation GET /libpod/containers/showmounted libpod showMountedContainers + // swagger:operation GET /libpod/containers/showmounted libpod libpodShowMountedContainers // --- // tags: // - containers @@ -745,7 +769,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/kill"), s.APIHandler(libpod.KillContainer)).Methods(http.MethodPost) - // swagger:operation POST /libpod/containers/{name}/mount libpod mountContainer + // swagger:operation POST /libpod/containers/{name}/mount libpod libpodMountContainer // --- // tags: // - containers @@ -1023,7 +1047,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/wait"), s.APIHandler(libpod.WaitContainer)).Methods(http.MethodPost) - // swagger:operation GET /libpod/containers/{name}/exists libpod containerExists + // swagger:operation GET /libpod/containers/{name}/exists libpod libpodContainerExists // --- // tags: // - containers @@ -1072,7 +1096,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/stop"), s.APIHandler(handlers.StopContainer)).Methods(http.MethodPost) - // swagger:operation POST /libpod/containers/{name}/attach libpod libpodAttach + // swagger:operation POST /libpod/containers/{name}/attach libpod libpodAttachContainer // --- // tags: // - containers @@ -1127,7 +1151,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/attach"), s.APIHandler(handlers.AttachContainer)).Methods(http.MethodPost) - // swagger:operation POST /libpod/containers/{name}/resize libpod libpodResize + // swagger:operation POST /libpod/containers/{name}/resize libpod libpodResizeContainer // --- // tags: // - containers diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 5b9ac7952..db04ecdc9 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -648,6 +648,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - `dangling=true` // - `label=key` or `label="key=value"` of an image label // - `reference`=(`<image-name>[:<tag>]`) + // - `id`=(`<image-id>`) // - `since`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) // type: string // produces: @@ -822,7 +823,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(handlers.SearchImages)).Methods(http.MethodGet) - // swagger:operation DELETE /libpod/images/{name} libpod libpodRemoveImage + // swagger:operation DELETE /libpod/images/{name:.*} libpod libpodRemoveImage // --- // tags: // - images @@ -830,7 +831,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: Delete an image from local store // parameters: // - in: path - // name: name + // name: name:.* // type: string // required: true // description: name or ID of image to delete diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index 1c7486711..af2330665 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -26,6 +26,25 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.Handle(VersionedPath("/libpod/pods/json"), s.APIHandler(libpod.Pods)).Methods(http.MethodGet) + // swagger:operation POST /libpod/pods/create pods CreatePod + // --- + // summary: Create a pod + // produces: + // - application/json + // parameters: + // - in: body + // name: create + // description: attributes for creating a pod + // schema: + // type: object + // $ref: "#/definitions/PodCreateConfig" + // responses: + // 200: + // $ref: "#/definitions/IdResponse" + // 400: + // $ref: "#/responses/BadParamError" + // 500: + // $ref: "#/responses/InternalError" r.Handle(VersionedPath("/libpod/pods/create"), s.APIHandler(libpod.PodCreate)).Methods(http.MethodPost) // swagger:operation POST /libpod/pods/prune pods PrunePods // --- diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index f270060a6..75f1fc6a5 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -1,22 +1,34 @@ package bindings import ( + "bufio" "context" "fmt" "io" + "io/ioutil" "net" "net/http" "net/url" + "os" "path/filepath" + "strconv" "strings" + "time" "github.com/containers/libpod/pkg/api/handlers" jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" + "k8s.io/client-go/util/homedir" ) var ( - defaultConnectionPath string = filepath.Join(fmt.Sprintf("v%s", handlers.MinimalApiVersion), "libpod") + basePath = &url.URL{ + Scheme: "http", + Host: "d", + Path: "/v" + handlers.MinimalApiVersion + "/libpod", + } ) type APIResponse struct { @@ -25,9 +37,28 @@ type APIResponse struct { } type Connection struct { - scheme string - address string - client *http.Client + _url *url.URL + client *http.Client +} + +type valueKey string + +const ( + clientKey = valueKey("client") +) + +// GetClient from context build by NewConnection() +func GetClient(ctx context.Context) (*Connection, error) { + c, ok := ctx.Value(clientKey).(*Connection) + if !ok { + return nil, errors.Errorf("ClientKey not set in context") + } + return c, nil +} + +// JoinURL elements with '/' +func JoinURL(elements ...string) string { + return strings.Join(elements, "/") } // NewConnection takes a URI as a string and returns a context with the @@ -36,46 +67,81 @@ type Connection struct { // // A valid URI connection should be scheme:// // For example tcp://localhost:<port> -// or unix://run/podman/podman.sock -func NewConnection(uri string) (context.Context, error) { - u, err := url.Parse(uri) - if err != nil { - return nil, err - } - // TODO once ssh is implemented, remove this block and - // add it to the conditional beneath it - if u.Scheme == "ssh" { - return nil, ErrNotImplemented +// or unix:///run/podman/podman.sock +// or ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True +func NewConnection(ctx context.Context, uri string, identity ...string) (context.Context, error) { + var ( + err error + secure bool + ) + if v, found := os.LookupEnv("PODMAN_HOST"); found { + uri = v } - if u.Scheme != "tcp" && u.Scheme != "unix" { - return nil, errors.Errorf("%s is not a support schema", u.Scheme) + + if v, found := os.LookupEnv("PODMAN_SSHKEY"); found { + identity = []string{v} } - if u.Scheme == "tcp" && !strings.HasPrefix(uri, "tcp://") { - return nil, errors.New("tcp URIs should begin with tcp://") + _url, err := url.Parse(uri) + if err != nil { + return nil, errors.Wrapf(err, "Value of PODMAN_HOST is not a valid url: %s", uri) } - address := u.Path - if u.Scheme == "tcp" { - address = u.Host + // Now we setup the http client to use the connection above + var client *http.Client + switch _url.Scheme { + case "ssh": + secure, err = strconv.ParseBool(_url.Query().Get("secure")) + if err != nil { + secure = false + } + client, err = sshClient(_url, identity[0], secure) + case "unix": + if !strings.HasPrefix(uri, "unix:///") { + // autofix unix://path_element vs unix:///path_element + _url.Path = JoinURL(_url.Host, _url.Path) + _url.Host = "" + } + client, err = unixClient(_url) + case "tcp": + if !strings.HasPrefix(uri, "tcp://") { + return nil, errors.New("tcp URIs should begin with tcp://") + } + client, err = tcpClient(_url) + default: + return nil, errors.Errorf("%s is not a support schema", _url.Scheme) + } + if err != nil { + return nil, errors.Wrapf(err, "Failed to create %sClient", _url.Scheme) } - newConn := newConnection(u.Scheme, address) - ctx := context.WithValue(context.Background(), "conn", &newConn) + + ctx = context.WithValue(ctx, clientKey, &Connection{_url, client}) if err := pingNewConnection(ctx); err != nil { return nil, err } return ctx, nil } +func tcpClient(_url *url.URL) (*http.Client, error) { + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("tcp", _url.Path) + }, + DisableCompression: true, + }, + }, nil +} + // pingNewConnection pings to make sure the RESTFUL service is up // and running. it should only be used where initializing a connection func pingNewConnection(ctx context.Context) error { - conn, err := GetConnectionFromContext(ctx) + client, err := GetClient(ctx) if err != nil { return err } // the ping endpoint sits at / in this case - response, err := conn.DoRequest(nil, http.MethodGet, "../../../_ping", nil) + response, err := client.DoRequest(nil, http.MethodGet, "../../../_ping", nil) if err != nil { return err } @@ -85,26 +151,58 @@ func pingNewConnection(ctx context.Context) error { return errors.Errorf("ping response was %q", response.StatusCode) } -// newConnection takes a scheme and address and creates a connection from it -func newConnection(scheme, address string) Connection { - client := http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial(scheme, address) +func sshClient(_url *url.URL, identity string, secure bool) (*http.Client, error) { + auth, err := publicKey(identity) + if err != nil { + return nil, errors.Wrapf(err, "Failed to parse identity %s: %v\n", _url.String(), identity) + } + + callback := ssh.InsecureIgnoreHostKey() + if secure { + key := hostKey(_url.Hostname()) + if key != nil { + callback = ssh.FixedHostKey(key) + } + } + + bastion, err := ssh.Dial("tcp", + net.JoinHostPort(_url.Hostname(), _url.Port()), + &ssh.ClientConfig{ + User: _url.User.Username(), + Auth: []ssh.AuthMethod{auth}, + HostKeyCallback: callback, + HostKeyAlgorithms: []string{ + ssh.KeyAlgoRSA, + ssh.KeyAlgoDSA, + ssh.KeyAlgoECDSA256, + ssh.KeyAlgoECDSA384, + ssh.KeyAlgoECDSA521, + ssh.KeyAlgoED25519, }, + Timeout: 5 * time.Second, }, + ) + if err != nil { + return nil, errors.Wrapf(err, "Connection to bastion host (%s) failed.", _url.String()) } - newConn := Connection{ - client: &client, - address: address, - scheme: scheme, - } - return newConn + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return bastion.Dial("unix", _url.Path) + }, + }}, nil } -func (c *Connection) makeEndpoint(u string) string { - // The d character in the url is discarded and is meaningless - return fmt.Sprintf("http://d/%s%s", defaultConnectionPath, u) +func unixClient(_url *url.URL) (*http.Client, error) { + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + d := net.Dialer{} + return d.DialContext(ctx, "unix", _url.Path) + }, + DisableCompression: true, + }, + }, nil } // DoRequest assembles the http request and returns the response @@ -121,7 +219,7 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, // Lets eventually use URL for this which might lead to safer // usage safeEndpoint := fmt.Sprintf(endpoint, safePathValues...) - e := c.makeEndpoint(safeEndpoint) + e := basePath.String() + safeEndpoint req, err := http.NewRequest(httpMethod, e, httpBody) if err != nil { return nil, err @@ -140,21 +238,11 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, if err == nil { break } + time.Sleep(time.Duration(i*100) * time.Millisecond) } return &APIResponse{response, req}, err } -// GetConnectionFromContext returns a bindings connection from the context -// being passed into each method. -func GetConnectionFromContext(ctx context.Context) (*Connection, error) { - c := ctx.Value("conn") - if c == nil { - return nil, errors.New("unable to get connection from context") - } - conn := c.(*Connection) - return conn, nil -} - // FiltersToString converts our typical filter format of a // map[string][]string to a query/html safe string. func FiltersToString(filters map[string][]string) (string, error) { @@ -189,3 +277,45 @@ func (h *APIResponse) IsClientError() bool { func (h *APIResponse) IsServerError() bool { return h.Response.StatusCode/100 == 5 } + +func publicKey(path string) (ssh.AuthMethod, error) { + key, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + return nil, err + } + + return ssh.PublicKeys(signer), nil +} + +func hostKey(host string) ssh.PublicKey { + // parse OpenSSH known_hosts file + // ssh or use ssh-keyscan to get initial key + known_hosts := filepath.Join(homedir.HomeDir(), ".ssh", "known_hosts") + fd, err := os.Open(known_hosts) + if err != nil { + logrus.Error(err) + return nil + } + + scanner := bufio.NewScanner(fd) + for scanner.Scan() { + _, hosts, key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes()) + if err != nil { + logrus.Errorf("Failed to parse known_hosts: %s", scanner.Text()) + continue + } + + for _, h := range hosts { + if h == host { + return key + } + } + } + + return nil +} diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index 04f7f8802..a437e9a9b 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -16,7 +16,7 @@ import ( // size information should also be included. Finally, the sync bool synchronizes the OCI runtime and // container state. func List(ctx context.Context, filters map[string][]string, all *bool, last *int, pod, size, sync *bool) ([]lpapiv2.ListContainer, error) { // nolint:typecheck - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -59,7 +59,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { var ( pruneResponse []string ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -82,7 +82,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { // that the container should be removed forcibly (example, even it is running). The volumes // bool dictates that a container's volumes should also be removed. func Remove(ctx context.Context, nameOrID string, force, volumes *bool) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -105,7 +105,7 @@ func Remove(ctx context.Context, nameOrID string, force, volumes *bool) error { // should be calculated. Calculating the size of a container requires extra work from the filesystem and // is therefore slower. func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectContainerData, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -125,7 +125,7 @@ func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectC // representation of a signal like 'SIGKILL'. The nameOrID can be a container name // or a partial/full ID func Kill(ctx context.Context, nameOrID string, signal string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -143,7 +143,7 @@ func Logs() {} // Pause pauses a given container. The nameOrID can be a container name // or a partial/full ID. func Pause(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -158,7 +158,7 @@ func Pause(ctx context.Context, nameOrID string) error { // or a partial/full ID. The optional timeout specifies the number of seconds to wait // for the running container to stop before killing it. func Restart(ctx context.Context, nameOrID string, timeout *int) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -177,7 +177,7 @@ func Restart(ctx context.Context, nameOrID string, timeout *int) error { // or a partial/full ID. The optional parameter for detach keys are to override the default // detach key sequence. func Start(ctx context.Context, nameOrID string, detachKeys *string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -198,7 +198,7 @@ func Top() {} // Unpause resumes the given paused container. The nameOrID can be a container name // or a partial/full ID. func Unpause(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -213,7 +213,7 @@ func Unpause(ctx context.Context, nameOrID string) error { // or a partial/full ID. func Wait(ctx context.Context, nameOrID string) (int32, error) { var exitCode int32 - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return exitCode, err } @@ -228,7 +228,7 @@ func Wait(ctx context.Context, nameOrID string) (int32, error) { // exists in local storage. The nameOrID can be a container name // or a partial/full ID. func Exists(ctx context.Context, nameOrID string) (bool, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return false, err } @@ -243,7 +243,7 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // or a partial/full ID func Stop(ctx context.Context, nameOrID string, timeout *int) error { params := make(map[string]string) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } diff --git a/pkg/bindings/containers/create.go b/pkg/bindings/containers/create.go new file mode 100644 index 000000000..2943cb522 --- /dev/null +++ b/pkg/bindings/containers/create.go @@ -0,0 +1,30 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/specgen" + jsoniter "github.com/json-iterator/go" +) + +func CreateWithSpec(ctx context.Context, s specgen.SpecGenerator) (utils.ContainerCreateResponse, error) { + var ccr utils.ContainerCreateResponse + conn, err := bindings.GetClient(ctx) + if err != nil { + return ccr, err + } + specgenString, err := jsoniter.MarshalToString(s) + if err != nil { + return ccr, nil + } + stringReader := strings.NewReader(specgenString) + response, err := conn.DoRequest(stringReader, http.MethodPost, "/containers/create", nil) + if err != nil { + return ccr, err + } + return ccr, response.Process(&ccr) +} diff --git a/pkg/bindings/containers/healthcheck.go b/pkg/bindings/containers/healthcheck.go index 9ed7f858d..dc607c1b3 100644 --- a/pkg/bindings/containers/healthcheck.go +++ b/pkg/bindings/containers/healthcheck.go @@ -11,7 +11,7 @@ import ( // RunHealthCheck executes the container's healthcheck and returns the health status of the // container. func RunHealthCheck(ctx context.Context, nameOrID string) (*libpod.HealthCheckStatus, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } diff --git a/pkg/bindings/containers/mount.go b/pkg/bindings/containers/mount.go index d68dee981..e0627d9a3 100644 --- a/pkg/bindings/containers/mount.go +++ b/pkg/bindings/containers/mount.go @@ -10,7 +10,7 @@ import ( // Mount mounts an existing container to the filesystem. It returns the path // of the mounted container in string format. func Mount(ctx context.Context, nameOrID string) (string, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return "", err } @@ -27,7 +27,7 @@ func Mount(ctx context.Context, nameOrID string) (string, error) { // Unmount unmounts a container from the filesystem. The container must not be running // or the unmount will fail. func Unmount(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -40,7 +40,7 @@ func Unmount(ctx context.Context, nameOrID string) error { // GetMountedContainerPaths returns a map of mounted containers and their mount locations. func GetMountedContainerPaths(ctx context.Context) (map[string]string, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index b19482943..271d58952 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -14,7 +14,7 @@ import ( // Exists a lightweight way to determine if an image exists in local storage. It returns a // boolean response. func Exists(ctx context.Context, nameOrID string) (bool, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return false, err } @@ -29,7 +29,7 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // ways to alter the image query. func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handlers.ImageSummary, error) { var imageSummary []*handlers.ImageSummary - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -54,7 +54,7 @@ func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handl // Get performs an image inspect. To have the on-disk size of the image calculated, you can // use the optional size parameter. func GetImage(ctx context.Context, nameOrID string, size *bool) (*inspect.ImageData, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -77,7 +77,7 @@ func ImageTree(ctx context.Context, nameOrId string) error { // History returns the parent layers of an image. func History(ctx context.Context, nameOrID string) ([]*handlers.HistoryResponse, error) { var history []*handlers.HistoryResponse - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -89,7 +89,7 @@ func History(ctx context.Context, nameOrID string) ([]*handlers.HistoryResponse, } func Load(ctx context.Context, r io.Reader) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -103,7 +103,7 @@ func Load(ctx context.Context, r io.Reader) error { // the image by removing all all containers, including those that are Running, first. func Remove(ctx context.Context, nameOrID string, force *bool) ([]map[string]string, error) { var deletes []map[string]string - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -121,7 +121,7 @@ func Remove(ctx context.Context, nameOrID string, force *bool) ([]map[string]str // Export saves an image from local storage as a tarball or image archive. The optional format // parameter is used to change the format of the output. func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, compress *bool) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -149,7 +149,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { var ( deleted []string ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -170,7 +170,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { // Tag adds an additional name to locally-stored image. Both the tag and repo parameters are required. func Tag(ctx context.Context, nameOrID, tag, repo string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } diff --git a/pkg/bindings/images/search.go b/pkg/bindings/images/search.go index 58b25425b..dca1b0e63 100644 --- a/pkg/bindings/images/search.go +++ b/pkg/bindings/images/search.go @@ -16,7 +16,7 @@ func Search(ctx context.Context, term string, limit *int, filters map[string][]s var ( searchResults []image.SearchResult ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go index 97bbb8c42..c95b22953 100644 --- a/pkg/bindings/network/network.go +++ b/pkg/bindings/network/network.go @@ -10,7 +10,7 @@ import ( func Create() {} func Inspect(ctx context.Context, nameOrID string) (map[string]interface{}, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -23,7 +23,7 @@ func Inspect(ctx context.Context, nameOrID string) (map[string]interface{}, erro } func Remove(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -38,7 +38,7 @@ func List(ctx context.Context) ([]*libcni.NetworkConfigList, error) { var ( netList []*libcni.NetworkConfigList ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index d079f01c2..838b22e43 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -16,7 +16,7 @@ func CreatePod() error { // Exists is a lightweight method to determine if a pod exists in local storage func Exists(ctx context.Context, nameOrID string) (bool, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return false, err } @@ -29,7 +29,7 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // Inspect returns low-level information about the given pod. func Inspect(ctx context.Context, nameOrID string) (*libpod.PodInspect, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -44,7 +44,7 @@ func Inspect(ctx context.Context, nameOrID string) (*libpod.PodInspect, error) { // Kill sends a SIGTERM to all the containers in a pod. The optional signal parameter // can be used to override SIGTERM. func Kill(ctx context.Context, nameOrID string, signal *string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -61,7 +61,7 @@ func Kill(ctx context.Context, nameOrID string, signal *string) error { // Pause pauses all running containers in a given pod. func Pause(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -74,7 +74,7 @@ func Pause(ctx context.Context, nameOrID string) error { // Prune removes all non-running pods in local storage. func Prune(ctx context.Context) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -87,11 +87,11 @@ func Prune(ctx context.Context) error { // List returns all pods in local storage. The optional filters parameter can // be used to refine which pods should be listed. -func List(ctx context.Context, filters map[string][]string) (*[]libpod.PodInspect, error) { +func List(ctx context.Context, filters map[string][]string) ([]*libpod.PodInspect, error) { var ( - inspect []libpod.PodInspect + inspect []*libpod.PodInspect ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -103,16 +103,16 @@ func List(ctx context.Context, filters map[string][]string) (*[]libpod.PodInspec } params["filters"] = stringFilter } - response, err := conn.DoRequest(nil, http.MethodPost, "/pods/json", params) + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/json", params) if err != nil { - return &inspect, err + return inspect, err } - return &inspect, response.Process(&inspect) + return inspect, response.Process(&inspect) } // Restart restarts all containers in a pod. func Restart(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -126,7 +126,7 @@ func Restart(ctx context.Context, nameOrID string) error { // Remove deletes a Pod from from local storage. The optional force parameter denotes // that the Pod can be removed even if in a running state. func Remove(ctx context.Context, nameOrID string, force *bool) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -143,11 +143,11 @@ func Remove(ctx context.Context, nameOrID string, force *bool) error { // Start starts all containers in a pod. func Start(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s/start", nil, nameOrID) + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/start", nil, nameOrID) if err != nil { return err } @@ -162,7 +162,7 @@ func Stats() error { // Stop stops all containers in a Pod. The optional timeout parameter can be // used to override the timeout before the container is killed. func Stop(ctx context.Context, nameOrID string, timeout *int) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -184,7 +184,7 @@ func Top() error { // Unpause unpauses all paused containers in a Pod. func Unpause(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index dba94cb35..98d64bbaa 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -114,7 +114,7 @@ func newBindingTest() *bindingTest { runRoot: filepath.Join(tmpPath, "run"), artifactDirPath: "", imageCacheDir: "", - sock: fmt.Sprintf("unix:%s", filepath.Join(tmpPath, "api.sock")), + sock: fmt.Sprintf("unix://%s", filepath.Join(tmpPath, "api.sock")), tempDirPath: tmpPath, } return &b @@ -162,16 +162,30 @@ func (b *bindingTest) restoreImageFromCache(i testImage) { p.Wait(45) } -// Run a container and add append the alpine image to it -func (b *bindingTest) RunTopContainer(name *string) { +// Run a container within or without a pod +// and add or append the alpine image to it +func (b *bindingTest) RunTopContainer(containerName *string, insidePod *bool, podName *string) { cmd := []string{"run", "-dt"} - if name != nil { - containerName := *name - cmd = append(cmd, "--name", containerName) + if *insidePod && podName != nil { + pName := *podName + cmd = append(cmd, "--pod", pName) + } else if containerName != nil { + cName := *containerName + cmd = append(cmd, "--name", cName) } cmd = append(cmd, alpine.name, "top") - p := b.runPodman(cmd) - p.Wait(45) + b.runPodman(cmd).Wait(45) +} + +// This method creates a pod with the given pod name. +// Podname is an optional parameter +func (b *bindingTest) Podcreate(name *string) { + if name != nil { + podname := *name + b.runPodman([]string{"pod", "create", "--name", podname}).Wait(45) + } else { + b.runPodman([]string{"pod", "create"}).Wait(45) + } } // StringInSlice returns a boolean based on whether a given diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 74e0cc67a..0b51c8c9e 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -38,7 +38,7 @@ var _ = Describe("Podman images", func() { bt.RestoreImagesFromCache() s = bt.startAPIService() time.Sleep(1 * time.Second) - connText, err = bindings.NewConnection(bt.sock) + connText, err = bindings.NewConnection(context.Background(), bt.sock) Expect(err).To(BeNil()) }) @@ -93,7 +93,7 @@ var _ = Describe("Podman images", func() { // Start a container with alpine image var top string = "top" - bt.RunTopContainer(&top) + bt.RunTopContainer(&top, &falseFlag, nil) // we should now have a container called "top" running containerResponse, err := containers.Inspect(connText, "top", &falseFlag) Expect(err).To(BeNil()) diff --git a/pkg/bindings/test/pods_test.go b/pkg/bindings/test/pods_test.go new file mode 100644 index 000000000..76ccd10f2 --- /dev/null +++ b/pkg/bindings/test/pods_test.go @@ -0,0 +1,202 @@ +package test_bindings + +import ( + "context" + "net/http" + "time" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/pods" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman images", func() { + var ( + bt *bindingTest + s *gexec.Session + connText context.Context + newpod string + err error + trueFlag bool = true + ) + + BeforeEach(func() { + bt = newBindingTest() + newpod = "newpod" + bt.RestoreImagesFromCache() + bt.Podcreate(&newpod) + s = bt.startAPIService() + time.Sleep(1 * time.Second) + connText, err = bindings.NewConnection(bt.sock) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + s.Kill() + bt.cleanup() + }) + + It("inspect pod", func() { + //Inspect an invalid pod name + _, err := pods.Inspect(connText, "dummyname") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + //Inspect an valid pod name + response, err := pods.Inspect(connText, newpod) + Expect(err).To(BeNil()) + Expect(response.Config.Name).To(Equal(newpod)) + }) + + // Test validates the list all api returns + It("list pod", func() { + //List all the pods in the current instance + podSummary, err := pods.List(connText, nil) + Expect(err).To(BeNil()) + Expect(len(podSummary)).To(Equal(1)) + // Adding an alpine container to the existing pod + bt.RunTopContainer(nil, &trueFlag, &newpod) + podSummary, err = pods.List(connText, nil) + // Verify no errors. + Expect(err).To(BeNil()) + // Verify number of containers in the pod. + Expect(len(podSummary[0].Containers)).To(Equal(2)) + + // Add multiple pods and verify them by name and size. + var newpod2 string = "newpod2" + bt.Podcreate(&newpod2) + podSummary, err = pods.List(connText, nil) + Expect(len(podSummary)).To(Equal(2)) + var names []string + for _, i := range podSummary { + names = append(names, i.Config.Name) + } + Expect(StringInSlice(newpod, names)).To(BeTrue()) + Expect(StringInSlice("newpod2", names)).To(BeTrue()) + + // Not working Because: code to list based on filter + // "not yet implemented", + // Validate list pod with filters + filters := make(map[string][]string) + filters["name"] = []string{newpod} + filteredPods, err := pods.List(connText, filters) + Expect(err).To(BeNil()) + Expect(len(filteredPods)).To(BeNumerically("==", 1)) + }) + + // The test validates if the exists responds + It("exists pod", func() { + response, err := pods.Exists(connText, "dummyName") + Expect(err).To(BeNil()) + Expect(response).To(BeFalse()) + + // Should exit with no error and response should be true + response, err = pods.Exists(connText, "newpod") + Expect(err).To(BeNil()) + Expect(response).To(BeTrue()) + }) + + // This test validates if All running containers within + // each specified pod are paused and unpaused + It("pause upause pod", func() { + // Pause invalid container + err := pods.Pause(connText, "dummyName") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Adding an alpine container to the existing pod + bt.RunTopContainer(nil, &trueFlag, &newpod) + response, err := pods.Inspect(connText, newpod) + Expect(err).To(BeNil()) + + // Binding needs to be modified to inspect the pod state. + // Since we dont have a pod state we inspect the states of the containers within the pod. + // Pause a valid container + err = pods.Pause(connText, newpod) + Expect(err).To(BeNil()) + response, err = pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStatePaused)) + } + + // Unpause a valid container + err = pods.Unpause(connText, newpod) + Expect(err).To(BeNil()) + response, err = pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateRunning)) + } + }) + + It("start stop restart pod", func() { + // Start an invalid pod + err = pods.Start(connText, "dummyName") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Stop an invalid pod + err = pods.Stop(connText, "dummyName", nil) + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Restart an invalid pod + err = pods.Restart(connText, "dummyName") + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Start a valid pod and inspect status of each container + err = pods.Start(connText, newpod) + Expect(err).To(BeNil()) + + response, err := pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateRunning)) + } + + // Start a already running container + // (Test fails for now needs to be fixed) + err = pods.Start(connText, newpod) + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotModified)) + + // Stop the running pods + err = pods.Stop(connText, newpod, nil) + Expect(err).To(BeNil()) + response, _ = pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateStopped)) + } + + // Stop a already running pod + // (Test fails for now needs to be fixed) + err = pods.Stop(connText, newpod, nil) + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotModified)) + + err = pods.Restart(connText, newpod) + Expect(err).To(BeNil()) + response, _ = pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateRunning)) + } + }) + + // Remove all stopped pods and their container to be implemented. + It("prune pod", func() { + }) +}) diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go index 05a4f73fd..8313a7460 100644 --- a/pkg/bindings/volumes/volumes.go +++ b/pkg/bindings/volumes/volumes.go @@ -16,7 +16,7 @@ func Create(ctx context.Context, config handlers.VolumeCreateConfig) (string, er var ( volumeID string ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return "", err } @@ -32,7 +32,7 @@ func Inspect(ctx context.Context, nameOrID string) (*libpod.InspectVolumeData, e var ( inspect libpod.InspectVolumeData ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -55,7 +55,7 @@ func Prune(ctx context.Context) ([]string, error) { var ( pruned []string ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func Prune(ctx context.Context) ([]string, error) { // Remove deletes the given volume from storage. The optional force parameter // is used to remove a volume even if it is being used by a container. func Remove(ctx context.Context, nameOrID string, force *bool) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } diff --git a/pkg/seccomp/seccomp.go b/pkg/seccomp/seccomp.go index dcf255378..eeba46a72 100644 --- a/pkg/seccomp/seccomp.go +++ b/pkg/seccomp/seccomp.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" ) -// ContianerImageLabel is the key of the image annotation embedding a seccomp +// ContainerImageLabel is the key of the image annotation embedding a seccomp // profile. const ContainerImageLabel = "io.containers.seccomp.profile" diff --git a/pkg/spec/config_linux.go b/pkg/spec/config_linux.go index 5f39b6d0d..544c0020d 100644 --- a/pkg/spec/config_linux.go +++ b/pkg/spec/config_linux.go @@ -32,8 +32,8 @@ func Device(d *configs.Device) spec.LinuxDevice { } } -// devicesFromPath computes a list of devices -func devicesFromPath(g *generate.Generator, devicePath string) error { +// DevicesFromPath computes a list of devices +func DevicesFromPath(g *generate.Generator, devicePath string) error { devs := strings.Split(devicePath, ":") resolvedDevicePath := devs[0] // check if it is a symbolic link @@ -216,7 +216,7 @@ func getDevices(path string) ([]*configs.Device, error) { return out, nil } -func (c *CreateConfig) addPrivilegedDevices(g *generate.Generator) error { +func addPrivilegedDevices(g *generate.Generator) error { hostDevices, err := getDevices("/dev") if err != nil { return err @@ -280,16 +280,16 @@ func (c *CreateConfig) createBlockIO() (*spec.LinuxBlockIO, error) { var lwds []spec.LinuxWeightDevice ret = bio for _, i := range c.Resources.BlkioWeightDevice { - wd, err := validateweightDevice(i) + wd, err := ValidateweightDevice(i) if err != nil { return ret, errors.Wrapf(err, "invalid values for blkio-weight-device") } - wdStat, err := getStatFromPath(wd.path) + wdStat, err := GetStatFromPath(wd.Path) if err != nil { - return ret, errors.Wrapf(err, "error getting stat from path %q", wd.path) + return ret, errors.Wrapf(err, "error getting stat from path %q", wd.Path) } lwd := spec.LinuxWeightDevice{ - Weight: &wd.weight, + Weight: &wd.Weight, } lwd.Major = int64(unix.Major(wdStat.Rdev)) lwd.Minor = int64(unix.Minor(wdStat.Rdev)) @@ -347,7 +347,7 @@ func makeThrottleArray(throttleInput []string, rateType int) ([]spec.LinuxThrott if err != nil { return []spec.LinuxThrottleDevice{}, err } - ltdStat, err := getStatFromPath(t.path) + ltdStat, err := GetStatFromPath(t.path) if err != nil { return ltds, errors.Wrapf(err, "error getting stat from path %q", t.path) } @@ -361,7 +361,7 @@ func makeThrottleArray(throttleInput []string, rateType int) ([]spec.LinuxThrott return ltds, nil } -func getStatFromPath(path string) (unix.Stat_t, error) { +func GetStatFromPath(path string) (unix.Stat_t, error) { s := unix.Stat_t{} err := unix.Stat(path, &s) return s, err diff --git a/pkg/spec/config_unsupported.go b/pkg/spec/config_unsupported.go index be3e7046d..568afde55 100644 --- a/pkg/spec/config_unsupported.go +++ b/pkg/spec/config_unsupported.go @@ -15,7 +15,7 @@ func addDevice(g *generate.Generator, device string) error { return errors.New("function not implemented") } -func (c *CreateConfig) addPrivilegedDevices(g *generate.Generator) error { +func addPrivilegedDevices(g *generate.Generator) error { return errors.New("function not implemented") } @@ -27,7 +27,7 @@ func makeThrottleArray(throttleInput []string, rateType int) ([]spec.LinuxThrott return nil, errors.New("function not implemented") } -func devicesFromPath(g *generate.Generator, devicePath string) error { +func DevicesFromPath(g *generate.Generator, devicePath string) error { return errors.New("function not implemented") } diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 8010be0d4..5011df496 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -126,6 +126,7 @@ type SecurityConfig struct { } // CreateConfig is a pre OCI spec structure. It represents user input from varlink or the CLI +// swagger:model CreateConfig type CreateConfig struct { Annotations map[string]string Args []string @@ -386,6 +387,6 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l // AddPrivilegedDevices iterates through host devices and adds all // host devices to the spec -func (c *CreateConfig) AddPrivilegedDevices(g *generate.Generator) error { - return c.addPrivilegedDevices(g) +func AddPrivilegedDevices(g *generate.Generator) error { + return addPrivilegedDevices(g) } diff --git a/pkg/spec/parse.go b/pkg/spec/parse.go index a5dfccdb9..38d93b87f 100644 --- a/pkg/spec/parse.go +++ b/pkg/spec/parse.go @@ -19,12 +19,12 @@ const Pod = "pod" // weightDevice is a structure that holds device:weight pair type weightDevice struct { - path string - weight uint16 + Path string + Weight uint16 } func (w *weightDevice) String() string { - return fmt.Sprintf("%s:%d", w.path, w.weight) + return fmt.Sprintf("%s:%d", w.Path, w.Weight) } // LinuxNS is a struct that contains namespace information @@ -59,9 +59,9 @@ func NS(s string) string { return "" } -// validateweightDevice validates that the specified string has a valid device-weight format +// ValidateweightDevice validates that the specified string has a valid device-weight format // for blkio-weight-device flag -func validateweightDevice(val string) (*weightDevice, error) { +func ValidateweightDevice(val string) (*weightDevice, error) { split := strings.SplitN(val, ":", 2) if len(split) != 2 { return nil, fmt.Errorf("bad format: %s", val) @@ -78,8 +78,8 @@ func validateweightDevice(val string) (*weightDevice, error) { } return &weightDevice{ - path: split[0], - weight: uint16(weight), + Path: split[0], + Weight: uint16(weight), }, nil } diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index b2a152a2d..a4ae22efd 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -16,9 +16,9 @@ import ( "github.com/pkg/errors" ) -const cpuPeriod = 100000 +const CpuPeriod = 100000 -func getAvailableGids() (int64, error) { +func GetAvailableGids() (int64, error) { idMap, err := user.ParseIDMapFile("/proc/self/gid_map") if err != nil { return 0, err @@ -80,7 +80,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } gid5Available := true if isRootless { - nGids, err := getAvailableGids() + nGids, err := GetAvailableGids() if err != nil { return nil, err } @@ -197,8 +197,8 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM addedResources = true } if config.Resources.CPUs != 0 { - g.SetLinuxResourcesCPUPeriod(cpuPeriod) - g.SetLinuxResourcesCPUQuota(int64(config.Resources.CPUs * cpuPeriod)) + g.SetLinuxResourcesCPUPeriod(CpuPeriod) + g.SetLinuxResourcesCPUQuota(int64(config.Resources.CPUs * CpuPeriod)) addedResources = true } if config.Resources.CPURtRuntime != 0 { @@ -223,12 +223,12 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM // If privileged, we need to add all the host devices to the // spec. We do not add the user provided ones because we are // already adding them all. - if err := config.AddPrivilegedDevices(&g); err != nil { + if err := AddPrivilegedDevices(&g); err != nil { return nil, err } } else { for _, devicePath := range config.Devices { - if err := devicesFromPath(&g, devicePath); err != nil { + if err := DevicesFromPath(&g, devicePath); err != nil { return nil, err } } @@ -241,23 +241,35 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } // SECURITY OPTS + var runtimeConfig *libpodconfig.Config + + if runtime != nil { + runtimeConfig, err = runtime.GetConfig() + if err != nil { + return nil, err + } + } + g.SetProcessNoNewPrivileges(config.Security.NoNewPrivs) if !config.Security.Privileged { g.SetProcessApparmorProfile(config.Security.ApparmorProfile) } - blockAccessToKernelFilesystems(config, &g) - - var runtimeConfig *libpodconfig.Config - - if runtime != nil { - runtimeConfig, err = runtime.GetConfig() - if err != nil { + // Unless already set via the CLI, check if we need to disable process + // labels or set the defaults. + if len(config.Security.LabelOpts) == 0 && runtimeConfig != nil { + if !runtimeConfig.EnableLabeling { + // Disabled in the config. + config.Security.LabelOpts = append(config.Security.LabelOpts, "disable") + } else if err := config.Security.SetLabelOpts(runtime, &config.Pid, &config.Ipc); err != nil { + // Defaults! return nil, err } } + BlockAccessToKernelFilesystems(config.Security.Privileged, config.Pid.PidMode.IsHost(), &g) + // RESOURCES - PIDS if config.Resources.PidsLimit > 0 { // if running on rootless on a cgroupv1 machine or using the cgroupfs manager, pids @@ -320,9 +332,9 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } // BIND MOUNTS - configSpec.Mounts = supercedeUserMounts(userMounts, configSpec.Mounts) + configSpec.Mounts = SupercedeUserMounts(userMounts, configSpec.Mounts) // Process mounts to ensure correct options - finalMounts, err := initFSMounts(configSpec.Mounts) + finalMounts, err := InitFSMounts(configSpec.Mounts) if err != nil { return nil, err } @@ -404,8 +416,8 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM return configSpec, nil } -func blockAccessToKernelFilesystems(config *CreateConfig, g *generate.Generator) { - if !config.Security.Privileged { +func BlockAccessToKernelFilesystems(privileged, pidModeIsHost bool, g *generate.Generator) { + if !privileged { for _, mp := range []string{ "/proc/acpi", "/proc/kcore", @@ -421,7 +433,7 @@ func blockAccessToKernelFilesystems(config *CreateConfig, g *generate.Generator) g.AddLinuxMaskedPaths(mp) } - if config.Pid.PidMode.IsHost() && rootless.IsRootless() { + if pidModeIsHost && rootless.IsRootless() { return } diff --git a/pkg/spec/storage.go b/pkg/spec/storage.go index 0e2098c1d..c365701de 100644 --- a/pkg/spec/storage.go +++ b/pkg/spec/storage.go @@ -739,6 +739,7 @@ func (config *CreateConfig) getImageVolumes() (map[string]spec.Mount, map[string for vol := range config.BuiltinImgVolumes { cleanDest := filepath.Clean(vol) + logrus.Debugf("Adding image volume at %s", cleanDest) if config.ImageVolumeType == "tmpfs" { // Tmpfs image volumes are handled as mounts mount := spec.Mount{ @@ -747,13 +748,13 @@ func (config *CreateConfig) getImageVolumes() (map[string]spec.Mount, map[string Type: TypeTmpfs, Options: []string{"rprivate", "rw", "nodev", "exec"}, } - mounts[vol] = mount + mounts[cleanDest] = mount } else { // Anonymous volumes have no name. namedVolume := new(libpod.ContainerNamedVolume) namedVolume.Options = []string{"rprivate", "rw", "nodev", "exec"} namedVolume.Dest = cleanDest - volumes[vol] = namedVolume + volumes[cleanDest] = namedVolume } } @@ -825,7 +826,7 @@ func (config *CreateConfig) addContainerInitBinary(path string) (spec.Mount, err // TODO: Should we unmount subtree mounts? E.g., if /tmp/ is mounted by // one mount, and we already have /tmp/a and /tmp/b, should we remove // the /tmp/a and /tmp/b mounts in favor of the more general /tmp? -func supercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.Mount { +func SupercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.Mount { if len(mounts) > 0 { // If we have overlappings mounts, remove them from the spec in favor of // the user-added volume mounts @@ -854,7 +855,7 @@ func supercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.M } // Ensure mount options on all mounts are correct -func initFSMounts(inputMounts []spec.Mount) ([]spec.Mount, error) { +func InitFSMounts(inputMounts []spec.Mount) ([]spec.Mount, error) { // We need to look up mounts so we can figure out the proper mount flags // to apply. systemMounts, err := pmount.GetMounts() diff --git a/pkg/specgen/config_linux_cgo.go b/pkg/specgen/config_linux_cgo.go new file mode 100644 index 000000000..6f547a40d --- /dev/null +++ b/pkg/specgen/config_linux_cgo.go @@ -0,0 +1,62 @@ +// +build linux,cgo + +package specgen + +import ( + "context" + "io/ioutil" + + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/seccomp" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + goSeccomp "github.com/seccomp/containers-golang" + "github.com/sirupsen/logrus" +) + +func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec, img *image.Image) (*spec.LinuxSeccomp, error) { + var seccompConfig *spec.LinuxSeccomp + var err error + + scp, err := seccomp.LookupPolicy(s.SeccompPolicy) + if err != nil { + return nil, err + } + + if scp == seccomp.PolicyImage { + labels, err := img.Labels(context.Background()) + if err != nil { + return nil, err + } + imagePolicy := labels[seccomp.ContainerImageLabel] + if len(imagePolicy) < 1 { + return nil, errors.New("no seccomp policy defined by image") + } + logrus.Debug("Loading seccomp profile from the security config") + seccompConfig, err = goSeccomp.LoadProfile(imagePolicy, configSpec) + if err != nil { + return nil, errors.Wrap(err, "loading seccomp profile failed") + } + return seccompConfig, nil + } + + if s.SeccompProfilePath != "" { + logrus.Debugf("Loading seccomp profile from %q", s.SeccompProfilePath) + seccompProfile, err := ioutil.ReadFile(s.SeccompProfilePath) + if err != nil { + return nil, errors.Wrapf(err, "opening seccomp profile (%s) failed", s.SeccompProfilePath) + } + seccompConfig, err = goSeccomp.LoadProfile(string(seccompProfile), configSpec) + if err != nil { + return nil, errors.Wrapf(err, "loading seccomp profile (%s) failed", s.SeccompProfilePath) + } + } else { + logrus.Debug("Loading default seccomp profile") + seccompConfig, err = goSeccomp.GetDefaultProfile(configSpec) + if err != nil { + return nil, errors.Wrapf(err, "loading seccomp profile (%s) failed", s.SeccompProfilePath) + } + } + + return seccompConfig, nil +} diff --git a/pkg/specgen/config_linux_nocgo.go b/pkg/specgen/config_linux_nocgo.go new file mode 100644 index 000000000..fc0c58c37 --- /dev/null +++ b/pkg/specgen/config_linux_nocgo.go @@ -0,0 +1,11 @@ +// +build linux,!cgo + +package specgen + +import ( + spec "github.com/opencontainers/runtime-spec/specs-go" +) + +func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec) (*spec.LinuxSeccomp, error) { + return nil, nil +} diff --git a/pkg/specgen/config_unsupported.go b/pkg/specgen/config_unsupported.go new file mode 100644 index 000000000..5d24ac39c --- /dev/null +++ b/pkg/specgen/config_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux + +package specgen + +import ( + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec) (*spec.LinuxSeccomp, error) { + return nil, errors.New("function not supported on non-linux OS's") +} diff --git a/pkg/specgen/create.go b/pkg/specgen/create.go new file mode 100644 index 000000000..c8fee5f05 --- /dev/null +++ b/pkg/specgen/create.go @@ -0,0 +1,187 @@ +package specgen + +import ( + "context" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/config" + "github.com/containers/libpod/libpod/define" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "os" +) + +// MakeContainer creates a container based on the SpecGenerator +func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, error) { + var pod *libpod.Pod + if err := s.validate(rt); err != nil { + return nil, errors.Wrap(err, "invalid config provided") + } + rtc, err := rt.GetConfig() + if err != nil { + return nil, err + } + + options, err := s.createContainerOptions(rt, pod) + if err != nil { + return nil, err + } + + podmanPath, err := os.Executable() + if err != nil { + return nil, err + } + options = append(options, s.createExitCommandOption(rtc, podmanPath)) + newImage, err := rt.ImageRuntime().NewFromLocal(s.Image) + if err != nil { + return nil, err + } + + // TODO mheon wants to talk with Dan about this + useImageVolumes := s.ImageVolumeMode == "bind" + options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, useImageVolumes)) + + runtimeSpec, err := s.toOCISpec(rt, newImage) + if err != nil { + return nil, err + } + return rt.NewContainer(context.Background(), runtimeSpec, options...) +} + +func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime, pod *libpod.Pod) ([]libpod.CtrCreateOption, error) { + var options []libpod.CtrCreateOption + var err error + + if s.Stdin { + options = append(options, libpod.WithStdin()) + } + if len(s.Systemd) > 0 { + options = append(options, libpod.WithSystemd()) + } + if len(s.Name) > 0 { + logrus.Debugf("setting container name %s", s.Name) + options = append(options, libpod.WithName(s.Name)) + } + if s.Pod != "" { + logrus.Debugf("adding container to pod %s", s.Pod) + options = append(options, rt.WithPod(pod)) + } + destinations := []string{} + // // Take all mount and named volume destinations. + for _, mount := range s.Mounts { + destinations = append(destinations, mount.Destination) + } + for _, volume := range s.Volumes { + destinations = append(destinations, volume.Dest) + } + options = append(options, libpod.WithUserVolumes(destinations)) + + if len(s.Volumes) != 0 { + options = append(options, libpod.WithNamedVolumes(s.Volumes)) + } + + if len(s.Command) != 0 { + options = append(options, libpod.WithCommand(s.Command)) + } + + options = append(options, libpod.WithEntrypoint(s.Entrypoint)) + if s.StopSignal != nil { + options = append(options, libpod.WithStopSignal(*s.StopSignal)) + } + if s.StopTimeout != nil { + options = append(options, libpod.WithStopTimeout(*s.StopTimeout)) + } + if s.LogConfiguration != nil { + if len(s.LogConfiguration.Path) > 0 { + options = append(options, libpod.WithLogPath(s.LogConfiguration.Path)) + } + if len(s.LogConfiguration.Options) > 0 && s.LogConfiguration.Options["tag"] != "" { + // Note: I'm really guessing here. + options = append(options, libpod.WithLogTag(s.LogConfiguration.Options["tag"])) + } + + if len(s.LogConfiguration.Driver) > 0 { + options = append(options, libpod.WithLogDriver(s.LogConfiguration.Driver)) + } + } + + // Security options + if len(s.SelinuxOpts) > 0 { + options = append(options, libpod.WithSecLabels(s.SelinuxOpts)) + } + options = append(options, libpod.WithPrivileged(s.Privileged)) + + // Get namespace related options + namespaceOptions, err := s.generateNamespaceContainerOpts(rt) + if err != nil { + return nil, err + } + options = append(options, namespaceOptions...) + + // TODO NetworkNS still needs to be done! + if len(s.ConmonPidFile) > 0 { + options = append(options, libpod.WithConmonPidFile(s.ConmonPidFile)) + } + options = append(options, libpod.WithLabels(s.Labels)) + if s.ShmSize != nil { + options = append(options, libpod.WithShmSize(*s.ShmSize)) + } + if s.Rootfs != "" { + options = append(options, libpod.WithRootFS(s.Rootfs)) + } + // Default used if not overridden on command line + + if s.RestartPolicy != "" { + if s.RestartPolicy == "unless-stopped" { + return nil, errors.Wrapf(define.ErrInvalidArg, "the unless-stopped restart policy is not supported") + } + if s.RestartRetries != nil { + options = append(options, libpod.WithRestartRetries(*s.RestartRetries)) + } + options = append(options, libpod.WithRestartPolicy(s.RestartPolicy)) + } + + if s.ContainerHealthCheckConfig.HealthConfig != nil { + options = append(options, libpod.WithHealthCheck(s.ContainerHealthCheckConfig.HealthConfig)) + logrus.Debugf("New container has a health check") + } + return options, nil +} + +func (s *SpecGenerator) createExitCommandOption(config *config.Config, podmanPath string) libpod.CtrCreateOption { + // We need a cleanup process for containers in the current model. + // But we can't assume that the caller is Podman - it could be another + // user of the API. + // As such, provide a way to specify a path to Podman, so we can + // still invoke a cleanup process. + + command := []string{podmanPath, + "--root", config.StorageConfig.GraphRoot, + "--runroot", config.StorageConfig.RunRoot, + "--log-level", logrus.GetLevel().String(), + "--cgroup-manager", config.CgroupManager, + "--tmpdir", config.TmpDir, + } + if config.OCIRuntime != "" { + command = append(command, []string{"--runtime", config.OCIRuntime}...) + } + if config.StorageConfig.GraphDriverName != "" { + command = append(command, []string{"--storage-driver", config.StorageConfig.GraphDriverName}...) + } + for _, opt := range config.StorageConfig.GraphDriverOptions { + command = append(command, []string{"--storage-opt", opt}...) + } + if config.EventsLogger != "" { + command = append(command, []string{"--events-backend", config.EventsLogger}...) + } + + // TODO Mheon wants to leave this for now + //if s.sys { + // command = append(command, "--syslog", "true") + //} + command = append(command, []string{"container", "cleanup"}...) + + if s.Remove { + command = append(command, "--rm") + } + return libpod.WithExitCommand(command) +} diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go new file mode 100644 index 000000000..025cb31e0 --- /dev/null +++ b/pkg/specgen/namespaces.go @@ -0,0 +1,467 @@ +package specgen + +import ( + "os" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/capabilities" + "github.com/cri-o/ocicni/pkg/ocicni" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type NamespaceMode string + +const ( + // Host means the the namespace is derived from + // the host + Host NamespaceMode = "host" + // Path is the path to a namespace + Path NamespaceMode = "path" + // FromContainer means namespace is derived from a + // different container + FromContainer NamespaceMode = "container" + // FromPod indicates the namespace is derived from a pod + FromPod NamespaceMode = "pod" + // Private indicates the namespace is private + Private NamespaceMode = "private" + // NoNetwork indicates no network namespace should + // be joined. loopback should still exists + NoNetwork NamespaceMode = "none" + // Bridge indicates that a CNI network stack + // should be used + Bridge NamespaceMode = "bridge" + // Slirp indicates that a slirp4ns network stack should + // be used + Slirp NamespaceMode = "slirp4ns" +) + +// Namespace describes the namespace +type Namespace struct { + NSMode NamespaceMode `json:"nsmode,omitempty"` + Value string `json:"string,omitempty"` +} + +// IsHost returns a bool if the namespace is host based +func (n *Namespace) IsHost() bool { + return n.NSMode == Host +} + +// IsPath indicates via bool if the namespace is based on a path +func (n *Namespace) IsPath() bool { + return n.NSMode == Path +} + +// IsContainer indicates via bool if the namespace is based on a container +func (n *Namespace) IsContainer() bool { + return n.NSMode == FromContainer +} + +// IsPod indicates via bool if the namespace is based on a pod +func (n *Namespace) IsPod() bool { + return n.NSMode == FromPod +} + +// IsPrivate indicates the namespace is private +func (n *Namespace) IsPrivate() bool { + return n.NSMode == Private +} + +// validate perform simple validation on the namespace to make sure it is not +// invalid from the get-go +func (n *Namespace) validate() error { + if n == nil { + return nil + } + switch n.NSMode { + case Host, Path, FromContainer, FromPod, Private, NoNetwork, Bridge, Slirp: + break + default: + return errors.Errorf("invalid network %q", n.NSMode) + } + // Path and From Container MUST have a string value set + if n.NSMode == Path || n.NSMode == FromContainer { + if len(n.Value) < 1 { + return errors.Errorf("namespace mode %s requires a value", n.NSMode) + } + } else { + // All others must NOT set a string value + if len(n.Value) > 0 { + return errors.Errorf("namespace value %s cannot be provided with namespace mode %s", n.Value, n.NSMode) + } + } + return nil +} + +func (s *SpecGenerator) generateNamespaceContainerOpts(rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) { + var portBindings []ocicni.PortMapping + options := make([]libpod.CtrCreateOption, 0) + + // Cgroups + switch { + case s.CgroupNS.IsPrivate(): + ns := s.CgroupNS.Value + if _, err := os.Stat(ns); err != nil { + return nil, err + } + case s.CgroupNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.CgroupNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.CgroupNS.Value) + } + options = append(options, libpod.WithCgroupNSFrom(connectedCtr)) + // TODO + //default: + // return nil, errors.New("cgroup name only supports private and container") + } + + if s.CgroupParent != "" { + options = append(options, libpod.WithCgroupParent(s.CgroupParent)) + } + + if s.CgroupsMode != "" { + options = append(options, libpod.WithCgroupsMode(s.CgroupsMode)) + } + + // ipc + switch { + case s.IpcNS.IsHost(): + options = append(options, libpod.WithShmDir("/dev/shm")) + case s.IpcNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.IpcNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.IpcNS.Value) + } + options = append(options, libpod.WithIPCNSFrom(connectedCtr)) + options = append(options, libpod.WithShmDir(connectedCtr.ShmDir())) + } + + // pid + if s.PidNS.IsContainer() { + connectedCtr, err := rt.LookupContainer(s.PidNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.PidNS.Value) + } + options = append(options, libpod.WithPIDNSFrom(connectedCtr)) + } + + // uts + switch { + case s.UtsNS.IsPod(): + connectedPod, err := rt.LookupPod(s.UtsNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "pod %q not found", s.UtsNS.Value) + } + options = append(options, libpod.WithUTSNSFromPod(connectedPod)) + case s.UtsNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.UtsNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.UtsNS.Value) + } + + options = append(options, libpod.WithUTSNSFrom(connectedCtr)) + } + + if s.UseImageHosts { + options = append(options, libpod.WithUseImageHosts()) + } else if len(s.HostAdd) > 0 { + options = append(options, libpod.WithHosts(s.HostAdd)) + } + + // User + + switch { + case s.UserNS.IsPath(): + ns := s.UserNS.Value + if ns == "" { + return nil, errors.Errorf("invalid empty user-defined user namespace") + } + _, err := os.Stat(ns) + if err != nil { + return nil, err + } + if s.IDMappings != nil { + options = append(options, libpod.WithIDMappings(*s.IDMappings)) + } + case s.UserNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.UserNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.UserNS.Value) + } + options = append(options, libpod.WithUserNSFrom(connectedCtr)) + default: + if s.IDMappings != nil { + options = append(options, libpod.WithIDMappings(*s.IDMappings)) + } + } + + options = append(options, libpod.WithUser(s.User)) + options = append(options, libpod.WithGroups(s.Groups)) + + if len(s.PortMappings) > 0 { + portBindings = s.PortMappings + } + + switch { + case s.NetNS.IsPath(): + ns := s.NetNS.Value + if ns == "" { + return nil, errors.Errorf("invalid empty user-defined network namespace") + } + _, err := os.Stat(ns) + if err != nil { + return nil, err + } + case s.NetNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.NetNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.NetNS.Value) + } + options = append(options, libpod.WithNetNSFrom(connectedCtr)) + case !s.NetNS.IsHost() && s.NetNS.NSMode != NoNetwork: + postConfigureNetNS := !s.UserNS.IsHost() + options = append(options, libpod.WithNetNS(portBindings, postConfigureNetNS, string(s.NetNS.NSMode), s.CNINetworks)) + } + + if len(s.DNSSearch) > 0 { + options = append(options, libpod.WithDNSSearch(s.DNSSearch)) + } + if len(s.DNSServer) > 0 { + // TODO I'm not sure how we are going to handle this given the input + if len(s.DNSServer) == 1 { //&& strings.ToLower(s.DNSServer[0].) == "none" { + options = append(options, libpod.WithUseImageResolvConf()) + } else { + var dnsServers []string + for _, d := range s.DNSServer { + dnsServers = append(dnsServers, d.String()) + } + options = append(options, libpod.WithDNS(dnsServers)) + } + } + if len(s.DNSOption) > 0 { + options = append(options, libpod.WithDNSOption(s.DNSOption)) + } + if s.StaticIP != nil { + options = append(options, libpod.WithStaticIP(*s.StaticIP)) + } + + if s.StaticMAC != nil { + options = append(options, libpod.WithStaticMAC(*s.StaticMAC)) + } + return options, nil +} + +func (s *SpecGenerator) pidConfigureGenerator(g *generate.Generator) error { + if s.PidNS.IsPath() { + return g.AddOrReplaceLinuxNamespace(string(spec.PIDNamespace), s.PidNS.Value) + } + if s.PidNS.IsHost() { + return g.RemoveLinuxNamespace(string(spec.PIDNamespace)) + } + if s.PidNS.IsContainer() { + logrus.Debugf("using container %s pidmode", s.PidNS.Value) + } + if s.PidNS.IsPod() { + logrus.Debug("using pod pidmode") + } + return nil +} + +func (s *SpecGenerator) utsConfigureGenerator(g *generate.Generator, runtime *libpod.Runtime) error { + hostname := s.Hostname + var err error + if hostname == "" { + switch { + case s.UtsNS.IsContainer(): + utsCtr, err := runtime.GetContainer(s.UtsNS.Value) + if err != nil { + return errors.Wrapf(err, "unable to retrieve hostname from dependency container %s", s.UtsNS.Value) + } + hostname = utsCtr.Hostname() + case s.NetNS.IsHost() || s.UtsNS.IsHost(): + hostname, err = os.Hostname() + if err != nil { + return errors.Wrap(err, "unable to retrieve hostname of the host") + } + default: + logrus.Debug("No hostname set; container's hostname will default to runtime default") + } + } + g.RemoveHostname() + if s.Hostname != "" || !s.UtsNS.IsHost() { + // Set the hostname in the OCI configuration only + // if specified by the user or if we are creating + // a new UTS namespace. + g.SetHostname(hostname) + } + g.AddProcessEnv("HOSTNAME", hostname) + + if s.UtsNS.IsPath() { + return g.AddOrReplaceLinuxNamespace(string(spec.UTSNamespace), s.UtsNS.Value) + } + if s.UtsNS.IsHost() { + return g.RemoveLinuxNamespace(string(spec.UTSNamespace)) + } + if s.UtsNS.IsContainer() { + logrus.Debugf("using container %s utsmode", s.UtsNS.Value) + } + return nil +} + +func (s *SpecGenerator) ipcConfigureGenerator(g *generate.Generator) error { + if s.IpcNS.IsPath() { + return g.AddOrReplaceLinuxNamespace(string(spec.IPCNamespace), s.IpcNS.Value) + } + if s.IpcNS.IsHost() { + return g.RemoveLinuxNamespace(s.IpcNS.Value) + } + if s.IpcNS.IsContainer() { + logrus.Debugf("Using container %s ipcmode", s.IpcNS.Value) + } + return nil +} + +func (s *SpecGenerator) cgroupConfigureGenerator(g *generate.Generator) error { + if s.CgroupNS.IsPath() { + return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), s.CgroupNS.Value) + } + if s.CgroupNS.IsHost() { + return g.RemoveLinuxNamespace(s.CgroupNS.Value) + } + if s.CgroupNS.IsPrivate() { + return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), "") + } + if s.CgroupNS.IsContainer() { + logrus.Debugf("Using container %s cgroup mode", s.CgroupNS.Value) + } + return nil +} + +func (s *SpecGenerator) networkConfigureGenerator(g *generate.Generator) error { + switch { + case s.NetNS.IsHost(): + logrus.Debug("Using host netmode") + if err := g.RemoveLinuxNamespace(string(spec.NetworkNamespace)); err != nil { + return err + } + + case s.NetNS.NSMode == NoNetwork: + logrus.Debug("Using none netmode") + case s.NetNS.NSMode == Bridge: + logrus.Debug("Using bridge netmode") + case s.NetNS.IsContainer(): + logrus.Debugf("using container %s netmode", s.NetNS.Value) + case s.NetNS.IsPath(): + logrus.Debug("Using ns netmode") + if err := g.AddOrReplaceLinuxNamespace(string(spec.NetworkNamespace), s.NetNS.Value); err != nil { + return err + } + case s.NetNS.IsPod(): + logrus.Debug("Using pod netmode, unless pod is not sharing") + case s.NetNS.NSMode == Slirp: + logrus.Debug("Using slirp4netns netmode") + default: + return errors.Errorf("unknown network mode") + } + + if g.Config.Annotations == nil { + g.Config.Annotations = make(map[string]string) + } + + if s.PublishImagePorts { + g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue + } else { + g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseFalse + } + + return nil +} + +func (s *SpecGenerator) userConfigureGenerator(g *generate.Generator) error { + if s.UserNS.IsPath() { + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), s.UserNS.Value); err != nil { + return err + } + // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping + g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) + g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) + } + + if s.IDMappings != nil { + if (len(s.IDMappings.UIDMap) > 0 || len(s.IDMappings.GIDMap) > 0) && !s.UserNS.IsHost() { + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { + return err + } + } + for _, uidmap := range s.IDMappings.UIDMap { + g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) + } + for _, gidmap := range s.IDMappings.GIDMap { + g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) + } + } + return nil +} + +func (s *SpecGenerator) securityConfigureGenerator(g *generate.Generator, newImage *image.Image) error { + // HANDLE CAPABILITIES + // NOTE: Must happen before SECCOMP + if s.Privileged { + g.SetupPrivileged(true) + } + + useNotRoot := func(user string) bool { + if user == "" || user == "root" || user == "0" { + return false + } + return true + } + configSpec := g.Config + var err error + var caplist []string + bounding := configSpec.Process.Capabilities.Bounding + if useNotRoot(s.User) { + configSpec.Process.Capabilities.Bounding = caplist + } + caplist, err = capabilities.MergeCapabilities(configSpec.Process.Capabilities.Bounding, s.CapAdd, s.CapDrop) + if err != nil { + return err + } + + configSpec.Process.Capabilities.Bounding = caplist + configSpec.Process.Capabilities.Permitted = caplist + configSpec.Process.Capabilities.Inheritable = caplist + configSpec.Process.Capabilities.Effective = caplist + configSpec.Process.Capabilities.Ambient = caplist + if useNotRoot(s.User) { + caplist, err = capabilities.MergeCapabilities(bounding, s.CapAdd, s.CapDrop) + if err != nil { + return err + } + } + configSpec.Process.Capabilities.Bounding = caplist + + // HANDLE SECCOMP + if s.SeccompProfilePath != "unconfined" { + seccompConfig, err := s.getSeccompConfig(configSpec, newImage) + if err != nil { + return err + } + configSpec.Linux.Seccomp = seccompConfig + } + + // Clear default Seccomp profile from Generator for privileged containers + if s.SeccompProfilePath == "unconfined" || s.Privileged { + configSpec.Linux.Seccomp = nil + } + + g.SetRootReadonly(s.ReadOnlyFilesystem) + for sysctlKey, sysctlVal := range s.Sysctl { + g.AddLinuxSysctl(sysctlKey, sysctlVal) + } + + return nil +} diff --git a/pkg/specgen/oci.go b/pkg/specgen/oci.go new file mode 100644 index 000000000..2523f21b3 --- /dev/null +++ b/pkg/specgen/oci.go @@ -0,0 +1,260 @@ +package specgen + +import ( + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/rootless" + createconfig "github.com/containers/libpod/pkg/spec" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" +) + +func (s *SpecGenerator) toOCISpec(rt *libpod.Runtime, newImage *image.Image) (*spec.Spec, error) { + var ( + inUserNS bool + ) + cgroupPerm := "ro" + g, err := generate.New("linux") + if err != nil { + return nil, err + } + // Remove the default /dev/shm mount to ensure we overwrite it + g.RemoveMount("/dev/shm") + g.HostSpecific = true + addCgroup := true + canMountSys := true + + isRootless := rootless.IsRootless() + if isRootless { + inUserNS = true + } + if !s.UserNS.IsHost() { + if s.UserNS.IsContainer() || s.UserNS.IsPath() { + inUserNS = true + } + if s.UserNS.IsPrivate() { + inUserNS = true + } + } + if inUserNS && s.NetNS.IsHost() { + canMountSys = false + } + + if s.Privileged && canMountSys { + cgroupPerm = "rw" + g.RemoveMount("/sys") + sysMnt := spec.Mount{ + Destination: "/sys", + Type: "sysfs", + Source: "sysfs", + Options: []string{"rprivate", "nosuid", "noexec", "nodev", "rw"}, + } + g.AddMount(sysMnt) + } else if !canMountSys { + addCgroup = false + g.RemoveMount("/sys") + r := "ro" + if s.Privileged { + r = "rw" + } + sysMnt := spec.Mount{ + Destination: "/sys", + Type: "bind", // should we use a constant for this, like createconfig? + Source: "/sys", + Options: []string{"rprivate", "nosuid", "noexec", "nodev", r, "rbind"}, + } + g.AddMount(sysMnt) + if !s.Privileged && isRootless { + g.AddLinuxMaskedPaths("/sys/kernel") + } + } + gid5Available := true + if isRootless { + nGids, err := createconfig.GetAvailableGids() + if err != nil { + return nil, err + } + gid5Available = nGids >= 5 + } + // When using a different user namespace, check that the GID 5 is mapped inside + // the container. + if gid5Available && (s.IDMappings != nil && len(s.IDMappings.GIDMap) > 0) { + mappingFound := false + for _, r := range s.IDMappings.GIDMap { + if r.ContainerID <= 5 && 5 < r.ContainerID+r.Size { + mappingFound = true + break + } + } + if !mappingFound { + gid5Available = false + } + + } + if !gid5Available { + // If we have no GID mappings, the gid=5 default option would fail, so drop it. + g.RemoveMount("/dev/pts") + devPts := spec.Mount{ + Destination: "/dev/pts", + Type: "devpts", + Source: "devpts", + Options: []string{"rprivate", "nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"}, + } + g.AddMount(devPts) + } + + if inUserNS && s.IpcNS.IsHost() { + g.RemoveMount("/dev/mqueue") + devMqueue := spec.Mount{ + Destination: "/dev/mqueue", + Type: "bind", // constant ? + Source: "/dev/mqueue", + Options: []string{"bind", "nosuid", "noexec", "nodev"}, + } + g.AddMount(devMqueue) + } + if inUserNS && s.PidNS.IsHost() { + g.RemoveMount("/proc") + procMount := spec.Mount{ + Destination: "/proc", + Type: createconfig.TypeBind, + Source: "/proc", + Options: []string{"rbind", "nosuid", "noexec", "nodev"}, + } + g.AddMount(procMount) + } + + if addCgroup { + cgroupMnt := spec.Mount{ + Destination: "/sys/fs/cgroup", + Type: "cgroup", + Source: "cgroup", + Options: []string{"rprivate", "nosuid", "noexec", "nodev", "relatime", cgroupPerm}, + } + g.AddMount(cgroupMnt) + } + g.SetProcessCwd(s.WorkDir) + g.SetProcessArgs(s.Command) + g.SetProcessTerminal(s.Terminal) + + for key, val := range s.Annotations { + g.AddAnnotation(key, val) + } + g.AddProcessEnv("container", "podman") + + g.Config.Linux.Resources = s.ResourceLimits + + // Devices + if s.Privileged { + // If privileged, we need to add all the host devices to the + // spec. We do not add the user provided ones because we are + // already adding them all. + if err := createconfig.AddPrivilegedDevices(&g); err != nil { + return nil, err + } + } else { + for _, device := range s.Devices { + if err := createconfig.DevicesFromPath(&g, device.Path); err != nil { + return nil, err + } + } + } + + // SECURITY OPTS + g.SetProcessNoNewPrivileges(s.NoNewPrivileges) + + if !s.Privileged { + g.SetProcessApparmorProfile(s.ApparmorProfile) + } + + createconfig.BlockAccessToKernelFilesystems(s.Privileged, s.PidNS.IsHost(), &g) + + for name, val := range s.Env { + g.AddProcessEnv(name, val) + } + + // TODO rlimits and ulimits needs further refinement by someone more + // familiar with the code. + //if err := addRlimits(config, &g); err != nil { + // return nil, err + //} + + // NAMESPACES + + if err := s.pidConfigureGenerator(&g); err != nil { + return nil, err + } + + if err := s.userConfigureGenerator(&g); err != nil { + return nil, err + } + + if err := s.networkConfigureGenerator(&g); err != nil { + return nil, err + } + + if err := s.utsConfigureGenerator(&g, rt); err != nil { + return nil, err + } + + if err := s.ipcConfigureGenerator(&g); err != nil { + return nil, err + } + + if err := s.cgroupConfigureGenerator(&g); err != nil { + return nil, err + } + configSpec := g.Config + + if err := s.securityConfigureGenerator(&g, newImage); err != nil { + return nil, err + } + + // BIND MOUNTS + configSpec.Mounts = createconfig.SupercedeUserMounts(s.Mounts, configSpec.Mounts) + // Process mounts to ensure correct options + finalMounts, err := createconfig.InitFSMounts(configSpec.Mounts) + if err != nil { + return nil, err + } + configSpec.Mounts = finalMounts + + // Add annotations + if configSpec.Annotations == nil { + configSpec.Annotations = make(map[string]string) + } + + // TODO cidfile is not in specgen; when wiring up cli, we will need to move this out of here + // leaving as a reminder + //if config.CidFile != "" { + // configSpec.Annotations[libpod.InspectAnnotationCIDFile] = config.CidFile + //} + + if s.Remove { + configSpec.Annotations[libpod.InspectAnnotationAutoremove] = libpod.InspectResponseTrue + } else { + configSpec.Annotations[libpod.InspectAnnotationAutoremove] = libpod.InspectResponseFalse + } + + if len(s.VolumesFrom) > 0 { + configSpec.Annotations[libpod.InspectAnnotationVolumesFrom] = strings.Join(s.VolumesFrom, ",") + } + + if s.Privileged { + configSpec.Annotations[libpod.InspectAnnotationPrivileged] = libpod.InspectResponseTrue + } else { + configSpec.Annotations[libpod.InspectAnnotationPrivileged] = libpod.InspectResponseFalse + } + + // TODO Init might not make it into the specgen and therefore is not available here. We should deal + // with this when we wire up the CLI; leaving as a reminder + //if s.Init { + // configSpec.Annotations[libpod.InspectAnnotationInit] = libpod.InspectResponseTrue + //} else { + // configSpec.Annotations[libpod.InspectAnnotationInit] = libpod.InspectResponseFalse + //} + + return configSpec, nil +} diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index e22ee598f..e1dfe4dc5 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -2,24 +2,28 @@ package specgen import ( "net" + "syscall" "github.com/containers/image/v5/manifest" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/rootless" "github.com/containers/storage" "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" ) -// TODO -// mheon provided this an off the cuff suggestion. Adding it here to retain -// for history as we implement it. When this struct is implemented, we need -// to remove the nolints. -type Namespace struct { - isHost bool //nolint - isPath string //nolint - isContainer string //nolint - isPod bool //nolint +// LogConfig describes the logging characteristics for a container +type LogConfig struct { + // LogDriver is the container's log driver. + // Optional. + Driver string `json:"driver,omitempty"` + // LogPath is the path the container's logs will be stored at. + // Only available if LogDriver is set to "json-file" or "k8s-file". + // Optional. + Path string `json:"path,omitempty"` + // A set of options to accompany the log driver. + // Optional. + Options map[string]string `json:"options,omitempty"` } // ContainerBasicConfig contains the basic parts of a container. @@ -62,7 +66,7 @@ type ContainerBasicConfig struct { // If not provided, the default, SIGTERM, will be used. // Will conflict with Systemd if Systemd is set to "true" or "always". // Optional. - StopSignal *uint `json:"stop_signal,omitempty"` + StopSignal *syscall.Signal `json:"stop_signal,omitempty"` // StopTimeout is a timeout between the container's stop signal being // sent and SIGKILL being sent. // If not provided, the default will be used. @@ -70,13 +74,10 @@ type ContainerBasicConfig struct { // instead. // Optional. StopTimeout *uint `json:"stop_timeout,omitempty"` - // LogDriver is the container's log driver. - // Optional. - LogDriver string `json:"log_driver,omitempty"` - // LogPath is the path the container's logs will be stored at. - // Only available if LogDriver is set to "json-file" or "k8s-file". - // Optional. - LogPath string `json:"log_path,omitempty"` + // LogConfiguration describes the logging for a container including + // driver, path, and options. + // Optional + LogConfiguration *LogConfig `json:"log_configuration,omitempty"` // ConmonPidFile is a path at which a PID file for Conmon will be // placed. // If not given, a default location will be used. @@ -111,12 +112,10 @@ type ContainerBasicConfig struct { // Namespace is the libpod namespace the container will be placed in. // Optional. Namespace string `json:"namespace,omitempty"` - // PidNS is the container's PID namespace. // It defaults to private. // Mandatory. PidNS Namespace `json:"pidns,omitempty"` - // UtsNS is the container's UTS namespace. // It defaults to private. // Must be set to Private to set Hostname. @@ -128,6 +127,11 @@ type ContainerBasicConfig struct { // Conflicts with UtsNS if UtsNS is not set to private. // Optional. Hostname string `json:"hostname,omitempty"` + // Sysctl sets kernel parameters for the container + Sysctl map[string]string `json:"sysctl,omitempty"` + // Remove indicates if the container should be removed once it has been started + // and exits + Remove bool `json:"remove"` } // ContainerStorageConfig contains information on the storage configuration of a @@ -175,7 +179,7 @@ type ContainerStorageConfig struct { // Mandatory. IpcNS Namespace `json:"ipcns,omitempty"` // ShmSize is the size of the tmpfs to mount in at /dev/shm, in bytes. - // Conflicts with ShmSize if ShmSize is not private. + // Conflicts with ShmSize if IpcNS is not private. // Optional. ShmSize *int64 `json:"shm_size,omitempty"` // WorkDir is the container's working directory. @@ -234,6 +238,9 @@ type ContainerSecurityConfig struct { // will use. // Optional. ApparmorProfile string `json:"apparmor_profile,omitempty"` + // SeccompPolicy determines which seccomp profile gets applied + // the container. valid values: empty,default,image + SeccompPolicy string `json:"seccomp_policy,omitempty"` // SeccompProfilePath is the path to a JSON file containing the // container's Seccomp profile. // If not specified, no Seccomp profile will be used. @@ -252,7 +259,10 @@ type ContainerSecurityConfig struct { // IDMappings are UID and GID mappings that will be used by user // namespaces. // Required if UserNS is private. - IDMappings storage.IDMappingOptions `json:"idmappings,omitempty"` + IDMappings *storage.IDMappingOptions `json:"idmappings,omitempty"` + // ReadOnlyFilesystem indicates that everything will be mounted + // as read-only + ReadOnlyFilesystem bool `json:"read_only_filesystem,omittempty"` } // ContainerCgroupConfig contains configuration information about a container's @@ -260,16 +270,13 @@ type ContainerSecurityConfig struct { type ContainerCgroupConfig struct { // CgroupNS is the container's cgroup namespace. // It defaults to private. - // Conflicts with NoCgroups if not set to host. // Mandatory. CgroupNS Namespace `json:"cgroupns,omitempty"` - // NoCgroups indicates that the container should not create CGroups. - // Conflicts with CgroupParent and CgroupNS if CgroupNS is not set to - // host. - NoCgroups bool `json:"no_cgroups,omitempty"` + // CgroupsMode sets a policy for how cgroups will be created in the + // container, including the ability to disable creation entirely. + CgroupsMode string `json:"cgroups_mode,omitempty"` // CgroupParent is the container's CGroup parent. // If not set, the default for the current cgroup driver will be used. - // Conflicts with NoCgroups. // Optional. CgroupParent string `json:"cgroup_parent,omitempty"` } @@ -348,7 +355,7 @@ type ContainerNetworkConfig struct { // ContainerResourceConfig contains information on container resource limits. type ContainerResourceConfig struct { - // ResourceLimits are resource limits to apply to the container. + // ResourceLimits are resource limits to apply to the container., // Can only be set as root on cgroups v1 systems, but can be set as // rootless as well for cgroups v2. // Optional. @@ -365,11 +372,12 @@ type ContainerResourceConfig struct { // ContainerHealthCheckConfig describes a container healthcheck with attributes // like command, retries, interval, start period, and timeout. type ContainerHealthCheckConfig struct { - HealthConfig manifest.Schema2HealthConfig `json:"healthconfig,omitempty"` + HealthConfig *manifest.Schema2HealthConfig `json:"healthconfig,omitempty"` } // SpecGenerator creates an OCI spec and Libpod configuration options to create // a container based on the given configuration. +// swagger:model SpecGenerator type SpecGenerator struct { ContainerBasicConfig ContainerStorageConfig @@ -381,19 +389,24 @@ type SpecGenerator struct { } // NewSpecGenerator returns a SpecGenerator struct given one of two mandatory inputs -func NewSpecGenerator(image, rootfs *string) (*SpecGenerator, error) { - _ = image - _ = rootfs - return &SpecGenerator{}, define.ErrNotImplemented -} - -// Validate verifies that the given SpecGenerator is valid and satisfies required -// input for creating a container. -func (s *SpecGenerator) Validate() error { - return define.ErrNotImplemented +func NewSpecGenerator(image string) *SpecGenerator { + net := ContainerNetworkConfig{ + NetNS: Namespace{ + NSMode: Bridge, + }, + } + csc := ContainerStorageConfig{Image: image} + if rootless.IsRootless() { + net.NetNS.NSMode = Slirp + } + return &SpecGenerator{ + ContainerStorageConfig: csc, + ContainerNetworkConfig: net, + } } -// MakeContainer creates a container based on the SpecGenerator -func (s *SpecGenerator) MakeContainer() (*libpod.Container, error) { - return nil, define.ErrNotImplemented +// NewSpecGenerator returns a SpecGenerator struct given one of two mandatory inputs +func NewSpecGeneratorWithRootfs(rootfs string) *SpecGenerator { + csc := ContainerStorageConfig{Rootfs: rootfs} + return &SpecGenerator{ContainerStorageConfig: csc} } diff --git a/pkg/specgen/validate.go b/pkg/specgen/validate.go new file mode 100644 index 000000000..78e4d8ad5 --- /dev/null +++ b/pkg/specgen/validate.go @@ -0,0 +1,159 @@ +package specgen + +import ( + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" +) + +var ( + // ErrInvalidSpecConfig describes an error that the given SpecGenerator is invalid + ErrInvalidSpecConfig error = errors.New("invalid configuration") + // SystemDValues describes the only values that SystemD can be + SystemDValues = []string{"true", "false", "always"} + // ImageVolumeModeValues describes the only values that ImageVolumeMode can be + ImageVolumeModeValues = []string{"ignore", "tmpfs", "anonymous"} +) + +func exclusiveOptions(opt1, opt2 string) error { + return errors.Errorf("%s and %s are mutually exclusive options", opt1, opt2) +} + +// Validate verifies that the given SpecGenerator is valid and satisfies required +// input for creating a container. +func (s *SpecGenerator) validate(rt *libpod.Runtime) error { + + // + // ContainerBasicConfig + // + // Rootfs and Image cannot both populated + if len(s.ContainerStorageConfig.Image) > 0 && len(s.ContainerStorageConfig.Rootfs) > 0 { + return errors.Wrap(ErrInvalidSpecConfig, "both image and rootfs cannot be simultaneously") + } + // Cannot set hostname and utsns + if len(s.ContainerBasicConfig.Hostname) > 0 && !s.ContainerBasicConfig.UtsNS.IsPrivate() { + return errors.Wrap(ErrInvalidSpecConfig, "cannot set hostname when creating an UTS namespace") + } + // systemd values must be true, false, or always + if len(s.ContainerBasicConfig.Systemd) > 0 && !util.StringInSlice(strings.ToLower(s.ContainerBasicConfig.Systemd), SystemDValues) { + return errors.Wrapf(ErrInvalidSpecConfig, "SystemD values must be one of %s", strings.Join(SystemDValues, ",")) + } + + // + // ContainerStorageConfig + // + // rootfs and image cannot both be set + if len(s.ContainerStorageConfig.Image) > 0 && len(s.ContainerStorageConfig.Rootfs) > 0 { + return exclusiveOptions("rootfs", "image") + } + // imagevolumemode must be one of ignore, tmpfs, or anonymous if given + if len(s.ContainerStorageConfig.ImageVolumeMode) > 0 && !util.StringInSlice(strings.ToLower(s.ContainerStorageConfig.ImageVolumeMode), ImageVolumeModeValues) { + return errors.Errorf("ImageVolumeMode values must be one of %s", strings.Join(ImageVolumeModeValues, ",")) + } + // shmsize conflicts with IPC namespace + if s.ContainerStorageConfig.ShmSize != nil && !s.ContainerStorageConfig.IpcNS.IsPrivate() { + return errors.New("cannot set shmsize when creating an IPC namespace") + } + + // + // ContainerSecurityConfig + // + // groups and privileged are exclusive + if len(s.Groups) > 0 && s.Privileged { + return exclusiveOptions("Groups", "privileged") + } + // capadd and privileged are exclusive + if len(s.CapAdd) > 0 && s.Privileged { + return exclusiveOptions("CapAdd", "privileged") + } + // selinuxprocesslabel and privileged are exclusive + if len(s.SelinuxProcessLabel) > 0 && s.Privileged { + return exclusiveOptions("SelinuxProcessLabel", "privileged") + } + // selinuxmounmtlabel and privileged are exclusive + if len(s.SelinuxMountLabel) > 0 && s.Privileged { + return exclusiveOptions("SelinuxMountLabel", "privileged") + } + // selinuxopts and privileged are exclusive + if len(s.SelinuxOpts) > 0 && s.Privileged { + return exclusiveOptions("SelinuxOpts", "privileged") + } + // apparmor and privileged are exclusive + if len(s.ApparmorProfile) > 0 && s.Privileged { + return exclusiveOptions("AppArmorProfile", "privileged") + } + // userns and idmappings conflict + if s.UserNS.IsPrivate() && s.IDMappings == nil { + return errors.Wrap(ErrInvalidSpecConfig, "IDMappings are required when not creating a User namespace") + } + + // + // ContainerCgroupConfig + // + // + // None for now + + // + // ContainerNetworkConfig + // + if !s.NetNS.IsPrivate() && s.ConfigureNetNS { + return errors.New("can only configure network namespace when creating a network a network namespace") + } + // useimageresolveconf conflicts with dnsserver, dnssearch, dnsoption + if s.UseImageResolvConf { + if len(s.DNSServer) > 0 { + return exclusiveOptions("UseImageResolvConf", "DNSServer") + } + if len(s.DNSSearch) > 0 { + return exclusiveOptions("UseImageResolvConf", "DNSSearch") + } + if len(s.DNSOption) > 0 { + return exclusiveOptions("UseImageResolvConf", "DNSOption") + } + } + // UseImageHosts and HostAdd are exclusive + if s.UseImageHosts && len(s.HostAdd) > 0 { + return exclusiveOptions("UseImageHosts", "HostAdd") + } + + // TODO the specgen does not appear to handle this? Should it + //switch config.Cgroup.Cgroups { + //case "disabled": + // if addedResources { + // return errors.New("cannot specify resource limits when cgroups are disabled is specified") + // } + // configSpec.Linux.Resources = &spec.LinuxResources{} + //case "enabled", "no-conmon", "": + // // Do nothing + //default: + // return errors.New("unrecognized option for cgroups; supported are 'default', 'disabled', 'no-conmon'") + //} + + // Namespaces + if err := s.UtsNS.validate(); err != nil { + return err + } + if err := s.IpcNS.validate(); err != nil { + return err + } + if err := s.NetNS.validate(); err != nil { + return err + } + if err := s.PidNS.validate(); err != nil { + return err + } + if err := s.CgroupNS.validate(); err != nil { + return err + } + if err := s.UserNS.validate(); err != nil { + return err + } + + // The following are defaults as needed by container creation + if len(s.WorkDir) < 1 { + s.WorkDir = "/" + } + return nil +} diff --git a/pkg/varlinkapi/pods.go b/pkg/varlinkapi/pods.go index 1ebe5d424..2ec45f7a1 100644 --- a/pkg/varlinkapi/pods.go +++ b/pkg/varlinkapi/pods.go @@ -16,6 +16,14 @@ import ( // CreatePod ... func (i *LibpodAPI) CreatePod(call iopodman.VarlinkCall, create iopodman.PodCreate) error { var options []libpod.PodCreateOption + if create.Infra { + options = append(options, libpod.WithInfraContainer()) + nsOptions, err := shared.GetNamespaceOptions(create.Share) + if err != nil { + return err + } + options = append(options, nsOptions...) + } if create.CgroupParent != "" { options = append(options, libpod.WithPodCgroupParent(create.CgroupParent)) } @@ -43,14 +51,6 @@ func (i *LibpodAPI) CreatePod(call iopodman.VarlinkCall, create iopodman.PodCrea options = append(options, libpod.WithInfraContainerPorts(portBindings)) } - if create.Infra { - options = append(options, libpod.WithInfraContainer()) - nsOptions, err := shared.GetNamespaceOptions(create.Share) - if err != nil { - return err - } - options = append(options, nsOptions...) - } options = append(options, libpod.WithPodCgroups()) pod, err := i.Runtime.NewPod(getContext(), options...) |