diff options
Diffstat (limited to 'pkg/api/handlers')
51 files changed, 2574 insertions, 1284 deletions
diff --git a/pkg/api/handlers/compat/container_start.go b/pkg/api/handlers/compat/container_start.go new file mode 100644 index 000000000..d26ef2c82 --- /dev/null +++ b/pkg/api/handlers/compat/container_start.go @@ -0,0 +1,60 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func StopContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + // /{version}/containers/(name)/stop + query := struct { + Timeout int `schema:"t"` + }{ + // override any golang type defaults + } + 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 + } + + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + state, err := con.State() + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "unable to get state for Container %s", name)) + return + } + // If the Container is stopped already, send a 304 + if state == define.ContainerStateStopped || state == define.ContainerStateExited { + utils.WriteResponse(w, http.StatusNotModified, "") + return + } + + var stopError error + if query.Timeout > 0 { + stopError = con.StopWithTimeout(uint(query.Timeout)) + } else { + stopError = con.Stop() + } + if stopError != nil { + utils.InternalServerError(w, errors.Wrapf(stopError, "failed to stop %s", name)) + return + } + + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/generic/containers.go b/pkg/api/handlers/compat/containers.go index 8dc73ae14..2ce113d30 100644 --- a/pkg/api/handlers/generic/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "encoding/binary" @@ -10,11 +10,12 @@ import ( "time" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/signal" "github.com/containers/libpod/pkg/util" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -35,12 +36,26 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - if query.Link { + + if query.Link && !utils.IsLibpodRequest(r) { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, utils.ErrLinkNotSupport) return } - utils.RemoveContainer(w, r, query.Force, query.Vols) + + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + if err := runtime.RemoveContainer(r.Context(), con, query.Force, query.Vols); err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") } func ListContainers(w http.ResponseWriter, r *http.Request) { @@ -58,6 +73,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { }{ // override any golang type defaults } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return @@ -71,7 +87,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - if _, found := mux.Vars(r)["limit"]; found { + if _, found := r.URL.Query()["limit"]; found && query.Limit != -1 { last := query.Limit if len(containers) > last { containers = containers[len(containers)-last:] @@ -86,7 +102,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { var list = make([]*handlers.Container, len(containers)) for i, ctnr := range containers { - api, err := handlers.LibpodToContainer(ctnr, infoData) + api, err := handlers.LibpodToContainer(ctnr, infoData, query.Size) if err != nil { utils.InternalServerError(w, err) return @@ -98,14 +114,25 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { func GetContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Size bool `schema:"size"` + }{ + // override any golang type defaults + } - name := mux.Vars(r)["name"] + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + name := utils.GetName(r) ctnr, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) return } - api, err := handlers.LibpodToContainerJSON(ctnr) + api, err := handlers.LibpodToContainerJSON(ctnr, query.Size) if err != nil { utils.InternalServerError(w, err) return @@ -115,18 +142,57 @@ func GetContainer(w http.ResponseWriter, r *http.Request) { func KillContainer(w http.ResponseWriter, r *http.Request) { // /{version}/containers/(name)/kill - con, err := utils.KillContainer(w, r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Signal string `schema:"signal"` + }{ + Signal: "KILL", + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + sig, err := signal.ParseSignalNameOrNumber(query.Signal) + if err != nil { + utils.InternalServerError(w, err) + return + } + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) if err != nil { + utils.ContainerNotFound(w, name, err) return } - // the kill behavior for docker differs from podman in that they appear to wait - // for the Container to croak so the exit code is accurate immediately after the - // kill is sent. libpod does not. but we can add a wait here only for the docker - // side of things and mimic that behavior - if _, err = con.Wait(); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "failed to wait for Container %s", con.ID())) + + state, err := con.State() + if err != nil { + utils.InternalServerError(w, err) return } + + // If the Container is stopped already, send a 409 + if state == define.ContainerStateStopped || state == define.ContainerStateExited { + utils.Error(w, fmt.Sprintf("Container %s is not running", name), http.StatusConflict, errors.New(fmt.Sprintf("Cannot kill Container %s, it is not running", name))) + return + } + + err = con.Kill(uint(sig)) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "unable to kill Container %s", name)) + } + + if utils.IsLibpodRequest(r) { + // the kill behavior for docker differs from podman in that they appear to wait + // for the Container to croak so the exit code is accurate immediately after the + // kill is sent. libpod does not. but we can add a wait here only for the docker + // side of things and mimic that behavior + if _, err = con.Wait(); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "failed to wait for Container %s", con.ID())) + return + } + } // Success utils.WriteResponse(w, http.StatusNoContent, "") } @@ -136,7 +202,7 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) { // /{version}/containers/(name)/wait exitCode, err := utils.WaitContainer(w, r) if err != nil { - msg = err.Error() + return } utils.WriteResponse(w, http.StatusOK, handlers.ContainerWaitOKBody{ StatusCode: int(exitCode), @@ -174,7 +240,7 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { return } - name := mux.Vars(r)["name"] + name := utils.GetName(r) ctnr, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) @@ -191,7 +257,7 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { } var since time.Time - if _, found := mux.Vars(r)["since"]; found { + if _, found := r.URL.Query()["since"]; found { since, err = util.ParseInputTime(query.Since) if err != nil { utils.BadRequest(w, "since", query.Since, err) @@ -200,7 +266,7 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { } var until time.Time - if _, found := mux.Vars(r)["until"]; found { + if _, found := r.URL.Query()["until"]; found { since, err = util.ParseInputTime(query.Until) if err != nil { utils.BadRequest(w, "until", query.Until, err) @@ -233,7 +299,7 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { var builder strings.Builder for ok := true; ok; ok = query.Follow { for line := range logChannel { - if _, found := mux.Vars(r)["until"]; found { + if _, found := r.URL.Query()["until"]; found { if line.Time.After(until) { break } @@ -266,7 +332,6 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { builder.WriteRune(' ') } builder.WriteString(line.Msg) - // Build header and output entry binary.BigEndian.PutUint32(header[4:], uint32(len(header)+builder.Len())) if _, err := w.Write(header[:]); err != nil { @@ -275,7 +340,6 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { if _, err := fmt.Fprint(w, builder.String()); err != nil { log.Errorf("unable to write builder string: %q", err) } - if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/pkg/api/handlers/containers_attach.go b/pkg/api/handlers/compat/containers_attach.go index eb306348b..da7b5bb0c 100644 --- a/pkg/api/handlers/containers_attach.go +++ b/pkg/api/handlers/compat/containers_attach.go @@ -1,4 +1,4 @@ -package handlers +package compat import ( "net/http" @@ -6,7 +6,6 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -30,12 +29,10 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } - muxVars := mux.Vars(r) - // Detach keys: explicitly set to "" is very different from unset // TODO: Our format for parsing these may be different from Docker. var detachKeys *string - if _, found := muxVars["detachKeys"]; found { + if _, found := r.URL.Query()["detachKeys"]; found { detachKeys = &query.DetachKeys } @@ -44,15 +41,15 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { streams.Stderr = true streams.Stdin = true useStreams := false - if _, found := muxVars["stdin"]; found { + if _, found := r.URL.Query()["stdin"]; found { streams.Stdin = query.Stdin useStreams = true } - if _, found := muxVars["stdout"]; found { + if _, found := r.URL.Query()["stdout"]; found { streams.Stdout = query.Stdout useStreams = true } - if _, found := muxVars["stderr"]; found { + if _, found := r.URL.Query()["stderr"]; found { streams.Stderr = query.Stderr useStreams = true } @@ -72,12 +69,12 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } // We only support stream=true or unset - if _, found := muxVars["stream"]; found && query.Stream { + if _, found := r.URL.Query()["stream"]; found && query.Stream { utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("the stream parameter to attach is not presently supported")) return } - name := getName(r) + name := utils.GetName(r) ctr, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) @@ -138,7 +135,7 @@ func ResizeContainer(w http.ResponseWriter, r *http.Request) { return } - name := getName(r) + name := utils.GetName(r) ctr, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) diff --git a/pkg/api/handlers/generic/containers_create.go b/pkg/api/handlers/compat/containers_create.go index edefd5757..12af40876 100644 --- a/pkg/api/handlers/generic/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "encoding/json" @@ -6,19 +6,17 @@ import ( "net/http" "strings" - "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/common/pkg/config" "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" createconfig "github.com/containers/libpod/pkg/spec" "github.com/containers/storage" - "github.com/docker/docker/pkg/signal" "github.com/gorilla/schema" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) @@ -48,39 +46,21 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "NewFromLocal()")) return } - cc, err := makeCreateConfig(input, newImage) + defaultContainerConfig, err := runtime.GetConfig() if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "makeCreatConfig()")) + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "GetConfig()")) return } - - cc.Name = query.Name - var pod *libpod.Pod - ctr, err := shared.CreateContainerFromCreateConfig(runtime, &cc, r.Context(), pod) + cc, err := makeCreateConfig(defaultContainerConfig, input, newImage) 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()")) + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "makeCreatConfig()")) return } - - response := ContainerCreateResponse{ - Id: ctr.ID(), - Warnings: []string{}} - - utils.WriteResponse(w, http.StatusCreated, response) + cc.Name = query.Name + utils.CreateContainer(r.Context(), w, runtime, &cc) } -func makeCreateConfig(input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { +func makeCreateConfig(defaultContainerConfig *config.Config, input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { var ( err error init bool @@ -101,7 +81,7 @@ func makeCreateConfig(input handlers.CreateContainerConfig, newImage *image2.Ima workDir = input.WorkingDir } - stopTimeout := uint(define.CtrRemoveTimeout) + stopTimeout := defaultContainerConfig.Engine.StopTimeout if input.StopTimeout != nil { stopTimeout = uint(*input.StopTimeout) } diff --git a/pkg/api/handlers/compat/containers_pause.go b/pkg/api/handlers/compat/containers_pause.go new file mode 100644 index 000000000..060bdbaeb --- /dev/null +++ b/pkg/api/handlers/compat/containers_pause.go @@ -0,0 +1,28 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" +) + +func PauseContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + // /{version}/containers/(name)/pause + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + // the api does not error if the Container is already paused, so just into it + if err := con.Pause(); err != nil { + utils.InternalServerError(w, err) + return + } + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/compat/containers_prune.go b/pkg/api/handlers/compat/containers_prune.go new file mode 100644 index 000000000..a56c3903d --- /dev/null +++ b/pkg/api/handlers/compat/containers_prune.go @@ -0,0 +1,64 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/docker/docker/api/types" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func PruneContainers(w http.ResponseWriter, r *http.Request) { + var ( + delContainers []string + space int64 + ) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + Filters map[string][]string `schema:"filters"` + }{} + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + filterFuncs, err := utils.GenerateFilterFuncsFromMap(runtime, query.Filters) + if err != nil { + utils.InternalServerError(w, err) + return + } + prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) + if err != nil { + utils.InternalServerError(w, err) + return + } + + // Libpod response differs + if utils.IsLibpodRequest(r) { + var response []handlers.LibpodContainersPruneReport + for ctrID, size := range prunedContainers { + response = append(response, handlers.LibpodContainersPruneReport{ID: ctrID, SpaceReclaimed: size}) + } + for ctrID, err := range pruneErrors { + response = append(response, handlers.LibpodContainersPruneReport{ID: ctrID, PruneError: err.Error()}) + } + utils.WriteResponse(w, http.StatusOK, response) + return + } + for ctrID, size := range prunedContainers { + if pruneErrors[ctrID] == nil { + space += size + delContainers = append(delContainers, ctrID) + } + } + report := types.ContainersPruneReport{ + ContainersDeleted: delContainers, + SpaceReclaimed: uint64(space), + } + utils.WriteResponse(w, http.StatusOK, report) +} diff --git a/pkg/api/handlers/compat/containers_restart.go b/pkg/api/handlers/compat/containers_restart.go new file mode 100644 index 000000000..343bf96d2 --- /dev/null +++ b/pkg/api/handlers/compat/containers_restart.go @@ -0,0 +1,45 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func RestartContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + // /{version}/containers/(name)/restart + query := struct { + Timeout int `schema:"t"` + }{ + // Override golang default values for types + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.BadRequest(w, "url", r.URL.String(), errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + timeout := con.StopTimeout() + if _, found := r.URL.Query()["t"]; found { + timeout = uint(query.Timeout) + } + + if err := con.RestartWithTimeout(r.Context(), timeout); err != nil { + utils.InternalServerError(w, err) + return + } + + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/compat/containers_start.go b/pkg/api/handlers/compat/containers_start.go new file mode 100644 index 000000000..67bd287ab --- /dev/null +++ b/pkg/api/handlers/compat/containers_start.go @@ -0,0 +1,51 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func StartContainer(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + DetachKeys string `schema:"detachKeys"` + }{ + // Override golang default values for types + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.BadRequest(w, "url", r.URL.String(), err) + return + } + if len(query.DetachKeys) > 0 { + // TODO - start does not support adding detach keys + utils.BadRequest(w, "detachKeys", query.DetachKeys, errors.New("the detachKeys parameter is not supported yet")) + return + } + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + state, err := con.State() + if err != nil { + utils.InternalServerError(w, err) + return + } + if state == define.ContainerStateRunning { + utils.WriteResponse(w, http.StatusNotModified, "") + return + } + if err := con.Start(r.Context(), false); err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/generic/containers_stats.go b/pkg/api/handlers/compat/containers_stats.go index e33d37606..53ad0a632 100644 --- a/pkg/api/handlers/generic/containers_stats.go +++ b/pkg/api/handlers/compat/containers_stats.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "encoding/json" @@ -7,11 +7,9 @@ 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" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -20,9 +18,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) @@ -36,7 +31,7 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { return } - name := mux.Vars(r)["name"] + name := utils.GetName(r) ctnr, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) @@ -62,17 +57,19 @@ 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() - preCPUStats = docker.CPUStats{ + systemUsage, _ := cgroups.GetSystemCPUUsage() + preCPUStats = CPUStats{ CPUUsage: docker.CPUUsage{ TotalUsage: stats.CPUNano, - PercpuUsage: []uint64{uint64(stats.CPU)}, - UsageInKernelmode: 0, - UsageInUsermode: 0, + PercpuUsage: stats.PerCPU, + UsageInKernelmode: stats.CPUSystemNano, + UsageInUsermode: stats.CPUNano - stats.CPUSystemNano, }, - SystemUsage: 0, + CPU: stats.CPU, + SystemUsage: systemUsage, OnlineCPUs: 0, ThrottlingData: docker.ThrottlingData{}, } @@ -126,8 +123,9 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { InstanceID: "", } - s := handlers.Stats{StatsJSON: docker.StatsJSON{ - Stats: docker.Stats{ + systemUsage, _ := cgroups.GetSystemCPUUsage() + s := StatsJSON{ + Stats: Stats{ Read: time.Now(), PreRead: preRead, PidsStats: docker.PidsStats{ @@ -144,14 +142,15 @@ 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: []uint64{uint64(stats.CPU)}, + PercpuUsage: cgroupStat.CPU.Usage.PerCPU, UsageInKernelmode: cgroupStat.CPU.Usage.Kernel, UsageInUsermode: cgroupStat.CPU.Usage.Total - cgroupStat.CPU.Usage.Kernel, }, - SystemUsage: 0, + CPU: stats.CPU, + SystemUsage: systemUsage, OnlineCPUs: uint32(len(cgroupStat.CPU.Usage.PerCPU)), ThrottlingData: docker.ThrottlingData{ Periods: 0, @@ -174,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/containers_top.go b/pkg/api/handlers/compat/containers_top.go index 6b7688eb0..202be55d1 100644 --- a/pkg/api/handlers/containers_top.go +++ b/pkg/api/handlers/compat/containers_top.go @@ -1,12 +1,12 @@ -package handlers +package compat import ( "net/http" "strings" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -30,7 +30,7 @@ func TopContainer(w http.ResponseWriter, r *http.Request) { return } - name := mux.Vars(r)["name"] + name := utils.GetName(r) c, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) @@ -43,7 +43,7 @@ func TopContainer(w http.ResponseWriter, r *http.Request) { return } - var body = ContainerTopOKBody{} + var body = handlers.ContainerTopOKBody{} if len(output) > 0 { body.Titles = strings.Split(output[0], "\t") for _, line := range output[1:] { diff --git a/pkg/api/handlers/compat/containers_unpause.go b/pkg/api/handlers/compat/containers_unpause.go new file mode 100644 index 000000000..adabdeaea --- /dev/null +++ b/pkg/api/handlers/compat/containers_unpause.go @@ -0,0 +1,28 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" +) + +func UnpauseContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + // /{version}/containers/(name)/unpause + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + if err := con.Unpause(); err != nil { + utils.InternalServerError(w, err) + return + } + + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/compat/events.go b/pkg/api/handlers/compat/events.go new file mode 100644 index 000000000..0f72ef328 --- /dev/null +++ b/pkg/api/handlers/compat/events.go @@ -0,0 +1,68 @@ +package compat + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/events" + "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 GetEvents(w http.ResponseWriter, r *http.Request) { + var ( + fromStart bool + eventsError error + decoder = r.Context().Value("decoder").(*schema.Decoder) + runtime = r.Context().Value("runtime").(*libpod.Runtime) + ) + + query := struct { + Since string `schema:"since"` + Until string `schema:"until"` + Filters map[string][]string `schema:"filters"` + }{} + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Failed to parse parameters", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + } + + var libpodFilters = []string{} + if _, found := r.URL.Query()["filters"]; found { + for k, v := range query.Filters { + libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) + } + } + + 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 = runtime.Events(readOpts) + }() + if eventsError != nil { + utils.InternalServerError(w, eventsError) + return + } + + coder := json.NewEncoder(w) + coder.SetEscapeHTML(true) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + for event := range eventChannel { + e := handlers.EventToApiEvent(event) + if err := coder.Encode(e); err != nil { + logrus.Errorf("unable to write json: %q", err) + } + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + } +} diff --git a/pkg/api/handlers/compat/exec.go b/pkg/api/handlers/compat/exec.go new file mode 100644 index 000000000..ec1a8ac96 --- /dev/null +++ b/pkg/api/handlers/compat/exec.go @@ -0,0 +1,107 @@ +package compat + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "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/mux" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ExecCreateHandler creates an exec session for a given container. +func ExecCreateHandler(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + input := new(handlers.ExecCreateConfig) + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error decoding request body as JSON")) + return + } + + ctrName := utils.GetName(r) + ctr, err := runtime.LookupContainer(ctrName) + if err != nil { + utils.ContainerNotFound(w, ctrName, err) + return + } + + libpodConfig := new(libpod.ExecConfig) + libpodConfig.Command = input.Cmd + libpodConfig.Terminal = input.Tty + libpodConfig.AttachStdin = input.AttachStdin + libpodConfig.AttachStderr = input.AttachStderr + libpodConfig.AttachStdout = input.AttachStdout + if input.DetachKeys != "" { + libpodConfig.DetachKeys = &input.DetachKeys + } + libpodConfig.Environment = make(map[string]string) + for _, envStr := range input.Env { + split := strings.SplitN(envStr, "=", 2) + if len(split) != 2 { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, errors.Errorf("environment variable %q badly formed, must be key=value", envStr)) + return + } + libpodConfig.Environment[split[0]] = split[1] + } + libpodConfig.WorkDir = input.WorkingDir + libpodConfig.Privileged = input.Privileged + libpodConfig.User = input.User + + sessID, err := ctr.ExecCreate(libpodConfig) + if err != nil { + if errors.Cause(err) == define.ErrCtrStateInvalid { + // Check if the container is paused. If so, return a 409 + state, err := ctr.State() + if err == nil { + // Ignore the error != nil case. We're already + // throwing an InternalServerError below. + if state == define.ContainerStatePaused { + utils.Error(w, "Container is paused", http.StatusConflict, errors.Errorf("cannot create exec session as container %s is paused", ctr.ID())) + return + } + } + } + utils.InternalServerError(w, err) + return + } + + resp := new(handlers.ExecCreateResponse) + resp.ID = sessID + + utils.WriteResponse(w, http.StatusCreated, resp) +} + +// ExecInspectHandler inspects a given exec session. +func ExecInspectHandler(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + sessionID := mux.Vars(r)["id"] + sessionCtr, err := runtime.GetExecSessionContainer(sessionID) + if err != nil { + utils.Error(w, fmt.Sprintf("No such exec session: %s", sessionID), http.StatusNotFound, err) + return + } + + logrus.Debugf("Inspecting exec session %s of container %s", sessionID, sessionCtr.ID()) + + session, err := sessionCtr.ExecSession(sessionID) + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error retrieving exec session %s from container %s", sessionID, sessionCtr.ID())) + return + } + + inspectOut, err := session.Inspect() + if err != nil { + utils.InternalServerError(w, err) + return + } + + utils.WriteResponse(w, http.StatusOK, inspectOut) +} diff --git a/pkg/api/handlers/generic/images.go b/pkg/api/handlers/compat/images.go index 93adb7f69..ea9cbd691 100644 --- a/pkg/api/handlers/generic/images.go +++ b/pkg/api/handlers/compat/images.go @@ -1,12 +1,12 @@ -package generic +package compat import ( "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "os" - "strconv" "strings" "github.com/containers/buildah" @@ -15,13 +15,11 @@ import ( 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/domain/entities" "github.com/containers/libpod/pkg/util" - "github.com/containers/storage" "github.com/docker/docker/api/types" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) func ExportImage(w http.ResponseWriter, r *http.Request) { @@ -29,7 +27,7 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { // 500 server runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) newImage, err := runtime.ImageRuntime().NewFromLocal(name) if err != nil { utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) @@ -59,16 +57,14 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { } func PruneImages(w http.ResponseWriter, r *http.Request) { - // 200 no error - // 500 internal var ( - dangling = true - err error + filters []string ) decoder := r.Context().Value("decoder").(*schema.Decoder) runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { + All bool Filters map[string][]string `schema:"filters"` }{ // This is where you can override the golang default value for one of fields @@ -79,60 +75,24 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { return } - // until ts is not supported on podman prune - if v, found := query.Filters["until"]; found { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "until=%s is not supported yet", v)) - return - } - // labels are not supported on podman prune - if _, found := query.Filters["since"]; found { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "labelis not supported yet")) - return - } - - if v, found := query.Filters["dangling"]; found { - dangling, err = strconv.ParseBool(v[0]) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "processing dangling filter")) - return + idr := []types.ImageDeleteResponseItem{} + for k, v := range query.Filters { + for _, val := range v { + filters = append(filters, fmt.Sprintf("%s=%s", k, val)) } } - - idr := []types.ImageDeleteResponseItem{} - // - // This code needs to be migrated to libpod to work correctly. I could not - // work my around the information docker needs with the existing prune in libpod. - // - pruneImages, err := runtime.ImageRuntime().GetPruneImages(!dangling, []image2.ImageFilter{}) + pruneCids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, filters) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to get images to prune")) + utils.InternalServerError(w, err) return } - for _, p := range pruneImages { - repotags, err := p.RepoTags() - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to get repotags for image")) - return - } - if err := p.Remove(r.Context(), true); err != nil { - if errors.Cause(err) == storage.ErrImageUsedByContainer { - logrus.Warnf("Failed to prune image %s as it is in use: %v", p.ID(), err) - continue - } - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to prune image")) - return - } - // newimageevent is not export therefore we cannot record the event. this will be fixed - // when the prune is fixed in libpod - // defer p.newImageEvent(events.Prune) - response := types.ImageDeleteResponseItem{ - Deleted: fmt.Sprintf("sha256:%s", p.ID()), // I ack this is not ideal - } - if len(repotags) > 0 { - response.Untagged = repotags[0] - } - idr = append(idr, response) + for _, p := range pruneCids { + idr = append(idr, types.ImageDeleteResponseItem{ + Deleted: p, + }) } + + //FIXME/TODO to do this exactly correct, pruneimages needs to return idrs and space-reclaimed, then we are golden ipr := types.ImagesPruneReport{ ImagesDeleted: idr, SpaceReclaimed: 1, // TODO we cannot supply this right now @@ -148,14 +108,14 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { - author string - changes string - comment string - container string + Author string `schema:"author"` + Changes string `schema:"changes"` + Comment string `schema:"comment"` + Container string `schema:"container"` //fromSrc string # fromSrc is currently unused - pause bool - repo string - tag string + Pause bool `schema:"pause"` + Repo string `schema:"repo"` + Tag string `schema:"tag"` }{ // This is where you can override the golang default value for one of fields } @@ -169,13 +129,13 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) return } - sc := image2.GetSystemContext(rtc.SignaturePolicyPath, "", false) + sc := image2.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) tag := "latest" options := libpod.ContainerCommitOptions{ Pause: true, } options.CommitOptions = buildah.CommitOptions{ - SignaturePolicyPath: rtc.SignaturePolicyPath, + SignaturePolicyPath: rtc.Engine.SignaturePolicyPath, ReportWriter: os.Stderr, SystemContext: sc, PreferredManifestType: manifest.DockerV2Schema2MediaType, @@ -187,22 +147,22 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { return } - if len(query.tag) > 0 { - tag = query.tag - } - options.Message = query.comment - options.Author = query.author - options.Pause = query.pause - options.Changes = strings.Fields(query.changes) - ctr, err := runtime.LookupContainer(query.container) + if len(query.Tag) > 0 { + tag = query.Tag + } + options.Message = query.Comment + options.Author = query.Author + options.Pause = query.Pause + options.Changes = strings.Fields(query.Changes) + ctr, err := runtime.LookupContainer(query.Container) if err != nil { utils.Error(w, "Something went wrong.", http.StatusNotFound, err) return } // I know mitr hates this ... but doing for now - if len(query.repo) > 1 { - destImage = fmt.Sprintf("%s:%s", query.repo, tag) + if len(query.Repo) > 1 { + destImage = fmt.Sprintf("%s:%s", query.Repo, tag) } commitImage, err := ctr.Commit(r.Context(), destImage, options) @@ -221,8 +181,8 @@ func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { - fromSrc string - changes []string + FromSrc string `schema:"fromSrc"` + Changes []string `schema:"changes"` }{ // This is where you can override the golang default value for one of fields } @@ -232,7 +192,7 @@ func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { return } // fromSrc – Source to import. The value may be a URL from which the image can be retrieved or - to read the image from the request body. This parameter may only be used when importing an image. - source := query.fromSrc + source := query.FromSrc if source == "-" { f, err := ioutil.TempFile("", "api_load.tar") if err != nil { @@ -240,11 +200,11 @@ func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { return } source = f.Name() - if err := handlers.SaveFromBody(f, r); err != nil { + if err := SaveFromBody(f, r); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) } } - iid, err := runtime.Import(r.Context(), source, "", query.changes, "", false) + iid, err := runtime.Import(r.Context(), source, "", query.Changes, "", false) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to import tarball")) return @@ -280,8 +240,8 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { - fromImage string - tag string + FromImage string `schema:"fromImage"` + Tag string `schema:"tag"` }{ // This is where you can override the golang default value for one of fields } @@ -296,9 +256,9 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { repo – Repository name given to an image when it is imported. The repo may include a tag. This parameter may only be used when importing an image. 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 { - fromImage = fmt.Sprintf("%s:%s", fromImage, query.tag) + fromImage := query.FromImage + if len(query.Tag) >= 1 { + fromImage = fmt.Sprintf("%s:%s", fromImage, query.Tag) } // TODO @@ -327,8 +287,8 @@ func GetImage(w http.ResponseWriter, r *http.Request) { // 200 no error // 404 no such // 500 internal - name := mux.Vars(r)["name"] - newImage, err := handlers.GetImage(r, name) + name := utils.GetName(r) + newImage, err := utils.GetImage(r, name) if err != nil { utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) return @@ -342,14 +302,12 @@ func GetImage(w http.ResponseWriter, r *http.Request) { } func GetImages(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 500 internal images, err := utils.GetImages(w, r) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed get images")) return } - var summaries = make([]*handlers.ImageSummary, len(images)+1) + var summaries = make([]*entities.ImageSummary, len(images)) for j, img := range images { is, err := handlers.ImageToImageSummary(img) if err != nil { @@ -360,3 +318,47 @@ func GetImages(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, summaries) } + +func LoadImages(w http.ResponseWriter, r *http.Request) { + // TODO this is basically wrong + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + Changes map[string]string `json:"changes"` + Message string `json:"message"` + Quiet bool `json:"quiet"` + }{ + // This is where you can override the golang default value for one of fields + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + var ( + err error + writer io.Writer + ) + f, err := ioutil.TempFile("", "api_load.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) + return + } + if err := SaveFromBody(f, r); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) + return + } + id, err := runtime.LoadImage(r.Context(), "", f.Name(), writer, "") + //id, err := runtime.Import(r.Context()) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to load image")) + return + } + utils.WriteResponse(w, http.StatusOK, struct { + Stream string `json:"stream"` + }{ + Stream: fmt.Sprintf("Loaded image: %s\n", id), + }) +} diff --git a/pkg/api/handlers/images_build.go b/pkg/api/handlers/compat/images_build.go index b29c45574..e208e6ddc 100644 --- a/pkg/api/handlers/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -1,4 +1,4 @@ -package handlers +package compat import ( "bytes" @@ -15,13 +15,15 @@ import ( "github.com/containers/buildah" "github.com/containers/buildah/imagebuildah" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/storage/pkg/archive" - "github.com/gorilla/mux" + "github.com/gorilla/schema" ) func BuildImage(w http.ResponseWriter, r *http.Request) { - authConfigs := map[string]AuthConfig{} + authConfigs := map[string]handlers.AuthConfig{} if hdr, found := r.Header["X-Registry-Config"]; found && len(hdr) > 0 { authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(hdr[0])) if json.NewDecoder(authConfigsJSON).Decode(&authConfigs) != nil { @@ -97,8 +99,8 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Outputs: "", Registry: "docker.io", } - - if err := decodeQuery(r, &query); err != nil { + decoder := r.Context().Value("decoder").(*schema.Decoder) + if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) return } @@ -114,24 +116,24 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { tag = tokens[1] } - if t, found := mux.Vars(r)["target"]; found { - name = t + if _, found := r.URL.Query()["target"]; found { + name = query.Target } var buildArgs = map[string]string{} - if a, found := mux.Vars(r)["buildargs"]; found { - if err := json.Unmarshal([]byte(a), &buildArgs); err != nil { - utils.BadRequest(w, "buildargs", a, err) + if _, found := r.URL.Query()["buildargs"]; found { + if err := json.Unmarshal([]byte(query.BuildArgs), &buildArgs); err != nil { + utils.BadRequest(w, "buildargs", query.BuildArgs, err) return } } // convert label formats var labels = []string{} - if l, found := mux.Vars(r)["labels"]; found { + if _, found := r.URL.Query()["labels"]; found { var m = map[string]string{} - if err := json.Unmarshal([]byte(l), &m); err != nil { - utils.BadRequest(w, "labels", l, err) + if err := json.Unmarshal([]byte(query.Labels), &m); err != nil { + utils.BadRequest(w, "labels", query.Labels, err) return } @@ -141,7 +143,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { } pullPolicy := buildah.PullIfMissing - if _, found := mux.Vars(r)["pull"]; found { + if _, found := r.URL.Query()["pull"]; found { if query.Pull { pullPolicy = buildah.PullAlways } @@ -220,7 +222,8 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Devices: nil, } - id, _, err := getRuntime(r).Build(r.Context(), buildOptions, query.Dockerfile) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + id, _, err := runtime.Build(r.Context(), buildOptions, query.Dockerfile) if err != nil { utils.InternalServerError(w, err) } diff --git a/pkg/api/handlers/compat/images_history.go b/pkg/api/handlers/compat/images_history.go new file mode 100644 index 000000000..afadf4c48 --- /dev/null +++ b/pkg/api/handlers/compat/images_history.go @@ -0,0 +1,40 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/pkg/errors" +) + +func HistoryImage(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + var allHistory []handlers.HistoryResponse + + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) + return + + } + history, err := newImage.History(r.Context()) + if err != nil { + utils.InternalServerError(w, err) + return + } + for _, h := range history { + l := handlers.HistoryResponse{ + ID: h.ID, + Created: h.Created.Unix(), + CreatedBy: h.CreatedBy, + Tags: h.Tags, + Size: h.Size, + Comment: h.Comment, + } + allHistory = append(allHistory, l) + } + utils.WriteResponse(w, http.StatusOK, allHistory) +} diff --git a/pkg/api/handlers/compat/images_remove.go b/pkg/api/handlers/compat/images_remove.go new file mode 100644 index 000000000..ed0153529 --- /dev/null +++ b/pkg/api/handlers/compat/images_remove.go @@ -0,0 +1,58 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func RemoveImage(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + Force bool `schema:"force"` + NoPrune bool `schema:"noprune"` + }{ + // This is where you can override the golang default value for one of fields + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + if _, found := r.URL.Query()["noprune"]; found { + if query.NoPrune { + utils.UnSupportedParameter("noprune") + } + } + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + + results, err := runtime.RemoveImage(r.Context(), newImage, query.Force) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + + response := make([]map[string]string, 0, len(results.Untagged)+1) + deleted := make(map[string]string, 1) + deleted["Deleted"] = results.Deleted + response = append(response, deleted) + + for _, u := range results.Untagged { + untagged := make(map[string]string, 1) + untagged["Untagged"] = u + response = append(response, untagged) + } + + utils.WriteResponse(w, http.StatusOK, response) + +} diff --git a/pkg/api/handlers/compat/images_save.go b/pkg/api/handlers/compat/images_save.go new file mode 100644 index 000000000..b39c719a0 --- /dev/null +++ b/pkg/api/handlers/compat/images_save.go @@ -0,0 +1,14 @@ +package compat + +import ( + "io" + "net/http" + "os" +) + +func SaveFromBody(f *os.File, r *http.Request) error { // nolint + if _, err := io.Copy(f, r.Body); err != nil { + return err + } + return f.Close() +} diff --git a/pkg/api/handlers/compat/images_search.go b/pkg/api/handlers/compat/images_search.go new file mode 100644 index 000000000..7283b22c4 --- /dev/null +++ b/pkg/api/handlers/compat/images_search.go @@ -0,0 +1,66 @@ +package compat + +import ( + "net/http" + "strconv" + + "github.com/containers/image/v5/types" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func SearchImages(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Term string `json:"term"` + Limit int `json:"limit"` + Filters map[string][]string `json:"filters"` + }{ + // This is where you can override the golang default value for one of fields + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + filter := image.SearchFilter{} + if len(query.Filters) > 0 { + if len(query.Filters["stars"]) > 0 { + stars, err := strconv.Atoi(query.Filters["stars"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + filter.Stars = stars + } + if len(query.Filters["is-official"]) > 0 { + isOfficial, err := strconv.ParseBool(query.Filters["is-official"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + filter.IsOfficial = types.NewOptionalBool(isOfficial) + } + if len(query.Filters["is-automated"]) > 0 { + isAutomated, err := strconv.ParseBool(query.Filters["is-automated"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + filter.IsAutomated = types.NewOptionalBool(isAutomated) + } + } + options := image.SearchOptions{ + Filter: filter, + Limit: query.Limit, + } + results, err := image.SearchImages(query.Term, options) + if err != nil { + utils.BadRequest(w, "term", query.Term, err) + return + } + utils.WriteResponse(w, http.StatusOK, results) +} diff --git a/pkg/api/handlers/compat/images_tag.go b/pkg/api/handlers/compat/images_tag.go new file mode 100644 index 000000000..722be5653 --- /dev/null +++ b/pkg/api/handlers/compat/images_tag.go @@ -0,0 +1,37 @@ +package compat + +import ( + "fmt" + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/pkg/errors" +) + +func TagImage(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + // /v1.xx/images/(name)/tag + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + tag := "latest" + if len(r.Form.Get("tag")) > 0 { + tag = r.Form.Get("tag") + } + if len(r.Form.Get("repo")) < 1 { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.New("repo parameter is required to tag an image")) + return + } + repo := r.Form.Get("repo") + tagName := fmt.Sprintf("%s:%s", repo, tag) + if err := newImage.TagImage(tagName); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusCreated, "") +} diff --git a/pkg/api/handlers/generic/info.go b/pkg/api/handlers/compat/info.go index c9e79233d..104d0793b 100644 --- a/pkg/api/handlers/generic/info.go +++ b/pkg/api/handlers/compat/info.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "fmt" @@ -9,8 +9,8 @@ import ( "strings" "time" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/config" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" @@ -60,7 +60,7 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { CPUCfsQuota: sysInfo.CPUCfsQuota, CPUSet: sysInfo.Cpuset, CPUShares: sysInfo.CPUShares, - CgroupDriver: configInfo.CgroupManager, + CgroupDriver: configInfo.Engine.CgroupManager, ClusterAdvertise: "", ClusterStore: "", ContainerdCommit: docker.Commit{}, @@ -69,7 +69,7 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { ContainersRunning: stateInfo[define.ContainerStateRunning], ContainersStopped: stateInfo[define.ContainerStateStopped] + stateInfo[define.ContainerStateExited], Debug: log.IsLevelEnabled(log.DebugLevel), - DefaultRuntime: configInfo.OCIRuntime, + DefaultRuntime: configInfo.Engine.OCIRuntime, DockerRootDir: storeInfo["GraphRoot"].(string), Driver: storeInfo["GraphDriverName"].(string), DriverStatus: getGraphStatus(storeInfo), @@ -152,7 +152,7 @@ func getSecOpts(sysInfo *sysinfo.SysInfo) []string { func getRuntimes(configInfo *config.Config) map[string]docker.Runtime { var runtimes = map[string]docker.Runtime{} - for name, paths := range configInfo.OCIRuntimes { + for name, paths := range configInfo.Engine.OCIRuntimes { runtimes[name] = docker.Runtime{ Path: paths[0], Args: nil, diff --git a/pkg/api/handlers/compat/ping.go b/pkg/api/handlers/compat/ping.go new file mode 100644 index 000000000..6e77e270f --- /dev/null +++ b/pkg/api/handlers/compat/ping.go @@ -0,0 +1,31 @@ +package compat + +import ( + "fmt" + "net/http" + + "github.com/containers/buildah" + "github.com/containers/libpod/pkg/api/handlers" +) + +// Ping returns headers to client about the service +// +// This handler must always be the same for the compatibility and libpod URL trees! +// Clients will use the Header availability to test which backend engine is in use. +func Ping(w http.ResponseWriter, r *http.Request) { + w.Header().Set("API-Version", handlers.DefaultApiVersion) + w.Header().Set("BuildKit-Version", "") + w.Header().Set("Docker-Experimental", "true") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Pragma", "no-cache") + + // API-Version and Libpod-API-Version may not always be equal + w.Header().Set("Libpod-API-Version", handlers.DefaultApiVersion) + w.Header().Set("Libpod-Buildha-Version", buildah.Version) + w.WriteHeader(http.StatusOK) + + if r.Method == http.MethodGet { + fmt.Fprint(w, "OK") + } + fmt.Fprint(w, "\n") +} diff --git a/pkg/api/handlers/generic/swagger.go b/pkg/api/handlers/compat/swagger.go index 27e1fc18d..cbd8e61fb 100644 --- a/pkg/api/handlers/generic/swagger.go +++ b/pkg/api/handlers/compat/swagger.go @@ -1,11 +1,15 @@ -package generic +package compat + +import ( + "github.com/containers/libpod/pkg/api/handlers/utils" +) // Create container // swagger:response ContainerCreateResponse type swagCtrCreateResponse struct { // in:body Body struct { - ContainerCreateResponse + utils.ContainerCreateResponse } } diff --git a/pkg/api/handlers/generic/system.go b/pkg/api/handlers/compat/system.go index edf1f8522..47e187ba1 100644 --- a/pkg/api/handlers/generic/system.go +++ b/pkg/api/handlers/compat/system.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "net/http" diff --git a/pkg/api/handlers/compat/types.go b/pkg/api/handlers/compat/types.go new file mode 100644 index 000000000..b8d06760f --- /dev/null +++ b/pkg/api/handlers/compat/types.go @@ -0,0 +1,55 @@ +package compat + +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/unsupported.go b/pkg/api/handlers/compat/unsupported.go index 956d31f8b..d9c3c3f49 100644 --- a/pkg/api/handlers/unsupported.go +++ b/pkg/api/handlers/compat/unsupported.go @@ -1,4 +1,4 @@ -package handlers +package compat import ( "fmt" diff --git a/pkg/api/handlers/generic/version.go b/pkg/api/handlers/compat/version.go index 39423914d..c7f7917ac 100644 --- a/pkg/api/handlers/generic/version.go +++ b/pkg/api/handlers/compat/version.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "fmt" @@ -14,11 +14,6 @@ import ( "github.com/pkg/errors" ) -const ( - DefaultApiVersion = "1.40" // See https://docs.docker.com/engine/api/v1.40/ - MinimalApiVersion = "1.24" -) - func VersionHandler(w http.ResponseWriter, r *http.Request) { // 200 ok // 500 internal @@ -41,14 +36,14 @@ func VersionHandler(w http.ResponseWriter, r *http.Request) { Name: "Podman Engine", Version: versionInfo.Version, Details: map[string]string{ - "APIVersion": DefaultApiVersion, + "APIVersion": handlers.DefaultApiVersion, "Arch": goRuntime.GOARCH, "BuildTime": time.Unix(versionInfo.Built, 0).Format(time.RFC3339), "Experimental": "true", "GitCommit": versionInfo.GitCommit, "GoVersion": versionInfo.GoVersion, "KernelVersion": hostInfo["kernel"].(string), - "MinAPIVersion": MinimalApiVersion, + "MinAPIVersion": handlers.MinimalApiVersion, "Os": goRuntime.GOOS, }, }} diff --git a/pkg/api/handlers/containers.go b/pkg/api/handlers/containers.go deleted file mode 100644 index b5c78ce53..000000000 --- a/pkg/api/handlers/containers.go +++ /dev/null @@ -1,247 +0,0 @@ -package handlers - -import ( - "fmt" - "github.com/docker/docker/api/types" - "net/http" - - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" - "github.com/gorilla/schema" - "github.com/pkg/errors" -) - -func StopContainer(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decoder").(*schema.Decoder) - - // /{version}/containers/(name)/stop - query := struct { - Timeout int `schema:"t"` - }{ - // override any golang type defaults - } - 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 - } - - name := getName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - state, err := con.State() - if err != nil { - 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 state == define.ContainerStateStopped || state == define.ContainerStateExited { - utils.Error(w, http.StatusText(http.StatusNotModified), http.StatusNotModified, - errors.Errorf("Container %s is already stopped ", name)) - return - } - - var stopError error - if query.Timeout > 0 { - stopError = con.StopWithTimeout(uint(query.Timeout)) - } else { - stopError = con.Stop() - } - if stopError != nil { - utils.InternalServerError(w, errors.Wrapf(stopError, "failed to stop %s", name)) - return - } - - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func UnpauseContainer(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - // /{version}/containers/(name)/unpause - name := getName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - // the api does not error if the Container is already paused, so just into it - if err := con.Unpause(); err != nil { - utils.InternalServerError(w, err) - return - } - - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func PauseContainer(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - // /{version}/containers/(name)/pause - name := getName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - // the api does not error if the Container is already paused, so just into it - if err := con.Pause(); err != nil { - utils.InternalServerError(w, err) - return - } - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func StartContainer(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - query := struct { - DetachKeys string `schema:"detachKeys"` - }{ - // Override golang default values for types - } - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - if len(query.DetachKeys) > 0 { - // TODO - start does not support adding detach keys - utils.Error(w, "Something went wrong", http.StatusBadRequest, errors.New("the detachKeys parameter is not supported yet")) - return - } - runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := getName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - state, err := con.State() - if err != nil { - utils.InternalServerError(w, err) - return - } - if state == define.ContainerStateRunning { - msg := fmt.Sprintf("Container %s is already running", name) - utils.Error(w, msg, http.StatusNotModified, errors.New(msg)) - return - } - if err := con.Start(r.Context(), false); err != nil { - utils.InternalServerError(w, err) - return - } - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func RestartContainer(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decoder").(*schema.Decoder) - // /{version}/containers/(name)/restart - query := struct { - Timeout int `schema:"t"` - }{ - // Override golang default values for types - } - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - - name := getName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - state, err := con.State() - if err != nil { - utils.InternalServerError(w, err) - return - } - - // FIXME: This is not in the swagger.yml... - // If the Container is stopped already, send a 409 - if state == define.ContainerStateStopped || state == define.ContainerStateExited { - msg := fmt.Sprintf("Container %s is not running", name) - utils.Error(w, msg, http.StatusConflict, errors.New(msg)) - return - } - - timeout := con.StopTimeout() - if _, found := mux.Vars(r)["t"]; found { - timeout = uint(query.Timeout) - } - - if err := con.RestartWithTimeout(r.Context(), timeout); err != nil { - utils.InternalServerError(w, err) - return - } - - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func PruneContainers(w http.ResponseWriter, r *http.Request) { - var ( - delContainers []string - space int64 - ) - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decoder").(*schema.Decoder) - - query := struct { - Filters map[string][]string `schema:"filter"` - }{} - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - - filterFuncs, err := utils.GenerateFilterFuncsFromMap(runtime, query.Filters) - if err != nil { - utils.InternalServerError(w, err) - return - } - prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) - if err != nil { - utils.InternalServerError(w, err) - return - } - - // Libpod response differs - if utils.IsLibpodRequest(r) { - var response []LibpodContainersPruneReport - for ctrID, size := range prunedContainers { - response = append(response, LibpodContainersPruneReport{ID: ctrID, SpaceReclaimed: size}) - } - for ctrID, err := range pruneErrors { - response = append(response, LibpodContainersPruneReport{ID: ctrID, PruneError: err.Error()}) - } - utils.WriteResponse(w, http.StatusOK, response) - return - } - for ctrID, size := range prunedContainers { - if pruneErrors[ctrID] == nil { - space += size - delContainers = append(delContainers, ctrID) - } - } - report := types.ContainersPruneReport{ - ContainersDeleted: delContainers, - SpaceReclaimed: uint64(space), - } - utils.WriteResponse(w, http.StatusOK, report) -} 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/events.go b/pkg/api/handlers/events.go deleted file mode 100644 index 44bf35254..000000000 --- a/pkg/api/handlers/events.go +++ /dev/null @@ -1,41 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" - "strings" - "time" - - "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/pkg/errors" -) - -func GetEvents(w http.ResponseWriter, r *http.Request) { - query := struct { - Since time.Time `schema:"since"` - Until time.Time `schema:"until"` - Filters map[string][]string `schema:"filters"` - }{} - if err := decodeQuery(r, &query); err != nil { - utils.Error(w, "Failed to parse parameters", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - } - - var libpodFilters = []string{} - if _, found := r.URL.Query()["filters"]; found { - for k, v := range query.Filters { - libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) - } - } - - libpodEvents, err := getRuntime(r).GetEvents(libpodFilters) - if err != nil { - utils.BadRequest(w, "filters", strings.Join(r.URL.Query()["filters"], ", "), err) - return - } - - var apiEvents = make([]*Event, len(libpodEvents)) - for _, v := range libpodEvents { - apiEvents = append(apiEvents, EventToApiEvent(v)) - } - utils.WriteJSON(w, http.StatusOK, apiEvents) -} diff --git a/pkg/api/handlers/generic/config.go b/pkg/api/handlers/generic/config.go deleted file mode 100644 index f715d25eb..000000000 --- a/pkg/api/handlers/generic/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package generic - -// 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/generic/ping.go b/pkg/api/handlers/generic/ping.go deleted file mode 100644 index 44a67d53f..000000000 --- a/pkg/api/handlers/generic/ping.go +++ /dev/null @@ -1,25 +0,0 @@ -package generic - -import ( - "fmt" - "net/http" -) - -func PingGET(w http.ResponseWriter, _ *http.Request) { - setHeaders(w) - fmt.Fprintln(w, "OK") -} - -func PingHEAD(w http.ResponseWriter, _ *http.Request) { - setHeaders(w) - fmt.Fprintln(w, "") -} - -func setHeaders(w http.ResponseWriter) { - w.Header().Set("API-Version", DefaultApiVersion) - w.Header().Set("BuildKit-Version", "") - w.Header().Set("Docker-Experimental", "true") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Pragma", "no-cache") - w.WriteHeader(http.StatusOK) -} diff --git a/pkg/api/handlers/handler.go b/pkg/api/handlers/handler.go index d60a5b239..2dd2c886b 100644 --- a/pkg/api/handlers/handler.go +++ b/pkg/api/handlers/handler.go @@ -1,47 +1,6 @@ package handlers -import ( - "net/http" - - "github.com/containers/libpod/libpod" - "github.com/gorilla/mux" - "github.com/gorilla/schema" - "github.com/pkg/errors" +const ( + DefaultApiVersion = "1.40" // See https://docs.docker.com/engine/api/v1.40/ + MinimalApiVersion = "1.24" ) - -// Convenience routines to reduce boiler plate in handlers - -func getVar(r *http.Request, k string) string { - return mux.Vars(r)[k] -} - -// func hasVar(r *http.Request, k string) bool { -// _, found := mux.Vars(r)[k] -// return found -// } - -func getName(r *http.Request) string { - return getVar(r, "name") -} - -func decodeQuery(r *http.Request, i interface{}) error { - decoder := r.Context().Value("decoder").(*schema.Decoder) - - if err := decoder.Decode(i, r.URL.Query()); err != nil { - return errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()) - } - return nil -} - -func getRuntime(r *http.Request) *libpod.Runtime { - return r.Context().Value("runtime").(*libpod.Runtime) -} - -// func getHeader(r *http.Request, k string) string { -// return r.Header.Get(k) -// } -// -// func hasHeader(r *http.Request, k string) bool { -// _, found := r.Header[k] -// return found -// } diff --git a/pkg/api/handlers/images.go b/pkg/api/handlers/images.go deleted file mode 100644 index b4acdc312..000000000 --- a/pkg/api/handlers/images.go +++ /dev/null @@ -1,202 +0,0 @@ -package handlers - -import ( - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "strconv" - - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/image" - "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" - "github.com/gorilla/schema" - "github.com/pkg/errors" -) - -func HistoryImage(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] - var allHistory []HistoryResponse - - newImage, err := runtime.ImageRuntime().NewFromLocal(name) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) - return - - } - history, err := newImage.History(r.Context()) - if err != nil { - utils.InternalServerError(w, err) - return - } - for _, h := range history { - l := HistoryResponse{ - ID: h.ID, - Created: h.Created.UnixNano(), - CreatedBy: h.CreatedBy, - Tags: h.Tags, - Size: h.Size, - Comment: h.Comment, - } - allHistory = append(allHistory, l) - } - utils.WriteResponse(w, http.StatusOK, allHistory) -} - -func TagImage(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - // /v1.xx/images/(name)/tag - name := mux.Vars(r)["name"] - newImage, err := runtime.ImageRuntime().NewFromLocal(name) - if err != nil { - utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) - return - } - tag := "latest" - if len(r.Form.Get("tag")) > 0 { - tag = r.Form.Get("tag") - } - if len(r.Form.Get("repo")) < 1 { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.New("repo parameter is required to tag an image")) - return - } - repo := r.Form.Get("repo") - tagName := fmt.Sprintf("%s:%s", repo, tag) - if err := newImage.TagImage(tagName); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - utils.WriteResponse(w, http.StatusCreated, "") -} - -func RemoveImage(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - query := struct { - noPrune bool - }{ - // This is where you can override the golang default value for one of fields - } - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - muxVars := mux.Vars(r) - if _, found := muxVars["noprune"]; found { - if query.noPrune { - utils.UnSupportedParameter("noprune") - } - } - name := mux.Vars(r)["name"] - newImage, err := runtime.ImageRuntime().NewFromLocal(name) - if err != nil { - utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) - return - } - - force := false - if len(r.Form.Get("force")) > 0 { - force, err = strconv.ParseBool(r.Form.Get("force")) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, err) - return - } - } - _, err = runtime.RemoveImage(r.Context(), newImage, force) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - // TODO - // This will need to be fixed for proper response, like Deleted: and Untagged: - m := make(map[string]string) - m["Deleted"] = newImage.ID() - foo := []map[string]string{} - foo = append(foo, m) - utils.WriteResponse(w, http.StatusOK, foo) - -} -func GetImage(r *http.Request, name string) (*image.Image, error) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - return runtime.ImageRuntime().NewFromLocal(name) -} - -func LoadImage(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - query := struct { - //quiet bool # quiet is currently unused - }{ - // This is where you can override the golang default value for one of fields - } - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - - var ( - err error - writer io.Writer - ) - f, err := ioutil.TempFile("", "api_load.tar") - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) - return - } - if err := SaveFromBody(f, r); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) - return - } - id, err := runtime.LoadImage(r.Context(), "", f.Name(), writer, "") - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to load image")) - return - } - utils.WriteResponse(w, http.StatusOK, struct { - Stream string `json:"stream"` - }{ - Stream: fmt.Sprintf("Loaded image: %s\n", id), - }) -} - -func SaveFromBody(f *os.File, r *http.Request) error { // nolint - if _, err := io.Copy(f, r.Body); err != nil { - return err - } - return f.Close() -} - -func SearchImages(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - query := struct { - Term string `json:"term"` - Limit int `json:"limit"` - Filters map[string][]string `json:"filters"` - }{ - // This is where you can override the golang default value for one of fields - } - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - // TODO filters are a bit undefined here in terms of what exactly the input looks - // like. We need to understand that a bit more. - options := image.SearchOptions{ - Filter: image.SearchFilter{}, - Limit: query.Limit, - } - results, err := image.SearchImages(query.Term, options) - if err != nil { - utils.InternalServerError(w, err) - } - utils.WriteResponse(w, http.StatusOK, results) -} diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index df16843c7..cdc34004f 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -1,60 +1,45 @@ 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/pkg/api/handlers" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) -func StopContainer(w http.ResponseWriter, r *http.Request) { - handlers.StopContainer(w, r) -} - func ContainerExists(w http.ResponseWriter, r *http.Request) { - // 404 no such container - // 200 ok runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) _, err := runtime.LookupContainer(name) if err != nil { - utils.ContainerNotFound(w, name, err) + if errors.Cause(err) == define.ErrNoSuchCtr { + utils.ContainerNotFound(w, name, err) + } + utils.InternalServerError(w, err) return + } utils.WriteResponse(w, http.StatusNoContent, "") } -func RemoveContainer(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - query := struct { - Force bool `schema:"force"` - Vols bool `schema:"v"` - }{ - // override any golang type defaults - } - - 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 - } - utils.RemoveContainer(w, r, query.Force, query.Vols) -} 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"` @@ -69,6 +54,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, @@ -76,20 +62,61 @@ 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 { + + all := query.All + 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) + + // Docker thinks that if status is given as an input, then we should override + // the all setting and always deal with all containers. + if len(query.Filters["status"]) > 0 { + all = true + } + if !all { + 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) } @@ -107,7 +134,7 @@ func GetContainer(w http.ResponseWriter, r *http.Request) { return } runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) container, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) @@ -121,39 +148,17 @@ func GetContainer(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, data) } -func KillContainer(w http.ResponseWriter, r *http.Request) { - // /{version}/containers/(name)/kill - _, err := utils.KillContainer(w, r) - if err != nil { - return - } - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - func WaitContainer(w http.ResponseWriter, r *http.Request) { exitCode, err := utils.WaitContainer(w, r) if err != nil { - utils.InternalServerError(w, err) return } utils.WriteResponse(w, http.StatusOK, strconv.Itoa(int(exitCode))) } -func LogsFromContainer(w http.ResponseWriter, r *http.Request) { - // follow - // since - // timestamps - // tail string -} - -func CreateContainer(w http.ResponseWriter, r *http.Request) { - -} - func UnmountContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) conn, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) @@ -169,7 +174,7 @@ func UnmountContainer(w http.ResponseWriter, r *http.Request) { } func MountContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) conn, err := runtime.LookupContainer(name) if err != nil { utils.ContainerNotFound(w, name, err) @@ -201,3 +206,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/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/healthcheck.go b/pkg/api/handlers/libpod/healthcheck.go index 0d7bf3ea7..6eb2ab0e3 100644 --- a/pkg/api/handlers/libpod/healthcheck.go +++ b/pkg/api/handlers/libpod/healthcheck.go @@ -5,21 +5,39 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" ) func RunHealthCheck(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 404 no such - // 500 internal runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) status, err := runtime.HealthCheck(name) if err != nil { if status == libpod.HealthCheckContainerNotFound { utils.ContainerNotFound(w, name, err) + return } + if status == libpod.HealthCheckNotDefined { + utils.Error(w, "no healthcheck defined", http.StatusConflict, err) + return + } + if status == libpod.HealthCheckContainerStopped { + utils.Error(w, "container not running", http.StatusConflict, err) + return + } + utils.InternalServerError(w, err) + return + } + ctr, err := runtime.LookupContainer(name) + if err != nil { utils.InternalServerError(w, err) + return } - utils.WriteResponse(w, http.StatusOK, status) + + hcLog, err := ctr.GetHealthCheckLog() + if err != nil { + utils.InternalServerError(w, err) + return + } + + utils.WriteResponse(w, http.StatusOK, hcLog) } diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index bbc8c9346..4b24d7d9f 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -1,15 +1,28 @@ package libpod import ( + "context" "fmt" + "io" "io/ioutil" "net/http" "os" + "strconv" + "strings" + "github.com/containers/buildah" + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/image/v5/types" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + image2 "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -26,11 +39,8 @@ import ( // create func ImageExists(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 404 no such - // 500 internal runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) _, err := runtime.ImageRuntime().NewFromLocal(name) if err != nil { @@ -41,22 +51,39 @@ func ImageExists(w http.ResponseWriter, r *http.Request) { } func ImageTree(w http.ResponseWriter, r *http.Request) { - // tree is a bit of a mess ... logic is in adapter and therefore not callable from here. needs rework - - // name := mux.Vars(r)["name"] - // _, layerInfoMap, _, err := s.Runtime.Tree(name) - // if err != nil { - // Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to find image information for %q", name)) - // return - // } - // it is not clear to me how to deal with this given all the processing of the image - // is in main. we need to discuss how that really should be and return something useful. - handlers.UnsupportedHandler(w, r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + + img, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + WhatRequires bool `schema:"whatrequires"` + }{ + WhatRequires: false, + } + 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 + } + + tree, err := img.GenerateTree(query.WhatRequires) + if err != nil { + utils.Error(w, "Server error", http.StatusInternalServerError, errors.Wrapf(err, "failed to generate image tree for %s", name)) + return + } + + utils.WriteResponse(w, http.StatusOK, tree) } func GetImage(w http.ResponseWriter, r *http.Request) { - name := mux.Vars(r)["name"] - newImage, err := handlers.GetImage(r, name) + name := utils.GetName(r) + newImage, err := utils.GetImage(r, name) if err != nil { utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) return @@ -67,15 +94,15 @@ func GetImage(w http.ResponseWriter, r *http.Request) { return } utils.WriteResponse(w, http.StatusOK, inspect) - } + func GetImages(w http.ResponseWriter, r *http.Request) { images, err := utils.GetImages(w, r) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed get images")) return } - var summaries = make([]*handlers.ImageSummary, len(images)) + var summaries = make([]*entities.ImageSummary, len(images)) for j, img := range images { is, err := handlers.ImageToImageSummary(img) if err != nil { @@ -83,7 +110,7 @@ func GetImages(w http.ResponseWriter, r *http.Request) { return } // libpod has additional fields that we need to populate. - is.CreatedTime = img.Created() + is.Created = img.Created().Unix() is.ReadOnly = img.IsReadOnly() summaries[j] = is } @@ -91,8 +118,9 @@ func GetImages(w http.ResponseWriter, r *http.Request) { } func PruneImages(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 500 internal + var ( + err error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { @@ -110,10 +138,21 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { var libpodFilters = []string{} if _, found := r.URL.Query()["filters"]; found { + dangling := query.Filters["all"] + if len(dangling) > 0 { + query.All, err = strconv.ParseBool(query.Filters["all"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + } + // dangling is special and not implemented in the libpod side of things + delete(query.Filters, "dangling") for k, v := range query.Filters { libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) } } + cids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, libpodFilters) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) @@ -129,7 +168,7 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { Compress bool `schema:"compress"` Format string `schema:"format"` }{ - // override any golang type defaults + Format: "docker-archive", } if err := decoder.Decode(&query, r.URL.Query()); err != nil { @@ -138,11 +177,6 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { return } - if len(query.Format) < 1 { - utils.InternalServerError(w, errors.New("format parameter cannot be empty.")) - return - } - tmpfile, err := ioutil.TempFile("", "api.tar") if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) @@ -152,12 +186,13 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) return } - name := mux.Vars(r)["name"] + name := utils.GetName(r) newImage, err := runtime.ImageRuntime().NewFromLocal(name) if err != nil { utils.ImageNotFound(w, name, err) return } + if err := newImage.Save(r.Context(), name, query.Format, tmpfile.Name(), []string{}, false, query.Compress); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) return @@ -171,3 +206,299 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { defer os.Remove(tmpfile.Name()) utils.WriteResponse(w, http.StatusOK, rdr) } + +func ImagesLoad(w http.ResponseWriter, r *http.Request) { + 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 + } + split := strings.Split(loadedImage, ",") + newImage, err := runtime.ImageRuntime().NewFromLocal(split[0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + // TODO this should go into libpod proper at some point. + if len(query.Reference) > 0 { + if err := newImage.TagImage(query.Reference); err != nil { + utils.InternalServerError(w, err) + return + } + } + utils.WriteResponse(w, http.StatusOK, handlers.LibpodImagesLoadReport{ID: loadedImage}) +} + +func ImagesImport(w http.ResponseWriter, r *http.Request) { + 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) { + 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) +} + +func CommitContainer(w http.ResponseWriter, r *http.Request) { + var ( + destImage string + mimeType string + ) + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + Author string `schema:"author"` + Changes []string `schema:"changes"` + Comment string `schema:"comment"` + Container string `schema:"container"` + Format string `schema:"format"` + Pause bool `schema:"pause"` + Repo string `schema:"repo"` + Tag string `schema:"tag"` + }{ + Format: "oci", + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.Error(w, "failed to get runtime config", http.StatusInternalServerError, errors.Wrap(err, "failed to get runtime config")) + return + } + sc := image2.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) + tag := "latest" + options := libpod.ContainerCommitOptions{ + Pause: true, + } + switch query.Format { + case "oci": + mimeType = buildah.OCIv1ImageManifest + if len(query.Comment) > 0 { + utils.InternalServerError(w, errors.New("messages are only compatible with the docker image format (-f docker)")) + return + } + case "docker": + mimeType = manifest.DockerV2Schema2MediaType + default: + utils.InternalServerError(w, errors.Errorf("unrecognized image format %q", query.Format)) + return + } + options.CommitOptions = buildah.CommitOptions{ + SignaturePolicyPath: rtc.Engine.SignaturePolicyPath, + ReportWriter: os.Stderr, + SystemContext: sc, + PreferredManifestType: mimeType, + } + + if len(query.Tag) > 0 { + tag = query.Tag + } + options.Message = query.Comment + options.Author = query.Author + options.Pause = query.Pause + options.Changes = query.Changes + ctr, err := runtime.LookupContainer(query.Container) + if err != nil { + utils.Error(w, "failed to lookup container", http.StatusNotFound, err) + return + } + + // I know mitr hates this ... but doing for now + if len(query.Repo) > 1 { + destImage = fmt.Sprintf("%s:%s", query.Repo, tag) + } + + commitImage, err := ctr.Commit(r.Context(), destImage, options) + if err != nil && !strings.Contains(err.Error(), "is not running") { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "CommitFailure")) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: commitImage.ID()}) // nolint +} diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go new file mode 100644 index 000000000..d87ed7eba --- /dev/null +++ b/pkg/api/handlers/libpod/manifests.go @@ -0,0 +1,166 @@ +package libpod + +import ( + "encoding/json" + "net/http" + + "github.com/containers/buildah/manifests" + copy2 "github.com/containers/image/v5/copy" + "github.com/containers/image/v5/transports/alltransports" + "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/gorilla/schema" + "github.com/opencontainers/go-digest" + "github.com/pkg/errors" +) + +func ManifestCreate(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Name []string `schema:"name"` + Image []string `schema:"image"` + All bool `schema:"all"` + }{ + // 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 + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.InternalServerError(w, err) + return + } + sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) + manID, err := image.CreateManifestList(runtime.ImageRuntime(), *sc, query.Name, query.Image, query.All) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: manID}) +} + +func ManifestInspect(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + data, err := newImage.InspectManifest() + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, data) +} + +func ManifestAdd(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + var manifestInput image.ManifestAddOpts + if err := json.NewDecoder(r.Body).Decode(&manifestInput); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.InternalServerError(w, err) + return + } + sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) + newID, err := newImage.AddManifest(*sc, manifestInput) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: newID}) +} + +func ManifestRemove(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Digest string `schema:"digest"` + }{ + // Add defaults here once needed. + } + name := utils.GetName(r) + 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 + } + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + d, err := digest.Parse(query.Digest) + if err != nil { + utils.Error(w, "invalid digest", http.StatusBadRequest, err) + return + } + newID, err := newImage.RemoveManifest(d) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: newID}) +} +func ManifestPush(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + All bool `schema:"all"` + Destination string `schema:"destination"` + }{ + // 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 + } + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + dest, err := alltransports.ParseImageName(query.Destination) + if err != nil { + utils.Error(w, "invalid destination parameter", http.StatusBadRequest, errors.Errorf("invalid destination parameter %q", query.Destination)) + return + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.InternalServerError(w, err) + return + } + sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) + opts := manifests.PushOptions{ + ImageListSelection: copy2.CopySpecificImages, + SystemContext: sc, + } + if query.All { + opts.ImageListSelection = copy2.CopyAllImages + } + newD, err := newImage.PushManifest(dest, opts) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, newD.String()) +} diff --git a/pkg/api/handlers/libpod/networks.go b/pkg/api/handlers/libpod/networks.go new file mode 100644 index 000000000..e8a92e93e --- /dev/null +++ b/pkg/api/handlers/libpod/networks.go @@ -0,0 +1,85 @@ +package libpod + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/network" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func CreateNetwork(w http.ResponseWriter, r *http.Request) {} +func ListNetworks(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + config, err := runtime.GetConfig() + if err != nil { + utils.InternalServerError(w, err) + return + } + configDir := config.Network.NetworkConfigDir + if len(configDir) < 1 { + configDir = network.CNIConfigDir + } + networks, err := network.LoadCNIConfsFromDir(configDir) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, networks) +} + +func RemoveNetwork(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 404 no such + // 500 internal + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Force bool `schema:"force"` + }{ + // override any golang type defaults + } + 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 + } + name := utils.GetName(r) + if err := network.RemoveNetwork(name); err != nil { + // If the network cannot be found, we return a 404. + if errors.Cause(err) == network.ErrNetworkNotFound { + utils.Error(w, "Something went wrong", http.StatusNotFound, err) + return + } + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, "") +} + +func InspectNetwork(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Force bool `schema:"force"` + }{ + // override any golang type defaults + } + 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 + } + name := utils.GetName(r) + n, err := network.InspectNetwork(name) + if err != nil { + // If the network cannot be found, we return a 404. + if errors.Cause(err) == network.ErrNetworkNotFound { + utils.Error(w, "Something went wrong", http.StatusNotFound, err) + return + } + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, n) +} diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 656a75646..7e9c2e2c0 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -4,108 +4,41 @@ import ( "encoding/json" "fmt" "net/http" - "strings" - "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/cmd/podman/shared/parse" "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/domain/entities" + "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/util" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" ) func PodCreate(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 500 internal var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) - options []libpod.PodCreateOption err error ) - labels := make(map[string]string) - input := handlers.PodCreateConfig{} - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + var psg specgen.PodSpecGenerator + if err := json.NewDecoder(r.Body).Decode(&psg); err != nil { + utils.Error(w, "Failed to decode specgen", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) return } - if len(input.InfraCommand) > 0 || len(input.InfraImage) > 0 { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, - errors.New("infra-command and infra-image are not implemented yet")) - return - } - // TODO long term we should break the following out of adapter and into libpod proper - // so that the cli and api can share the creation of a pod with the same options - if len(input.CGroupParent) > 0 { - options = append(options, libpod.WithPodCgroupParent(input.CGroupParent)) - } - - if len(input.Labels) > 0 { - if err := parse.ReadKVStrings(labels, []string{}, input.Labels); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - } - - if len(labels) != 0 { - options = append(options, libpod.WithPodLabels(labels)) - } - - if len(input.Name) > 0 { - options = append(options, libpod.WithPodName(input.Name)) - } - - if len(input.Hostname) > 0 { - options = append(options, libpod.WithPodHostname(input.Hostname)) - } - - if input.Infra { - // TODO infra-image and infra-command are not supported in the libpod API yet. Will fix - // when implemented in libpod - options = append(options, libpod.WithInfraContainer()) - sharedNamespaces := shared.DefaultKernelNamespaces - if len(input.Share) > 0 { - sharedNamespaces = input.Share - } - nsOptions, err := shared.GetNamespaceOptions(strings.Split(sharedNamespaces, ",")) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - options = append(options, nsOptions...) - } - - if len(input.Publish) > 0 { - portBindings, err := shared.CreatePortBindings(input.Publish) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - options = append(options, libpod.WithInfraContainerPorts(portBindings)) - - } - // always have containers use pod cgroups - // User Opt out is not yet supported - options = append(options, libpod.WithPodCgroups()) - - pod, err := runtime.NewPod(r.Context(), options...) + pod, err := psg.MakePod(runtime) 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()}) + 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 - ) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { Filters map[string][]string `schema:"filters"` @@ -118,30 +51,17 @@ func Pods(w http.ResponseWriter, r *http.Request) { return } - if _, found := r.URL.Query()["filters"]; found { - utils.Error(w, "filters are not implemented yet", http.StatusInternalServerError, define.ErrNotImplemented) - return - } - - pods, err := runtime.GetAllPods() + pods, err := utils.GetPods(w, r) if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - for _, pod := range pods { - data, err := pod.Inspect() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - podInspectData = append(podInspectData, data) - } - utils.WriteResponse(w, http.StatusOK, podInspectData) + utils.WriteResponse(w, http.StatusOK, pods) } func PodInspect(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) @@ -156,14 +76,12 @@ func PodInspect(w http.ResponseWriter, r *http.Request) { } func PodStop(w http.ResponseWriter, r *http.Request) { - // 200 - // 304 not modified - // 404 no such - // 500 internal var ( stopError error runtime = r.Context().Value("runtime").(*libpod.Runtime) decoder = r.Context().Value("decoder").(*schema.Decoder) + responses map[string]error + errs []error ) query := struct { Timeout int `schema:"t"` @@ -176,90 +94,75 @@ func PodStop(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - allContainersStopped := true - name := mux.Vars(r)["name"] + name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) return } - // TODO we need to implement a pod.State/Status in libpod internal so libpod api - // users dont have to run through all containers. - podContainers, err := pod.AllContainers() + status, err := pod.GetPodStatus() if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - - for _, con := range podContainers { - containerState, err := con.State() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - if containerState == define.ContainerStateRunning { - allContainersStopped = false - break - } - } - if allContainersStopped { - alreadyStopped := errors.Errorf("pod %s is already stopped", pod.ID()) - utils.Error(w, "Something went wrong", http.StatusNotModified, alreadyStopped) + if status != define.PodStateRunning { + utils.WriteResponse(w, http.StatusNotModified, "") return } if query.Timeout > 0 { - _, stopError = pod.StopWithTimeout(r.Context(), false, query.Timeout) + responses, stopError = pod.StopWithTimeout(r.Context(), false, query.Timeout) } else { - _, stopError = pod.Stop(r.Context(), false) + responses, stopError = pod.Stop(r.Context(), false) } if stopError != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + for _, err := range responses { + errs = append(errs, err) + } + report := entities.PodStopReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodStart(w http.ResponseWriter, r *http.Request) { + var ( + errs []error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) - allContainersRunning := true - name := mux.Vars(r)["name"] + name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) return } - - // TODO we need to implement a pod.State/Status in libpod internal so libpod api - // users dont have to run through all containers. - podContainers, err := pod.AllContainers() + status, err := pod.GetPodStatus() if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - - for _, con := range podContainers { - containerState, err := con.State() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - if containerState != define.ContainerStateRunning { - allContainersRunning = false - break - } - } - if allContainersRunning { - alreadyRunning := errors.Errorf("pod %s is already running", pod.ID()) - utils.Error(w, "Something went wrong", http.StatusNotModified, alreadyRunning) + if status == define.PodStateRunning { + utils.WriteResponse(w, http.StatusNotModified, "") return } - if _, err := pod.Start(r.Context()); err != nil { + responses, err := pod.Start(r.Context()) + if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + for _, err := range responses { + errs = append(errs, err) + } + report := entities.PodStartReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodDelete(w http.ResponseWriter, r *http.Request) { @@ -268,7 +171,7 @@ func PodDelete(w http.ResponseWriter, r *http.Request) { decoder = r.Context().Value("decoder").(*schema.Decoder) ) query := struct { - force bool `schema:"force"` + Force bool `schema:"force"` }{ // override any golang type defaults } @@ -278,109 +181,110 @@ func PodDelete(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - name := mux.Vars(r)["name"] + name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) return } - if err := runtime.RemovePod(r.Context(), pod, true, query.force); err != nil { + if err := runtime.RemovePod(r.Context(), pod, true, query.Force); err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusNoContent, "") + report := entities.PodRmReport{ + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodRestart(w http.ResponseWriter, r *http.Request) { + var ( + errs []error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) return } - _, err = pod.Restart(r.Context()) + responses, err := pod.Restart(r.Context()) if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + for _, err := range responses { + errs = append(errs, err) + } + report := entities.PodRestartReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodPrune(w http.ResponseWriter, r *http.Request) { var ( - err error - pods []*libpod.Pod runtime = r.Context().Value("runtime").(*libpod.Runtime) - decoder = r.Context().Value("decoder").(*schema.Decoder) ) - query := struct { - force bool `schema:"force"` - }{ - // override any golang type defaults - } - - 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 query.force { - pods, err = runtime.GetAllPods() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - } else { - // TODO We need to make a libpod.PruneVolumes or this code will be a mess. Volumes - // already does this right. It will also help clean this code path up with less - // conditionals. We do this when we integrate with libpod again. - utils.Error(w, "not implemented", http.StatusInternalServerError, errors.New("not implemented")) + pruned, err := runtime.PrunePods() + if err != nil { + utils.InternalServerError(w, err) return } - for _, p := range pods { - if err := runtime.RemovePod(r.Context(), p, true, query.force); err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - } - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusOK, pruned) } func PodPause(w http.ResponseWriter, r *http.Request) { + var ( + errs []error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) return } - _, err = pod.Pause() + responses, err := pod.Pause() if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusNoContent, "") + for _, v := range responses { + errs = append(errs, v) + } + report := entities.PodPauseReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodUnpause(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 404 no such - // 500 internal + var ( + errs []error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) return } - _, err = pod.Unpause() + responses, err := pod.Unpause() if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + utils.Error(w, "failed to pause pod", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + for _, v := range responses { + errs = append(errs, v) + } + report := entities.PodUnpauseReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, &report) } func PodKill(w http.ResponseWriter, r *http.Request) { @@ -388,9 +292,10 @@ func PodKill(w http.ResponseWriter, r *http.Request) { runtime = r.Context().Value("runtime").(*libpod.Runtime) decoder = r.Context().Value("decoder").(*schema.Decoder) signal = "SIGKILL" + errs []error ) query := struct { - signal string `schema:"signal"` + Signal string `schema:"signal"` }{ // override any golang type defaults } @@ -399,16 +304,15 @@ func PodKill(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - muxVars := mux.Vars(r) - if _, found := muxVars["signal"]; found { - signal = query.signal + if _, found := r.URL.Query()["signal"]; found { + signal = query.Signal } sig, err := util.ParseSignal(signal) if err != nil { utils.InternalServerError(w, errors.Wrapf(err, "unable to parse signal value")) } - name := mux.Vars(r)["name"] + name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) @@ -431,21 +335,32 @@ func PodKill(w http.ResponseWriter, r *http.Request) { utils.Error(w, msg, http.StatusConflict, errors.Errorf("cannot kill a pod with no running containers: %s", pod.ID())) return } - _, err = pod.Kill(uint(sig)) + + responses, err := pod.Kill(uint(sig)) if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + utils.Error(w, "failed to kill pod", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + + for _, v := range responses { + if v != nil { + errs = append(errs, v) + } + } + report := &entities.PodKillReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodExists(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] + name := utils.GetName(r) _, err := runtime.LookupPod(name) if err != nil { 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..1fad2dd1a --- /dev/null +++ b/pkg/api/handlers/libpod/swagger.go @@ -0,0 +1,94 @@ +package libpod + +import ( + "net/http" + "os" + + "github.com/containers/image/v5/manifest" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" +) + +// DefaultPodmanSwaggerSpec provides the default path to the podman swagger spec file +const DefaultPodmanSwaggerSpec = "/usr/share/containers/podman/swagger.yaml" + +// List Containers +// swagger:response ListContainers +type swagInspectPodResponse struct { + // in:body + Body []ListContainer +} + +// Inspect Manifest +// swagger:response InspectManifest +type swagInspectManifestResponse struct { + // in:body + Body manifest.List +} + +// Kill Pod +// swagger:response PodKillReport +type swagKillPodResponse struct { + // in:body + Body entities.PodKillReport +} + +// Pause pod +// swagger:response PodPauseReport +type swagPausePodResponse struct { + // in:body + Body entities.PodPauseReport +} + +// Unpause pod +// swagger:response PodUnpauseReport +type swagUnpausePodResponse struct { + // in:body + Body entities.PodUnpauseReport +} + +// Stop pod +// swagger:response PodStopReport +type swagStopPodResponse struct { + // in:body + Body entities.PodStopReport +} + +// Restart pod +// swagger:response PodRestartReport +type swagRestartPodResponse struct { + // in:body + Body entities.PodRestartReport +} + +// Start pod +// swagger:response PodStartReport +type swagStartPodResponse struct { + // in:body + Body entities.PodStartReport +} + +// Rm pod +// swagger:response PodRmReport +type swagRmPodResponse struct { + // in:body + Body entities.PodRmReport +} + +func ServeSwagger(w http.ResponseWriter, r *http.Request) { + path := DefaultPodmanSwaggerSpec + if p, found := os.LookupEnv("PODMAN_SWAGGER_SPEC"); found { + path = p + } + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + utils.InternalServerError(w, errors.Errorf("file %q does not exist", path)) + return + } + utils.InternalServerError(w, err) + return + } + w.Header().Set("Content-Type", "text/yaml") + http.ServeFile(w, r, path) +} 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/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 3e0e597c6..5a6fc021e 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -6,17 +6,15 @@ import ( "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/filters" "github.com/gorilla/schema" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) func CreateVolume(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 500 internal var ( volumeOptions []libpod.VolumeCreateOption runtime = r.Context().Value("runtime").(*libpod.Runtime) @@ -26,13 +24,12 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { }{ // override any golang type defaults } - input := handlers.VolumeCreateConfig{} + input := entities.VolumeCreateOptions{} 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 } - // decode params from body if err := json.NewDecoder(r.Body).Decode(&input); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) @@ -48,72 +45,124 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { if len(input.Label) > 0 { volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label)) } - if len(input.Opts) > 0 { - parsedOptions, err := shared.ParseVolumeOptions(input.Opts) + if len(input.Options) > 0 { + parsedOptions, err := shared.ParseVolumeOptions(input.Options) if err != nil { utils.InternalServerError(w, err) + return } volumeOptions = append(volumeOptions, parsedOptions...) } vol, err := runtime.NewVolume(r.Context(), volumeOptions...) if err != nil { utils.InternalServerError(w, err) + return + } + config, err := vol.Config() + if err != nil { + utils.InternalServerError(w, err) + return } - utils.WriteResponse(w, http.StatusOK, vol.Name()) + volResponse := entities.VolumeConfigResponse{ + Name: config.Name, + Driver: config.Driver, + Mountpoint: config.MountPoint, + CreatedAt: config.CreatedTime, + Labels: config.Labels, + Options: config.Options, + UID: config.UID, + GID: config.GID, + } + utils.WriteResponse(w, http.StatusOK, volResponse) } func InspectVolume(w http.ResponseWriter, r *http.Request) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) ) - name := mux.Vars(r)["name"] + name := utils.GetName(r) vol, err := runtime.GetVolume(name) if err != nil { utils.VolumeNotFound(w, name, err) + return } - inspect, err := vol.Inspect() - if err != nil { - utils.InternalServerError(w, err) + volResponse := entities.VolumeConfigResponse{ + Name: vol.Name(), + Driver: vol.Driver(), + Mountpoint: vol.MountPoint(), + CreatedAt: vol.CreatedTime(), + Labels: vol.Labels(), + Scope: vol.Scope(), + Options: vol.Options(), + UID: vol.UID(), + GID: vol.GID(), } - utils.WriteResponse(w, http.StatusOK, inspect) + utils.WriteResponse(w, http.StatusOK, volResponse) } func ListVolumes(w http.ResponseWriter, r *http.Request) { - //var ( - // runtime = r.Context().Value("runtime").(*libpod.Runtime) - // decoder = r.Context().Value("decoder").(*schema.Decoder) - //) - //query := struct { - // Filter string `json:"filter"` - //}{ - // // override any golang type defaults - //} - // - //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 - //} - /* - This is all in main in cmd and needs to be extracted from there first. - */ + var ( + decoder = r.Context().Value("decoder").(*schema.Decoder) + runtime = r.Context().Value("runtime").(*libpod.Runtime) + volumeConfigs []*entities.VolumeListReport + ) + query := struct { + Filters map[string][]string `schema:"filters"` + }{ + // override any golang type defaults + } + 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 + } + + volumeFilters, err := filters.GenerateVolumeFilters(query.Filters) + if err != nil { + utils.InternalServerError(w, err) + return + } + + vols, err := runtime.Volumes(volumeFilters...) + if err != nil { + utils.InternalServerError(w, err) + return + } + for _, v := range vols { + config := entities.VolumeConfigResponse{ + Name: v.Name(), + Driver: v.Driver(), + Mountpoint: v.MountPoint(), + CreatedAt: v.CreatedTime(), + Labels: v.Labels(), + Scope: v.Scope(), + Options: v.Options(), + UID: v.UID(), + GID: v.GID(), + } + volumeConfigs = append(volumeConfigs, &entities.VolumeListReport{VolumeConfigResponse: config}) + } + utils.WriteResponse(w, http.StatusOK, volumeConfigs) } func PruneVolumes(w http.ResponseWriter, r *http.Request) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) + reports []*entities.VolumePruneReport ) - pruned, errs := runtime.PruneVolumes(r.Context()) - if errs != nil { - if len(errs) > 1 { - for _, err := range errs { - log.Infof("Request Failed(%s): %s", http.StatusText(http.StatusInternalServerError), err.Error()) - } - } - utils.InternalServerError(w, errs[len(errs)-1]) + pruned, err := runtime.PruneVolumes(r.Context()) + if err != nil { + utils.InternalServerError(w, err) + return + } + for k, v := range pruned { + reports = append(reports, &entities.VolumePruneReport{ + Err: v, + Id: k, + }) } - utils.WriteResponse(w, http.StatusOK, pruned) + utils.WriteResponse(w, http.StatusOK, reports) } func RemoveVolume(w http.ResponseWriter, r *http.Request) { @@ -132,13 +181,19 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - name := mux.Vars(r)["name"] + name := utils.GetName(r) vol, err := runtime.LookupVolume(name) if err != nil { utils.VolumeNotFound(w, name, err) + return } if err := runtime.RemoveVolume(r.Context(), vol, query.Force); err != nil { + if errors.Cause(err) == define.ErrVolumeBeingUsed { + utils.Error(w, "volumes being used", http.StatusConflict, err) + return + } utils.InternalServerError(w, err) + return } utils.WriteResponse(w, http.StatusNoContent, "") } diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger.go index faae98798..e6e937729 100644 --- a/pkg/api/handlers/swagger.go +++ b/pkg/api/handlers/swagger.go @@ -1,9 +1,10 @@ package handlers import ( - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/inspect" "github.com/docker/docker/api/types" ) @@ -26,6 +27,27 @@ type swagImageInspect struct { } } +// Load response +// swagger:response DocsLibpodImagesLoadResponse +type swagLibpodImagesLoadResponse struct { + // in:body + Body []LibpodImagesLoadReport +} + +// Import response +// swagger:response DocsLibpodImagesImportResponse +type swagLibpodImagesImportResponse struct { + // in:body + Body LibpodImagesImportReport +} + +// Pull response +// swagger:response DocsLibpodImagesPullResponse +type swagLibpodImagesPullResponse struct { + // in:body + Body LibpodImagesPullReport +} + // Delete response // swagger:response DocsImageDeleteResponse type swagImageDeleteResponse struct { @@ -83,19 +105,12 @@ type swagDockerTopResponse struct { } } -// List containers -// swagger:response LibpodListContainersResponse -type swagLibpodListContainersResponse struct { - // in:body - Body []shared.PsContainerOutput -} - // Inspect container // swagger:response LibpodInspectContainerResponse type swagLibpodInspectContainerResponse struct { // in:body Body struct { - libpod.InspectContainerData + define.InspectContainerData } } @@ -103,7 +118,7 @@ type swagLibpodInspectContainerResponse struct { // swagger:response ListPodsResponse type swagListPodsResponse struct { // in:body - Body []libpod.PodInspect + Body []entities.ListPodsReport } // Inspect pod @@ -123,3 +138,12 @@ type swagInspectVolumeResponse struct { libpod.InspectVolumeData } } + +// Image tree response +// swagger:response LibpodImageTreeResponse +type swagImageTreeResponse struct { + // in:body + Body struct { + ImageTreeResponse + } +} diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 33cd51164..1ca5db3f9 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -13,6 +13,7 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/events" libpodImage "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" docker "github.com/docker/docker/api/types" dockerContainer "github.com/docker/docker/api/types/container" dockerEvents "github.com/docker/docker/api/types/events" @@ -33,10 +34,16 @@ type ContainerConfig struct { dockerContainer.Config } -type ImageSummary struct { - docker.ImageSummary - CreatedTime time.Time `json:"CreatedTime,omitempty"` - ReadOnly bool `json:"ReadOnly,omitempty"` +type LibpodImagesLoadReport struct { + ID string `json:"id"` +} + +type LibpodImagesImportReport struct { + ID string `json:"id"` +} + +type LibpodImagesPullReport struct { + ID string `json:"id"` } type ContainersPruneReport struct { @@ -66,14 +73,6 @@ type Container struct { docker.ContainerCreateConfig } -type ContainerStats struct { - docker.ContainerStats -} - -type Ping struct { - docker.Ping -} - type Version struct { docker.Version } @@ -124,37 +123,16 @@ type CreateContainerConfig struct { NetworkingConfig dockerNetwork.NetworkingConfig } -type VolumeCreateConfig struct { - Name string `json:"name"` - Driver string `schema:"driver"` - Label map[string]string `schema:"label"` - Opts map[string]string `schema:"opts"` -} - +// swagger:model IDResponse type IDResponse struct { + // ID ID string `json:"id"` } -type Stats struct { - docker.StatsJSON -} - type ContainerTopOKBody struct { dockerContainer.ContainerTopOKBody } -type PodCreateConfig struct { - Name string `json:"name"` - CGroupParent string `json:"cgroup-parent"` - Hostname string `json:"hostname"` - Infra bool `json:"infra"` - InfraCommand string `json:"infra-command"` - InfraImage string `json:"infra-image"` - Labels []string `json:"labels"` - Publish []string `json:"publish"` - Share string `json:"share"` -} - type ErrorModel struct { Message string `json:"message"` } @@ -181,6 +159,14 @@ type ImageTreeResponse struct { Layers []ImageLayer `json:"layers"` } +type ExecCreateConfig struct { + docker.ExecConfig +} + +type ExecCreateResponse struct { + docker.IDResponse +} + func EventToApiEvent(e *events.Event) *Event { return &Event{dockerEvents.Message{ Type: e.Type.String(), @@ -199,23 +185,13 @@ func EventToApiEvent(e *events.Event) *Event { }} } -func ImageToImageSummary(l *libpodImage.Image) (*ImageSummary, error) { +func ImageToImageSummary(l *libpodImage.Image) (*entities.ImageSummary, error) { containers, err := l.Containers() if err != nil { return nil, errors.Wrapf(err, "Failed to obtain Containers for image %s", l.ID()) } containerCount := len(containers) - var digests []string - for _, d := range l.Digests() { - digests = append(digests, string(d)) - } - - tags, err := l.RepoTags() - if err != nil { - return nil, errors.Wrapf(err, "Failed to obtain RepoTags for image %s", l.ID()) - } - // FIXME: GetParent() panics // parent, err := l.GetParent(context.TODO()) // if err != nil { @@ -231,20 +207,43 @@ func ImageToImageSummary(l *libpodImage.Image) (*ImageSummary, error) { if err != nil { return nil, errors.Wrapf(err, "Failed to obtain Size for image %s", l.ID()) } - dockerSummary := docker.ImageSummary{ - Containers: int64(containerCount), - Created: l.Created().Unix(), - ID: l.ID(), - Labels: labels, - ParentID: l.Parent, - RepoDigests: digests, - RepoTags: tags, - SharedSize: 0, - Size: int64(*size), - VirtualSize: int64(*size), - } - is := ImageSummary{ - ImageSummary: dockerSummary, + + repoTags, err := l.RepoTags() + if err != nil { + return nil, errors.Wrapf(err, "Failed to obtain RepoTags for image %s", l.ID()) + } + + history, err := l.History(context.TODO()) + if err != nil { + return nil, errors.Wrapf(err, "Failed to obtain History for image %s", l.ID()) + } + historyIds := make([]string, len(history)) + for i, h := range history { + historyIds[i] = h.ID + } + + digests := make([]string, len(l.Digests())) + for i, d := range l.Digests() { + digests[i] = string(d) + } + + is := entities.ImageSummary{ + ID: l.ID(), + ParentId: l.Parent, + RepoTags: repoTags, + Created: l.Created().Unix(), + Size: int64(*size), + SharedSize: 0, + VirtualSize: l.VirtualSize, + Labels: labels, + Containers: containerCount, + ReadOnly: l.IsReadOnly(), + Dangling: l.Dangling(), + Names: l.Names(), + Digest: string(l.Digest()), + Digests: digests, + ConfigDigest: string(l.ConfigDigest), + History: historyIds, } return &is, nil } @@ -341,35 +340,45 @@ func ImageDataToImageInspect(ctx context.Context, l *libpodImage.Image) (*ImageI } -func LibpodToContainer(l *libpod.Container, infoData []define.InfoData) (*Container, error) { +func LibpodToContainer(l *libpod.Container, infoData []define.InfoData, sz bool) (*Container, error) { imageId, imageName := l.Image() - sizeRW, err := l.RWSize() - if err != nil { - return nil, err - } - SizeRootFs, err := l.RootFsSize() - if err != nil { + var ( + err error + sizeRootFs int64 + sizeRW int64 + state define.ContainerStatus + ) + + if state, err = l.State(); err != nil { return nil, err } + stateStr := state.String() + if stateStr == "configured" { + stateStr = "created" + } - state, err := l.State() - if err != nil { - return nil, err + if sz { + if sizeRW, err = l.RWSize(); err != nil { + return nil, err + } + if sizeRootFs, err = l.RootFsSize(); err != nil { + return nil, err + } } return &Container{docker.Container{ ID: l.ID(), - Names: []string{l.Name()}, + Names: []string{fmt.Sprintf("/%s", l.Name())}, Image: imageName, ImageID: imageId, Command: strings.Join(l.Command(), " "), Created: l.CreatedTime().Unix(), Ports: nil, SizeRw: sizeRW, - SizeRootFs: SizeRootFs, + SizeRootFs: sizeRootFs, Labels: l.Labels(), - State: string(state), + State: stateStr, Status: "", HostConfig: struct { NetworkMode string `json:",omitempty"` @@ -382,9 +391,9 @@ func LibpodToContainer(l *libpod.Container, infoData []define.InfoData) (*Contai }, nil } -func LibpodToContainerJSON(l *libpod.Container) (*docker.ContainerJSON, error) { +func LibpodToContainerJSON(l *libpod.Container, sz bool) (*docker.ContainerJSON, error) { _, imageName := l.Image() - inspect, err := l.Inspect(true) + inspect, err := l.Inspect(sz) if err != nil { return nil, err } @@ -431,7 +440,7 @@ func LibpodToContainerJSON(l *libpod.Container) (*docker.ContainerJSON, error) { HostsPath: inspect.HostsPath, LogPath: l.LogPath(), Node: nil, - Name: l.Name(), + Name: fmt.Sprintf("/%s", l.Name()), RestartCount: 0, Driver: inspect.Driver, Platform: "linux", diff --git a/pkg/api/handlers/utils/containers.go b/pkg/api/handlers/utils/containers.go index 2c986db3a..bbe4cee3c 100644 --- a/pkg/api/handlers/utils/containers.go +++ b/pkg/api/handlers/utils/containers.go @@ -1,77 +1,33 @@ package utils import ( - "fmt" + "context" "net/http" - "syscall" "time" "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" - "github.com/gorilla/mux" + createconfig "github.com/containers/libpod/pkg/spec" "github.com/gorilla/schema" "github.com/pkg/errors" ) -func KillContainer(w http.ResponseWriter, r *http.Request) (*libpod.Container, error) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decorder").(*schema.Decoder) - query := struct { - Signal syscall.Signal `schema:"signal"` - }{ - Signal: syscall.SIGKILL, - } - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return nil, err - } - name := mux.Vars(r)["name"] - con, err := runtime.LookupContainer(name) - if err != nil { - ContainerNotFound(w, name, err) - return nil, err - } - - state, err := con.State() - if err != nil { - InternalServerError(w, err) - return con, err - } - - // If the Container is stopped already, send a 409 - if state == define.ContainerStateStopped || state == define.ContainerStateExited { - Error(w, fmt.Sprintf("Container %s is not running", name), http.StatusConflict, errors.New(fmt.Sprintf("Cannot kill Container %s, it is not running", name))) - return con, err - } - - err = con.Kill(uint(query.Signal)) - if err != nil { - Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "unable to kill Container %s", name)) - } - return con, err -} - -func RemoveContainer(w http.ResponseWriter, r *http.Request, force, vols bool) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := mux.Vars(r)["name"] - con, err := runtime.LookupContainer(name) - if err != nil { - ContainerNotFound(w, name, err) - return - } - - if err := runtime.RemoveContainer(r.Context(), con, force, vols); err != nil { - InternalServerError(w, err) - return - } - WriteResponse(w, http.StatusNoContent, "") +// 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 WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { + var ( + err error + interval time.Duration + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) - // /{version}/containers/(name)/restart query := struct { Interval string `schema:"interval"` Condition string `schema:"condition"` @@ -82,25 +38,34 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return 0, err } - - if len(query.Condition) > 0 { - UnSupportedParameter("condition") + if _, found := r.URL.Query()["interval"]; found { + interval, err = time.ParseDuration(query.Interval) + if err != nil { + InternalServerError(w, err) + return 0, err + } + } else { + interval, err = time.ParseDuration("250ms") + if err != nil { + InternalServerError(w, err) + return 0, err + } } - - name := mux.Vars(r)["name"] + condition := define.ContainerStateStopped + if _, found := r.URL.Query()["condition"]; found { + condition, err = define.StringToContainerStatus(query.Condition) + if err != nil { + InternalServerError(w, err) + return 0, err + } + } + name := GetName(r) con, err := runtime.LookupContainer(name) if err != nil { ContainerNotFound(w, name, err) return 0, err } - if len(query.Interval) > 0 { - d, err := time.ParseDuration(query.Interval) - if err != nil { - Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %s for interval", query.Interval)) - } - return con.WaitWithInterval(d) - } - return con.Wait() + return con.WaitForConditionWithInterval(interval, condition) } // GenerateFilterFuncsFromMap is used to generate un-executed functions that can be used to filter @@ -120,3 +85,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/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index 9d2081cd8..8d499f40b 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -21,8 +21,9 @@ func Error(w http.ResponseWriter, apiMessage string, code int, err error) { // Log detailed message of what happened to machine running podman service log.Infof("Request Failed(%s): %s", http.StatusText(code), err.Error()) em := ErrorModel{ - Because: (errors.Cause(err)).Error(), - Message: err.Error(), + Because: (errors.Cause(err)).Error(), + Message: err.Error(), + ResponseCode: code, } WriteJSON(w, code, em) } @@ -79,6 +80,8 @@ type ErrorModel struct { // human error message, formatted for a human to read // example: human error message Message string `json:"message"` + // http response code + ResponseCode int `json:"response"` } func (e ErrorModel) Error() string { @@ -89,6 +92,10 @@ func (e ErrorModel) Cause() error { return errors.New(e.Because) } +func (e ErrorModel) Code() int { + return e.ResponseCode +} + // UnsupportedParameter logs a given param by its string name as not supported. func UnSupportedParameter(param string) { log.Infof("API parameter %q: not supported", param) diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go index f2ce26f1a..32b8c5b0a 100644 --- a/pkg/api/handlers/utils/handler.go +++ b/pkg/api/handlers/utils/handler.go @@ -5,9 +5,12 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strings" + "github.com/gorilla/mux" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -20,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") @@ -59,3 +70,18 @@ func FilterMapToString(filters map[string][]string) (string, error) { } return string(f), nil } + +func getVar(r *http.Request, k string) string { + val := mux.Vars(r)[k] + safeVal, err := url.PathUnescape(val) + if err != nil { + logrus.Error(errors.Wrapf(err, "failed to unescape mux key %s, value %s", k, val)) + return val + } + return safeVal +} + +// GetName extracts the name from the mux +func GetName(r *http.Request) string { + return getVar(r, "name") +} diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go index a0d340471..696d5f745 100644 --- a/pkg/api/handlers/utils/images.go +++ b/pkg/api/handlers/utils/images.go @@ -15,19 +15,36 @@ func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) { decoder := r.Context().Value("decoder").(*schema.Decoder) runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { - // all bool # all is currently unused + All bool Filters map[string][]string `schema:"filters"` - // digests bool # digests is currently unused + Digests bool }{ // This is where you can override the golang default value for one of fields } + // TODO I think all is implemented with a filter? + if err := decoder.Decode(&query, r.URL.Query()); err != nil { return nil, err } - var filters = []string{} - if _, found := r.URL.Query()["filters"]; found { - filters = append(filters, fmt.Sprintf("reference=%s", "")) + if _, found := r.URL.Query()["digests"]; found && query.Digests { + UnSupportedParameter("digests") + } + + if len(query.Filters) > 0 { + for k, v := range query.Filters { + for _, val := range v { + filters = append(filters, fmt.Sprintf("%s=%s", k, val)) + } + } + return runtime.ImageRuntime().GetImagesWithFilters(filters) + } else { + return runtime.ImageRuntime().GetImages() } - return runtime.ImageRuntime().GetImagesWithFilters(filters) + +} + +func GetImage(r *http.Request, name string) (*image.Image, error) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + return runtime.ImageRuntime().NewFromLocal(name) } diff --git a/pkg/api/handlers/utils/pods.go b/pkg/api/handlers/utils/pods.go new file mode 100644 index 000000000..79d1a5090 --- /dev/null +++ b/pkg/api/handlers/utils/pods.go @@ -0,0 +1,84 @@ +package utils + +import ( + "fmt" + "net/http" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/gorilla/schema" +) + +func GetPods(w http.ResponseWriter, r *http.Request) ([]*entities.ListPodsReport, error) { + var ( + lps []*entities.ListPodsReport + pods []*libpod.Pod + podErr error + ) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + All bool + Filters map[string][]string `schema:"filters"` + Digests bool + }{} + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + return nil, err + } + var filters = []string{} + if _, found := r.URL.Query()["digests"]; found && query.Digests { + UnSupportedParameter("digests") + } + + if len(query.Filters) > 0 { + for k, v := range query.Filters { + for _, val := range v { + filters = append(filters, fmt.Sprintf("%s=%s", k, val)) + } + } + filterFuncs, err := shared.GenerateFilterFunction(runtime, filters) + if err != nil { + return nil, err + } + pods, podErr = shared.FilterAllPodsWithFilterFunc(runtime, filterFuncs...) + } else { + pods, podErr = runtime.GetAllPods() + } + if podErr != nil { + return nil, podErr + } + for _, pod := range pods { + status, err := pod.GetPodStatus() + if err != nil { + return nil, err + } + ctrs, err := pod.AllContainers() + if err != nil { + return nil, err + } + lp := entities.ListPodsReport{ + Cgroup: pod.CgroupParent(), + Created: pod.CreatedTime(), + Id: pod.ID(), + Name: pod.Name(), + Namespace: pod.Namespace(), + Status: status, + } + for _, ctr := range ctrs { + state, err := ctr.State() + if err != nil { + return nil, err + } + lp.Containers = append(lp.Containers, &entities.ListPodContainer{ + Id: ctr.ID(), + Names: ctr.Name(), + Status: state.String(), + }) + } + lps = append(lps, &lp) + } + return lps, nil +} |