github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/api/handlers/libpod/images.go (about)

     1  package libpod
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/containers/buildah"
    14  	"github.com/containers/common/libimage"
    15  	"github.com/containers/image/v5/manifest"
    16  	"github.com/containers/image/v5/types"
    17  	"github.com/hanks177/podman/v4/libpod"
    18  	"github.com/hanks177/podman/v4/libpod/define"
    19  	"github.com/hanks177/podman/v4/pkg/api/handlers"
    20  	"github.com/hanks177/podman/v4/pkg/api/handlers/utils"
    21  	api "github.com/hanks177/podman/v4/pkg/api/types"
    22  	"github.com/hanks177/podman/v4/pkg/auth"
    23  	"github.com/hanks177/podman/v4/pkg/domain/entities"
    24  	"github.com/hanks177/podman/v4/pkg/domain/infra/abi"
    25  	"github.com/hanks177/podman/v4/pkg/errorhandling"
    26  	"github.com/hanks177/podman/v4/pkg/util"
    27  	utils2 "github.com/hanks177/podman/v4/utils"
    28  	"github.com/containers/storage"
    29  	"github.com/gorilla/schema"
    30  	"github.com/pkg/errors"
    31  )
    32  
    33  // Commit
    34  // author string
    35  // "container"
    36  // repo string
    37  // tag string
    38  // message
    39  // pause bool
    40  // changes []string
    41  
    42  // create
    43  
    44  func ImageExists(w http.ResponseWriter, r *http.Request) {
    45  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
    46  	name := utils.GetName(r)
    47  
    48  	ir := abi.ImageEngine{Libpod: runtime}
    49  	report, err := ir.Exists(r.Context(), name)
    50  	if err != nil {
    51  		utils.Error(w, http.StatusNotFound, errors.Wrapf(err, "failed to find image %s", name))
    52  		return
    53  	}
    54  	if !report.Value {
    55  		utils.Error(w, http.StatusNotFound, errors.Errorf("failed to find image %s", name))
    56  		return
    57  	}
    58  	utils.WriteResponse(w, http.StatusNoContent, "")
    59  }
    60  
    61  func ImageTree(w http.ResponseWriter, r *http.Request) {
    62  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
    63  	name := utils.GetName(r)
    64  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
    65  	query := struct {
    66  		WhatRequires bool `schema:"whatrequires"`
    67  	}{
    68  		WhatRequires: false,
    69  	}
    70  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
    71  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
    72  		return
    73  	}
    74  	ir := abi.ImageEngine{Libpod: runtime}
    75  	options := entities.ImageTreeOptions{WhatRequires: query.WhatRequires}
    76  	report, err := ir.Tree(r.Context(), name, options)
    77  	if err != nil {
    78  		if errors.Cause(err) == storage.ErrImageUnknown {
    79  			utils.Error(w, http.StatusNotFound, errors.Wrapf(err, "failed to find image %s", name))
    80  			return
    81  		}
    82  		utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "failed to generate image tree for %s", name))
    83  		return
    84  	}
    85  	utils.WriteResponse(w, http.StatusOK, report)
    86  }
    87  
    88  func GetImage(w http.ResponseWriter, r *http.Request) {
    89  	name := utils.GetName(r)
    90  	newImage, err := utils.GetImage(r, name)
    91  	if err != nil {
    92  		utils.Error(w, http.StatusNotFound, errors.Wrapf(err, "failed to find image %s", name))
    93  		return
    94  	}
    95  	options := &libimage.InspectOptions{WithParent: true, WithSize: true}
    96  	inspect, err := newImage.Inspect(r.Context(), options)
    97  	if err != nil {
    98  		utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "failed in inspect image %s", inspect.ID))
    99  		return
   100  	}
   101  	utils.WriteResponse(w, http.StatusOK, inspect)
   102  }
   103  
   104  func PruneImages(w http.ResponseWriter, r *http.Request) {
   105  	var err error
   106  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   107  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   108  	query := struct {
   109  		All      bool `schema:"all"`
   110  		External bool `schema:"external"`
   111  	}{
   112  		// override any golang type defaults
   113  	}
   114  
   115  	filterMap, err := util.PrepareFilters(r)
   116  	if err != nil {
   117  		utils.Error(w, http.StatusInternalServerError,
   118  			errors.
   119  				Wrapf(err, "failed to decode filter parameters for %s", r.URL.String()))
   120  		return
   121  	}
   122  
   123  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   124  		utils.Error(w, http.StatusInternalServerError,
   125  			errors.
   126  				Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   127  		return
   128  	}
   129  
   130  	libpodFilters := []string{}
   131  	if _, found := r.URL.Query()["filters"]; found {
   132  		dangling := (*filterMap)["all"]
   133  		if len(dangling) > 0 {
   134  			query.All, err = strconv.ParseBool((*filterMap)["all"][0])
   135  			if err != nil {
   136  				utils.InternalServerError(w, err)
   137  				return
   138  			}
   139  		}
   140  		// dangling is special and not implemented in the libpod side of things
   141  		delete(*filterMap, "dangling")
   142  		for k, v := range *filterMap {
   143  			libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0]))
   144  		}
   145  	}
   146  
   147  	imageEngine := abi.ImageEngine{Libpod: runtime}
   148  
   149  	pruneOptions := entities.ImagePruneOptions{
   150  		All:      query.All,
   151  		External: query.External,
   152  		Filter:   libpodFilters,
   153  	}
   154  	imagePruneReports, err := imageEngine.Prune(r.Context(), pruneOptions)
   155  	if err != nil {
   156  		utils.Error(w, http.StatusInternalServerError, err)
   157  		return
   158  	}
   159  	utils.WriteResponse(w, http.StatusOK, imagePruneReports)
   160  }
   161  
   162  func ExportImage(w http.ResponseWriter, r *http.Request) {
   163  	var output string
   164  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   165  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   166  	query := struct {
   167  		Compress bool   `schema:"compress"`
   168  		Format   string `schema:"format"`
   169  	}{
   170  		Format: define.OCIArchive,
   171  	}
   172  
   173  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   174  		utils.Error(w, http.StatusBadRequest,
   175  			errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   176  		return
   177  	}
   178  	name := utils.GetName(r)
   179  
   180  	if _, _, err := runtime.LibimageRuntime().LookupImage(name, nil); err != nil {
   181  		utils.ImageNotFound(w, name, err)
   182  		return
   183  	}
   184  
   185  	switch query.Format {
   186  	case define.OCIArchive, define.V2s2Archive:
   187  		tmpfile, err := ioutil.TempFile("", "api.tar")
   188  		if err != nil {
   189  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
   190  			return
   191  		}
   192  		output = tmpfile.Name()
   193  		if err := tmpfile.Close(); err != nil {
   194  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile"))
   195  			return
   196  		}
   197  	case define.OCIManifestDir, define.V2s2ManifestDir:
   198  		tmpdir, err := ioutil.TempDir("", "save")
   199  		if err != nil {
   200  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempdir"))
   201  			return
   202  		}
   203  		output = tmpdir
   204  	default:
   205  		utils.Error(w, http.StatusInternalServerError, errors.Errorf("unknown format %q", query.Format))
   206  		return
   207  	}
   208  
   209  	imageEngine := abi.ImageEngine{Libpod: runtime}
   210  
   211  	saveOptions := entities.ImageSaveOptions{
   212  		Compress: query.Compress,
   213  		Format:   query.Format,
   214  		Output:   output,
   215  	}
   216  	if err := imageEngine.Save(r.Context(), name, nil, saveOptions); err != nil {
   217  		utils.Error(w, http.StatusBadRequest, err)
   218  		return
   219  	}
   220  	defer os.RemoveAll(output)
   221  	// if dir format, we need to tar it
   222  	if query.Format == "oci-dir" || query.Format == "docker-dir" {
   223  		rdr, err := utils2.Tar(output)
   224  		if err != nil {
   225  			utils.InternalServerError(w, err)
   226  			return
   227  		}
   228  		defer rdr.Close()
   229  		utils.WriteResponse(w, http.StatusOK, rdr)
   230  		return
   231  	}
   232  	rdr, err := os.Open(output)
   233  	if err != nil {
   234  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile"))
   235  		return
   236  	}
   237  	defer rdr.Close()
   238  	utils.WriteResponse(w, http.StatusOK, rdr)
   239  }
   240  
   241  func ExportImages(w http.ResponseWriter, r *http.Request) {
   242  	var output string
   243  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   244  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   245  	query := struct {
   246  		Compress                    bool     `schema:"compress"`
   247  		Format                      string   `schema:"format"`
   248  		OciAcceptUncompressedLayers bool     `schema:"ociAcceptUncompressedLayers"`
   249  		References                  []string `schema:"references"`
   250  	}{
   251  		Format: define.OCIArchive,
   252  	}
   253  
   254  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   255  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   256  		return
   257  	}
   258  
   259  	// References are mandatory!
   260  	if len(query.References) == 0 {
   261  		utils.Error(w, http.StatusBadRequest, errors.New("No references"))
   262  		return
   263  	}
   264  
   265  	// Format is mandatory! Currently, we only support multi-image docker
   266  	// archives.
   267  	if len(query.References) > 1 && query.Format != define.V2s2Archive {
   268  		utils.Error(w, http.StatusInternalServerError, errors.Errorf("multi-image archives must use format of %s", define.V2s2Archive))
   269  		return
   270  	}
   271  
   272  	// if format is dir, server will save to an archive
   273  	// the client will unArchive after receive the archive file
   274  	// so must convert is at here
   275  	switch query.Format {
   276  	case define.OCIManifestDir:
   277  		query.Format = define.OCIArchive
   278  	case define.V2s2ManifestDir:
   279  		query.Format = define.V2s2Archive
   280  	}
   281  
   282  	switch query.Format {
   283  	case define.V2s2Archive, define.OCIArchive:
   284  		tmpfile, err := ioutil.TempFile("", "api.tar")
   285  		if err != nil {
   286  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
   287  			return
   288  		}
   289  		output = tmpfile.Name()
   290  		if err := tmpfile.Close(); err != nil {
   291  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile"))
   292  			return
   293  		}
   294  	case define.OCIManifestDir, define.V2s2ManifestDir:
   295  		tmpdir, err := ioutil.TempDir("", "save")
   296  		if err != nil {
   297  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tmpdir"))
   298  			return
   299  		}
   300  		output = tmpdir
   301  	default:
   302  		utils.Error(w, http.StatusInternalServerError, errors.Errorf("unsupported format %q", query.Format))
   303  		return
   304  	}
   305  	defer os.RemoveAll(output)
   306  
   307  	// Use the ABI image engine to share as much code as possible.
   308  	opts := entities.ImageSaveOptions{
   309  		Compress:                    query.Compress,
   310  		Format:                      query.Format,
   311  		MultiImageArchive:           len(query.References) > 1,
   312  		OciAcceptUncompressedLayers: query.OciAcceptUncompressedLayers,
   313  		Output:                      output,
   314  	}
   315  
   316  	imageEngine := abi.ImageEngine{Libpod: runtime}
   317  	if err := imageEngine.Save(r.Context(), query.References[0], query.References[1:], opts); err != nil {
   318  		utils.Error(w, http.StatusBadRequest, err)
   319  		return
   320  	}
   321  
   322  	rdr, err := os.Open(output)
   323  	if err != nil {
   324  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile"))
   325  		return
   326  	}
   327  	defer rdr.Close()
   328  	utils.WriteResponse(w, http.StatusOK, rdr)
   329  }
   330  
   331  func ImagesLoad(w http.ResponseWriter, r *http.Request) {
   332  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   333  
   334  	tmpfile, err := ioutil.TempFile("", "libpod-images-load.tar")
   335  	if err != nil {
   336  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
   337  		return
   338  	}
   339  	defer os.Remove(tmpfile.Name())
   340  
   341  	_, err = io.Copy(tmpfile, r.Body)
   342  	tmpfile.Close()
   343  
   344  	if err != nil && err != io.EOF {
   345  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to write archive to temporary file"))
   346  		return
   347  	}
   348  
   349  	imageEngine := abi.ImageEngine{Libpod: runtime}
   350  
   351  	loadOptions := entities.ImageLoadOptions{Input: tmpfile.Name()}
   352  	loadReport, err := imageEngine.Load(r.Context(), loadOptions)
   353  	if err != nil {
   354  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to load image"))
   355  		return
   356  	}
   357  	utils.WriteResponse(w, http.StatusOK, loadReport)
   358  }
   359  
   360  func ImagesImport(w http.ResponseWriter, r *http.Request) {
   361  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   362  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   363  	query := struct {
   364  		Changes      []string `schema:"changes"`
   365  		Message      string   `schema:"message"`
   366  		Reference    string   `schema:"reference"`
   367  		URL          string   `schema:"URL"`
   368  		OS           string   `schema:"OS"`
   369  		Architecture string   `schema:"Architecture"`
   370  		Variant      string   `schema:"Variant"`
   371  	}{
   372  		// Add defaults here once needed.
   373  	}
   374  
   375  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   376  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   377  		return
   378  	}
   379  
   380  	// Check if we need to load the image from a URL or from the request's body.
   381  	source := query.URL
   382  	if len(query.URL) == 0 {
   383  		tmpfile, err := ioutil.TempFile("", "libpod-images-import.tar")
   384  		if err != nil {
   385  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
   386  			return
   387  		}
   388  		defer os.Remove(tmpfile.Name())
   389  		defer tmpfile.Close()
   390  
   391  		if _, err := io.Copy(tmpfile, r.Body); err != nil && err != io.EOF {
   392  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to write archive to temporary file"))
   393  			return
   394  		}
   395  
   396  		tmpfile.Close()
   397  		source = tmpfile.Name()
   398  	}
   399  
   400  	imageEngine := abi.ImageEngine{Libpod: runtime}
   401  	importOptions := entities.ImageImportOptions{
   402  		Changes:      query.Changes,
   403  		Message:      query.Message,
   404  		Reference:    query.Reference,
   405  		OS:           query.OS,
   406  		Architecture: query.Architecture,
   407  		Variant:      query.Variant,
   408  		Source:       source,
   409  	}
   410  	report, err := imageEngine.Import(r.Context(), importOptions)
   411  	if err != nil {
   412  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to import tarball"))
   413  		return
   414  	}
   415  
   416  	utils.WriteResponse(w, http.StatusOK, report)
   417  }
   418  
   419  // PushImage is the handler for the compat http endpoint for pushing images.
   420  func PushImage(w http.ResponseWriter, r *http.Request) {
   421  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   422  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   423  
   424  	query := struct {
   425  		Destination string `schema:"destination"`
   426  		TLSVerify   bool   `schema:"tlsVerify"`
   427  		Format      string `schema:"format"`
   428  		All         bool   `schema:"all"`
   429  	}{
   430  		// This is where you can override the golang default value for one of fields
   431  	}
   432  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   433  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   434  		return
   435  	}
   436  
   437  	source := strings.TrimSuffix(utils.GetName(r), "/push") // GetName returns the entire path
   438  	if _, err := utils.ParseStorageReference(source); err != nil {
   439  		utils.Error(w, http.StatusBadRequest, err)
   440  		return
   441  	}
   442  
   443  	destination := query.Destination
   444  	if destination == "" {
   445  		destination = source
   446  	}
   447  
   448  	if err := utils.IsRegistryReference(destination); err != nil {
   449  		utils.Error(w, http.StatusBadRequest, err)
   450  		return
   451  	}
   452  
   453  	authconf, authfile, err := auth.GetCredentials(r)
   454  	if err != nil {
   455  		utils.Error(w, http.StatusBadRequest, err)
   456  		return
   457  	}
   458  	defer auth.RemoveAuthfile(authfile)
   459  	var username, password string
   460  	if authconf != nil {
   461  		username = authconf.Username
   462  		password = authconf.Password
   463  	}
   464  	options := entities.ImagePushOptions{
   465  		Authfile: authfile,
   466  		Username: username,
   467  		Password: password,
   468  		Format:   query.Format,
   469  		All:      query.All,
   470  		Quiet:    true,
   471  	}
   472  	if _, found := r.URL.Query()["tlsVerify"]; found {
   473  		options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
   474  	}
   475  
   476  	imageEngine := abi.ImageEngine{Libpod: runtime}
   477  	if err := imageEngine.Push(context.Background(), source, destination, options); err != nil {
   478  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "error pushing image %q", destination))
   479  		return
   480  	}
   481  
   482  	utils.WriteResponse(w, http.StatusOK, "")
   483  }
   484  
   485  func CommitContainer(w http.ResponseWriter, r *http.Request) {
   486  	var (
   487  		destImage string
   488  		mimeType  string
   489  	)
   490  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   491  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   492  
   493  	query := struct {
   494  		Author    string   `schema:"author"`
   495  		Changes   []string `schema:"changes"`
   496  		Comment   string   `schema:"comment"`
   497  		Container string   `schema:"container"`
   498  		Format    string   `schema:"format"`
   499  		Pause     bool     `schema:"pause"`
   500  		Squash    bool     `schema:"squash"`
   501  		Repo      string   `schema:"repo"`
   502  		Tag       string   `schema:"tag"`
   503  	}{
   504  		Format: "oci",
   505  	}
   506  
   507  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   508  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   509  		return
   510  	}
   511  	rtc, err := runtime.GetConfig()
   512  	if err != nil {
   513  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to get runtime config"))
   514  		return
   515  	}
   516  	sc := runtime.SystemContext()
   517  	tag := "latest"
   518  	options := libpod.ContainerCommitOptions{
   519  		Pause: true,
   520  	}
   521  	switch query.Format {
   522  	case "oci":
   523  		mimeType = buildah.OCIv1ImageManifest
   524  		if len(query.Comment) > 0 {
   525  			utils.InternalServerError(w, errors.New("messages are only compatible with the docker image format (-f docker)"))
   526  			return
   527  		}
   528  	case "docker":
   529  		mimeType = manifest.DockerV2Schema2MediaType
   530  	default:
   531  		utils.InternalServerError(w, errors.Errorf("unrecognized image format %q", query.Format))
   532  		return
   533  	}
   534  	options.CommitOptions = buildah.CommitOptions{
   535  		SignaturePolicyPath:   rtc.Engine.SignaturePolicyPath,
   536  		ReportWriter:          os.Stderr,
   537  		SystemContext:         sc,
   538  		PreferredManifestType: mimeType,
   539  	}
   540  
   541  	if len(query.Tag) > 0 {
   542  		tag = query.Tag
   543  	}
   544  	options.Message = query.Comment
   545  	options.Author = query.Author
   546  	options.Pause = query.Pause
   547  	options.Squash = query.Squash
   548  	options.Changes = query.Changes
   549  	ctr, err := runtime.LookupContainer(query.Container)
   550  	if err != nil {
   551  		utils.Error(w, http.StatusNotFound, err)
   552  		return
   553  	}
   554  
   555  	if len(query.Repo) > 0 {
   556  		destImage = fmt.Sprintf("%s:%s", query.Repo, tag)
   557  	}
   558  	commitImage, err := ctr.Commit(r.Context(), destImage, options)
   559  	if err != nil && !strings.Contains(err.Error(), "is not running") {
   560  		utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "CommitFailure"))
   561  		return
   562  	}
   563  	utils.WriteResponse(w, http.StatusOK, entities.IDResponse{ID: commitImage.ID()}) // nolint
   564  }
   565  
   566  func UntagImage(w http.ResponseWriter, r *http.Request) {
   567  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   568  
   569  	tags := []string{} // Note: if empty, all tags will be removed from the image.
   570  	repo := r.Form.Get("repo")
   571  	tag := r.Form.Get("tag")
   572  
   573  	// Do the parameter dance.
   574  	switch {
   575  	// If tag is set, repo must be as well.
   576  	case len(repo) == 0 && len(tag) > 0:
   577  		utils.Error(w, http.StatusBadRequest, errors.New("repo parameter is required to tag an image"))
   578  		return
   579  
   580  	case len(repo) == 0:
   581  		break
   582  
   583  	// If repo is specified, we need to add that to the tags.
   584  	default:
   585  		if len(tag) == 0 {
   586  			// Normalize tag to "latest" if empty.
   587  			tag = "latest"
   588  		}
   589  		tags = append(tags, fmt.Sprintf("%s:%s", repo, tag))
   590  	}
   591  
   592  	// Now use the ABI implementation to prevent us from having duplicate
   593  	// code.
   594  	opts := entities.ImageUntagOptions{}
   595  	imageEngine := abi.ImageEngine{Libpod: runtime}
   596  
   597  	name := utils.GetName(r)
   598  	if err := imageEngine.Untag(r.Context(), name, tags, opts); err != nil {
   599  		if errors.Cause(err) == storage.ErrImageUnknown {
   600  			utils.ImageNotFound(w, name, errors.Wrapf(err, "failed to find image %s", name))
   601  		} else {
   602  			utils.Error(w, http.StatusInternalServerError, err)
   603  		}
   604  		return
   605  	}
   606  	utils.WriteResponse(w, http.StatusCreated, "")
   607  }
   608  
   609  // ImagesBatchRemove is the endpoint for batch image removal.
   610  func ImagesBatchRemove(w http.ResponseWriter, r *http.Request) {
   611  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   612  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   613  	query := struct {
   614  		All    bool     `schema:"all"`
   615  		Force  bool     `schema:"force"`
   616  		Ignore bool     `schema:"ignore"`
   617  		Images []string `schema:"images"`
   618  	}{}
   619  
   620  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   621  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   622  		return
   623  	}
   624  
   625  	opts := entities.ImageRemoveOptions{All: query.All, Force: query.Force, Ignore: query.Ignore}
   626  	imageEngine := abi.ImageEngine{Libpod: runtime}
   627  	rmReport, rmErrors := imageEngine.Remove(r.Context(), query.Images, opts)
   628  	strErrs := errorhandling.ErrorsToStrings(rmErrors)
   629  	report := handlers.LibpodImagesRemoveReport{ImageRemoveReport: *rmReport, Errors: strErrs}
   630  	utils.WriteResponse(w, http.StatusOK, report)
   631  }
   632  
   633  // ImagesRemove is the endpoint for removing one image.
   634  func ImagesRemove(w http.ResponseWriter, r *http.Request) {
   635  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   636  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   637  	query := struct {
   638  		Force bool `schema:"force"`
   639  	}{
   640  		Force: false,
   641  	}
   642  
   643  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   644  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   645  		return
   646  	}
   647  
   648  	opts := entities.ImageRemoveOptions{Force: query.Force}
   649  	imageEngine := abi.ImageEngine{Libpod: runtime}
   650  	rmReport, rmErrors := imageEngine.Remove(r.Context(), []string{utils.GetName(r)}, opts)
   651  
   652  	// In contrast to batch-removal, where we're only setting the exit
   653  	// code, we need to have another closer look at the errors here and set
   654  	// the appropriate http status code.
   655  
   656  	switch rmReport.ExitCode {
   657  	case 0:
   658  		report := handlers.LibpodImagesRemoveReport{ImageRemoveReport: *rmReport, Errors: []string{}}
   659  		utils.WriteResponse(w, http.StatusOK, report)
   660  	case 1:
   661  		// 404 - no such image
   662  		utils.Error(w, http.StatusNotFound, errorhandling.JoinErrors(rmErrors))
   663  	case 2:
   664  		// 409 - conflict error (in use by containers)
   665  		utils.Error(w, http.StatusConflict, errorhandling.JoinErrors(rmErrors))
   666  	default:
   667  		// 500 - internal error
   668  		utils.Error(w, http.StatusInternalServerError, errorhandling.JoinErrors(rmErrors))
   669  	}
   670  }