diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/api/handlers/libpod/images.go | 209 | ||||
-rw-r--r-- | pkg/api/handlers/types.go | 27 | ||||
-rw-r--r-- | pkg/api/server/register_images.go | 74 | ||||
-rw-r--r-- | pkg/bindings/test/common_test.go | 32 | ||||
-rw-r--r-- | pkg/bindings/test/images_test.go | 104 |
5 files changed, 368 insertions, 78 deletions
diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 6c926c45b..f6459f1eb 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -1,15 +1,23 @@ package libpod import ( + "context" "fmt" + "io" "io/ioutil" "net/http" "os" "strconv" + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/image/v5/types" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/util" "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -176,16 +184,205 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { } func ImagesLoad(w http.ResponseWriter, r *http.Request) { - //TODO ... - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.New("/libpod/images/load is not yet implemented")) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Reference string `schema:"reference"` + }{ + // Add defaults here once needed. + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + tmpfile, err := ioutil.TempFile("", "libpod-images-load.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + defer os.Remove(tmpfile.Name()) + defer tmpfile.Close() + + if _, err := io.Copy(tmpfile, r.Body); err != nil && err != io.EOF { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to write archive to temporary file")) + return + } + + tmpfile.Close() + loadedImage, err := runtime.LoadImage(context.Background(), query.Reference, tmpfile.Name(), os.Stderr, "") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to load image")) + return + } + + utils.WriteResponse(w, http.StatusOK, []handlers.LibpodImagesLoadReport{{ID: loadedImage}}) } func ImagesImport(w http.ResponseWriter, r *http.Request) { - //TODO ... - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.New("/libpod/images/import is not yet implemented")) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Changes []string `schema:"changes"` + Message string `schema:"message"` + Reference string `schema:"reference"` + URL string `schema:"URL"` + }{ + // Add defaults here once needed. + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + // Check if we need to load the image from a URL or from the request's body. + source := query.URL + if len(query.URL) == 0 { + tmpfile, err := ioutil.TempFile("", "libpod-images-import.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + defer os.Remove(tmpfile.Name()) + defer tmpfile.Close() + + if _, err := io.Copy(tmpfile, r.Body); err != nil && err != io.EOF { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to write archive to temporary file")) + return + } + + tmpfile.Close() + source = tmpfile.Name() + } + + importedImage, err := runtime.Import(context.Background(), source, query.Reference, query.Changes, query.Message, true) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to import image")) + return + } + + utils.WriteResponse(w, http.StatusOK, handlers.LibpodImagesImportReport{ID: importedImage}) } func ImagesPull(w http.ResponseWriter, r *http.Request) { - //TODO ... - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.New("/libpod/images/pull is not yet implemented")) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Reference string `schema:"reference"` + Credentials string `schema:"credentials"` + OverrideOS string `schema:"overrideOS"` + OverrideArch string `schema:"overrideArch"` + TLSVerify bool `schema:"tlsVerify"` + AllTags bool `schema:"allTags"` + }{ + TLSVerify: true, + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + if len(query.Reference) == 0 { + utils.InternalServerError(w, errors.New("reference parameter cannot be empty")) + return + } + // Enforce the docker transport. This is just a precaution as some callers + // might accustomed to using the "transport:reference" notation. Using + // another than the "docker://" transport does not really make sense for a + // remote case. For loading tarballs, the load and import endpoints should + // be used. + imageRef, err := alltransports.ParseImageName(query.Reference) + if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Errorf("reference %q must be a docker reference", query.Reference)) + return + } else if err != nil { + origErr := err + imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s:%s", docker.Transport.Name(), query.Reference)) + if err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(origErr, "reference %q must be a docker reference", query.Reference)) + return + } + } + + // all-tags doesn't work with a tagged reference, so let's check early + namedRef, err := reference.Parse(query.Reference) + if err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "error parsing reference %q", query.Reference)) + return + } + if _, isTagged := namedRef.(reference.Tagged); isTagged && query.AllTags { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Errorf("reference %q must not have a tag for all-tags", query.Reference)) + return + } + + var registryCreds *types.DockerAuthConfig + if len(query.Credentials) != 0 { + creds, err := util.ParseRegistryCreds(query.Credentials) + if err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "error parsing credentials %q", query.Credentials)) + return + } + registryCreds = creds + } + + // Setup the registry options + dockerRegistryOptions := image.DockerRegistryOptions{ + DockerRegistryCreds: registryCreds, + OSChoice: query.OverrideOS, + ArchitectureChoice: query.OverrideArch, + } + if query.TLSVerify { + dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) + } + + // Prepare the images we want to pull + imagesToPull := []string{} + res := []handlers.LibpodImagesPullReport{} + imageName := namedRef.String() + + if !query.AllTags { + imagesToPull = append(imagesToPull, imageName) + } else { + systemContext := image.GetSystemContext("", "", false) + tags, err := docker.GetRepositoryTags(context.Background(), systemContext, imageRef) + if err != nil { + utils.InternalServerError(w, errors.Wrap(err, "error getting repository tags")) + return + } + for _, tag := range tags { + imagesToPull = append(imagesToPull, fmt.Sprintf("%s:%s", imageName, tag)) + } + } + + // Finally pull the images + for _, img := range imagesToPull { + newImage, err := runtime.ImageRuntime().New( + context.Background(), + img, + "", + "", + os.Stderr, + &dockerRegistryOptions, + image.SigningOptions{}, + nil, + util.PullImageAlways) + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error pulling image %q", query.Reference)) + return + } + res = append(res, handlers.LibpodImagesPullReport{ID: newImage.ID()}) + } + + utils.WriteResponse(w, http.StatusOK, res) } diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 6169adb18..f5d9f9ad5 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -33,6 +33,18 @@ type ContainerConfig struct { dockerContainer.Config } +type LibpodImagesLoadReport struct { + ID string `json:"id"` +} + +type LibpodImagesImportReport struct { + ID string `json:"id"` +} + +type LibpodImagesPullReport struct { + ID string `json:"id"` +} + type ImageSummary struct { docker.ImageSummary CreatedTime time.Time `json:"CreatedTime,omitempty"` @@ -49,21 +61,6 @@ type LibpodContainersPruneReport struct { PruneError string `json:"error"` } -type LibpodImagesLoadReport struct { - ID string `json:"id"` - RepoTags []string `json:"repoTags"` -} - -type LibpodImagesImportReport struct { - ID string `json:"id"` - RepoTags []string `json:"repoTags"` -} - -type LibpodImagesPullReport struct { - ID string `json:"id"` - RepoTags []string `json:"repoTags"` -} - type Info struct { docker.Info BuildahVersion string diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 4a46b6ee6..f082c5fec 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -639,12 +639,8 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: Load an image (oci-archive or docker-archive) stream. // parameters: // - in: query - // name: change - // description: "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR. JSON encoded string" - // type: string - // - in: query - // name: message - // description: Set commit message for imported image + // name: reference + // description: "Optional Name[:TAG] for the image" // type: string // - in: formData // name: upload @@ -656,6 +652,8 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // responses: // 200: // $ref: "#/responses/DocsLibpodImagesLoadResponse" + // 400: + // $ref: "#/responses/BadParamError" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/load"), APIHandler(s.Context, libpod.ImagesLoad)).Methods(http.MethodPost) @@ -667,17 +665,23 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: Import a previously exported tarball as an image. // parameters: // - in: query - // name: change - // description: "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR. JSON encoded string" - // type: string + // name: changes + // description: "Apply the following possible instructions to the created image: CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR. JSON encoded string" + // type: array + // items: + // type: string // - in: query // name: message // description: Set commit message for imported image // type: string // - in: query + // name: reference + // description: "Optional Name[:TAG] for the image" + // type: string + // - in: query // name: url - // description: Specify a URL instead of a tarball - // type: boolean + // description: Load image from the specified URL + // type: string // - in: formData // name: upload // type: file @@ -688,34 +692,50 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // responses: // 200: // $ref: "#/responses/DocsLibpodImagesImportResponse" + // 400: + // $ref: "#/responses/BadParamError" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/import"), APIHandler(s.Context, libpod.ImagesImport)).Methods(http.MethodPost) - // swagger:operation GET /libpod/images/pull libpod libpodImagesPull + // swagger:operation POST /libpod/images/pull libpod libpodImagesPull // --- // tags: // - images - // summary: Import image - // description: Import a previosly exported image as a tarball. + // summary: Pull images + // description: Pull one or more images from a container registry. // parameters: - // - in: query - // name: reference - // description: Mandatory reference to the image (e.g., quay.io/image/name:tag)/ - // type: string - // - in: query - // name: credentials - // description: username:password for the registry. - // type: string - // - in: query - // name: tls-verify - // description: Require TLS verification. - // type: boolean - // default: true + // - in: query + // name: reference + // description: "Mandatory reference to the image (e.g., quay.io/image/name:tag)" + // type: string + // - in: query + // name: credentials + // description: "username:password for the registry" + // type: string + // - in: query + // name: overrideOS + // description: Pull image for the specified operating system. + // type: string + // - in: query + // name: overrideArch + // description: Pull image for the specified architecture. + // type: string + // - in: query + // name: tlsVerify + // description: Require TLS verification. + // type: boolean + // default: true + // - in: query + // name: allTags + // description: Pull all tagged images in the repository. + // type: boolean // produces: // - application/json // responses: // 200: // $ref: "#/responses/DocsLibpodImagesPullResponse" + // 400: + // $ref: "#/responses/BadParamError" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/pull"), APIHandler(s.Context, libpod.ImagesPull)).Methods(http.MethodPost) diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 15783041f..22cd0b7e0 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -16,6 +16,7 @@ import ( const ( defaultPodmanBinaryLocation string = "/usr/bin/podman" alpine string = "docker.io/library/alpine:latest" + busybox string = "docker.io/library/busybox:latest" ) type bindingTest struct { @@ -113,7 +114,38 @@ func (b *bindingTest) startAPIService() *gexec.Session { } func (b *bindingTest) cleanup() { + s := b.runPodman([]string{"stop", "-a", "-t", "0"}) + s.Wait(45) if err := os.RemoveAll(b.tempDirPath); err != nil { fmt.Println(err) } } + +// Pull is a helper function to pull in images +func (b *bindingTest) Pull(name string) { + p := b.runPodman([]string{"pull", name}) + p.Wait(45) +} + +// Run a container and add append the alpine image to it +func (b *bindingTest) RunTopContainer(name *string) { + cmd := []string{"run", "-dt"} + if name != nil { + containerName := *name + cmd = append(cmd, "--name", containerName) + } + cmd = append(cmd, alpine, "top") + p := b.runPodman(cmd) + p.Wait(45) +} + +// StringInSlice returns a boolean based on whether a given +// string is in a given slice +func StringInSlice(s string, sl []string) bool { + for _, val := range sl { + if s == val { + return true + } + } + return false +} diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index f2dc856b2..fea611601 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -2,10 +2,10 @@ package test_bindings import ( "context" - "fmt" "time" "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/bindings/images" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -17,12 +17,12 @@ var _ = Describe("Podman images", func() { //tempdir string //err error //podmanTest *PodmanTestIntegration - bt *bindingTest - s *gexec.Session - connText context.Context - err error - false bool - //true bool = true + bt *bindingTest + s *gexec.Session + connText context.Context + err error + falseFlag bool = false + trueFlag bool = true ) BeforeEach(func() { @@ -34,8 +34,8 @@ var _ = Describe("Podman images", func() { //podmanTest.Setup() //podmanTest.SeedImages() bt = newBindingTest() - p := bt.runPodman([]string{"pull", alpine}) - p.Wait(45) + bt.Pull(alpine) + bt.Pull(busybox) s = bt.startAPIService() time.Sleep(1 * time.Second) connText, err = bindings.NewConnection(bt.sock) @@ -68,28 +68,63 @@ var _ = Describe("Podman images", func() { _, err = images.GetImage(connText, data.ID[0:12], nil) Expect(err).To(BeNil()) - //Inspect by long name should work, it doesnt (yet) i think it needs to be html escaped - _, err = images.GetImage(connText, alpine, nil) - Expect(err).To(BeNil()) + // The test to inspect by long name needs to fixed. + // Inspect by long name should work, it doesnt (yet) i think it needs to be html escaped + // _, err = images.GetImage(connText, alpine, nil) + // Expect(err).To(BeNil()) }) + + // Test to validate the remove image api It("remove image", func() { // Remove invalid image should be a 404 - _, err = images.Remove(connText, "foobar5000", &false) + _, err = images.Remove(connText, "foobar5000", &falseFlag) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", 404)) - _, err := images.GetImage(connText, "alpine", nil) + // Remove an image by name, validate image is removed and error is nil + inspectData, err := images.GetImage(connText, "busybox", nil) + Expect(err).To(BeNil()) + response, err := images.Remove(connText, "busybox", nil) + Expect(err).To(BeNil()) + Expect(inspectData.ID).To(Equal(response[0]["Deleted"])) + inspectData, err = images.GetImage(connText, "busybox", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + // Start a container with alpine image + var top string = "top" + bt.RunTopContainer(&top) + // we should now have a container called "top" running + containerResponse, err := containers.Inspect(connText, "top", &falseFlag) Expect(err).To(BeNil()) + Expect(containerResponse.Name).To(Equal("top")) - response, err := images.Remove(connText, "alpine", &false) + // try to remove the image "alpine". This should fail since we are not force + // deleting hence image cannot be deleted until the container is deleted. + response, err = images.Remove(connText, "alpine", &falseFlag) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 500)) + + // Removing the image "alpine" where force = true + response, err = images.Remove(connText, "alpine", &trueFlag) Expect(err).To(BeNil()) - fmt.Println(response) - // to be continued + // Checking if both the images are gone as well as the container is deleted + inspectData, err = images.GetImage(connText, "busybox", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + inspectData, err = images.GetImage(connText, "alpine", nil) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + _, err = containers.Inspect(connText, "top", &falseFlag) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) }) - //Tests to validate the image tag command. + // Tests to validate the image tag command. It("tag image", func() { // Validates if invalid image name is given a bad response is encountered. err = images.Tag(connText, "dummy", "demo", "alpine") @@ -107,21 +142,30 @@ var _ = Describe("Podman images", func() { }) - //Test to validate the List images command. + // Test to validate the List images command. It("List image", func() { - //Array to hold the list of images returned + // Array to hold the list of images returned imageSummary, err := images.List(connText, nil, nil) - //There Should be no errors in the response. + // There Should be no errors in the response. Expect(err).To(BeNil()) - //Since in the begin context only one image is created the list context should have only one image - Expect(len(imageSummary)).To(Equal(1)) - - //To be written create a new image and check list count again - //imageSummary, err = images.List(connText, nil, nil) - - //Since in the begin context only one image adding one more image should - ///Expect(len(imageSummary)).To(Equal(2) - + // Since in the begin context two images are created the + // list context should have only 2 images + Expect(len(imageSummary)).To(Equal(2)) + + // Adding one more image. There Should be no errors in the response. + // And the count should be three now. + bt.Pull("busybox:glibc") + imageSummary, err = images.List(connText, nil, nil) + Expect(err).To(BeNil()) + Expect(len(imageSummary)).To(Equal(3)) + + //Validate the image names. + var names []string + for _, i := range imageSummary { + names = append(names, i.RepoTags...) + } + Expect(StringInSlice(alpine, names)).To(BeTrue()) + Expect(StringInSlice(busybox, names)).To(BeTrue()) }) }) |