diff options
Diffstat (limited to 'pkg')
52 files changed, 1776 insertions, 852 deletions
diff --git a/pkg/adapter/pods.go b/pkg/adapter/pods.go index 5891c361f..f89fc7011 100644 --- a/pkg/adapter/pods.go +++ b/pkg/adapter/pods.go @@ -58,9 +58,9 @@ func (r *LocalRuntime) PrunePods(ctx context.Context, cli *cliconfig.PodPruneVal } logrus.Debugf("Setting maximum rm workers to %d", maxWorkers) - states := []string{shared.PodStateStopped, shared.PodStateExited} + states := []string{define.PodStateStopped, define.PodStateExited} if cli.Force { - states = append(states, shared.PodStateRunning) + states = append(states, define.PodStateRunning) } pods, err := r.GetPodsByStatus(states) diff --git a/pkg/adapter/pods_remote.go b/pkg/adapter/pods_remote.go index 16d34769e..5ef1a9216 100644 --- a/pkg/adapter/pods_remote.go +++ b/pkg/adapter/pods_remote.go @@ -540,9 +540,9 @@ func (r *LocalRuntime) PrunePods(ctx context.Context, cli *cliconfig.PodPruneVal ok = []string{} failures = map[string]error{} ) - states := []string{shared.PodStateStopped, shared.PodStateExited} + states := []string{define.PodStateStopped, define.PodStateExited} if cli.Force { - states = append(states, shared.PodStateRunning) + states = append(states, define.PodStateRunning) } ids, err := iopodman.GetPodsByStatus().Call(r.Conn, states) diff --git a/pkg/api/handlers/generic/containers_create.go b/pkg/api/handlers/containers_create.go index edefd5757..4781b23bc 100644 --- a/pkg/api/handlers/generic/containers_create.go +++ b/pkg/api/handlers/containers_create.go @@ -1,4 +1,4 @@ -package generic +package handlers import ( "encoding/json" @@ -10,7 +10,6 @@ import ( "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" createconfig "github.com/containers/libpod/pkg/spec" @@ -25,7 +24,7 @@ import ( func CreateContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) - input := handlers.CreateContainerConfig{} + input := CreateContainerConfig{} query := struct { Name string `schema:"name"` }{ @@ -74,13 +73,13 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { } response := ContainerCreateResponse{ - Id: ctr.ID(), + ID: ctr.ID(), Warnings: []string{}} utils.WriteResponse(w, http.StatusCreated, response) } -func makeCreateConfig(input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { +func makeCreateConfig(input CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { var ( err error init bool diff --git a/pkg/api/handlers/decoder.go b/pkg/api/handlers/decoder.go new file mode 100644 index 000000000..890d77ecc --- /dev/null +++ b/pkg/api/handlers/decoder.go @@ -0,0 +1,91 @@ +package handlers + +import ( + "encoding/json" + "reflect" + "time" + + "github.com/gorilla/schema" + "github.com/sirupsen/logrus" +) + +// NewAPIDecoder returns a configured schema.Decoder +func NewAPIDecoder() *schema.Decoder { + _ = ParseDateTime + + d := schema.NewDecoder() + d.IgnoreUnknownKeys(true) + d.RegisterConverter(map[string][]string{}, convertUrlValuesString) + d.RegisterConverter(time.Time{}, convertTimeString) + return d +} + +// On client: +// v := map[string][]string{ +// "dangling": {"true"}, +// } +// +// payload, err := jsoniter.MarshalToString(v) +// if err != nil { +// panic(err) +// } +// payload = url.QueryEscape(payload) +func convertUrlValuesString(query string) reflect.Value { + f := map[string][]string{} + + err := json.Unmarshal([]byte(query), &f) + if err != nil { + logrus.Infof("convertUrlValuesString: Failed to Unmarshal %s: %s", query, err.Error()) + } + + return reflect.ValueOf(f) +} + +// isZero() can be used to determine if parsing failed. +func convertTimeString(query string) reflect.Value { + var ( + err error + t time.Time + ) + + for _, f := range []string{ + time.UnixDate, + time.ANSIC, + time.RFC1123, + time.RFC1123Z, + time.RFC3339, + time.RFC3339Nano, + time.RFC822, + time.RFC822Z, + time.RFC850, + time.RubyDate, + time.Stamp, + time.StampMicro, + time.StampMilli, + time.StampNano, + } { + t, err = time.Parse(f, query) + if err == nil { + return reflect.ValueOf(t) + } + + if _, isParseError := err.(*time.ParseError); isParseError { + // Try next format + continue + } else { + break + } + } + + // We've exhausted all formats, or something bad happened + if err != nil { + logrus.Infof("convertTimeString: Failed to parse %s: %s", query, err.Error()) + } + return reflect.ValueOf(time.Time{}) +} + +// ParseDateTime is a helper function to aid in parsing different Time/Date formats +// isZero() can be used to determine if parsing failed. +func ParseDateTime(query string) time.Time { + return convertTimeString(query).Interface().(time.Time) +} diff --git a/pkg/api/handlers/events.go b/pkg/api/handlers/events.go index 900efa3da..44bf35254 100644 --- a/pkg/api/handlers/events.go +++ b/pkg/api/handlers/events.go @@ -1,9 +1,10 @@ package handlers import ( - "encoding/json" "fmt" "net/http" + "strings" + "time" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/pkg/errors" @@ -11,30 +12,24 @@ import ( func GetEvents(w http.ResponseWriter, r *http.Request) { query := struct { - Since string `json:"since"` - Until string `json:"until"` - Filters string `json:"filters"` + 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 filters = map[string][]string{} - if found := hasVar(r, "filters"); found { - if err := json.Unmarshal([]byte(query.Filters), &filters); err != nil { - utils.BadRequest(w, "filters", query.Filters, err) - return + 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])) } } - var libpodFilters = make([]string, len(filters)) - for k, v := range filters { - libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) - } - libpodEvents, err := getRuntime(r).GetEvents(libpodFilters) if err != nil { - utils.BadRequest(w, "filters", query.Filters, err) + utils.BadRequest(w, "filters", strings.Join(r.URL.Query()["filters"], ", "), err) return } 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/images.go b/pkg/api/handlers/generic/images.go index 395f64064..ba1cf47b4 100644 --- a/pkg/api/handlers/generic/images.go +++ b/pkg/api/handlers/generic/images.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "net/http" "os" - "strconv" "strings" "github.com/containers/buildah" @@ -16,12 +15,10 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "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) { @@ -59,17 +56,14 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { } func PruneImages(w http.ResponseWriter, r *http.Request) { - // 200 no error - // 500 internal var ( - dangling bool = true - err error + filters []string ) decoder := r.Context().Value("decoder").(*schema.Decoder) runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { - filters map[string]string + Filters map[string][]string `schema:"filters"` }{ // This is where you can override the golang default value for one of fields } @@ -79,61 +73,24 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { return } - // FIXME This is likely wrong due to it not being a map[string][]string - - // until ts is not supported on podman prune - if len(query.filters["until"]) > 0 { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "until is not supported yet")) - return - } - // labels are not supported on podman prune - if len(query.filters["label"]) > 0 { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "labelis not supported yet")) - return - } - - if len(query.filters["dangling"]) > 0 { - dangling, err = strconv.ParseBool(query.filters["dangling"]) - 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(), false, 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 exacty 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 @@ -343,8 +300,6 @@ 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")) diff --git a/pkg/api/handlers/generic/ping.go b/pkg/api/handlers/generic/ping.go index 44a67d53f..00afd86bc 100644 --- a/pkg/api/handlers/generic/ping.go +++ b/pkg/api/handlers/generic/ping.go @@ -3,6 +3,8 @@ package generic import ( "fmt" "net/http" + + "github.com/containers/libpod/pkg/api/handlers" ) func PingGET(w http.ResponseWriter, _ *http.Request) { @@ -16,7 +18,7 @@ func PingHEAD(w http.ResponseWriter, _ *http.Request) { } func setHeaders(w http.ResponseWriter) { - w.Header().Set("API-Version", DefaultApiVersion) + 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") diff --git a/pkg/api/handlers/generic/swagger.go b/pkg/api/handlers/generic/swagger.go index 27e1fc18d..bfe527c41 100644 --- a/pkg/api/handlers/generic/swagger.go +++ b/pkg/api/handlers/generic/swagger.go @@ -1,11 +1,13 @@ package generic +import "github.com/containers/libpod/pkg/api/handlers" + // Create container // swagger:response ContainerCreateResponse type swagCtrCreateResponse struct { // in:body Body struct { - ContainerCreateResponse + handlers.ContainerCreateResponse } } diff --git a/pkg/api/handlers/handler.go b/pkg/api/handlers/handler.go index 4f303f6ab..d60a5b239 100644 --- a/pkg/api/handlers/handler.go +++ b/pkg/api/handlers/handler.go @@ -15,10 +15,11 @@ 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 hasVar(r *http.Request, k string) bool { +// _, found := mux.Vars(r)[k] +// return found +// } + func getName(r *http.Request) string { return getVar(r, "name") } diff --git a/pkg/api/handlers/images.go b/pkg/api/handlers/images.go index b4acdc312..3f66a63c8 100644 --- a/pkg/api/handlers/images.go +++ b/pkg/api/handlers/images.go @@ -3,7 +3,6 @@ package handlers import ( "fmt" "io" - "io/ioutil" "net/http" "os" "strconv" @@ -127,46 +126,6 @@ func GetImage(r *http.Request, name string) (*image.Image, error) { 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 diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index df16843c7..db1cf26ff 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -19,8 +19,6 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { } 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"] _, err := runtime.LookupContainer(name) @@ -147,10 +145,6 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { // 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"] diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 0d4e220a8..9f0876618 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -1,9 +1,12 @@ package libpod import ( + "fmt" + "io" "io/ioutil" "net/http" "os" + "strconv" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/api/handlers" @@ -42,12 +45,12 @@ 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 { + // 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) @@ -90,13 +93,14 @@ func GetImages(w http.ResponseWriter, r *http.Request) { } func PruneImages(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 500 internal + var ( + all bool + err error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - All bool `schema:"all"` - Filters []string `schema:"filters"` + Filters map[string][]string `schema:"filters"` }{ // override any golang type defaults } @@ -106,7 +110,19 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - cids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, query.Filters) + + var libpodFilters = []string{} + if _, found := r.URL.Query()["filters"]; found { + all, err = strconv.ParseBool(query.Filters["all"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + for k, v := range query.Filters { + libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) + } + } + cids, err := runtime.ImageRuntime().PruneImages(r.Context(), all, libpodFilters) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) return @@ -163,3 +179,47 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { defer os.Remove(tmpfile.Name()) utils.WriteResponse(w, http.StatusOK, rdr) } + +func ImportImage(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 := handlers.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/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 14f8e8de7..50d763338 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -108,7 +108,7 @@ func Pods(w http.ResponseWriter, r *http.Request) { ) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - filters []string `schema:"filters"` + Filters map[string][]string `schema:"filters"` }{ // override any golang type defaults } @@ -117,10 +117,12 @@ func Pods(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - if len(query.filters) > 0 { + + if _, found := r.URL.Query()["filters"]; found { utils.Error(w, "filters are not implemented yet", http.StatusInternalServerError, define.ErrNotImplemented) return } + pods, err := runtime.GetAllPods() if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) @@ -164,7 +166,7 @@ func PodStop(w http.ResponseWriter, r *http.Request) { decoder = r.Context().Value("decoder").(*schema.Decoder) ) query := struct { - timeout int `schema:"t"` + Timeout int `schema:"t"` }{ // override any golang type defaults } @@ -207,8 +209,8 @@ func PodStop(w http.ResponseWriter, r *http.Request) { return } - if query.timeout > 0 { - _, stopError = pod.StopWithTimeout(r.Context(), false, query.timeout) + if query.Timeout > 0 { + _, stopError = pod.StopWithTimeout(r.Context(), false, query.Timeout) } else { _, stopError = pod.Stop(r.Context(), false) } @@ -266,7 +268,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 } @@ -282,7 +284,7 @@ func PodDelete(w http.ResponseWriter, r *http.Request) { 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 } @@ -307,43 +309,14 @@ func PodRestart(w http.ResponseWriter, r *http.Request) { 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) { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 33cd51164..33cf1e95d 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -537,3 +537,11 @@ func portsToPortSet(input map[string]struct{}) (nat.PortSet, error) { } return ports, nil } + +// ContainerCreateResponse is the response struct for creating a container +type ContainerCreateResponse struct { + // ID of the container created + ID string `json:"id"` + // Warnings during container creation + Warnings []string `json:"Warnings"` +} diff --git a/pkg/api/handlers/utils/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/images.go b/pkg/api/handlers/utils/images.go index 9445298ca..2b651584a 100644 --- a/pkg/api/handlers/utils/images.go +++ b/pkg/api/handlers/utils/images.go @@ -6,6 +6,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" + "github.com/gorilla/mux" "github.com/gorilla/schema" ) @@ -15,17 +16,23 @@ 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 - filters []string - //digests bool # digests is currently unused + All bool + Filters map[string][]string `schema:"filters"` + 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 } - filters := query.filters - if len(filters) < 1 { + var filters = []string{} + if _, found := mux.Vars(r)["digests"]; found && query.Digests { + UnSupportedParameter("digests") + } + + if _, found := r.URL.Query()["filters"]; found { filters = append(filters, fmt.Sprintf("reference=%s", "")) } return runtime.ImageRuntime().GetImagesWithFilters(filters) diff --git a/pkg/api/handlers/generic/version.go b/pkg/api/handlers/version.go index 39423914d..94166952c 100644 --- a/pkg/api/handlers/generic/version.go +++ b/pkg/api/handlers/version.go @@ -1,4 +1,4 @@ -package generic +package handlers import ( "fmt" @@ -8,7 +8,6 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" docker "github.com/docker/docker/api/types" "github.com/pkg/errors" @@ -53,7 +52,7 @@ func VersionHandler(w http.ResponseWriter, r *http.Request) { }, }} - utils.WriteResponse(w, http.StatusOK, handlers.Version{Version: docker.Version{ + utils.WriteResponse(w, http.StatusOK, Version{Version: docker.Version{ Platform: struct { Name string }{ diff --git a/pkg/api/server/docs.go b/pkg/api/server/docs.go index e028c6302..c989c7927 100644 --- a/pkg/api/server/docs.go +++ b/pkg/api/server/docs.go @@ -12,7 +12,8 @@ // Version: 0.0.1 // License: Apache-2.0 https://opensource.org/licenses/Apache-2.0 // Contact: Podman <podman@lists.podman.io> https://podman.io/community/ -// Extensions: +// +// InfoExtensions: // x-logo: // - url: https://raw.githubusercontent.com/containers/libpod/master/logo/podman-logo.png // - altText: "Podman logo" diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index b2d2ab388..abae81c38 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -33,7 +33,7 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { // $ref: "#/responses/ConflictError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/create"), APIHandler(s.Context, generic.CreateContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/create"), APIHandler(s.Context, handlers.CreateContainer)).Methods(http.MethodPost) // swagger:operation GET /containers/json compat listContainers // --- // tags: @@ -550,7 +550,7 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { libpod endpoints */ - r.HandleFunc(VersionedPath("/libpod/containers/create"), APIHandler(s.Context, libpod.CreateContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/create"), APIHandler(s.Context, handlers.CreateContainer)).Methods(http.MethodPost) // swagger:operation GET /libpod/containers/json libpod libpodListContainers // --- // tags: diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 6e8b79313..f1cc0574c 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -55,6 +55,27 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - images (compat) // summary: List Images // description: Returns a list of images on the server. Note that it uses a different, smaller representation of an image than inspecting a single image. + // parameters: + // - name: all + // in: query + // description: "Show all images. Only images from a final layer (no children) are shown by default." + // type: boolean + // default: false + // - name: filters + // in: query + // description: | + // A JSON encoded value of the filters (a `map[string][]string`) to process on the images list. Available filters: + // - `before`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) + // - `dangling=true` + // - `label=key` or `label="key=value"` of an image label + // - `reference`=(`<image-name>[:<tag>]`) + // - `since`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) + // type: string + // - name: digests + // in: query + // description: Not supported + // type: boolean + // default: false // produces: // - application/json // responses: @@ -63,7 +84,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/images/json"), APIHandler(s.Context, generic.GetImages)).Methods(http.MethodGet) - // swagger:operation POST /images/load compat loadImage + // swagger:operation POST /images/load compat importImage // --- // tags: // - images (compat) @@ -86,7 +107,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: no error // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/load"), APIHandler(s.Context, handlers.LoadImage)).Methods(http.MethodPost) + r.Handle(VersionedPath("/images/load"), APIHandler(s.Context, libpod.ImportImage)).Methods(http.MethodPost) // swagger:operation POST /images/prune compat pruneImages // --- // tags: @@ -174,7 +195,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/ConflictError' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/name"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) + r.Handle(VersionedPath("/images/{name}"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) // swagger:operation GET /images/{name}/get compat exportImage // --- // tags: @@ -585,6 +606,22 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - images // summary: List Images // description: Returns a list of images on the server + // parameters: + // - name: all + // in: query + // description: Show all images. Only images from a final layer (no children) are shown by default. + // type: boolean + // default: false + // - name: filters + // in: query + // description: | + // A JSON encoded value of the filters (a `map[string][]string`) to process on the images list. Available filters: + // - `before`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) + // - `dangling=true` + // - `label=key` or `label="key=value"` of an image label + // - `reference`=(`<image-name>[:<tag>]`) + // - `since`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) + // type: string // produces: // - application/json // responses: @@ -593,7 +630,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/json"), APIHandler(s.Context, libpod.GetImages)).Methods(http.MethodGet) - // swagger:operation POST /libpod/images/load libpod libpodLoadImage + // swagger:operation POST /libpod/images/load libpod libpodImportImage // --- // tags: // - images @@ -604,6 +641,14 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // name: quiet // type: boolean // description: not supported + // - in: query + // name: change + // description: "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR. JSON encoded string" + // type: string + // - in: query + // name: message + // description: Set commit message for imported image + // type: string // - in: body // name: request // description: tarball of container image @@ -617,7 +662,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: no error // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/load"), APIHandler(s.Context, handlers.LoadImage)).Methods(http.MethodPost) + r.Handle(VersionedPath("/libpod/images/load"), APIHandler(s.Context, libpod.ImportImage)).Methods(http.MethodPost) // swagger:operation POST /libpod/images/prune libpod libpodPruneImages // --- // tags: @@ -635,10 +680,6 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // (or `0`), all unused images are pruned. // - `until=<string>` Prune images created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. // - `label` (`label=<key>`, `label=<key>=<value>`, `label!=<key>`, or `label!=<key>=<value>`) Prune images with (or without, in case `label!=...` is used) the specified labels. - // - in: query - // name: all - // type: boolean - // description: prune all images // produces: // - application/json // responses: @@ -707,7 +748,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/ConflictError' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/name"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) + r.Handle(VersionedPath("/libpod/images/{name}"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) // swagger:operation GET /libpod/images/{name}/get libpod libpoodExportImage // --- // tags: diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index 1ef14b58c..5c7b51871 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -30,17 +30,15 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // swagger:operation POST /libpod/pods/prune pods PrunePods // --- // summary: Prune unused pods - // parameters: - // - in: query - // name: force - // description: force delete - // type: boolean - // default: false // produces: // - application/json // responses: - // 204: - // description: no error + // 200: + // description: tbd + // schema: + // type: object + // additionalProperties: + // type: string // 400: // $ref: "#/responses/BadParamError" // 500: @@ -60,7 +58,7 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // - in: query // name: force // type: boolean - // description: force delete + // description : force removal of a running pod by first stopping all containers, then removing all containers in the pod // responses: // 204: // description: no error diff --git a/pkg/api/server/register_version.go b/pkg/api/server/register_version.go index 94216b1b6..d3b47c2a9 100644 --- a/pkg/api/server/register_version.go +++ b/pkg/api/server/register_version.go @@ -1,12 +1,12 @@ package server import ( - "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/containers/libpod/pkg/api/handlers" "github.com/gorilla/mux" ) func (s *APIServer) registerVersionHandlers(r *mux.Router) error { - r.Handle("/version", APIHandler(s.Context, generic.VersionHandler)) - r.Handle(VersionedPath("/version"), APIHandler(s.Context, generic.VersionHandler)) + r.Handle("/version", APIHandler(s.Context, handlers.VersionHandler)) + r.Handle(VersionedPath("/version"), APIHandler(s.Context, handlers.VersionHandler)) return nil } diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 847d11c3c..8c940763e 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -11,6 +11,7 @@ import ( "time" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" "github.com/coreos/go-systemd/activation" "github.com/gorilla/mux" "github.com/gorilla/schema" @@ -71,7 +72,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li ReadTimeout: 20 * time.Second, WriteTimeout: 2 * time.Minute, }, - Decoder: schema.NewDecoder(), + Decoder: handlers.NewAPIDecoder(), Context: nil, Runtime: runtime, Listener: *listener, @@ -85,6 +86,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li }) ctx, cancelFn := context.WithCancel(context.Background()) + server.CancelFunc = cancelFn // TODO: Use ConnContext when ported to go 1.13 ctx = context.WithValue(ctx, "decoder", server.Decoder) @@ -92,9 +94,6 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li ctx = context.WithValue(ctx, "shutdownFunc", server.Shutdown) server.Context = ctx - server.CancelFunc = cancelFn - server.Decoder.IgnoreUnknownKeys(true) - router.NotFoundHandler = http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { // We can track user errors... diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 551a63c62..3dec6ca20 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -1,14 +1,22 @@ package bindings import ( + "context" "fmt" "io" + "net" "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/containers/libpod/pkg/api/handlers" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" ) -const ( - defaultConnection string = "http://localhost:8080/v1.24/libpod" - pingConnection string = "http://localhost:8080/_ping" +var ( + defaultConnectionPath string = filepath.Join(fmt.Sprintf("v%s", handlers.MinimalApiVersion), "libpod") ) type APIResponse struct { @@ -17,46 +25,170 @@ type APIResponse struct { } type Connection struct { - url string - client *http.Client + scheme string + address string + client *http.Client } -func NewConnection(url string) (Connection, error) { - if len(url) < 1 { - url = defaultConnection +// NewConnection takes a URI as a string and returns a context with the +// Connection embedded as a value. This context needs to be passed to each +// endpoint to work correctly. +// +// A valid URI connection should be scheme:// +// For example tcp://localhost:<port> +// or unix://run/podman/podman.sock +func NewConnection(uri string) (context.Context, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err } - newConn := Connection{ - url: url, - client: &http.Client{}, + // TODO once ssh is implemented, remove this block and + // add it to the conditional beneath it + if u.Scheme == "ssh" { + return nil, ErrNotImplemented + } + if u.Scheme != "tcp" && u.Scheme != "unix" { + return nil, errors.Errorf("%s is not a support schema", u.Scheme) + } + + if u.Scheme == "tcp" && !strings.HasPrefix(uri, "tcp://") { + return nil, errors.New("tcp URIs should begin with tcp://") + } + + address := u.Path + if u.Scheme == "tcp" { + address = u.Host + } + newConn := newConnection(u.Scheme, address) + ctx := context.WithValue(context.Background(), "conn", &newConn) + if err := pingNewConnection(ctx); err != nil { + return nil, err } - response, err := http.Get(pingConnection) + return ctx, nil +} + +// pingNewConnection pings to make sure the RESTFUL service is up +// and running. it should only be used where initializing a connection +func pingNewConnection(ctx context.Context) error { + conn, err := GetConnectionFromContext(ctx) if err != nil { - return newConn, err + return err + } + // the ping endpoint sits at / in this case + response, err := conn.DoRequest(nil, http.MethodGet, "../../../_ping", nil) + if err != nil { + return err + } + if response.StatusCode == http.StatusOK { + return nil } - if err := response.Body.Close(); err != nil { - return newConn, err + return errors.Errorf("ping response was %q", response.StatusCode) +} + +// newConnection takes a scheme and address and creates a connection from it +func newConnection(scheme, address string) Connection { + client := http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial(scheme, address) + }, + }, } - return newConn, err + newConn := Connection{ + client: &client, + address: address, + scheme: scheme, + } + return newConn } -func (c Connection) makeEndpoint(u string) string { - return fmt.Sprintf("%s%s", defaultConnection, u) +func (c *Connection) makeEndpoint(u string) string { + // The d character in the url is discarded and is meaningless + return fmt.Sprintf("http://d/%s%s", defaultConnectionPath, u) } -func (c Connection) newRequest(httpMethod, endpoint string, httpBody io.Reader, params map[string]string) (*APIResponse, error) { - e := c.makeEndpoint(endpoint) +// DoRequest assembles the http request and returns the response +func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, queryParams map[string]string, pathValues ...string) (*APIResponse, error) { + var ( + err error + response *http.Response + ) + safePathValues := make([]interface{}, len(pathValues)) + // Make sure path values are http url safe + for _, pv := range pathValues { + safePathValues = append(safePathValues, url.QueryEscape(pv)) + } + safeEndpoint := fmt.Sprintf(endpoint, safePathValues...) + + e := c.makeEndpoint(safeEndpoint) req, err := http.NewRequest(httpMethod, e, httpBody) if err != nil { return nil, err } - if len(params) > 0 { + if len(queryParams) > 0 { // if more desirable we could use url to form the encoded endpoint with params r := req.URL.Query() - for k, v := range params { - r.Add(k, v) + for k, v := range queryParams { + r.Add(k, url.QueryEscape(v)) } req.URL.RawQuery = r.Encode() } - response, err := c.client.Do(req) // nolint + // Give the Do three chances in the case of a comm/service hiccup + for i := 0; i < 3; i++ { + response, err = c.client.Do(req) // nolint + if err == nil { + break + } + } return &APIResponse{response, req}, err } + +// GetConnectionFromContext returns a bindings connection from the context +// being passed into each method. +func GetConnectionFromContext(ctx context.Context) (*Connection, error) { + c := ctx.Value("conn") + if c == nil { + return nil, errors.New("unable to get connection from context") + } + conn := c.(Connection) + return &conn, nil +} + +// FiltersToHTML converts our typical filter format of a +// map[string][]string to a query/html safe string. +func FiltersToHTML(filters map[string][]string) (string, error) { + lowerCaseKeys := make(map[string][]string) + for k, v := range filters { + lowerCaseKeys[strings.ToLower(k)] = v + } + unsafeString, err := jsoniter.MarshalToString(lowerCaseKeys) + if err != nil { + return "", err + } + return url.QueryEscape(unsafeString), nil +} + +// IsInformation returns true if the response code is 1xx +func (h *APIResponse) IsInformational() bool { + return h.Response.StatusCode/100 == 1 +} + +// IsSuccess returns true if the response code is 2xx +func (h *APIResponse) IsSuccess() bool { + return h.Response.StatusCode/100 == 2 +} + +// IsRedirection returns true if the response code is 3xx +func (h *APIResponse) IsRedirection() bool { + return h.Response.StatusCode/100 == 3 +} + +// IsClientError returns true if the response code is 4xx +func (h *APIResponse) IsClientError() bool { + return h.Response.StatusCode/100 == 4 +} + +// IsServerError returns true if the response code is 5xx +func (h *APIResponse) IsServerError() bool { + return h.Response.StatusCode/100 == 5 +} diff --git a/pkg/bindings/containers.go b/pkg/bindings/containers.go deleted file mode 100644 index 057580088..000000000 --- a/pkg/bindings/containers.go +++ /dev/null @@ -1,139 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/libpod" -) - -func (c Connection) ListContainers(filter []string, last int, size, sync bool) ([]shared.PsContainerOutput, error) { // nolint:typecheck - images := []shared.PsContainerOutput{} - params := make(map[string]string) - params["last"] = strconv.Itoa(last) - params["size"] = strconv.FormatBool(size) - params["sync"] = strconv.FormatBool(sync) - response, err := c.newRequest(http.MethodGet, "/containers/json", nil, params) - if err != nil { - return images, err - } - return images, response.Process(nil) -} - -func (c Connection) PruneContainers() ([]string, error) { - var ( - pruned []string - ) - response, err := c.newRequest(http.MethodPost, "/containers/prune", nil, nil) - if err != nil { - return pruned, err - } - return pruned, response.Process(nil) -} - -func (c Connection) RemoveContainer(nameOrID string, force, volumes bool) error { - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - params["vols"] = strconv.FormatBool(volumes) - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/containers/%s", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) InspectContainer(nameOrID string, size bool) (*libpod.InspectContainerData, error) { - params := make(map[string]string) - params["size"] = strconv.FormatBool(size) - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/containers/%s/json", nameOrID), nil, params) - if err != nil { - return nil, err - } - inspect := libpod.InspectContainerData{} - return &inspect, response.Process(&inspect) -} - -func (c Connection) KillContainer(nameOrID string, signal int) error { - params := make(map[string]string) - params["signal"] = strconv.Itoa(signal) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/kill", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) - -} -func (c Connection) ContainerLogs() {} -func (c Connection) PauseContainer(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/pause", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) RestartContainer(nameOrID string, timeout int) error { - // TODO how do we distinguish between an actual zero value and not wanting to change the timeout value - params := make(map[string]string) - params["timeout"] = strconv.Itoa(timeout) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/restart", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) StartContainer(nameOrID, detachKeys string) error { - params := make(map[string]string) - if len(detachKeys) > 0 { - params["detachKeys"] = detachKeys - } - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/start", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) ContainerStats() {} -func (c Connection) ContainerTop() {} - -func (c Connection) UnpauseContainer(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/unpause", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) WaitContainer(nameOrID string) error { - // TODO when returns are ironed out, we can should use the newRequest approach - _, err := http.Post(c.makeEndpoint(fmt.Sprintf("containers/%s/wait", nameOrID)), "application/json", nil) // nolint - return err -} - -func (c Connection) ContainerExists(nameOrID string) (bool, error) { - response, err := http.Get(c.makeEndpoint(fmt.Sprintf("/containers/%s/exists", nameOrID))) // nolint - defer closeResponseBody(response) - if err != nil { - return false, err - } - if response.StatusCode == http.StatusOK { - return true, nil - } - return false, nil -} - -func (c Connection) StopContainer(nameOrID string, timeout *int) error { - params := make(map[string]string) - if timeout != nil { - params["t"] = strconv.Itoa(*timeout) - } - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/stop", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go new file mode 100644 index 000000000..334a656d4 --- /dev/null +++ b/pkg/bindings/containers/containers.go @@ -0,0 +1,255 @@ +package containers + +import ( + "context" + "net/http" + "strconv" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/bindings" +) + +// List obtains a list of containers in local storage. All parameters to this method are optional. +// The filters are used to determine which containers are listed. The last parameter indicates to only return +// the most recent number of containers. The pod and size booleans indicate that pod information and rootfs +// size information should also be included. Finally, the sync bool synchronizes the OCI runtime and +// container state. +func List(ctx context.Context, filters map[string][]string, last *int, pod, size, sync *bool) ([]*shared.PsContainerOutput, error) { // nolint:typecheck + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + var images []*shared.PsContainerOutput + params := make(map[string]string) + if last != nil { + params["last"] = strconv.Itoa(*last) + } + if pod != nil { + params["pod"] = strconv.FormatBool(*pod) + } + if size != nil { + params["size"] = strconv.FormatBool(*size) + } + if sync != nil { + params["sync"] = strconv.FormatBool(*sync) + } + if filters != nil { + filterString, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = filterString + } + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/json", params) + if err != nil { + return images, err + } + return images, response.Process(nil) +} + +// Prune removes stopped and exited containers from local storage. The optional filters can be +// used for more granular selection of containers. The main error returned indicates if there were runtime +// errors like finding containers. Errors specific to the removal of a container are in the PruneContainerResponse +// structure. +func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { + var ( + pruneResponse []string + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if filters != nil { + filterString, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = filterString + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/prune", params) + if err != nil { + return pruneResponse, err + } + return pruneResponse, response.Process(pruneResponse) +} + +// Remove removes a container from local storage. The force bool designates +// that the container should be removed forcibly (example, even it is running). The volumes +// bool dictates that a container's volumes should also be removed. +func Remove(ctx context.Context, nameOrID string, force, volumes *bool) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if force != nil { + params["force"] = strconv.FormatBool(*force) + } + if volumes != nil { + params["vols"] = strconv.FormatBool(*volumes) + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/containers/%s", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Inspect returns low level information about a Container. The nameOrID can be a container name +// or a partial/full ID. The size bool determines whether the size of the container's root filesystem +// should be calculated. Calculating the size of a container requires extra work from the filesystem and +// is therefore slower. +func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectContainerData, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if size != nil { + params["size"] = strconv.FormatBool(*size) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/json", params, nameOrID) + if err != nil { + return nil, err + } + inspect := libpod.InspectContainerData{} + return &inspect, response.Process(&inspect) +} + +// Kill sends a given signal to a given container. The signal should be the string +// representation of a signal like 'SIGKILL'. The nameOrID can be a container name +// or a partial/full ID +func Kill(ctx context.Context, nameOrID string, signal string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + params["signal"] = signal + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/kill", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) + +} +func Logs() {} + +// Pause pauses a given container. The nameOrID can be a container name +// or a partial/full ID. +func Pause(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/pause", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Restart restarts a running container. The nameOrID can be a container name +// or a partial/full ID. The optional timeout specifies the number of seconds to wait +// for the running container to stop before killing it. +func Restart(ctx context.Context, nameOrID string, timeout *int) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if timeout != nil { + params["t"] = strconv.Itoa(*timeout) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/restart", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Start starts a non-running container.The nameOrID can be a container name +// or a partial/full ID. The optional parameter for detach keys are to override the default +// detach key sequence. +func Start(ctx context.Context, nameOrID string, detachKeys *string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if detachKeys != nil { + params["detachKeys"] = *detachKeys + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/start", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func Stats() {} +func Top() {} + +// Unpause resumes the given paused container. The nameOrID can be a container name +// or a partial/full ID. +func Unpause(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/unpause", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Wait blocks until the given container exits and returns its exit code. The nameOrID can be a container name +// or a partial/full ID. +func Wait(ctx context.Context, nameOrID string) (int32, error) { + var exitCode int32 + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return exitCode, err + } + response, err := conn.DoRequest(nil, http.MethodPost, "containers/%s/wait", nil, nameOrID) + if err != nil { + return exitCode, err + } + return exitCode, response.Process(&exitCode) +} + +// Exists is a quick, light-weight way to determine if a given container +// exists in local storage. The nameOrID can be a container name +// or a partial/full ID. +func Exists(ctx context.Context, nameOrID string) (bool, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return false, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "containers/%s/exists", nil, nameOrID) + if err != nil { + return false, err + } + return response.IsSuccess(), nil +} + +// Stop stops a running container. The timeout is optional. The nameOrID can be a container name +// or a partial/full ID +func Stop(ctx context.Context, nameOrID string, timeout *int) error { + params := make(map[string]string) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + if timeout != nil { + params["t"] = strconv.Itoa(*timeout) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/stop", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/bindings/containers/healthcheck.go b/pkg/bindings/containers/healthcheck.go new file mode 100644 index 000000000..9ed7f858d --- /dev/null +++ b/pkg/bindings/containers/healthcheck.go @@ -0,0 +1,26 @@ +package containers + +import ( + "context" + "github.com/containers/libpod/pkg/bindings" + "net/http" + + "github.com/containers/libpod/libpod" +) + +// RunHealthCheck executes the container's healthcheck and returns the health status of the +// container. +func RunHealthCheck(ctx context.Context, nameOrID string) (*libpod.HealthCheckStatus, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + var ( + status libpod.HealthCheckStatus + ) + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/runhealthcheck", nil, nameOrID) + if err != nil { + return nil, err + } + return &status, response.Process(&status) +} diff --git a/pkg/bindings/containers/mount.go b/pkg/bindings/containers/mount.go new file mode 100644 index 000000000..d68dee981 --- /dev/null +++ b/pkg/bindings/containers/mount.go @@ -0,0 +1,53 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/containers/libpod/pkg/bindings" +) + +// Mount mounts an existing container to the filesystem. It returns the path +// of the mounted container in string format. +func Mount(ctx context.Context, nameOrID string) (string, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return "", err + } + var ( + path string + ) + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/mount", nil, nameOrID) + if err != nil { + return path, err + } + return path, response.Process(&path) +} + +// Unmount unmounts a container from the filesystem. The container must not be running +// or the unmount will fail. +func Unmount(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/unmount", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// GetMountedContainerPaths returns a map of mounted containers and their mount locations. +func GetMountedContainerPaths(ctx context.Context) (map[string]string, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + mounts := make(map[string]string) + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/showmounted", nil) + if err != nil { + return mounts, err + } + return mounts, response.Process(&mounts) +} diff --git a/pkg/bindings/errors.go b/pkg/bindings/errors.go index 9a02925a3..8bd40f804 100644 --- a/pkg/bindings/errors.go +++ b/pkg/bindings/errors.go @@ -7,7 +7,6 @@ import ( "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) var ( @@ -37,10 +36,10 @@ func (a APIResponse) Process(unmarshalInto interface{}) error { return handleError(data) } -func closeResponseBody(r *http.Response) { - if r != nil { - if err := r.Body.Close(); err != nil { - logrus.Error(errors.Wrap(err, "unable to close response body")) - } +func CheckResponseCode(inError error) (int, error) { + e, ok := inError.(utils.ErrorModel) + if !ok { + return -1, errors.New("error is not type ErrorModel") } + return e.Code(), nil } diff --git a/pkg/bindings/generate.go b/pkg/bindings/generate.go deleted file mode 100644 index 534909062..000000000 --- a/pkg/bindings/generate.go +++ /dev/null @@ -1,4 +0,0 @@ -package bindings - -func (c Connection) GenerateKube() {} -func (c Connection) GenerateSystemd() {} diff --git a/pkg/bindings/generate/generate.go b/pkg/bindings/generate/generate.go new file mode 100644 index 000000000..2916754b8 --- /dev/null +++ b/pkg/bindings/generate/generate.go @@ -0,0 +1,4 @@ +package generate + +func GenerateKube() {} +func GenerateSystemd() {} diff --git a/pkg/bindings/healthcheck.go b/pkg/bindings/healthcheck.go deleted file mode 100644 index 32515e332..000000000 --- a/pkg/bindings/healthcheck.go +++ /dev/null @@ -1,19 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - - "github.com/containers/libpod/libpod" -) - -func (c Connection) RunHealthCheck(nameOrID string) (*libpod.HealthCheckStatus, error) { - var ( - status libpod.HealthCheckStatus - ) - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/containers/%s/runhealthcheck", nameOrID), nil, nil) - if err != nil { - return nil, err - } - return &status, response.Process(&status) -} diff --git a/pkg/bindings/images.go b/pkg/bindings/images.go deleted file mode 100644 index 3abc8c372..000000000 --- a/pkg/bindings/images.go +++ /dev/null @@ -1,111 +0,0 @@ -package bindings - -import ( - "fmt" - "io" - "net/http" - "strconv" - - "github.com/containers/libpod/pkg/api/handlers" - "github.com/containers/libpod/pkg/inspect" -) - -func (c Connection) ImageExists(nameOrID string) (bool, error) { - response, err := http.Get(c.makeEndpoint(fmt.Sprintf("/images/%s/exists", nameOrID))) // nolint - defer closeResponseBody(response) - if err != nil { - return false, err - } - if response.StatusCode == http.StatusOK { - return true, nil - } - return false, nil -} - -func (c Connection) ListImages() ([]handlers.ImageSummary, error) { - imageSummary := []handlers.ImageSummary{} - response, err := c.newRequest(http.MethodGet, "/images/json", nil, nil) - if err != nil { - return imageSummary, err - } - return imageSummary, response.Process(&imageSummary) -} - -func (c Connection) GetImage(nameOrID string) (*inspect.ImageData, error) { - inspectedData := inspect.ImageData{} - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/images/%s/json", nameOrID), nil, nil) - if err != nil { - return &inspectedData, err - } - return &inspectedData, response.Process(&inspectedData) -} - -func (c Connection) ImageTree(nameOrId string) error { - return ErrNotImplemented -} - -func (c Connection) ImageHistory(nameOrID string) ([]handlers.HistoryResponse, error) { - history := []handlers.HistoryResponse{} - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/images/%s/history", nameOrID), nil, nil) - if err != nil { - return history, err - } - return history, response.Process(&history) -} - -func (c Connection) LoadImage(r io.Reader) error { - // TODO this still needs error handling added - _, err := http.Post(c.makeEndpoint("/images/loads"), "application/json", r) //nolint - return err -} - -func (c Connection) RemoveImage(nameOrID string, force bool) ([]map[string]string, error) { - deletes := []map[string]string{} - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/images/%s", nameOrID), nil, params) - if err != nil { - return nil, err - } - return deletes, response.Process(&deletes) -} - -func (c Connection) ExportImage(nameOrID string, w io.Writer, format string, compress bool) error { - params := make(map[string]string) - params["format"] = format - params["compress"] = strconv.FormatBool(compress) - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/images/%s/get", nameOrID), nil, params) - if err != nil { - return err - } - if err := response.Process(nil); err != nil { - return err - } - _, err = io.Copy(w, response.Body) - return err -} - -func (c Connection) PruneImages(all bool, filters []string) ([]string, error) { - var ( - deleted []string - ) - params := make(map[string]string) - // FIXME How do we do []strings? - //params["filters"] = format - response, err := c.newRequest(http.MethodPost, "/images/prune", nil, params) - if err != nil { - return deleted, err - } - return deleted, response.Process(nil) -} - -func (c Connection) TagImage(nameOrID string) error { - var () - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/images/%s/tag", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) BuildImage(nameOrId string) {} diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go new file mode 100644 index 000000000..deaf93f0e --- /dev/null +++ b/pkg/bindings/images/images.go @@ -0,0 +1,187 @@ +package images + +import ( + "context" + "io" + "net/http" + "strconv" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/inspect" +) + +// Exists a lightweight way to determine if an image exists in local storage. It returns a +// boolean response. +func Exists(ctx context.Context, nameOrID string) (bool, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return false, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/exists", nil, nameOrID) + if err != nil { + return false, err + } + return response.IsSuccess(), nil +} + +// List returns a list of images in local storage. The all boolean and filters parameters are optional +// ways to alter the image query. +func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handlers.ImageSummary, error) { + var imageSummary []*handlers.ImageSummary + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if all != nil { + params["all"] = strconv.FormatBool(*all) + } + if filters != nil { + strFilters, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = strFilters + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/json", params) + if err != nil { + return imageSummary, err + } + return imageSummary, response.Process(&imageSummary) +} + +// Get performs an image inspect. To have the on-disk size of the image calculated, you can +// use the optional size parameter. +func GetImage(ctx context.Context, nameOrID string, size *bool) (*inspect.ImageData, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if size != nil { + params["size"] = strconv.FormatBool(*size) + } + inspectedData := inspect.ImageData{} + response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/json", params, nameOrID) + if err != nil { + return &inspectedData, err + } + return &inspectedData, response.Process(&inspectedData) +} + +func ImageTree(ctx context.Context, nameOrId string) error { + return bindings.ErrNotImplemented +} + +// History returns the parent layers of an image. +func History(ctx context.Context, nameOrID string) ([]*handlers.HistoryResponse, error) { + var history []*handlers.HistoryResponse + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/history", nil, nameOrID) + if err != nil { + return history, err + } + return history, response.Process(&history) +} + +func Load(ctx context.Context, r io.Reader) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + // TODO this still needs error handling added + //_, err := http.Post(c.makeEndpoint("/images/loads"), "application/json", r) //nolint + _ = conn + return bindings.ErrNotImplemented +} + +// Remove deletes an image from local storage. The optional force parameter will forcibly remove +// the image by removing all all containers, including those that are Running, first. +func Remove(ctx context.Context, nameOrID string, force *bool) ([]map[string]string, error) { + var deletes []map[string]string + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if force != nil { + params["force"] = strconv.FormatBool(*force) + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/images/%s", params, nameOrID) + if err != nil { + return nil, err + } + return deletes, response.Process(&deletes) +} + +// Export saves an image from local storage as a tarball or image archive. The optional format +// parameter is used to change the format of the output. +func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, compress *bool) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if format != nil { + params["format"] = *format + } + if compress != nil { + params["compress"] = strconv.FormatBool(*compress) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/get", params, nameOrID) + if err != nil { + return err + } + if err := response.Process(nil); err != nil { + return err + } + _, err = io.Copy(w, response.Body) + return err +} + +// Prune removes unused images from local storage. The optional filters can be used to further +// define which images should be pruned. +func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { + var ( + deleted []string + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if filters != nil { + stringFilter, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = stringFilter + } + response, err := conn.DoRequest(nil, http.MethodPost, "/images/prune", params) + if err != nil { + return deleted, err + } + return deleted, response.Process(nil) +} + +// Tag adds an additional name to locally-stored image. Both the tag and repo parameters are required. +func Tag(ctx context.Context, nameOrID, tag, repo string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + params["tag"] = tag + params["repo"] = repo + response, err := conn.DoRequest(nil, http.MethodPost, "/images/%s/tag", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func Build(nameOrId string) {} diff --git a/pkg/bindings/images/search.go b/pkg/bindings/images/search.go new file mode 100644 index 000000000..d98ddf18d --- /dev/null +++ b/pkg/bindings/images/search.go @@ -0,0 +1,40 @@ +package images + +import ( + "context" + "net/http" + "strconv" + + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/bindings" +) + +// Search looks for the given image (term) in container image registries. The optional limit parameter sets +// a maximum number of results returned. The optional filters parameter allow for more specific image +// searches. +func Search(ctx context.Context, term string, limit *int, filters map[string][]string) ([]image.SearchResult, error) { + var ( + searchResults []image.SearchResult + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + params["term"] = term + if limit != nil { + params["limit"] = strconv.Itoa(*limit) + } + if filters != nil { + stringFilter, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = stringFilter + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/search", params) + if err != nil { + return searchResults, nil + } + return searchResults, response.Process(&searchResults) +} diff --git a/pkg/bindings/mount.go b/pkg/bindings/mount.go deleted file mode 100644 index 2e3d6d7f6..000000000 --- a/pkg/bindings/mount.go +++ /dev/null @@ -1,26 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" -) - -func (c Connection) MountContainer(nameOrID string) (string, error) { - var ( - path string - ) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/mount", nameOrID), nil, nil) - if err != nil { - return path, err - } - return path, response.Process(&path) -} - -func (c Connection) GetMountedContainerPaths() (map[string]string, error) { - mounts := make(map[string]string) - response, err := c.newRequest(http.MethodGet, "/containers/showmounted", nil, nil) - if err != nil { - return mounts, err - } - return mounts, response.Process(&mounts) -} diff --git a/pkg/bindings/network.go b/pkg/bindings/network.go deleted file mode 100644 index 383615e5d..000000000 --- a/pkg/bindings/network.go +++ /dev/null @@ -1,37 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - - "github.com/containernetworking/cni/libcni" -) - -func (c Connection) CreateNetwork() {} -func (c Connection) InspectNetwork(nameOrID string) (map[string]interface{}, error) { - n := make(map[string]interface{}) - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/networks/%s/json", nameOrID), nil, nil) - if err != nil { - return n, err - } - return n, response.Process(&n) -} - -func (c Connection) RemoveNetwork(nameOrID string) error { - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/networks/%s", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) ListNetworks() ([]*libcni.NetworkConfigList, error) { - var ( - netList []*libcni.NetworkConfigList - ) - response, err := c.newRequest(http.MethodGet, "/networks/json", nil, nil) - if err != nil { - return netList, err - } - return netList, response.Process(&netList) -} diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go new file mode 100644 index 000000000..97bbb8c42 --- /dev/null +++ b/pkg/bindings/network/network.go @@ -0,0 +1,50 @@ +package network + +import ( + "context" + "net/http" + + "github.com/containernetworking/cni/libcni" + "github.com/containers/libpod/pkg/bindings" +) + +func Create() {} +func Inspect(ctx context.Context, nameOrID string) (map[string]interface{}, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + n := make(map[string]interface{}) + response, err := conn.DoRequest(nil, http.MethodGet, "/networks/%s/json", nil, nameOrID) + if err != nil { + return n, err + } + return n, response.Process(&n) +} + +func Remove(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/networks/%s", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func List(ctx context.Context) ([]*libcni.NetworkConfigList, error) { + var ( + netList []*libcni.NetworkConfigList + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/networks/json", nil) + if err != nil { + return netList, err + } + return netList, response.Process(&netList) +} diff --git a/pkg/bindings/play.go b/pkg/bindings/play.go deleted file mode 100644 index a9dee82b1..000000000 --- a/pkg/bindings/play.go +++ /dev/null @@ -1,3 +0,0 @@ -package bindings - -func (c Connection) PlayKube() {} diff --git a/pkg/bindings/play/play.go b/pkg/bindings/play/play.go new file mode 100644 index 000000000..a6f03cad2 --- /dev/null +++ b/pkg/bindings/play/play.go @@ -0,0 +1,7 @@ +package play + +import "github.com/containers/libpod/pkg/bindings" + +func PlayKube() error { + return bindings.ErrNotImplemented +} diff --git a/pkg/bindings/pods.go b/pkg/bindings/pods.go deleted file mode 100644 index 704d71477..000000000 --- a/pkg/bindings/pods.go +++ /dev/null @@ -1,129 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/containers/libpod/libpod" -) - -func (c Connection) CreatePod() error { - // TODO - return ErrNotImplemented -} - -func (c Connection) PodExists(nameOrID string) (bool, error) { - response, err := http.Get(c.makeEndpoint(fmt.Sprintf("/pods/%s/exists", nameOrID))) // nolint - defer closeResponseBody(response) - if err != nil { - return false, err - } - return response.StatusCode == http.StatusOK, err -} - -func (c Connection) InspectPod(nameOrID string) (*libpod.PodInspect, error) { - inspect := libpod.PodInspect{} - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/pods/%s/json", nameOrID), nil, nil) - if err != nil { - return &inspect, err - } - return &inspect, response.Process(&inspect) -} - -func (c Connection) KillPod(nameOrID string, signal int) error { - params := make(map[string]string) - params["signal"] = strconv.Itoa(signal) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/kill", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) PausePod(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/pause", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) PrunePods(force bool) error { - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodPost, "/pods/prune", nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) ListPods(filters []string) (*[]libpod.PodInspect, error) { - var ( - inspect []libpod.PodInspect - ) - params := make(map[string]string) - // TODO I dont remember how to do this for []string{} - // FIXME - //params["filters"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodPost, "/pods/json", nil, params) - if err != nil { - return &inspect, err - } - return &inspect, response.Process(&inspect) -} - -func (c Connection) RestartPod(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/restart", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) RemovePod(nameOrID string, force bool) error { - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/pods/%s", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) StartPod(nameOrID string) error { - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/pods/%s/start", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) PodStats() error { - // TODO - return ErrNotImplemented -} - -func (c Connection) StopPod(nameOrID string, timeout int) error { - params := make(map[string]string) - params["t"] = strconv.Itoa(timeout) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/stop", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) PodTop() error { - // TODO - return ErrNotImplemented // nolint:typecheck -} - -func (c Connection) UnpausePod(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/unpause", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go new file mode 100644 index 000000000..a6b74c21d --- /dev/null +++ b/pkg/bindings/pods/pods.go @@ -0,0 +1,196 @@ +package pods + +import ( + "context" + "net/http" + "strconv" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/bindings" +) + +func CreatePod() error { + // TODO + return bindings.ErrNotImplemented +} + +// Exists is a lightweight method to determine if a pod exists in local storage +func Exists(ctx context.Context, nameOrID string) (bool, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return false, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/%s/exists", nil, nameOrID) + if err != nil { + return false, err + } + return response.IsSuccess(), nil +} + +// Inspect returns low-level information about the given pod. +func Inspect(ctx context.Context, nameOrID string) (*libpod.PodInspect, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + inspect := libpod.PodInspect{} + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/%s/json", nil, nameOrID) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +// Kill sends a SIGTERM to all the containers in a pod. The optional signal parameter +// can be used to override SIGTERM. +func Kill(ctx context.Context, nameOrID string, signal *string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if signal != nil { + params["signal"] = *signal + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/kill", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Pause pauses all running containers in a given pod. +func Pause(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/pause", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Prune removes all non-running pods in local storage. +func Prune(ctx context.Context) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/prune", nil) + if err != nil { + return err + } + return response.Process(nil) +} + +// List returns all pods in local storage. The optional filters parameter can +// be used to refine which pods should be listed. +func List(ctx context.Context, filters map[string][]string) (*[]libpod.PodInspect, error) { + var ( + inspect []libpod.PodInspect + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if filters != nil { + stringFilter, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = stringFilter + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/json", params) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +// Restart restarts all containers in a pod. +func Restart(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/restart", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Remove deletes a Pod from from local storage. The optional force parameter denotes +// that the Pod can be removed even if in a running state. +func Remove(ctx context.Context, nameOrID string, force *bool) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if force != nil { + params["force"] = strconv.FormatBool(*force) + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Start starts all containers in a pod. +func Start(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s/start", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func Stats() error { + // TODO + return bindings.ErrNotImplemented +} + +// Stop stops all containers in a Pod. The optional timeout parameter can be +// used to override the timeout before the container is killed. +func Stop(ctx context.Context, nameOrID string, timeout *int) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if timeout != nil { + params["t"] = strconv.Itoa(*timeout) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/stop", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func Top() error { + // TODO + return bindings.ErrNotImplemented // nolint:typecheck +} + +// Unpause unpauses all paused containers in a Pod. +func Unpause(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/unpause", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/bindings/search.go b/pkg/bindings/search.go deleted file mode 100644 index 0f462357c..000000000 --- a/pkg/bindings/search.go +++ /dev/null @@ -1,39 +0,0 @@ -package bindings - -import ( - "net/http" - "strconv" - - "github.com/containers/libpod/libpod/image" -) - -type ImageSearchFilters struct { - Automated bool `json:"automated"` - Official bool `json:"official"` - Stars int `json:"stars"` -} - -// TODO This method can be concluded when we determine how we want the filters to work on the -// API end -func (i *ImageSearchFilters) ToMapJSON() string { - return "" -} - -func (c Connection) SearchImages(term string, limit int, filters *ImageSearchFilters) ([]image.SearchResult, error) { - var ( - searchResults []image.SearchResult - ) - params := make(map[string]string) - params["term"] = term - if limit > 0 { - params["limit"] = strconv.Itoa(limit) - } - if filters != nil { - params["filters"] = filters.ToMapJSON() - } - response, err := c.newRequest(http.MethodGet, "/images/search", nil, params) - if err != nil { - return searchResults, nil - } - return searchResults, response.Process(&searchResults) -} diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go new file mode 100644 index 000000000..4f2a98f2b --- /dev/null +++ b/pkg/bindings/test/common_test.go @@ -0,0 +1,112 @@ +package test_bindings + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega/gexec" + "github.com/pkg/errors" +) + +const ( + defaultPodmanBinaryLocation string = "/usr/bin/podman" +) + +type bindingTest struct { + artifactDirPath string + imageCacheDir string + sock string + tempDirPath string + runRoot string + crioRoot string +} + +func (b *bindingTest) runPodman(command []string) *gexec.Session { + var cmd []string + podmanBinary := defaultPodmanBinaryLocation + val, ok := os.LookupEnv("PODMAN_BINARY") + if ok { + podmanBinary = val + } + val, ok = os.LookupEnv("CGROUP_MANAGER") + if ok { + cmd = append(cmd, "--cgroup-manager", val) + } + val, ok = os.LookupEnv("CNI_CONFIG_DIR") + if ok { + cmd = append(cmd, "--cni-config-dir", val) + } + val, ok = os.LookupEnv("CONMON") + if ok { + cmd = append(cmd, "--conmon", val) + } + val, ok = os.LookupEnv("ROOT") + if ok { + cmd = append(cmd, "--root", val) + } else { + cmd = append(cmd, "--root", b.crioRoot) + } + val, ok = os.LookupEnv("OCI_RUNTIME") + if ok { + cmd = append(cmd, "--runtime", val) + } + val, ok = os.LookupEnv("RUNROOT") + if ok { + cmd = append(cmd, "--runroot", val) + } else { + cmd = append(cmd, "--runroot", b.runRoot) + } + val, ok = os.LookupEnv("STORAGE_DRIVER") + if ok { + cmd = append(cmd, "--storage-driver", val) + } + val, ok = os.LookupEnv("STORAGE_OPTIONS") + if ok { + cmd = append(cmd, "--storage", val) + } + cmd = append(cmd, command...) + c := exec.Command(podmanBinary, cmd...) + fmt.Printf("Running: %s %s\n", podmanBinary, strings.Join(cmd, " ")) + session, err := gexec.Start(c, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter) + if err != nil { + panic(errors.Errorf("unable to run podman command: %q", cmd)) + } + return session +} + +func newBindingTest() *bindingTest { + tmpPath, _ := createTempDirInTempDir() + b := bindingTest{ + crioRoot: filepath.Join(tmpPath, "crio"), + runRoot: filepath.Join(tmpPath, "run"), + artifactDirPath: "", + imageCacheDir: "", + sock: fmt.Sprintf("unix:%s", filepath.Join(tmpPath, "api.sock")), + tempDirPath: tmpPath, + } + return &b +} + +// createTempDirinTempDir create a temp dir with prefix podman_test +func createTempDirInTempDir() (string, error) { + return ioutil.TempDir("", "libpod_api") +} + +func (b *bindingTest) startAPIService() *gexec.Session { + var ( + cmd []string + ) + cmd = append(cmd, "--log-level=debug", "service", "--timeout=999999", b.sock) + return b.runPodman(cmd) +} + +func (b *bindingTest) cleanup() { + if err := os.RemoveAll(b.tempDirPath); err != nil { + fmt.Println(err) + } +} diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go new file mode 100644 index 000000000..d600197bb --- /dev/null +++ b/pkg/bindings/test/images_test.go @@ -0,0 +1,92 @@ +package test_bindings + +import ( + "context" + "fmt" + "time" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/images" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman images", func() { + var ( + //tempdir string + //err error + //podmanTest *PodmanTestIntegration + bt *bindingTest + s *gexec.Session + connText context.Context + err error + false bool + //true bool = true + ) + + BeforeEach(func() { + //tempdir, err = CreateTempDirInTempDir() + //if err != nil { + // os.Exit(1) + //} + //podmanTest = PodmanTestCreate(tempdir) + //podmanTest.Setup() + //podmanTest.SeedImages() + bt = newBindingTest() + p := bt.runPodman([]string{"pull", "docker.io/library/alpine:latest"}) + p.Wait(45) + s = bt.startAPIService() + time.Sleep(1 * time.Second) + connText, err = bindings.NewConnection(bt.sock) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + //podmanTest.Cleanup() + //f := CurrentGinkgoTestDescription() + //processTestResult(f) + s.Kill() + bt.cleanup() + }) + It("inspect image", func() { + // Inspect invalid image be 404 + _, err = images.GetImage(connText, "foobar5000", nil) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + // Inspect by short name + data, err := images.GetImage(connText, "alpine", nil) + Expect(err).To(BeNil()) + + // Inspect with full ID + _, err = images.GetImage(connText, data.ID, nil) + Expect(err).To(BeNil()) + + // Inspect with partial ID + _, err = images.GetImage(connText, data.ID[0:12], nil) + Expect(err).To(BeNil()) + // Inspect by ID + // Inspect by long name should work, it doesnt (yet) i think it needs to be html escaped + //_, err = images.GetImage(connText, ) + //Expect(err).To(BeNil()) + }) + It("remove image", func() { + // Remove invalid image should be a 404 + _, err = images.RemoveImage(connText, "foobar5000", &false) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + _, err := images.GetImage(connText, "alpine", nil) + Expect(err).To(BeNil()) + + response, err := images.RemoveImage(connText, "alpine", &false) + Expect(err).To(BeNil()) + fmt.Println(response) + // to be continued + + }) + +}) diff --git a/pkg/bindings/volumes.go b/pkg/bindings/volumes.go deleted file mode 100644 index 219f924e7..000000000 --- a/pkg/bindings/volumes.go +++ /dev/null @@ -1,60 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/api/handlers" -) - -func (c Connection) CreateVolume(config handlers.VolumeCreateConfig) (string, error) { - var ( - volumeID string - ) - response, err := c.newRequest(http.MethodPost, "/volumes/create", nil, nil) - if err != nil { - return volumeID, err - } - return volumeID, response.Process(&volumeID) -} - -func (c Connection) InspectVolume(nameOrID string) (*libpod.InspectVolumeData, error) { - var ( - inspect libpod.InspectVolumeData - ) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/volumes/%s/json", nameOrID), nil, nil) - if err != nil { - return &inspect, err - } - return &inspect, response.Process(&inspect) -} - -func (c Connection) ListVolumes() error { - // TODO - // The API side of things for this one does a lot in main and therefore - // is not implemented yet. - return ErrNotImplemented // nolint:typecheck -} - -func (c Connection) PruneVolumes() ([]string, error) { - var ( - pruned []string - ) - response, err := c.newRequest(http.MethodPost, "/volumes/prune", nil, nil) - if err != nil { - return pruned, err - } - return pruned, response.Process(&pruned) -} - -func (c Connection) RemoveVolume(nameOrID string, force bool) error { - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodPost, "/volumes/prune", nil, params) - if err != nil { - return err - } - return response.Process(nil) -} diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go new file mode 100644 index 000000000..05a4f73fd --- /dev/null +++ b/pkg/bindings/volumes/volumes.go @@ -0,0 +1,85 @@ +package volumes + +import ( + "context" + "net/http" + "strconv" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" +) + +// Create creates a volume given its configuration. +func Create(ctx context.Context, config handlers.VolumeCreateConfig) (string, error) { + // TODO This is incomplete. The config needs to be sent via the body + var ( + volumeID string + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return "", err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/create", nil) + if err != nil { + return volumeID, err + } + return volumeID, response.Process(&volumeID) +} + +// Inspect returns low-level information about a volume. +func Inspect(ctx context.Context, nameOrID string) (*libpod.InspectVolumeData, error) { + var ( + inspect libpod.InspectVolumeData + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/json", nil, nameOrID) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +func List() error { + // TODO + // The API side of things for this one does a lot in main and therefore + // is not implemented yet. + return bindings.ErrNotImplemented // nolint:typecheck +} + +// Prune removes unused volumes from the local filesystem. +func Prune(ctx context.Context) ([]string, error) { + var ( + pruned []string + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/prune", nil) + if err != nil { + return pruned, err + } + return pruned, response.Process(&pruned) +} + +// Remove deletes the given volume from storage. The optional force parameter +// is used to remove a volume even if it is being used by a container. +func Remove(ctx context.Context, nameOrID string, force *bool) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if force != nil { + params["force"] = strconv.FormatBool(*force) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/prune", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/inspect/inspect.go b/pkg/inspect/inspect.go index 8249dc4aa..569f208d9 100644 --- a/pkg/inspect/inspect.go +++ b/pkg/inspect/inspect.go @@ -3,6 +3,7 @@ package inspect import ( "time" + "github.com/containers/image/v5/manifest" "github.com/containers/libpod/libpod/driver" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go/v1" @@ -10,28 +11,29 @@ import ( // ImageData holds the inspect information of an image type ImageData struct { - ID string `json:"Id"` - Digest digest.Digest `json:"Digest"` - RepoTags []string `json:"RepoTags"` - RepoDigests []string `json:"RepoDigests"` - Parent string `json:"Parent"` - Comment string `json:"Comment"` - Created *time.Time `json:"Created"` - Config *v1.ImageConfig `json:"Config"` - Version string `json:"Version"` - Author string `json:"Author"` - Architecture string `json:"Architecture"` - Os string `json:"Os"` - Size int64 `json:"Size"` - VirtualSize int64 `json:"VirtualSize"` - GraphDriver *driver.Data `json:"GraphDriver"` - RootFS *RootFS `json:"RootFS"` - Labels map[string]string `json:"Labels"` - Annotations map[string]string `json:"Annotations"` - ManifestType string `json:"ManifestType"` - User string `json:"User"` - History []v1.History `json:"History"` - NamesHistory []string `json:"NamesHistory"` + ID string `json:"Id"` + Digest digest.Digest `json:"Digest"` + RepoTags []string `json:"RepoTags"` + RepoDigests []string `json:"RepoDigests"` + Parent string `json:"Parent"` + Comment string `json:"Comment"` + Created *time.Time `json:"Created"` + Config *v1.ImageConfig `json:"Config"` + Version string `json:"Version"` + Author string `json:"Author"` + Architecture string `json:"Architecture"` + Os string `json:"Os"` + Size int64 `json:"Size"` + VirtualSize int64 `json:"VirtualSize"` + GraphDriver *driver.Data `json:"GraphDriver"` + RootFS *RootFS `json:"RootFS"` + Labels map[string]string `json:"Labels"` + Annotations map[string]string `json:"Annotations"` + ManifestType string `json:"ManifestType"` + User string `json:"User"` + History []v1.History `json:"History"` + NamesHistory []string `json:"NamesHistory"` + HealthCheck *manifest.Schema2HealthConfig `json:"Healthcheck,omitempty"` } // RootFS holds the root fs information of an image diff --git a/pkg/util/camelcase/LICENSE.md b/pkg/util/camelcase/LICENSE.md new file mode 100644 index 000000000..aa4a536ca --- /dev/null +++ b/pkg/util/camelcase/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Fatih Arslan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pkg/util/camelcase/README.md b/pkg/util/camelcase/README.md new file mode 100644 index 000000000..105a6ae33 --- /dev/null +++ b/pkg/util/camelcase/README.md @@ -0,0 +1,58 @@ +# CamelCase [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/camelcase) [![Build Status](http://img.shields.io/travis/fatih/camelcase.svg?style=flat-square)](https://travis-ci.org/fatih/camelcase) + +CamelCase is a Golang (Go) package to split the words of a camelcase type +string into a slice of words. It can be used to convert a camelcase word (lower +or upper case) into any type of word. + +## Splitting rules: + +1. If string is not valid UTF-8, return it without splitting as + single item array. +2. Assign all unicode characters into one of 4 sets: lower case + letters, upper case letters, numbers, and all other characters. +3. Iterate through characters of string, introducing splits + between adjacent characters that belong to different sets. +4. Iterate through array of split strings, and if a given string + is upper case: + * if subsequent string is lower case: + * move last character of upper case string to beginning of + lower case string + +## Install + +```bash +go get github.com/fatih/camelcase +``` + +## Usage and examples + +```go +splitted := camelcase.Split("GolangPackage") + +fmt.Println(splitted[0], splitted[1]) // prints: "Golang", "Package" +``` + +Both lower camel case and upper camel case are supported. For more info please +check: [http://en.wikipedia.org/wiki/CamelCase](http://en.wikipedia.org/wiki/CamelCase) + +Below are some example cases: + +``` +"" => [] +"lowercase" => ["lowercase"] +"Class" => ["Class"] +"MyClass" => ["My", "Class"] +"MyC" => ["My", "C"] +"HTML" => ["HTML"] +"PDFLoader" => ["PDF", "Loader"] +"AString" => ["A", "String"] +"SimpleXMLParser" => ["Simple", "XML", "Parser"] +"vimRPCPlugin" => ["vim", "RPC", "Plugin"] +"GL11Version" => ["GL", "11", "Version"] +"99Bottles" => ["99", "Bottles"] +"May5" => ["May", "5"] +"BFG9000" => ["BFG", "9000"] +"BöseÜberraschung" => ["Böse", "Überraschung"] +"Two spaces" => ["Two", " ", "spaces"] +"BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] +``` diff --git a/pkg/util/camelcase/camelcase.go b/pkg/util/camelcase/camelcase.go new file mode 100644 index 000000000..0a82d1005 --- /dev/null +++ b/pkg/util/camelcase/camelcase.go @@ -0,0 +1,91 @@ +// Package camelcase is a micro package to split the words of a camelcase type +// string into a slice of words. +package camelcase + +import ( + "unicode" + "unicode/utf8" +) + +// Split splits the camelcase word and returns a list of words. It also +// supports digits. Both lower camel case and upper camel case are supported. +// For more info please check: http://en.wikipedia.org/wiki/CamelCase +// +// Examples +// +// "" => [""] +// "lowercase" => ["lowercase"] +// "Class" => ["Class"] +// "MyClass" => ["My", "Class"] +// "MyC" => ["My", "C"] +// "HTML" => ["HTML"] +// "PDFLoader" => ["PDF", "Loader"] +// "AString" => ["A", "String"] +// "SimpleXMLParser" => ["Simple", "XML", "Parser"] +// "vimRPCPlugin" => ["vim", "RPC", "Plugin"] +// "GL11Version" => ["GL", "11", "Version"] +// "99Bottles" => ["99", "Bottles"] +// "May5" => ["May", "5"] +// "BFG9000" => ["BFG", "9000"] +// "BöseÜberraschung" => ["Böse", "Überraschung"] +// "Two spaces" => ["Two", " ", "spaces"] +// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] +// +// Splitting rules +// +// 1) If string is not valid UTF-8, return it without splitting as +// single item array. +// 2) Assign all unicode characters into one of 4 sets: lower case +// letters, upper case letters, numbers, and all other characters. +// 3) Iterate through characters of string, introducing splits +// between adjacent characters that belong to different sets. +// 4) Iterate through array of split strings, and if a given string +// is upper case: +// if subsequent string is lower case: +// move last character of upper case string to beginning of +// lower case string +func Split(src string) (entries []string) { + // don't split invalid utf8 + if !utf8.ValidString(src) { + return []string{src} + } + entries = []string{} + var runes [][]rune + lastClass := 0 + class := 0 + // split into fields based on class of unicode character + for _, r := range src { + switch { + case unicode.IsLower(r): + class = 1 + case unicode.IsUpper(r): + class = 2 + case unicode.IsDigit(r): + class = 3 + default: + class = 4 + } + if class == lastClass { + runes[len(runes)-1] = append(runes[len(runes)-1], r) + } else { + runes = append(runes, []rune{r}) + } + lastClass = class + } + // handle upper case -> lower case sequences, e.g. + // "PDFL", "oader" -> "PDF", "Loader" + for i := 0; i < len(runes)-1; i++ { + if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) { + runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...) + runes[i] = runes[i][:len(runes[i])-1] + } + } + // construct []string from results + for _, s := range runes { + if len(s) > 0 { + entries = append(entries, string(s)) + } + } + + return entries +} |