diff options
Diffstat (limited to 'pkg/api')
23 files changed, 624 insertions, 439 deletions
diff --git a/pkg/api/handlers/compat/changes.go b/pkg/api/handlers/compat/changes.go new file mode 100644 index 000000000..6907c487e --- /dev/null +++ b/pkg/api/handlers/compat/changes.go @@ -0,0 +1,20 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" +) + +func Changes(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + id := utils.GetName(r) + changes, err := runtime.GetDiff("", id) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteJSON(w, 200, changes) +} diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index c53af0f26..239e41af4 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -2,6 +2,7 @@ package compat import ( "encoding/binary" + "encoding/json" "fmt" "net/http" "strconv" @@ -16,6 +17,9 @@ import ( "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/signal" "github.com/containers/libpod/pkg/util" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" "github.com/gorilla/schema" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -96,7 +100,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { // TODO filters still need to be applied var list = make([]*handlers.Container, len(containers)) for i, ctnr := range containers { - api, err := handlers.LibpodToContainer(ctnr, query.Size) + api, err := LibpodToContainer(ctnr, query.Size) if err != nil { utils.InternalServerError(w, err) return @@ -126,7 +130,7 @@ func GetContainer(w http.ResponseWriter, r *http.Request) { utils.ContainerNotFound(w, name, err) return } - api, err := handlers.LibpodToContainerJSON(ctnr, query.Size) + api, err := LibpodToContainerJSON(ctnr, query.Size) if err != nil { utils.InternalServerError(w, err) return @@ -261,6 +265,7 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { var until time.Time if _, found := r.URL.Query()["until"]; found { + // FIXME: until != since but the logs backend does not yet support until. since, err = util.ParseInputTime(query.Until) if err != nil { utils.BadRequest(w, "until", query.Until, err) @@ -340,3 +345,192 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { } } } + +func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error) { + imageId, imageName := l.Image() + + 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" + } + + if sz { + if sizeRW, err = l.RWSize(); err != nil { + return nil, err + } + if sizeRootFs, err = l.RootFsSize(); err != nil { + return nil, err + } + } + + return &handlers.Container{Container: types.Container{ + ID: l.ID(), + 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, + Labels: l.Labels(), + State: stateStr, + Status: "", + HostConfig: struct { + NetworkMode string `json:",omitempty"` + }{ + "host"}, + NetworkSettings: nil, + Mounts: nil, + }, + ContainerCreateConfig: types.ContainerCreateConfig{}, + }, nil +} + +func LibpodToContainerJSON(l *libpod.Container, sz bool) (*types.ContainerJSON, error) { + _, imageName := l.Image() + inspect, err := l.Inspect(sz) + if err != nil { + return nil, err + } + i, err := json.Marshal(inspect.State) + if err != nil { + return nil, err + } + state := types.ContainerState{} + if err := json.Unmarshal(i, &state); err != nil { + return nil, err + } + + // docker considers paused to be running + if state.Paused { + state.Running = true + } + + h, err := json.Marshal(inspect.HostConfig) + if err != nil { + return nil, err + } + hc := container.HostConfig{} + if err := json.Unmarshal(h, &hc); err != nil { + return nil, err + } + g, err := json.Marshal(inspect.GraphDriver) + if err != nil { + return nil, err + } + graphDriver := types.GraphDriverData{} + if err := json.Unmarshal(g, &graphDriver); err != nil { + return nil, err + } + + cb := types.ContainerJSONBase{ + ID: l.ID(), + Created: l.CreatedTime().String(), + Path: "", + Args: nil, + State: &state, + Image: imageName, + ResolvConfPath: inspect.ResolvConfPath, + HostnamePath: inspect.HostnamePath, + HostsPath: inspect.HostsPath, + LogPath: l.LogPath(), + Node: nil, + Name: fmt.Sprintf("/%s", l.Name()), + RestartCount: 0, + Driver: inspect.Driver, + Platform: "linux", + MountLabel: inspect.MountLabel, + ProcessLabel: inspect.ProcessLabel, + AppArmorProfile: inspect.AppArmorProfile, + ExecIDs: inspect.ExecIDs, + HostConfig: &hc, + GraphDriver: graphDriver, + SizeRw: inspect.SizeRw, + SizeRootFs: &inspect.SizeRootFs, + } + + stopTimeout := int(l.StopTimeout()) + + ports := make(nat.PortSet) + for p := range inspect.HostConfig.PortBindings { + splitp := strings.Split(p, "/") + port, err := nat.NewPort(splitp[0], splitp[1]) + if err != nil { + return nil, err + } + ports[port] = struct{}{} + } + + config := container.Config{ + Hostname: l.Hostname(), + Domainname: inspect.Config.DomainName, + User: l.User(), + AttachStdin: inspect.Config.AttachStdin, + AttachStdout: inspect.Config.AttachStdout, + AttachStderr: inspect.Config.AttachStderr, + ExposedPorts: ports, + Tty: inspect.Config.Tty, + OpenStdin: inspect.Config.OpenStdin, + StdinOnce: inspect.Config.StdinOnce, + Env: inspect.Config.Env, + Cmd: inspect.Config.Cmd, + Healthcheck: nil, + ArgsEscaped: false, + Image: imageName, + Volumes: nil, + WorkingDir: l.WorkingDir(), + Entrypoint: l.Entrypoint(), + NetworkDisabled: false, + MacAddress: "", + OnBuild: nil, + Labels: l.Labels(), + StopSignal: string(l.StopSignal()), + StopTimeout: &stopTimeout, + Shell: nil, + } + + m, err := json.Marshal(inspect.Mounts) + if err != nil { + return nil, err + } + mounts := []types.MountPoint{} + if err := json.Unmarshal(m, &mounts); err != nil { + return nil, err + } + + networkSettingsDefault := types.DefaultNetworkSettings{ + EndpointID: "", + Gateway: "", + GlobalIPv6Address: "", + GlobalIPv6PrefixLen: 0, + IPAddress: "", + IPPrefixLen: 0, + IPv6Gateway: "", + MacAddress: l.Config().StaticMAC.String(), + } + + networkSettings := types.NetworkSettings{ + NetworkSettingsBase: types.NetworkSettingsBase{}, + DefaultNetworkSettings: networkSettingsDefault, + Networks: nil, + } + + c := types.ContainerJSON{ + ContainerJSONBase: &cb, + Mounts: mounts, + Config: &config, + NetworkSettings: &networkSettings, + } + return &c, nil +} diff --git a/pkg/api/handlers/compat/containers_attach.go b/pkg/api/handlers/compat/containers_attach.go index da7b5bb0c..80ad52aee 100644 --- a/pkg/api/handlers/compat/containers_attach.go +++ b/pkg/api/handlers/compat/containers_attach.go @@ -1,6 +1,7 @@ package compat import ( + "fmt" "net/http" "github.com/containers/libpod/libpod" @@ -23,7 +24,9 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { Stdin bool `schema:"stdin"` Stdout bool `schema:"stdout"` Stderr bool `schema:"stderr"` - }{} + }{ + Stream: true, + } if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, "Error parsing parameters", http.StatusBadRequest, err) return @@ -61,16 +64,9 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } - // TODO: Investigate supporting these. - // Logs replays container logs over the attach socket. - // Stream seems to break things up somehow? Not 100% clear. - if query.Logs { - utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("the logs parameter to attach is not presently supported")) - return - } - // We only support stream=true or unset - 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")) + // At least one of these must be set + if !query.Stream && !query.Logs { + utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("at least one of Logs or Stream must be set")) return } @@ -86,7 +82,13 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - if !(state == define.ContainerStateCreated || state == define.ContainerStateRunning) { + // For Docker compatibility, we need to re-initialize containers in these states. + if state == define.ContainerStateConfigured || state == define.ContainerStateExited { + if err := ctr.Init(r.Context()); err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error preparing container %s for attach", ctr.ID())) + return + } + } else if !(state == define.ContainerStateCreated || state == define.ContainerStateRunning) { utils.InternalServerError(w, errors.Wrapf(define.ErrCtrStateInvalid, "can only attach to created or running containers")) return } @@ -98,20 +100,23 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusSwitchingProtocols) - connection, buffer, err := hijacker.Hijack() if err != nil { utils.InternalServerError(w, errors.Wrapf(err, "error hijacking connection")) return } + // This header string sourced from Docker: + // https://raw.githubusercontent.com/moby/moby/b95fad8e51bd064be4f4e58a996924f343846c85/api/server/router/container/container_routes.go + // Using literally to ensure compatability with existing clients. + fmt.Fprintf(connection, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") + logrus.Debugf("Hijack for attach of container %s successful", ctr.ID()) // Perform HTTP attach. // HTTPAttach will handle everything about the connection from here on // (including closing it and writing errors to it). - if err := ctr.HTTPAttach(connection, buffer, streams, detachKeys, nil); err != nil { + if err := ctr.HTTPAttach(connection, buffer, streams, detachKeys, nil, query.Stream, query.Logs); err != nil { // We can't really do anything about errors anymore. HTTPAttach // should be writing them to the connection. logrus.Errorf("Error attaching to container %s: %v", ctr.ID(), err) diff --git a/pkg/api/handlers/compat/containers_prune.go b/pkg/api/handlers/compat/containers_prune.go index a56c3903d..b4e98ac1f 100644 --- a/pkg/api/handlers/compat/containers_prune.go +++ b/pkg/api/handlers/compat/containers_prune.go @@ -4,8 +4,9 @@ import ( "net/http" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/api/handlers" + lpfilters "github.com/containers/libpod/libpod/filters" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" "github.com/docker/docker/api/types" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -15,6 +16,7 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) { var ( delContainers []string space int64 + filterFuncs []libpod.ContainerFilter ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) @@ -26,11 +28,15 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) { 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 + for k, v := range query.Filters { + for _, val := range v { + generatedFunc, err := lpfilters.GenerateContainerFilterFuncs(k, val, runtime) + if err != nil { + utils.InternalServerError(w, err) + return + } + filterFuncs = append(filterFuncs, generatedFunc) + } } prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) if err != nil { @@ -40,14 +46,11 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) { // 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()}) + report := &entities.ContainerPruneReport{ + Err: pruneErrors, + ID: prunedContainers, } - utils.WriteResponse(w, http.StatusOK, response) + utils.WriteResponse(w, http.StatusOK, report) return } for ctrID, size := range prunedContainers { diff --git a/pkg/api/handlers/compat/events.go b/pkg/api/handlers/compat/events.go index 0f72ef328..8ef32716d 100644 --- a/pkg/api/handlers/compat/events.go +++ b/pkg/api/handlers/compat/events.go @@ -1,7 +1,6 @@ package compat import ( - "encoding/json" "fmt" "net/http" @@ -10,6 +9,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/gorilla/schema" + jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -48,14 +48,27 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { }() if eventsError != nil { utils.InternalServerError(w, eventsError) + close(eventChannel) return } - coder := json.NewEncoder(w) - coder.SetEscapeHTML(true) + // If client disappears we need to stop listening for events + go func(done <-chan struct{}) { + <-done + close(eventChannel) + }(r.Context().Done()) + // Headers need to be written out before turning Writer() over to json encoder w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + json := jsoniter.ConfigCompatibleWithStandardLibrary + coder := json.NewEncoder(w) + coder.SetEscapeHTML(true) + for event := range eventChannel { e := handlers.EventToApiEvent(event) if err := coder.Encode(e); err != nil { diff --git a/pkg/api/handlers/compat/images_search.go b/pkg/api/handlers/compat/images_search.go index 7283b22c4..8da685527 100644 --- a/pkg/api/handlers/compat/images_search.go +++ b/pkg/api/handlers/compat/images_search.go @@ -57,6 +57,7 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { Filter: filter, Limit: query.Limit, } + results, err := image.SearchImages(query.Term, options) if err != nil { utils.BadRequest(w, "term", query.Term, err) diff --git a/pkg/api/handlers/compat/swagger.go b/pkg/api/handlers/compat/swagger.go index cbd8e61fb..ce83aa32f 100644 --- a/pkg/api/handlers/compat/swagger.go +++ b/pkg/api/handlers/compat/swagger.go @@ -1,7 +1,8 @@ package compat import ( - "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/storage/pkg/archive" ) // Create container @@ -9,7 +10,7 @@ import ( type swagCtrCreateResponse struct { // in:body Body struct { - utils.ContainerCreateResponse + entities.ContainerCreateResponse } } @@ -25,3 +26,12 @@ type swagCtrWaitResponse struct { } } } + +// Object Changes +// swagger:response Changes +type swagChangesResponse struct { + // in:body + Body struct { + Changes []archive.Change + } +} diff --git a/pkg/api/handlers/compat/unsupported.go b/pkg/api/handlers/compat/unsupported.go index d9c3c3f49..55660882f 100644 --- a/pkg/api/handlers/compat/unsupported.go +++ b/pkg/api/handlers/compat/unsupported.go @@ -4,6 +4,8 @@ import ( "fmt" "net/http" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/api/handlers/utils" log "github.com/sirupsen/logrus" ) @@ -13,5 +15,5 @@ func UnsupportedHandler(w http.ResponseWriter, r *http.Request) { log.Infof("Request Failed: %s", msg) utils.WriteJSON(w, http.StatusInternalServerError, - utils.ErrorModel{Message: msg}) + entities.ErrorModel{Message: msg}) } diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 5cbfb11eb..3902bdc9b 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -32,10 +32,6 @@ func ContainerExists(w http.ResponseWriter, r *http.Request) { } func ListContainers(w http.ResponseWriter, r *http.Request) { - var ( - //filterFuncs []libpod.ContainerFilter - //pss []entities.ListContainer - ) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { All bool `schema:"all"` @@ -54,10 +50,10 @@ 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 := entities.ContainerListOptions{ All: query.All, + Filters: query.Filters, Last: query.Last, Size: query.Size, Sort: "", @@ -285,3 +281,23 @@ func Restore(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, entities.RestoreReport{Id: ctr.ID()}) } + +func InitContainer(w http.ResponseWriter, r *http.Request) { + name := utils.GetName(r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + err = ctr.Init(r.Context()) + if errors.Cause(err) == define.ErrCtrStateInvalid { + utils.Error(w, "container already initialized", http.StatusNotModified, err) + return + } + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/libpod/containers_create.go b/pkg/api/handlers/libpod/containers_create.go index 38a341a89..f64132d55 100644 --- a/pkg/api/handlers/libpod/containers_create.go +++ b/pkg/api/handlers/libpod/containers_create.go @@ -4,6 +4,8 @@ import ( "encoding/json" "net/http" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/specgen" @@ -29,6 +31,6 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - response := utils.ContainerCreateResponse{ID: ctr.ID()} + response := entities.ContainerCreateResponse{ID: ctr.ID()} utils.WriteJSON(w, http.StatusCreated, response) } diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 850de4598..284b33637 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -111,7 +111,7 @@ func GetImages(w http.ResponseWriter, r *http.Request) { return } // libpod has additional fields that we need to populate. - is.Created = img.Created().Unix() + is.Created = img.Created() is.ReadOnly = img.IsReadOnly() summaries[j] = is } @@ -645,3 +645,56 @@ func UntagImage(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusCreated, "") } + +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 []string `json:"filters"` + TLSVerify bool `json:"tlsVerify"` + }{ + // 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 + } + + options := image.SearchOptions{ + Limit: query.Limit, + } + if _, found := r.URL.Query()["tlsVerify"]; found { + options.InsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) + } + + if _, found := r.URL.Query()["filters"]; found { + filter, err := image.ParseSearchFilter(query.Filters) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse filters parameter for %s", r.URL.String())) + return + } + options.Filter = *filter + } + + searchResults, err := image.SearchImages(query.Term, options) + if err != nil { + utils.BadRequest(w, "term", query.Term, err) + return + } + // Convert from image.SearchResults to entities.ImageSearchReport. We don't + // want to leak any low-level packages into the remote client, which + // requires converting. + reports := make([]entities.ImageSearchReport, len(searchResults)) + for i := range searchResults { + reports[i].Index = searchResults[i].Index + reports[i].Name = searchResults[i].Name + reports[i].Description = searchResults[i].Index + reports[i].Stars = searchResults[i].Stars + reports[i].Official = searchResults[i].Official + reports[i].Automated = searchResults[i].Automated + } + + utils.WriteResponse(w, http.StatusOK, reports) +} diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index a890169a1..92556bb61 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -12,6 +12,7 @@ import ( "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/specgen/generate" "github.com/containers/libpod/pkg/util" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -27,7 +28,7 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Failed to decode specgen", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) return } - pod, err := psg.MakePod(runtime) + pod, err := generate.MakePod(&psg, runtime) if err != nil { http_code := http.StatusInternalServerError if errors.Cause(err) == define.ErrPodExists { @@ -73,8 +74,9 @@ func PodInspect(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } + report := entities.PodInspectReport{ - PodInspect: podData, + InspectPodData: podData, } utils.WriteResponse(w, http.StatusOK, report) } diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 5a6fc021e..18c561a0d 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -4,12 +4,12 @@ import ( "encoding/json" "net/http" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/filters" + "github.com/containers/libpod/pkg/domain/infra/abi/parse" "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -46,7 +46,7 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label)) } if len(input.Options) > 0 { - parsedOptions, err := shared.ParseVolumeOptions(input.Options) + parsedOptions, err := parse.ParseVolumeOptions(input.Options) if err != nil { utils.InternalServerError(w, err) return diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger/swagger.go index 33a9fdd58..ba97a4755 100644 --- a/pkg/api/handlers/swagger.go +++ b/pkg/api/handlers/swagger/swagger.go @@ -1,9 +1,10 @@ -package handlers +package swagger import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/inspect" "github.com/docker/docker/api/types" @@ -14,7 +15,7 @@ import ( type swagHistory struct { // in:body Body struct { - HistoryResponse + handlers.HistoryResponse } } @@ -23,7 +24,7 @@ type swagHistory struct { type swagImageInspect struct { // in:body Body struct { - ImageInspect + handlers.ImageInspect } } @@ -45,7 +46,7 @@ type swagLibpodImagesImportResponse struct { // swagger:response DocsLibpodImagesPullResponse type swagLibpodImagesPullResponse struct { // in:body - Body LibpodImagesPullReport + Body handlers.LibpodImagesPullReport } // Delete response @@ -77,14 +78,14 @@ type swagLibpodInspectImageResponse struct { // swagger:response DocsContainerPruneReport type swagContainerPruneReport struct { // in: body - Body []ContainersPruneReport + Body []handlers.ContainersPruneReport } // Prune containers // swagger:response DocsLibpodPruneResponse type swagLibpodContainerPruneReport struct { // in: body - Body []LibpodContainersPruneReport + Body []handlers.LibpodContainersPruneReport } // Inspect container @@ -101,7 +102,7 @@ type swagContainerInspectResponse struct { type swagContainerTopResponse struct { // in:body Body struct { - ContainerTopOKBody + handlers.ContainerTopOKBody } } @@ -110,7 +111,7 @@ type swagContainerTopResponse struct { type swagPodTopResponse struct { // in:body Body struct { - PodTopOKBody + handlers.PodTopOKBody } } @@ -153,6 +154,6 @@ type swagInspectVolumeResponse struct { type swagImageTreeResponse struct { // in:body Body struct { - ImageTreeResponse + handlers.ImageTreeResponse } } diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index f1c932ebc..4c081cf85 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -5,12 +5,9 @@ import ( "encoding/json" "fmt" "strconv" - "strings" "time" "github.com/containers/image/v5/manifest" - "github.com/containers/libpod/libpod" - "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" @@ -146,10 +143,6 @@ type PodCreateConfig struct { Share string `json:"share"` } -type ErrorModel struct { - Message string `json:"message"` -} - type Event struct { dockerEvents.Message } @@ -180,6 +173,31 @@ type ExecCreateResponse struct { docker.IDResponse } +func (e *Event) ToLibpodEvent() *events.Event { + exitCode, err := strconv.Atoi(e.Actor.Attributes["containerExitCode"]) + if err != nil { + return nil + } + status, err := events.StringToStatus(e.Action) + if err != nil { + return nil + } + t, err := events.StringToType(e.Type) + if err != nil { + return nil + } + lp := events.Event{ + ContainerExitCode: exitCode, + ID: e.Actor.ID, + Image: e.Actor.Attributes["image"], + Name: e.Actor.Attributes["name"], + Status: status, + Time: time.Unix(e.Time, e.TimeNano), + Type: t, + } + return &lp +} + func EventToApiEvent(e *events.Event) *Event { return &Event{dockerEvents.Message{ Type: e.Type.String(), @@ -244,7 +262,7 @@ func ImageToImageSummary(l *libpodImage.Image) (*entities.ImageSummary, error) { ID: l.ID(), ParentId: l.Parent, RepoTags: repoTags, - Created: l.Created().Unix(), + Created: l.Created(), Size: int64(*size), SharedSize: 0, VirtualSize: l.VirtualSize, @@ -353,195 +371,6 @@ func ImageDataToImageInspect(ctx context.Context, l *libpodImage.Image) (*ImageI } -func LibpodToContainer(l *libpod.Container, sz bool) (*Container, error) { - imageId, imageName := l.Image() - - 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" - } - - 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{fmt.Sprintf("/%s", l.Name())}, - Image: imageName, - ImageID: imageId, - Command: strings.Join(l.Command(), " "), - Created: l.CreatedTime().Unix(), - Ports: nil, - SizeRw: sizeRW, - SizeRootFs: sizeRootFs, - Labels: l.Labels(), - State: stateStr, - Status: "", - HostConfig: struct { - NetworkMode string `json:",omitempty"` - }{ - "host"}, - NetworkSettings: nil, - Mounts: nil, - }, - docker.ContainerCreateConfig{}, - }, nil -} - -func LibpodToContainerJSON(l *libpod.Container, sz bool) (*docker.ContainerJSON, error) { - _, imageName := l.Image() - inspect, err := l.Inspect(sz) - if err != nil { - return nil, err - } - i, err := json.Marshal(inspect.State) - if err != nil { - return nil, err - } - state := docker.ContainerState{} - if err := json.Unmarshal(i, &state); err != nil { - return nil, err - } - - // docker considers paused to be running - if state.Paused { - state.Running = true - } - - h, err := json.Marshal(inspect.HostConfig) - if err != nil { - return nil, err - } - hc := dockerContainer.HostConfig{} - if err := json.Unmarshal(h, &hc); err != nil { - return nil, err - } - g, err := json.Marshal(inspect.GraphDriver) - if err != nil { - return nil, err - } - graphDriver := docker.GraphDriverData{} - if err := json.Unmarshal(g, &graphDriver); err != nil { - return nil, err - } - - cb := docker.ContainerJSONBase{ - ID: l.ID(), - Created: l.CreatedTime().String(), - Path: "", - Args: nil, - State: &state, - Image: imageName, - ResolvConfPath: inspect.ResolvConfPath, - HostnamePath: inspect.HostnamePath, - HostsPath: inspect.HostsPath, - LogPath: l.LogPath(), - Node: nil, - Name: fmt.Sprintf("/%s", l.Name()), - RestartCount: 0, - Driver: inspect.Driver, - Platform: "linux", - MountLabel: inspect.MountLabel, - ProcessLabel: inspect.ProcessLabel, - AppArmorProfile: inspect.AppArmorProfile, - ExecIDs: inspect.ExecIDs, - HostConfig: &hc, - GraphDriver: graphDriver, - SizeRw: inspect.SizeRw, - SizeRootFs: &inspect.SizeRootFs, - } - - stopTimeout := int(l.StopTimeout()) - - ports := make(nat.PortSet) - for p := range inspect.HostConfig.PortBindings { - splitp := strings.Split(p, "/") - port, err := nat.NewPort(splitp[0], splitp[1]) - if err != nil { - return nil, err - } - ports[port] = struct{}{} - } - - config := dockerContainer.Config{ - Hostname: l.Hostname(), - Domainname: inspect.Config.DomainName, - User: l.User(), - AttachStdin: inspect.Config.AttachStdin, - AttachStdout: inspect.Config.AttachStdout, - AttachStderr: inspect.Config.AttachStderr, - ExposedPorts: ports, - Tty: inspect.Config.Tty, - OpenStdin: inspect.Config.OpenStdin, - StdinOnce: inspect.Config.StdinOnce, - Env: inspect.Config.Env, - Cmd: inspect.Config.Cmd, - Healthcheck: nil, - ArgsEscaped: false, - Image: imageName, - Volumes: nil, - WorkingDir: l.WorkingDir(), - Entrypoint: l.Entrypoint(), - NetworkDisabled: false, - MacAddress: "", - OnBuild: nil, - Labels: l.Labels(), - StopSignal: string(l.StopSignal()), - StopTimeout: &stopTimeout, - Shell: nil, - } - - m, err := json.Marshal(inspect.Mounts) - if err != nil { - return nil, err - } - mounts := []docker.MountPoint{} - if err := json.Unmarshal(m, &mounts); err != nil { - return nil, err - } - - networkSettingsDefault := docker.DefaultNetworkSettings{ - EndpointID: "", - Gateway: "", - GlobalIPv6Address: "", - GlobalIPv6PrefixLen: 0, - IPAddress: "", - IPPrefixLen: 0, - IPv6Gateway: "", - MacAddress: l.Config().StaticMAC.String(), - } - - networkSettings := docker.NetworkSettings{ - NetworkSettingsBase: docker.NetworkSettingsBase{}, - DefaultNetworkSettings: networkSettingsDefault, - Networks: nil, - } - - c := docker.ContainerJSON{ - ContainerJSONBase: &cb, - Mounts: mounts, - Config: &config, - NetworkSettings: &networkSettings, - } - return &c, nil -} - // portsToPortSet converts libpods exposed ports to dockers structs func portsToPortSet(input map[string]struct{}) (nat.PortSet, error) { ports := make(nat.PortSet) diff --git a/pkg/api/handlers/utils/containers.go b/pkg/api/handlers/utils/containers.go index bbe4cee3c..a46b308b5 100644 --- a/pkg/api/handlers/utils/containers.go +++ b/pkg/api/handlers/utils/containers.go @@ -5,22 +5,14 @@ import ( "net/http" "time" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" createconfig "github.com/containers/libpod/pkg/spec" "github.com/gorilla/schema" "github.com/pkg/errors" ) -// ContainerCreateResponse is the response struct for creating a container -type ContainerCreateResponse struct { - // ID of the container created - ID string `json:"Id"` - // Warnings during container creation - Warnings []string `json:"Warnings"` -} - func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { var ( err error @@ -68,33 +60,15 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { return con.WaitForConditionWithInterval(interval, condition) } -// GenerateFilterFuncsFromMap is used to generate un-executed functions that can be used to filter -// containers. It is specifically designed for the RESTFUL API input. -func GenerateFilterFuncsFromMap(r *libpod.Runtime, filters map[string][]string) ([]libpod.ContainerFilter, error) { - var ( - filterFuncs []libpod.ContainerFilter - ) - for k, v := range filters { - for _, val := range v { - f, err := shared.GenerateContainerFilterFuncs(k, val, r) - if err != nil { - return filterFuncs, err - } - filterFuncs = append(filterFuncs, f) - } - } - 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) + ctr, err := createconfig.CreateContainerFromCreateConfig(runtime, cc, ctx, pod) if err != nil { Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "CreateContainerFromCreateConfig()")) return } - response := ContainerCreateResponse{ + response := entities.ContainerCreateResponse{ ID: ctr.ID(), Warnings: []string{}} diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index 8d499f40b..aafc64353 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -20,7 +21,7 @@ var ( 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{ + em := entities.ErrorModel{ Because: (errors.Cause(err)).Error(), Message: err.Error(), ResponseCode: code, @@ -73,29 +74,6 @@ func BadRequest(w http.ResponseWriter, key string, value string, err error) { Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, e) } -type ErrorModel struct { - // API root cause formatted for automated parsing - // example: API root cause - Because string `json:"cause"` - // 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 { - return e.Message -} - -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/pods.go b/pkg/api/handlers/utils/pods.go index d47053eda..fb795fa6a 100644 --- a/pkg/api/handlers/utils/pods.go +++ b/pkg/api/handlers/utils/pods.go @@ -1,20 +1,19 @@ package utils import ( - "fmt" "net/http" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" + lpfilters "github.com/containers/libpod/libpod/filters" "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 + lps []*entities.ListPodsReport + pods []*libpod.Pod + filters []libpod.PodFilter ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) @@ -28,28 +27,24 @@ func GetPods(w http.ResponseWriter, r *http.Request) ([]*entities.ListPodsReport 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)) + for k, v := range query.Filters { + for _, filter := range v { + f, err := lpfilters.GeneratePodFilterFunc(k, filter) + if err != nil { + return nil, err } + filters = append(filters, f) } - 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 + pods, err := runtime.Pods(filters...) + if err != nil { + return nil, err } + for _, pod := range pods { status, err := pod.GetPodStatus() if err != nil { diff --git a/pkg/api/server/handler_api.go b/pkg/api/server/handler_api.go index 30a1680c9..7a7db12f3 100644 --- a/pkg/api/server/handler_api.go +++ b/pkg/api/server/handler_api.go @@ -19,7 +19,7 @@ func (s *APIServer) APIHandler(h http.HandlerFunc) http.HandlerFunc { if err != nil { buf := make([]byte, 1<<20) n := runtime.Stack(buf, true) - log.Warnf("Recovering from podman handler panic: %v, %s", err, buf[:n]) + log.Warnf("Recovering from API handler panic: %v, %s", err, buf[:n]) // Try to inform client things went south... won't work if handler already started writing response body utils.InternalServerError(w, fmt.Errorf("%v", err)) } @@ -27,12 +27,7 @@ func (s *APIServer) APIHandler(h http.HandlerFunc) http.HandlerFunc { // Wrapper to hide some boiler plate fn := func(w http.ResponseWriter, r *http.Request) { - // Connection counting, ugh. Needed to support the sliding window for idle checking. - s.ConnectionCh <- EnterHandler - defer func() { s.ConnectionCh <- ExitHandler }() - - log.Debugf("APIHandler -- Method: %s URL: %s (conn %d/%d)", - r.Method, r.URL.String(), s.ActiveConnections, s.TotalConnections) + log.Debugf("APIHandler -- Method: %s URL: %s", r.Method, r.URL.String()) if err := r.ParseForm(); err != nil { log.Infof("Failed Request: unable to parse form: %q", err) diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index f126112d0..378d1e06c 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -517,13 +517,13 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // name: logs // required: false // type: boolean - // description: Not yet supported + // description: Stream all logs from the container across the connection. Happens before streaming attach (if requested). At least one of logs or stream must be set // - in: query // name: stream // required: false // type: boolean // default: true - // description: If passed, must be set to true; stream=false is not yet supported + // description: Attach to the container. If unset, and logs is set, only the container's logs will be sent. At least one of stream or logs must be set // - in: query // name: stdout // required: false @@ -955,7 +955,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // "$ref": "#/responses/NoSuchContainer" // 500: // "$ref": "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/pause"), s.APIHandler(compat.PauseContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/pause"), s.APIHandler(compat.PauseContainer)).Methods(http.MethodPost) // swagger:operation POST /libpod/containers/{name}/restart libpod libpodRestartContainer // --- // tags: @@ -1194,13 +1194,13 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // name: logs // required: false // type: boolean - // description: Not yet supported + // description: Stream all logs from the container across the connection. Happens before streaming attach (if requested). At least one of logs or stream must be set // - in: query // name: stream // required: false // type: boolean // default: true - // description: If passed, must be set to true; stream=false is not yet supported + // description: Attach to the container. If unset, and logs is set, only the container's logs will be sent. At least one of stream or logs must be set // - in: query // name: stdout // required: false @@ -1282,7 +1282,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/export"), s.APIHandler(compat.ExportContainer)).Methods(http.MethodGet) - // swagger:operation GET /libpod/containers/{name}/checkout libpod libpodCheckpointContainer + // swagger:operation POST /libpod/containers/{name}/checkpoint libpod libpodCheckpointContainer // --- // tags: // - containers @@ -1323,7 +1323,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/checkpoint"), s.APIHandler(libpod.Checkpoint)).Methods(http.MethodPost) - // swagger:operation GET /libpod/containers/{name} restore libpod libpodRestoreContainer + // swagger:operation POST /libpod/containers/{name}/restore libpod libpodRestoreContainer // --- // tags: // - containers @@ -1377,5 +1377,62 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/restore"), s.APIHandler(libpod.Restore)).Methods(http.MethodPost) + // swagger:operation GET /containers/{name}/changes libpod libpodChangesContainer + // swagger:operation GET /libpod/containers/{name}/changes compat changesContainer + // --- + // tags: + // - containers + // - containers (compat) + // summary: Report on changes to container's filesystem; adds, deletes or modifications. + // description: | + // Returns which files in a container's filesystem have been added, deleted, or modified. The Kind of modification can be one of: + // + // 0: Modified + // 1: Added + // 2: Deleted + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or id of the container + // responses: + // 200: + // description: Array of Changes + // content: + // application/json: + // schema: + // $ref: "#/responses/Changes" + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name}/changes"), s.APIHandler(compat.Changes)).Methods(http.MethodGet) + r.HandleFunc("/containers/{name}/changes", s.APIHandler(compat.Changes)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/changes"), s.APIHandler(compat.Changes)).Methods(http.MethodGet) + // swagger:operation POST /libpod/containers/{name}/init libpod libpodInitContainer + // --- + // tags: + // - containers + // summary: Initialize a container + // description: Performs all tasks necessary for initializing the container but does not start the container. + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // 204: + // description: no error + // 304: + // description: container already initialized + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/init"), s.APIHandler(libpod.InitContainer)).Methods(http.MethodPost) return nil } diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index d45423096..6cc6f0cfa 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -919,7 +919,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/DocsSearchResponse" // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(compat.SearchImages)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(libpod.SearchImages)).Methods(http.MethodGet) // swagger:operation DELETE /libpod/images/{name:.*} libpod libpodRemoveImage // --- // tags: @@ -1125,5 +1125,36 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/{name:.*}/untag"), s.APIHandler(libpod.UntagImage)).Methods(http.MethodPost) + + // swagger:operation GET /libpod/images/{name}/changes libpod libpodChangesImages + // --- + // tags: + // - images + // summary: Report on changes to images's filesystem; adds, deletes or modifications. + // description: | + // Returns which files in a images's filesystem have been added, deleted, or modified. The Kind of modification can be one of: + // + // 0: Modified + // 1: Added + // 2: Deleted + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or id of the container + // responses: + // 200: + // description: Array of Changes + // content: + // application/json: + // schema: + // $ref: "#/responses/Changes" + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/images/{name}/changes"), s.APIHandler(compat.Changes)).Methods(http.MethodGet) + return nil } diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 59f1f95cb..5f1a86183 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -2,11 +2,14 @@ package server import ( "context" + "log" "net" "net/http" "os" "os/signal" + "runtime" "strings" + "sync" "syscall" "time" @@ -20,26 +23,19 @@ import ( ) type APIServer struct { - http.Server // The HTTP work happens here - *schema.Decoder // Decoder for Query parameters to structs - context.Context // Context to carry objects to handlers - *libpod.Runtime // Where the real work happens - net.Listener // mux for routing HTTP API calls to libpod routines - context.CancelFunc // Stop APIServer - *time.Timer // Hold timer for sliding window - time.Duration // Duration of client access sliding window - ActiveConnections uint64 // Number of handlers holding a connection - TotalConnections uint64 // Number of connections handled - ConnectionCh chan int // Channel for signalling handler enter/exit + http.Server // The HTTP work happens here + *schema.Decoder // Decoder for Query parameters to structs + context.Context // Context to carry objects to handlers + *libpod.Runtime // Where the real work happens + net.Listener // mux for routing HTTP API calls to libpod routines + context.CancelFunc // Stop APIServer + idleTracker *IdleTracker // Track connections to support idle shutdown } // Number of seconds to wait for next request, if exceeded shutdown server const ( DefaultServiceDuration = 300 * time.Second UnlimitedServiceDuration = 0 * time.Second - EnterHandler = 1 - ExitHandler = -1 - NOOPHandler = 0 ) // NewServer will create and configure a new API server with all defaults @@ -56,7 +52,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li // If listener not provided try socket activation protocol if listener == nil { if _, found := os.LookupEnv("LISTEN_FDS"); !found { - return nil, errors.Errorf("Cannot create Server, no listener provided and socket activation protocol is not active.") + return nil, errors.Errorf("Cannot create API Server, no listener provided and socket activation protocol is not active.") } listeners, err := activation.Listeners() @@ -70,17 +66,20 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li } router := mux.NewRouter().UseEncodedPath() + idle := NewIdleTracker(duration) + server := APIServer{ Server: http.Server{ Handler: router, ReadHeaderTimeout: 20 * time.Second, IdleTimeout: duration, + ConnState: idle.ConnState, + ErrorLog: log.New(logrus.StandardLogger().Out, "", 0), }, - Decoder: handlers.NewAPIDecoder(), - Runtime: runtime, - Listener: *listener, - Duration: duration, - ConnectionCh: make(chan int), + Decoder: handlers.NewAPIDecoder(), + idleTracker: idle, + Listener: *listener, + Runtime: runtime, } router.NotFoundHandler = http.HandlerFunc( @@ -120,11 +119,11 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li router.Walk(func(route *mux.Route, r *mux.Router, ancestors []*mux.Route) error { // nolint path, err := route.GetPathTemplate() if err != nil { - path = "" + path = "<N/A>" } methods, err := route.GetMethods() if err != nil { - methods = []string{} + methods = []string{"<N/A>"} } logrus.Debugf("Methods: %s Path: %s", strings.Join(methods, ", "), path) return nil @@ -136,24 +135,20 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li // Serve starts responding to HTTP requests func (s *APIServer) Serve() error { - // This is initialized here as Timer is not needed until Serve'ing - if s.Duration > 0 { - s.Timer = time.AfterFunc(s.Duration, func() { - s.ConnectionCh <- NOOPHandler - }) - go s.ReadChannelWithTimeout() - } else { - go s.ReadChannelNoTimeout() - } - sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) errChan := make(chan error, 1) go func() { + <-s.idleTracker.Done() + logrus.Debugf("API Server idle for %v", s.idleTracker.Duration) + _ = s.Shutdown() + }() + + go func() { err := s.Server.Serve(s.Listener) if err != nil && err != http.ErrServerClosed { - errChan <- errors.Wrap(err, "Failed to start APIServer") + errChan <- errors.Wrap(err, "failed to start API server") return } errChan <- nil @@ -169,72 +164,30 @@ func (s *APIServer) Serve() error { return nil } -func (s *APIServer) ReadChannelWithTimeout() { - // stalker to count the connections. Should the timer expire it will shutdown the service. - for delta := range s.ConnectionCh { - switch delta { - case EnterHandler: - s.Timer.Stop() - s.ActiveConnections += 1 - s.TotalConnections += 1 - case ExitHandler: - s.Timer.Stop() - s.ActiveConnections -= 1 - if s.ActiveConnections == 0 { - // Server will be shutdown iff the timer expires before being reset or stopped - s.Timer = time.AfterFunc(s.Duration, func() { - if err := s.Shutdown(); err != nil { - logrus.Errorf("Failed to shutdown APIServer: %v", err) - os.Exit(1) - } - }) - } else { - s.Timer.Reset(s.Duration) - } - case NOOPHandler: - // push the check out another duration... - s.Timer.Reset(s.Duration) - default: - logrus.Warnf("ConnectionCh received unsupported input %d", delta) - } - } -} - -func (s *APIServer) ReadChannelNoTimeout() { - // stalker to count the connections. - for delta := range s.ConnectionCh { - switch delta { - case EnterHandler: - s.ActiveConnections += 1 - s.TotalConnections += 1 - case ExitHandler: - s.ActiveConnections -= 1 - case NOOPHandler: - default: - logrus.Warnf("ConnectionCh received unsupported input %d", delta) - } - } -} - // Shutdown is a clean shutdown waiting on existing clients func (s *APIServer) Shutdown() error { + if logrus.IsLevelEnabled(logrus.DebugLevel) { + _, file, line, _ := runtime.Caller(1) + logrus.Debugf("APIServer.Shutdown by %s:%d, %d/%d connection(s)", + file, line, s.idleTracker.ActiveConnections(), s.idleTracker.TotalConnections()) + } + // Duration == 0 flags no auto-shutdown of the server - if s.Duration == 0 { + if s.idleTracker.Duration == 0 { logrus.Debug("APIServer.Shutdown ignored as Duration == 0") return nil } - logrus.Debugf("APIServer.Shutdown called %v, conn %d/%d", time.Now(), s.ActiveConnections, s.TotalConnections) - // Gracefully shutdown server - ctx, cancel := context.WithTimeout(context.Background(), s.Duration) + // Gracefully shutdown server, duration of wait same as idle window + ctx, cancel := context.WithTimeout(context.Background(), s.idleTracker.Duration) defer cancel() - go func() { err := s.Server.Shutdown(ctx) if err != nil && err != context.Canceled && err != http.ErrServerClosed { logrus.Errorf("Failed to cleanly shutdown APIServer: %s", err.Error()) } }() + <-ctx.Done() return nil } @@ -242,3 +195,55 @@ func (s *APIServer) Shutdown() error { func (s *APIServer) Close() error { return s.Server.Close() } + +type IdleTracker struct { + active map[net.Conn]struct{} + total int + mux sync.Mutex + timer *time.Timer + Duration time.Duration +} + +func NewIdleTracker(idle time.Duration) *IdleTracker { + return &IdleTracker{ + active: make(map[net.Conn]struct{}), + Duration: idle, + timer: time.NewTimer(idle), + } +} + +func (t *IdleTracker) ConnState(conn net.Conn, state http.ConnState) { + t.mux.Lock() + defer t.mux.Unlock() + + oldActive := len(t.active) + logrus.Debugf("IdleTracker %p:%v %d/%d connection(s)", conn, state, t.ActiveConnections(), t.TotalConnections()) + switch state { + case http.StateNew, http.StateActive, http.StateHijacked: + t.active[conn] = struct{}{} + // stop the timer if we transitioned from idle + if oldActive == 0 { + t.timer.Stop() + } + t.total += 1 + case http.StateIdle, http.StateClosed: + delete(t.active, conn) + // Restart the timer if we've become idle + if oldActive > 0 && len(t.active) == 0 { + t.timer.Stop() + t.timer.Reset(t.Duration) + } + } +} + +func (t *IdleTracker) ActiveConnections() int { + return len(t.active) +} + +func (t *IdleTracker) TotalConnections() int { + return t.total +} + +func (t *IdleTracker) Done() <-chan time.Time { + return t.timer.C +} diff --git a/pkg/api/server/swagger.go b/pkg/api/server/swagger.go index 2433a6a05..75dcc71a6 100644 --- a/pkg/api/server/swagger.go +++ b/pkg/api/server/swagger.go @@ -3,7 +3,6 @@ package server import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/domain/entities" ) @@ -12,7 +11,7 @@ import ( type swagErrNoSuchImage struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -21,7 +20,7 @@ type swagErrNoSuchImage struct { type swagErrNoSuchContainer struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -30,7 +29,7 @@ type swagErrNoSuchContainer struct { type swagErrNoSuchExecInstance struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -39,7 +38,7 @@ type swagErrNoSuchExecInstance struct { type swagErrNoSuchVolume struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -48,7 +47,7 @@ type swagErrNoSuchVolume struct { type swagErrNoSuchPod struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -57,7 +56,7 @@ type swagErrNoSuchPod struct { type swagErrNoSuchManifest struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -66,7 +65,7 @@ type swagErrNoSuchManifest struct { type swagInternalError struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -75,7 +74,7 @@ type swagInternalError struct { type swagConflictError struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -84,7 +83,7 @@ type swagConflictError struct { type swagBadParamError struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -93,7 +92,7 @@ type swagBadParamError struct { type swagContainerAlreadyStartedError struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -102,7 +101,7 @@ type swagContainerAlreadyStartedError struct { type swagContainerAlreadyStopped struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -111,7 +110,7 @@ type swagContainerAlreadyStopped struct { type swagPodAlreadyStartedError struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } @@ -120,7 +119,7 @@ type swagPodAlreadyStartedError struct { type swagPodAlreadyStopped struct { // in:body Body struct { - utils.ErrorModel + entities.ErrorModel } } |