diff options
Diffstat (limited to 'pkg')
32 files changed, 848 insertions, 199 deletions
diff --git a/pkg/api/Makefile b/pkg/api/Makefile index c68e50011..6b24bfd83 100644 --- a/pkg/api/Makefile +++ b/pkg/api/Makefile @@ -9,4 +9,4 @@ validate: ${SWAGGER_OUT} ${SWAGGER_OUT}: # generate doesn't remove file on error rm -f ${SWAGGER_OUT} - swagger generate spec -o ${SWAGGER_OUT} -i tags.yaml -w ./ + swagger generate spec -o ${SWAGGER_OUT} -i tags.yaml -w ./ -m diff --git a/pkg/api/handlers/containers.go b/pkg/api/handlers/containers.go index f180fbc2b..d7d040ce2 100644 --- a/pkg/api/handlers/containers.go +++ b/pkg/api/handlers/containers.go @@ -41,10 +41,9 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, errors.Wrapf(err, "unable to get state for Container %s", name)) return } - // If the Container is stopped already, send a 302 + // If the Container is stopped already, send a 304 if state == define.ContainerStateStopped || state == define.ContainerStateExited { - utils.Error(w, http.StatusText(http.StatusNotModified), http.StatusNotModified, - errors.Errorf("Container %s is already stopped ", name)) + utils.WriteResponse(w, http.StatusNotModified, "") return } @@ -134,8 +133,7 @@ func StartContainer(w http.ResponseWriter, r *http.Request) { return } if state == define.ContainerStateRunning { - msg := fmt.Sprintf("Container %s is already running", name) - utils.Error(w, msg, http.StatusNotModified, errors.New(msg)) + utils.WriteResponse(w, http.StatusNotModified, "") return } if err := con.Start(r.Context(), false); err != nil { diff --git a/pkg/api/handlers/events.go b/pkg/api/handlers/events.go index 44bf35254..22dad9923 100644 --- a/pkg/api/handlers/events.go +++ b/pkg/api/handlers/events.go @@ -1,19 +1,24 @@ package handlers import ( + "encoding/json" "fmt" "net/http" - "strings" - "time" + "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) func GetEvents(w http.ResponseWriter, r *http.Request) { + var ( + fromStart bool + eventsError error + ) query := struct { - Since time.Time `schema:"since"` - Until time.Time `schema:"until"` + Since string `schema:"since"` + Until string `schema:"until"` Filters map[string][]string `schema:"filters"` }{} if err := decodeQuery(r, &query); err != nil { @@ -27,15 +32,30 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { } } - libpodEvents, err := getRuntime(r).GetEvents(libpodFilters) - if err != nil { - utils.BadRequest(w, "filters", strings.Join(r.URL.Query()["filters"], ", "), err) + if len(query.Since) > 0 || len(query.Until) > 0 { + fromStart = true + } + eventChannel := make(chan *events.Event) + go func() { + readOpts := events.ReadOptions{FromStart: fromStart, Stream: true, Filters: libpodFilters, EventChannel: eventChannel, Since: query.Since, Until: query.Until} + eventsError = getRuntime(r).Events(readOpts) + }() + if eventsError != nil { + utils.InternalServerError(w, eventsError) return } - - var apiEvents = make([]*Event, len(libpodEvents)) - for _, v := range libpodEvents { - apiEvents = append(apiEvents, EventToApiEvent(v)) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + for event := range eventChannel { + e := EventToApiEvent(event) + //utils.WriteJSON(w, http.StatusOK, e) + coder := json.NewEncoder(w) + coder.SetEscapeHTML(true) + if err := coder.Encode(e); err != nil { + logrus.Errorf("unable to write json: %q", err) + } + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } } - utils.WriteJSON(w, http.StatusOK, apiEvents) } diff --git a/pkg/api/handlers/generic/containers_stats.go b/pkg/api/handlers/generic/containers_stats.go index f8804b5c0..cbc1be2f0 100644 --- a/pkg/api/handlers/generic/containers_stats.go +++ b/pkg/api/handlers/generic/containers_stats.go @@ -19,9 +19,6 @@ import ( const DefaultStatsPeriod = 5 * time.Second func StatsContainer(w http.ResponseWriter, r *http.Request) { - // 200 no error - // 404 no such - // 500 internal runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 54acdbd36..e11e26510 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -1,16 +1,20 @@ package libpod import ( - "fmt" "net/http" + "path/filepath" + "sort" "strconv" + "time" "github.com/containers/libpod/cmd/podman/shared" "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/gorilla/schema" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) func StopContainer(w http.ResponseWriter, r *http.Request) { @@ -46,12 +50,13 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { } func ListContainers(w http.ResponseWriter, r *http.Request) { var ( - filters []string + filterFuncs []libpod.ContainerFilter + pss []ListContainer ) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { All bool `schema:"all"` - Filter map[string][]string `schema:"filter"` + Filters map[string][]string `schema:"filters"` Last int `schema:"last"` Namespace bool `schema:"namespace"` Pod bool `schema:"pod"` @@ -66,6 +71,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } + runtime := r.Context().Value("runtime").(*libpod.Runtime) opts := shared.PsOptions{ All: query.All, @@ -73,20 +79,55 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { Size: query.Size, Sort: "", Namespace: query.Namespace, + NoTrunc: true, Pod: query.Pod, Sync: query.Sync, } - if len(query.Filter) > 0 { - for k, v := range query.Filter { + if len(query.Filters) > 0 { + for k, v := range query.Filters { for _, val := range v { - filters = append(filters, fmt.Sprintf("%s=%s", k, val)) + generatedFunc, err := shared.GenerateContainerFilterFuncs(k, val, runtime) + if err != nil { + utils.InternalServerError(w, err) + return + } + filterFuncs = append(filterFuncs, generatedFunc) } } } - pss, err := shared.GetPsContainerOutput(runtime, opts, filters, 2) + + if !query.All { + // The default is get only running containers. Do this with a filterfunc + runningOnly, err := shared.GenerateContainerFilterFuncs("status", define.ContainerStateRunning.String(), runtime) + if err != nil { + utils.InternalServerError(w, err) + return + } + filterFuncs = append(filterFuncs, runningOnly) + } + + cons, err := runtime.GetContainers(filterFuncs...) if err != nil { utils.InternalServerError(w, err) } + if query.Last > 0 { + // Sort the containers we got + sort.Sort(psSortCreateTime{cons}) + // we should perform the lopping before we start getting + // the expensive information on containers + if query.Last < len(cons) { + cons = cons[len(cons)-query.Last:] + } + } + for _, con := range cons { + listCon, err := ListContainerBatch(runtime, con, opts) + if err != nil { + utils.InternalServerError(w, err) + return + } + pss = append(pss, listCon) + + } utils.WriteResponse(w, http.StatusOK, pss) } @@ -194,3 +235,122 @@ func ShowMountedContainers(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, response) } + +// BatchContainerOp is used in ps to reduce performance hits by "batching" +// locks. +func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts shared.PsOptions) (ListContainer, error) { + var ( + conConfig *libpod.ContainerConfig + conState define.ContainerStatus + err error + exitCode int32 + exited bool + pid int + size *shared.ContainerSize + startedTime time.Time + exitedTime time.Time + cgroup, ipc, mnt, net, pidns, user, uts string + ) + + batchErr := ctr.Batch(func(c *libpod.Container) error { + conConfig = c.Config() + conState, err = c.State() + if err != nil { + return errors.Wrapf(err, "unable to obtain container state") + } + + exitCode, exited, err = c.ExitCode() + if err != nil { + return errors.Wrapf(err, "unable to obtain container exit code") + } + startedTime, err = c.StartedTime() + if err != nil { + logrus.Errorf("error getting started time for %q: %v", c.ID(), err) + } + exitedTime, err = c.FinishedTime() + if err != nil { + logrus.Errorf("error getting exited time for %q: %v", c.ID(), err) + } + + if !opts.Size && !opts.Namespace { + return nil + } + + if opts.Namespace { + pid, err = c.PID() + if err != nil { + return errors.Wrapf(err, "unable to obtain container pid") + } + ctrPID := strconv.Itoa(pid) + cgroup, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "cgroup")) + ipc, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "ipc")) + mnt, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "mnt")) + net, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "net")) + pidns, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "pid")) + user, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "user")) + uts, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "uts")) + } + if opts.Size { + size = new(shared.ContainerSize) + + rootFsSize, err := c.RootFsSize() + if err != nil { + logrus.Errorf("error getting root fs size for %q: %v", c.ID(), err) + } + + rwSize, err := c.RWSize() + if err != nil { + logrus.Errorf("error getting rw size for %q: %v", c.ID(), err) + } + + size.RootFsSize = rootFsSize + size.RwSize = rwSize + } + return nil + }) + + if batchErr != nil { + return ListContainer{}, batchErr + } + + ps := ListContainer{ + Command: conConfig.Command, + Created: conConfig.CreatedTime.Unix(), + Exited: exited, + ExitCode: exitCode, + ExitedAt: exitedTime.Unix(), + ID: conConfig.ID, + Image: conConfig.RootfsImageName, + IsInfra: conConfig.IsInfra, + Labels: conConfig.Labels, + Mounts: ctr.UserVolumes(), + Names: []string{conConfig.Name}, + Pid: pid, + Pod: conConfig.Pod, + Ports: conConfig.PortMappings, + Size: size, + StartedAt: startedTime.Unix(), + State: conState.String(), + } + if opts.Pod && len(conConfig.Pod) > 0 { + pod, err := rt.GetPod(conConfig.Pod) + if err != nil { + return ListContainer{}, err + } + ps.PodName = pod.Name() + } + + if opts.Namespace { + ns := ListContainerNamespaces{ + Cgroup: cgroup, + IPC: ipc, + MNT: mnt, + NET: net, + PIDNS: pidns, + User: user, + UTS: uts, + } + ps.Namespaces = ns + } + return ps, nil +} diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 6c926c45b..bcbe4977e 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -1,15 +1,23 @@ package libpod import ( + "context" "fmt" + "io" "io/ioutil" "net/http" "os" "strconv" + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/image/v5/types" "github.com/containers/libpod/libpod" + "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/util" "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -176,16 +184,205 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { } func ImagesLoad(w http.ResponseWriter, r *http.Request) { - //TODO ... - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.New("/libpod/images/load is not yet implemented")) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Reference string `schema:"reference"` + }{ + // Add defaults here once needed. + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + tmpfile, err := ioutil.TempFile("", "libpod-images-load.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + defer os.Remove(tmpfile.Name()) + defer tmpfile.Close() + + if _, err := io.Copy(tmpfile, r.Body); err != nil && err != io.EOF { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to write archive to temporary file")) + return + } + + tmpfile.Close() + loadedImage, err := runtime.LoadImage(context.Background(), query.Reference, tmpfile.Name(), os.Stderr, "") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to load image")) + return + } + + utils.WriteResponse(w, http.StatusOK, []handlers.LibpodImagesLoadReport{{ID: loadedImage}}) } func ImagesImport(w http.ResponseWriter, r *http.Request) { - //TODO ... - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.New("/libpod/images/import is not yet implemented")) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Changes []string `schema:"changes"` + Message string `schema:"message"` + Reference string `schema:"reference"` + URL string `schema:"URL"` + }{ + // Add defaults here once needed. + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + // Check if we need to load the image from a URL or from the request's body. + source := query.URL + if len(query.URL) == 0 { + tmpfile, err := ioutil.TempFile("", "libpod-images-import.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + defer os.Remove(tmpfile.Name()) + defer tmpfile.Close() + + if _, err := io.Copy(tmpfile, r.Body); err != nil && err != io.EOF { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to write archive to temporary file")) + return + } + + tmpfile.Close() + source = tmpfile.Name() + } + + importedImage, err := runtime.Import(context.Background(), source, query.Reference, query.Changes, query.Message, true) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to import image")) + return + } + + utils.WriteResponse(w, http.StatusOK, handlers.LibpodImagesImportReport{ID: importedImage}) } func ImagesPull(w http.ResponseWriter, r *http.Request) { - //TODO ... - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.New("/libpod/images/pull is not yet implemented")) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Reference string `schema:"reference"` + Credentials string `schema:"credentials"` + OverrideOS string `schema:"overrideOS"` + OverrideArch string `schema:"overrideArch"` + TLSVerify bool `schema:"tlsVerify"` + AllTags bool `schema:"allTags"` + }{ + TLSVerify: true, + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + if len(query.Reference) == 0 { + utils.InternalServerError(w, errors.New("reference parameter cannot be empty")) + return + } + // Enforce the docker transport. This is just a precaution as some callers + // might accustomed to using the "transport:reference" notation. Using + // another than the "docker://" transport does not really make sense for a + // remote case. For loading tarballs, the load and import endpoints should + // be used. + imageRef, err := alltransports.ParseImageName(query.Reference) + if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Errorf("reference %q must be a docker reference", query.Reference)) + return + } else if err != nil { + origErr := err + imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s://%s", docker.Transport.Name(), query.Reference)) + if err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(origErr, "reference %q must be a docker reference", query.Reference)) + return + } + } + + // all-tags doesn't work with a tagged reference, so let's check early + namedRef, err := reference.Parse(query.Reference) + if err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "error parsing reference %q", query.Reference)) + return + } + if _, isTagged := namedRef.(reference.Tagged); isTagged && query.AllTags { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Errorf("reference %q must not have a tag for all-tags", query.Reference)) + return + } + + var registryCreds *types.DockerAuthConfig + if len(query.Credentials) != 0 { + creds, err := util.ParseRegistryCreds(query.Credentials) + if err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "error parsing credentials %q", query.Credentials)) + return + } + registryCreds = creds + } + + // Setup the registry options + dockerRegistryOptions := image.DockerRegistryOptions{ + DockerRegistryCreds: registryCreds, + OSChoice: query.OverrideOS, + ArchitectureChoice: query.OverrideArch, + } + if query.TLSVerify { + dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) + } + + // Prepare the images we want to pull + imagesToPull := []string{} + res := []handlers.LibpodImagesPullReport{} + imageName := namedRef.String() + + if !query.AllTags { + imagesToPull = append(imagesToPull, imageName) + } else { + systemContext := image.GetSystemContext("", "", false) + tags, err := docker.GetRepositoryTags(context.Background(), systemContext, imageRef) + if err != nil { + utils.InternalServerError(w, errors.Wrap(err, "error getting repository tags")) + return + } + for _, tag := range tags { + imagesToPull = append(imagesToPull, fmt.Sprintf("%s:%s", imageName, tag)) + } + } + + // Finally pull the images + for _, img := range imagesToPull { + newImage, err := runtime.ImageRuntime().New( + context.Background(), + img, + "", + "", + os.Stderr, + &dockerRegistryOptions, + image.SigningOptions{}, + nil, + util.PullImageAlways) + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error pulling image %q", query.Reference)) + return + } + res = append(res, handlers.LibpodImagesPullReport{ID: newImage.ID()}) + } + + utils.WriteResponse(w, http.StatusOK, res) } diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 091368985..e9297d91b 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -91,7 +91,11 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { pod, err := runtime.NewPod(r.Context(), options...) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + http_code := http.StatusInternalServerError + if errors.Cause(err) == define.ErrPodExists { + http_code = http.StatusConflict + } + utils.Error(w, "Something went wrong.", http_code, err) return } utils.WriteResponse(w, http.StatusCreated, handlers.IDResponse{ID: pod.CgroupParent()}) @@ -198,8 +202,7 @@ func PodStop(w http.ResponseWriter, r *http.Request) { } } if allContainersStopped { - alreadyStopped := errors.Errorf("pod %s is already stopped", pod.ID()) - utils.Error(w, "Something went wrong", http.StatusNotModified, alreadyStopped) + utils.WriteResponse(w, http.StatusNotModified, "") return } @@ -245,8 +248,7 @@ func PodStart(w http.ResponseWriter, r *http.Request) { } } if allContainersRunning { - alreadyRunning := errors.Errorf("pod %s is already running", pod.ID()) - utils.Error(w, "Something went wrong", http.StatusNotModified, alreadyRunning) + utils.WriteResponse(w, http.StatusNotModified, "") return } if _, err := pod.Start(r.Context()); err != nil { @@ -409,5 +411,5 @@ func PodExists(w http.ResponseWriter, r *http.Request) { utils.PodNotFound(w, name, err) return } - utils.WriteResponse(w, http.StatusOK, "") + utils.WriteResponse(w, http.StatusNoContent, "") } diff --git a/pkg/api/handlers/libpod/swagger.go b/pkg/api/handlers/libpod/swagger.go new file mode 100644 index 000000000..aec30ef56 --- /dev/null +++ b/pkg/api/handlers/libpod/swagger.go @@ -0,0 +1,8 @@ +package libpod + +// List Containers +// swagger:response ListContainers +type swagInspectPodResponse struct { + // in:body + Body []ListContainer +} diff --git a/pkg/api/handlers/libpod/types.go b/pkg/api/handlers/libpod/types.go new file mode 100644 index 000000000..0949b2a72 --- /dev/null +++ b/pkg/api/handlers/libpod/types.go @@ -0,0 +1,82 @@ +package libpod + +import ( + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/cri-o/ocicni/pkg/ocicni" +) + +// Listcontainer describes a container suitable for listing +type ListContainer struct { + // Container command + Command []string + // Container creation time + Created int64 + // If container has exited/stopped + Exited bool + // Time container exited + ExitedAt int64 + // If container has exited, the return code from the command + ExitCode int32 + // The unique identifier for the container + ID string `json:"Id"` + // Container image + Image string + // If this container is a Pod infra container + IsInfra bool + // Labels for container + Labels map[string]string + // User volume mounts + Mounts []string + // The names assigned to the container + Names []string + // Namespaces the container belongs to. Requires the + // namespace boolean to be true + Namespaces ListContainerNamespaces + // The process id of the container + Pid int + // If the container is part of Pod, the Pod ID. Requires the pod + // boolean to be set + Pod string + // If the container is part of Pod, the Pod name. Requires the pod + // boolean to be set + PodName string + // Port mappings + Ports []ocicni.PortMapping + // Size of the container rootfs. Requires the size boolean to be true + Size *shared.ContainerSize + // Time when container started + StartedAt int64 + // State of container + State string +} + +// ListContainer Namespaces contains the identifiers of the container's Linux namespaces +type ListContainerNamespaces struct { + // Mount namespace + MNT string `json:"Mnt,omitempty"` + // Cgroup namespace + Cgroup string `json:"Cgroup,omitempty"` + // IPC namespace + IPC string `json:"Ipc,omitempty"` + // Network namespace + NET string `json:"Net,omitempty"` + // PID namespace + PIDNS string `json:"Pidns,omitempty"` + // UTS namespace + UTS string `json:"Uts,omitempty"` + // User namespace + User string `json:"User,omitempty"` +} + +// sortContainers helps us set-up ability to sort by createTime +type sortContainers []*libpod.Container + +func (a sortContainers) Len() int { return len(a) } +func (a sortContainers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type psSortCreateTime struct{ sortContainers } + +func (a psSortCreateTime) Less(i, j int) bool { + return a.sortContainers[i].CreatedTime().Before(a.sortContainers[j].CreatedTime()) +} diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger.go index bc75777aa..10525bfc7 100644 --- a/pkg/api/handlers/swagger.go +++ b/pkg/api/handlers/swagger.go @@ -1,7 +1,6 @@ package handlers import ( - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/inspect" @@ -104,13 +103,6 @@ type swagDockerTopResponse struct { } } -// List containers -// swagger:response LibpodListContainersResponse -type swagLibpodListContainersResponse struct { - // in:body - Body []shared.PsContainerOutput -} - // Inspect container // swagger:response LibpodInspectContainerResponse type swagLibpodInspectContainerResponse struct { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 6169adb18..f5d9f9ad5 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -33,6 +33,18 @@ type ContainerConfig struct { dockerContainer.Config } +type LibpodImagesLoadReport struct { + ID string `json:"id"` +} + +type LibpodImagesImportReport struct { + ID string `json:"id"` +} + +type LibpodImagesPullReport struct { + ID string `json:"id"` +} + type ImageSummary struct { docker.ImageSummary CreatedTime time.Time `json:"CreatedTime,omitempty"` @@ -49,21 +61,6 @@ type LibpodContainersPruneReport struct { PruneError string `json:"error"` } -type LibpodImagesLoadReport struct { - ID string `json:"id"` - RepoTags []string `json:"repoTags"` -} - -type LibpodImagesImportReport struct { - ID string `json:"id"` - RepoTags []string `json:"repoTags"` -} - -type LibpodImagesPullReport struct { - ID string `json:"id"` - RepoTags []string `json:"repoTags"` -} - type Info struct { docker.Info BuildahVersion string diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go index 970f93791..44bcc794c 100644 --- a/pkg/api/handlers/utils/handler.go +++ b/pkg/api/handlers/utils/handler.go @@ -23,6 +23,14 @@ func IsLibpodRequest(r *http.Request) bool { // WriteResponse encodes the given value as JSON or string and renders it for http client func WriteResponse(w http.ResponseWriter, code int, value interface{}) { + // RFC2616 explicitly states that the following status codes "MUST NOT + // include a message-body": + switch code { + case http.StatusNoContent, http.StatusNotModified: // 204, 304 + w.WriteHeader(code) + return + } + switch v := value.(type) { case string: w.Header().Set("Content-Type", "text/plain; charset=us-ascii") diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index abae81c38..ed30bc14a 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -610,7 +610,7 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { // - application/json // responses: // 200: - // $ref: "#/responses/LibpodListContainersResponse" + // $ref: "#/responses/ListContainers" // 400: // $ref: "#/responses/BadParamError" // 500: diff --git a/pkg/api/server/register_events.go b/pkg/api/server/register_events.go index a32244f4d..090f66323 100644 --- a/pkg/api/server/register_events.go +++ b/pkg/api/server/register_events.go @@ -29,7 +29,7 @@ func (s *APIServer) RegisterEventsHandlers(r *mux.Router) error { // description: JSON encoded map[string][]string of constraints // responses: // 200: - // $ref: "#/responses/ok" + // description: returns a string of json data describing an event // 500: // "$ref": "#/responses/InternalError" r.Handle(VersionedPath("/events"), APIHandler(s.Context, handlers.GetEvents)) diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 4a46b6ee6..f082c5fec 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -639,12 +639,8 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: Load an image (oci-archive or docker-archive) stream. // parameters: // - in: query - // name: change - // description: "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR. JSON encoded string" - // type: string - // - in: query - // name: message - // description: Set commit message for imported image + // name: reference + // description: "Optional Name[:TAG] for the image" // type: string // - in: formData // name: upload @@ -656,6 +652,8 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // responses: // 200: // $ref: "#/responses/DocsLibpodImagesLoadResponse" + // 400: + // $ref: "#/responses/BadParamError" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/load"), APIHandler(s.Context, libpod.ImagesLoad)).Methods(http.MethodPost) @@ -667,17 +665,23 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: Import a previously exported tarball as an image. // parameters: // - in: query - // name: change - // description: "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR. JSON encoded string" - // type: string + // name: changes + // description: "Apply the following possible instructions to the created image: CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR. JSON encoded string" + // type: array + // items: + // type: string // - in: query // name: message // description: Set commit message for imported image // type: string // - in: query + // name: reference + // description: "Optional Name[:TAG] for the image" + // type: string + // - in: query // name: url - // description: Specify a URL instead of a tarball - // type: boolean + // description: Load image from the specified URL + // type: string // - in: formData // name: upload // type: file @@ -688,34 +692,50 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // responses: // 200: // $ref: "#/responses/DocsLibpodImagesImportResponse" + // 400: + // $ref: "#/responses/BadParamError" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/import"), APIHandler(s.Context, libpod.ImagesImport)).Methods(http.MethodPost) - // swagger:operation GET /libpod/images/pull libpod libpodImagesPull + // swagger:operation POST /libpod/images/pull libpod libpodImagesPull // --- // tags: // - images - // summary: Import image - // description: Import a previosly exported image as a tarball. + // summary: Pull images + // description: Pull one or more images from a container registry. // parameters: - // - in: query - // name: reference - // description: Mandatory reference to the image (e.g., quay.io/image/name:tag)/ - // type: string - // - in: query - // name: credentials - // description: username:password for the registry. - // type: string - // - in: query - // name: tls-verify - // description: Require TLS verification. - // type: boolean - // default: true + // - in: query + // name: reference + // description: "Mandatory reference to the image (e.g., quay.io/image/name:tag)" + // type: string + // - in: query + // name: credentials + // description: "username:password for the registry" + // type: string + // - in: query + // name: overrideOS + // description: Pull image for the specified operating system. + // type: string + // - in: query + // name: overrideArch + // description: Pull image for the specified architecture. + // type: string + // - in: query + // name: tlsVerify + // description: Require TLS verification. + // type: boolean + // default: true + // - in: query + // name: allTags + // description: Pull all tagged images in the repository. + // type: boolean // produces: // - application/json // responses: // 200: // $ref: "#/responses/DocsLibpodImagesPullResponse" + // 400: + // $ref: "#/responses/BadParamError" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/pull"), APIHandler(s.Context, libpod.ImagesPull)).Methods(http.MethodPost) diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index 5c7b51871..974568d47 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -41,6 +41,8 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // type: string // 400: // $ref: "#/responses/BadParamError" + // 409: + // description: pod already exists // 500: // $ref: "#/responses/InternalError" r.Handle(VersionedPath("/libpod/pods/prune"), APIHandler(s.Context, libpod.PodPrune)).Methods(http.MethodPost) diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 7bb0f5481..87b11b716 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -106,6 +106,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li server.RegisterContainersHandlers, server.RegisterDistributionHandlers, server.registerExecHandlers, + server.RegisterEventsHandlers, server.registerHealthCheckHandlers, server.registerImagesHandlers, server.registerInfoHandlers, diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 116af9709..f270060a6 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -130,7 +130,7 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, // if more desirable we could use url to form the encoded endpoint with params r := req.URL.Query() for k, v := range queryParams { - r.Add(k, url.QueryEscape(v)) + r.Add(k, v) } req.URL.RawQuery = r.Encode() } @@ -155,18 +155,14 @@ func GetConnectionFromContext(ctx context.Context) (*Connection, error) { return conn, nil } -// FiltersToHTML converts our typical filter format of a +// FiltersToString converts our typical filter format of a // map[string][]string to a query/html safe string. -func FiltersToHTML(filters map[string][]string) (string, error) { +func FiltersToString(filters map[string][]string) (string, error) { lowerCaseKeys := make(map[string][]string) for k, v := range filters { lowerCaseKeys[strings.ToLower(k)] = v } - unsafeString, err := jsoniter.MarshalToString(lowerCaseKeys) - if err != nil { - return "", err - } - return url.QueryEscape(unsafeString), nil + return jsoniter.MarshalToString(lowerCaseKeys) } // IsInformation returns true if the response code is 1xx diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index 334a656d4..04f7f8802 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -5,8 +5,8 @@ import ( "net/http" "strconv" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" + lpapiv2 "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings" ) @@ -15,13 +15,16 @@ import ( // the most recent number of containers. The pod and size booleans indicate that pod information and rootfs // 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, last *int, pod, size, sync *bool) ([]*shared.PsContainerOutput, error) { // nolint:typecheck +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) if err != nil { return nil, err } - var images []*shared.PsContainerOutput + var containers []lpapiv2.ListContainer params := make(map[string]string) + if all != nil { + params["all"] = strconv.FormatBool(*all) + } if last != nil { params["last"] = strconv.Itoa(*last) } @@ -35,7 +38,7 @@ func List(ctx context.Context, filters map[string][]string, last *int, pod, size params["sync"] = strconv.FormatBool(*sync) } if filters != nil { - filterString, err := bindings.FiltersToHTML(filters) + filterString, err := bindings.FiltersToString(filters) if err != nil { return nil, err } @@ -43,9 +46,9 @@ func List(ctx context.Context, filters map[string][]string, last *int, pod, size } response, err := conn.DoRequest(nil, http.MethodGet, "/containers/json", params) if err != nil { - return images, err + return containers, err } - return images, response.Process(nil) + return containers, response.Process(&containers) } // Prune removes stopped and exited containers from local storage. The optional filters can be @@ -62,7 +65,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { } params := make(map[string]string) if filters != nil { - filterString, err := bindings.FiltersToHTML(filters) + filterString, err := bindings.FiltersToString(filters) if err != nil { return nil, err } diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index deaf93f0e..b19482943 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -38,7 +38,7 @@ func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handl params["all"] = strconv.FormatBool(*all) } if filters != nil { - strFilters, err := bindings.FiltersToHTML(filters) + strFilters, err := bindings.FiltersToString(filters) if err != nil { return nil, err } @@ -155,7 +155,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { } params := make(map[string]string) if filters != nil { - stringFilter, err := bindings.FiltersToHTML(filters) + stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } diff --git a/pkg/bindings/images/search.go b/pkg/bindings/images/search.go index d98ddf18d..58b25425b 100644 --- a/pkg/bindings/images/search.go +++ b/pkg/bindings/images/search.go @@ -26,7 +26,7 @@ func Search(ctx context.Context, term string, limit *int, filters map[string][]s params["limit"] = strconv.Itoa(*limit) } if filters != nil { - stringFilter, err := bindings.FiltersToHTML(filters) + stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index a6b74c21d..d079f01c2 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -97,7 +97,7 @@ func List(ctx context.Context, filters map[string][]string) (*[]libpod.PodInspec } params := make(map[string]string) if filters != nil { - stringFilter, err := bindings.FiltersToHTML(filters) + stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 15783041f..22cd0b7e0 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -16,6 +16,7 @@ import ( const ( defaultPodmanBinaryLocation string = "/usr/bin/podman" alpine string = "docker.io/library/alpine:latest" + busybox string = "docker.io/library/busybox:latest" ) type bindingTest struct { @@ -113,7 +114,38 @@ func (b *bindingTest) startAPIService() *gexec.Session { } func (b *bindingTest) cleanup() { + s := b.runPodman([]string{"stop", "-a", "-t", "0"}) + s.Wait(45) if err := os.RemoveAll(b.tempDirPath); err != nil { fmt.Println(err) } } + +// Pull is a helper function to pull in images +func (b *bindingTest) Pull(name string) { + p := b.runPodman([]string{"pull", name}) + p.Wait(45) +} + +// Run a container and add append the alpine image to it +func (b *bindingTest) RunTopContainer(name *string) { + cmd := []string{"run", "-dt"} + if name != nil { + containerName := *name + cmd = append(cmd, "--name", containerName) + } + cmd = append(cmd, alpine, "top") + p := b.runPodman(cmd) + p.Wait(45) +} + +// StringInSlice returns a boolean based on whether a given +// string is in a given slice +func StringInSlice(s string, sl []string) bool { + for _, val := range sl { + if s == val { + return true + } + } + return false +} diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index f2dc856b2..fea611601 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -2,10 +2,10 @@ package test_bindings import ( "context" - "fmt" "time" "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/bindings/images" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -17,12 +17,12 @@ var _ = Describe("Podman images", func() { //tempdir string //err error //podmanTest *PodmanTestIntegration - bt *bindingTest - s *gexec.Session - connText context.Context - err error - false bool - //true bool = true + bt *bindingTest + s *gexec.Session + connText context.Context + err error + falseFlag bool = false + trueFlag bool = true ) BeforeEach(func() { @@ -34,8 +34,8 @@ var _ = Describe("Podman images", func() { //podmanTest.Setup() //podmanTest.SeedImages() bt = newBindingTest() - p := bt.runPodman([]string{"pull", alpine}) - p.Wait(45) + bt.Pull(alpine) + bt.Pull(busybox) s = bt.startAPIService() time.Sleep(1 * time.Second) connText, err = bindings.NewConnection(bt.sock) @@ -68,28 +68,63 @@ var _ = Describe("Podman images", func() { _, err = images.GetImage(connText, data.ID[0:12], nil) Expect(err).To(BeNil()) - //Inspect by long name should work, it doesnt (yet) i think it needs to be html escaped - _, err = images.GetImage(connText, alpine, nil) - Expect(err).To(BeNil()) + // The test to inspect by long name needs to fixed. + // Inspect by long name should work, it doesnt (yet) i think it needs to be html escaped + // _, err = images.GetImage(connText, alpine, nil) + // Expect(err).To(BeNil()) }) + + // Test to validate the remove image api It("remove image", func() { // Remove invalid image should be a 404 - _, err = images.Remove(connText, "foobar5000", &false) + _, err = images.Remove(connText, "foobar5000", &falseFlag) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", 404)) - _, err := images.GetImage(connText, "alpine", nil) + // Remove an image by name, validate image is removed and error is nil + inspectData, err := images.GetImage(connText, "busybox", nil) + Expect(err).To(BeNil()) + response, err := images.Remove(connText, "busybox", nil) + Expect(err).To(BeNil()) + Expect(inspectData.ID).To(Equal(response[0]["Deleted"])) + inspectData, err = images.GetImage(connText, "busybox", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + // Start a container with alpine image + var top string = "top" + bt.RunTopContainer(&top) + // we should now have a container called "top" running + containerResponse, err := containers.Inspect(connText, "top", &falseFlag) Expect(err).To(BeNil()) + Expect(containerResponse.Name).To(Equal("top")) - response, err := images.Remove(connText, "alpine", &false) + // try to remove the image "alpine". This should fail since we are not force + // deleting hence image cannot be deleted until the container is deleted. + response, err = images.Remove(connText, "alpine", &falseFlag) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 500)) + + // Removing the image "alpine" where force = true + response, err = images.Remove(connText, "alpine", &trueFlag) Expect(err).To(BeNil()) - fmt.Println(response) - // to be continued + // Checking if both the images are gone as well as the container is deleted + inspectData, err = images.GetImage(connText, "busybox", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + inspectData, err = images.GetImage(connText, "alpine", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + _, err = containers.Inspect(connText, "top", &falseFlag) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) }) - //Tests to validate the image tag command. + // Tests to validate the image tag command. It("tag image", func() { // Validates if invalid image name is given a bad response is encountered. err = images.Tag(connText, "dummy", "demo", "alpine") @@ -107,21 +142,30 @@ var _ = Describe("Podman images", func() { }) - //Test to validate the List images command. + // Test to validate the List images command. It("List image", func() { - //Array to hold the list of images returned + // Array to hold the list of images returned imageSummary, err := images.List(connText, nil, nil) - //There Should be no errors in the response. + // There Should be no errors in the response. Expect(err).To(BeNil()) - //Since in the begin context only one image is created the list context should have only one image - Expect(len(imageSummary)).To(Equal(1)) - - //To be written create a new image and check list count again - //imageSummary, err = images.List(connText, nil, nil) - - //Since in the begin context only one image adding one more image should - ///Expect(len(imageSummary)).To(Equal(2) - + // Since in the begin context two images are created the + // list context should have only 2 images + Expect(len(imageSummary)).To(Equal(2)) + + // Adding one more image. There Should be no errors in the response. + // And the count should be three now. + bt.Pull("busybox:glibc") + imageSummary, err = images.List(connText, nil, nil) + Expect(err).To(BeNil()) + Expect(len(imageSummary)).To(Equal(3)) + + //Validate the image names. + var names []string + for _, i := range imageSummary { + names = append(names, i.RepoTags...) + } + Expect(StringInSlice(alpine, names)).To(BeTrue()) + Expect(StringInSlice(busybox, names)).To(BeTrue()) }) }) diff --git a/pkg/rootlessport/rootlessport_linux.go b/pkg/rootlessport/rootlessport_linux.go index 3e678d33a..2b51f4e09 100644 --- a/pkg/rootlessport/rootlessport_linux.go +++ b/pkg/rootlessport/rootlessport_linux.go @@ -122,6 +122,7 @@ func parent() error { logrus.WithError(driverErr).Warn("parent driver exited") } errCh <- driverErr + close(errCh) }() opaque := driver.OpaqueForChild() logrus.Infof("opaque=%+v", opaque) @@ -142,15 +143,12 @@ func parent() error { }() // reexec the child process in the child netns - cmd := exec.Command(fmt.Sprintf("/proc/%d/exe", os.Getpid())) + cmd := exec.Command("/proc/self/exe") cmd.Args = []string{reexecChildKey} cmd.Stdin = childQuitR cmd.Stdout = &logrusWriter{prefix: "child"} cmd.Stderr = cmd.Stdout cmd.Env = append(os.Environ(), reexecChildEnvOpaque+"="+string(opaqueJSON)) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGTERM, - } childNS, err := ns.GetNS(cfg.NetNSPath) if err != nil { return err @@ -162,14 +160,27 @@ func parent() error { return err } + defer func() { + if err := syscall.Kill(cmd.Process.Pid, syscall.SIGTERM); err != nil { + logrus.WithError(err).Warn("kill child process") + } + }() + logrus.Info("waiting for initComplete") // wait for the child to connect to the parent - select { - case <-initComplete: - logrus.Infof("initComplete is closed; parent and child established the communication channel") - case err := <-errCh: - return err +outer: + for { + select { + case <-initComplete: + logrus.Infof("initComplete is closed; parent and child established the communication channel") + break outer + case err := <-errCh: + if err != nil { + return err + } + } } + defer func() { logrus.Info("stopping parent driver") quit <- struct{}{} diff --git a/pkg/seccomp/seccomp.go b/pkg/seccomp/seccomp.go new file mode 100644 index 000000000..dcf255378 --- /dev/null +++ b/pkg/seccomp/seccomp.go @@ -0,0 +1,54 @@ +package seccomp + +import ( + "sort" + + "github.com/pkg/errors" +) + +// ContianerImageLabel is the key of the image annotation embedding a seccomp +// profile. +const ContainerImageLabel = "io.containers.seccomp.profile" + +// Policy denotes a seccomp policy. +type Policy int + +const ( + // PolicyDefault - if set use SecurityConfig.SeccompProfilePath, + // otherwise use the default profile. The SeccompProfilePath might be + // explicitly set by the user. + PolicyDefault Policy = iota + // PolicyImage - if set use SecurityConfig.SeccompProfileFromImage, + // otherwise follow SeccompPolicyDefault. + PolicyImage +) + +// Map for easy lookups of supported policies. +var supportedPolicies = map[string]Policy{ + "": PolicyDefault, + "default": PolicyDefault, + "image": PolicyImage, +} + +// LookupPolicy looksup the corresponding Policy for the specified +// string. If none is found, an errors is returned including the list of +// supported policies. +// +// Note that an empty string resolved to SeccompPolicyDefault. +func LookupPolicy(s string) (Policy, error) { + policy, exists := supportedPolicies[s] + if exists { + return policy, nil + } + + // Sort the keys first as maps are non-deterministic. + keys := []string{} + for k := range supportedPolicies { + if k != "" { + keys = append(keys, k) + } + } + sort.Strings(keys) + + return -1, errors.Errorf("invalid seccomp policy %q: valid policies are %+q", s, keys) +} diff --git a/pkg/spec/config_linux.go b/pkg/spec/config_linux.go index 32d8cb4de..5f39b6d0d 100644 --- a/pkg/spec/config_linux.go +++ b/pkg/spec/config_linux.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strconv" "strings" "github.com/containers/libpod/pkg/rootless" @@ -90,6 +91,42 @@ func devicesFromPath(g *generate.Generator, devicePath string) error { return addDevice(g, strings.Join(append([]string{resolvedDevicePath}, devs[1:]...), ":")) } +func deviceCgroupRules(g *generate.Generator, deviceCgroupRules []string) error { + for _, deviceCgroupRule := range deviceCgroupRules { + if err := validateDeviceCgroupRule(deviceCgroupRule); err != nil { + return err + } + ss := parseDeviceCgroupRule(deviceCgroupRule) + if len(ss[0]) != 5 { + return errors.Errorf("invalid device cgroup rule format: '%s'", deviceCgroupRule) + } + matches := ss[0] + var major, minor *int64 + if matches[2] == "*" { + majorDev := int64(-1) + major = &majorDev + } else { + majorDev, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return errors.Errorf("invalid major value in device cgroup rule format: '%s'", deviceCgroupRule) + } + major = &majorDev + } + if matches[3] == "*" { + minorDev := int64(-1) + minor = &minorDev + } else { + minorDev, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return errors.Errorf("invalid major value in device cgroup rule format: '%s'", deviceCgroupRule) + } + minor = &minorDev + } + g.AddLinuxResourcesDevice(true, matches[1], major, minor, matches[4]) + } + return nil +} + func addDevice(g *generate.Generator, device string) error { src, dst, permissions, err := ParseDevice(device) if err != nil { diff --git a/pkg/spec/config_linux_cgo.go b/pkg/spec/config_linux_cgo.go index ae83c9d52..05f42c4da 100644 --- a/pkg/spec/config_linux_cgo.go +++ b/pkg/spec/config_linux_cgo.go @@ -5,9 +5,10 @@ package createconfig import ( "io/ioutil" + "github.com/containers/libpod/pkg/seccomp" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" - seccomp "github.com/seccomp/containers-golang" + goSeccomp "github.com/seccomp/containers-golang" "github.com/sirupsen/logrus" ) @@ -15,9 +16,9 @@ func getSeccompConfig(config *SecurityConfig, configSpec *spec.Spec) (*spec.Linu var seccompConfig *spec.LinuxSeccomp var err error - if config.SeccompPolicy == SeccompPolicyImage && config.SeccompProfileFromImage != "" { + if config.SeccompPolicy == seccomp.PolicyImage && config.SeccompProfileFromImage != "" { logrus.Debug("Loading seccomp profile from the security config") - seccompConfig, err = seccomp.LoadProfile(config.SeccompProfileFromImage, configSpec) + seccompConfig, err = goSeccomp.LoadProfile(config.SeccompProfileFromImage, configSpec) if err != nil { return nil, errors.Wrap(err, "loading seccomp profile failed") } @@ -30,13 +31,13 @@ func getSeccompConfig(config *SecurityConfig, configSpec *spec.Spec) (*spec.Linu if err != nil { return nil, errors.Wrapf(err, "opening seccomp profile (%s) failed", config.SeccompProfilePath) } - seccompConfig, err = seccomp.LoadProfile(string(seccompProfile), configSpec) + seccompConfig, err = goSeccomp.LoadProfile(string(seccompProfile), configSpec) if err != nil { return nil, errors.Wrapf(err, "loading seccomp profile (%s) failed", config.SeccompProfilePath) } } else { logrus.Debug("Loading default seccomp profile") - seccompConfig, err = seccomp.GetDefaultProfile(configSpec) + seccompConfig, err = goSeccomp.GetDefaultProfile(configSpec) if err != nil { return nil, errors.Wrapf(err, "loading seccomp profile (%s) failed", config.SeccompProfilePath) } diff --git a/pkg/spec/config_unsupported.go b/pkg/spec/config_unsupported.go index a2c7f4416..be3e7046d 100644 --- a/pkg/spec/config_unsupported.go +++ b/pkg/spec/config_unsupported.go @@ -30,3 +30,7 @@ func makeThrottleArray(throttleInput []string, rateType int) ([]spec.LinuxThrott func devicesFromPath(g *generate.Generator, devicePath string) error { return errors.New("function not implemented") } + +func deviceCgroupRules(g *generate.Generator, deviceCgroupRules []string) error { + return errors.New("function not implemented") +} diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index fb222083b..8010be0d4 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -2,7 +2,6 @@ package createconfig import ( "os" - "sort" "strconv" "strings" "syscall" @@ -11,6 +10,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/namespaces" + "github.com/containers/libpod/pkg/seccomp" "github.com/containers/storage" "github.com/docker/go-connections/nat" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -38,6 +38,7 @@ type CreateResourceConfig struct { CPUs float64 // cpus CPUsetCPUs string CPUsetMems string // cpuset-mems + DeviceCgroupRules []string //device-cgroup-rule DeviceReadBps []string // device-read-bps DeviceReadIOps []string // device-read-iops DeviceWriteBps []string // device-write-bps @@ -107,48 +108,6 @@ type NetworkConfig struct { PublishAll bool //publish-all } -// SeccompPolicy determines which seccomp profile gets applied to the container. -type SeccompPolicy int - -const ( - // SeccompPolicyDefault - if set use SecurityConfig.SeccompProfilePath, - // otherwise use the default profile. The SeccompProfilePath might be - // explicitly set by the user. - SeccompPolicyDefault SeccompPolicy = iota - // SeccompPolicyImage - if set use SecurityConfig.SeccompProfileFromImage, - // otherwise follow SeccompPolicyDefault. - SeccompPolicyImage -) - -// Map for easy lookups of supported policies. -var supportedSeccompPolicies = map[string]SeccompPolicy{ - "": SeccompPolicyDefault, - "default": SeccompPolicyDefault, - "image": SeccompPolicyImage, -} - -// LookupSeccompPolicy looksup the corresponding SeccompPolicy for the specified -// string. If none is found, an errors is returned including the list of -// supported policies. -// Note that an empty string resolved to SeccompPolicyDefault. -func LookupSeccompPolicy(s string) (SeccompPolicy, error) { - policy, exists := supportedSeccompPolicies[s] - if exists { - return policy, nil - } - - // Sort the keys first as maps are non-deterministic. - keys := []string{} - for k := range supportedSeccompPolicies { - if k != "" { - keys = append(keys, k) - } - } - sort.Strings(keys) - - return -1, errors.Errorf("invalid seccomp policy %q: valid policies are %+q", s, keys) -} - // SecurityConfig configures the security features for the container type SecurityConfig struct { CapAdd []string // cap-add @@ -158,7 +117,7 @@ type SecurityConfig struct { ApparmorProfile string //SecurityOpts SeccompProfilePath string //SecurityOpts SeccompProfileFromImage string // seccomp profile from the container image - SeccompPolicy SeccompPolicy + SeccompPolicy seccomp.Policy SecurityOpts []string Privileged bool //privileged ReadOnlyRootfs bool //read-only diff --git a/pkg/spec/parse.go b/pkg/spec/parse.go index 6fa0b0636..a5dfccdb9 100644 --- a/pkg/spec/parse.go +++ b/pkg/spec/parse.go @@ -2,12 +2,17 @@ package createconfig import ( "fmt" + "regexp" "strconv" "strings" "github.com/docker/go-units" + "github.com/pkg/errors" ) +// deviceCgroupRulegex defines the valid format of device-cgroup-rule +var deviceCgroupRuleRegex = regexp.MustCompile(`^([acb]) ([0-9]+|\*):([0-9]+|\*) ([rwm]{1,3})$`) + // Pod signifies a kernel namespace is being shared // by a container with the pod it is associated with const Pod = "pod" @@ -205,3 +210,16 @@ func IsValidDeviceMode(mode string) bool { } return true } + +// validateDeviceCgroupRule validates the format of deviceCgroupRule +func validateDeviceCgroupRule(deviceCgroupRule string) error { + if !deviceCgroupRuleRegex.MatchString(deviceCgroupRule) { + return errors.Errorf("invalid device cgroup rule format: '%s'", deviceCgroupRule) + } + return nil +} + +// parseDeviceCgroupRule matches and parses the deviceCgroupRule into slice +func parseDeviceCgroupRule(deviceCgroupRule string) [][]string { + return deviceCgroupRuleRegex.FindAllStringSubmatch(deviceCgroupRule, -1) +} diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index cae055bb0..b2a152a2d 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -232,6 +232,12 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM return nil, err } } + if len(config.Resources.DeviceCgroupRules) != 0 { + if err := deviceCgroupRules(&g, config.Resources.DeviceCgroupRules); err != nil { + return nil, err + } + addedResources = true + } } // SECURITY OPTS |