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

     1  package compat
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/containers/buildah"
    12  	"github.com/containers/common/libimage"
    13  	"github.com/containers/common/pkg/config"
    14  	"github.com/containers/common/pkg/filters"
    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/pkg/api/handlers"
    19  	"github.com/hanks177/podman/v4/pkg/api/handlers/utils"
    20  	api "github.com/hanks177/podman/v4/pkg/api/types"
    21  	"github.com/hanks177/podman/v4/pkg/auth"
    22  	"github.com/hanks177/podman/v4/pkg/domain/entities"
    23  	"github.com/hanks177/podman/v4/pkg/domain/infra/abi"
    24  	"github.com/containers/storage"
    25  	"github.com/gorilla/schema"
    26  	"github.com/opencontainers/go-digest"
    27  	"github.com/pkg/errors"
    28  	"github.com/sirupsen/logrus"
    29  )
    30  
    31  // mergeNameAndTagOrDigest creates an image reference as string from the
    32  // provided image name and tagOrDigest which can be a tag, a digest or empty.
    33  func mergeNameAndTagOrDigest(name, tagOrDigest string) string {
    34  	if len(tagOrDigest) == 0 {
    35  		return name
    36  	}
    37  
    38  	separator := ":" // default to tag
    39  	if _, err := digest.Parse(tagOrDigest); err == nil {
    40  		// We have a digest, so let's change the separator.
    41  		separator = "@"
    42  	}
    43  	return fmt.Sprintf("%s%s%s", name, separator, tagOrDigest)
    44  }
    45  
    46  func ExportImage(w http.ResponseWriter, r *http.Request) {
    47  	// 200 ok
    48  	// 500 server
    49  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
    50  
    51  	tmpfile, err := ioutil.TempFile("", "api.tar")
    52  	if err != nil {
    53  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
    54  		return
    55  	}
    56  	defer os.Remove(tmpfile.Name())
    57  
    58  	name := utils.GetName(r)
    59  	possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
    60  	if err != nil {
    61  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
    62  		return
    63  	}
    64  
    65  	imageEngine := abi.ImageEngine{Libpod: runtime}
    66  
    67  	saveOptions := entities.ImageSaveOptions{
    68  		Format: "docker-archive",
    69  		Output: tmpfile.Name(),
    70  	}
    71  
    72  	if err := imageEngine.Save(r.Context(), possiblyNormalizedName, nil, saveOptions); err != nil {
    73  		if errors.Cause(err) == storage.ErrImageUnknown {
    74  			utils.ImageNotFound(w, name, errors.Wrapf(err, "failed to find image %s", name))
    75  			return
    76  		}
    77  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
    78  		return
    79  	}
    80  
    81  	if err := tmpfile.Close(); err != nil {
    82  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile"))
    83  		return
    84  	}
    85  
    86  	rdr, err := os.Open(tmpfile.Name())
    87  	if err != nil {
    88  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile"))
    89  		return
    90  	}
    91  	defer rdr.Close()
    92  	utils.WriteResponse(w, http.StatusOK, rdr)
    93  }
    94  
    95  func CommitContainer(w http.ResponseWriter, r *http.Request) {
    96  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
    97  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
    98  
    99  	query := struct {
   100  		Author    string   `schema:"author"`
   101  		Changes   []string `schema:"changes"`
   102  		Comment   string   `schema:"comment"`
   103  		Container string   `schema:"container"`
   104  		Pause     bool     `schema:"pause"`
   105  		Squash    bool     `schema:"squash"`
   106  		Repo      string   `schema:"repo"`
   107  		Tag       string   `schema:"tag"`
   108  		// fromSrc   string  # fromSrc is currently unused
   109  	}{
   110  		Tag: "latest",
   111  	}
   112  
   113  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   114  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   115  		return
   116  	}
   117  	rtc, err := runtime.GetConfig()
   118  	if err != nil {
   119  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "Decode()"))
   120  		return
   121  	}
   122  	sc := runtime.SystemContext()
   123  	options := libpod.ContainerCommitOptions{
   124  		Pause: true,
   125  	}
   126  	options.CommitOptions = buildah.CommitOptions{
   127  		SignaturePolicyPath:   rtc.Engine.SignaturePolicyPath,
   128  		ReportWriter:          os.Stderr,
   129  		SystemContext:         sc,
   130  		PreferredManifestType: manifest.DockerV2Schema2MediaType,
   131  	}
   132  
   133  	input := handlers.CreateContainerConfig{}
   134  	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
   135  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "Decode()"))
   136  		return
   137  	}
   138  
   139  	options.Message = query.Comment
   140  	options.Author = query.Author
   141  	options.Pause = query.Pause
   142  	options.Squash = query.Squash
   143  	for _, change := range query.Changes {
   144  		options.Changes = append(options.Changes, strings.Split(change, "\n")...)
   145  	}
   146  	ctr, err := runtime.LookupContainer(query.Container)
   147  	if err != nil {
   148  		utils.Error(w, http.StatusNotFound, err)
   149  		return
   150  	}
   151  
   152  	var destImage string
   153  	if len(query.Repo) > 1 {
   154  		destImage = fmt.Sprintf("%s:%s", query.Repo, query.Tag)
   155  		possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, destImage)
   156  		if err != nil {
   157  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
   158  			return
   159  		}
   160  		destImage = possiblyNormalizedName
   161  	}
   162  
   163  	commitImage, err := ctr.Commit(r.Context(), destImage, options)
   164  	if err != nil && !strings.Contains(err.Error(), "is not running") {
   165  		utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "CommitFailure"))
   166  		return
   167  	}
   168  	utils.WriteResponse(w, http.StatusCreated, entities.IDResponse{ID: commitImage.ID()}) // nolint
   169  }
   170  
   171  func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) {
   172  	// 200 no error
   173  	// 404 repo does not exist or no read access
   174  	// 500 internal
   175  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   176  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   177  
   178  	query := struct {
   179  		Changes  []string `schema:"changes"`
   180  		FromSrc  string   `schema:"fromSrc"`
   181  		Message  string   `schema:"message"`
   182  		Platform string   `schema:"platform"`
   183  		Repo     string   `shchema:"repo"`
   184  	}{
   185  		// This is where you can override the golang default value for one of fields
   186  	}
   187  
   188  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   189  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   190  		return
   191  	}
   192  	// fromSrc – Source to import. The value may be a URL from which the image can be retrieved or - to read the image from the request body. This parameter may only be used when importing an image.
   193  	source := query.FromSrc
   194  	if source == "-" {
   195  		f, err := ioutil.TempFile("", "api_load.tar")
   196  		if err != nil {
   197  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile"))
   198  			return
   199  		}
   200  
   201  		source = f.Name()
   202  		if err := SaveFromBody(f, r); err != nil {
   203  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file"))
   204  		}
   205  	}
   206  
   207  	reference := query.Repo
   208  	if query.Repo != "" {
   209  		possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, reference)
   210  		if err != nil {
   211  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
   212  			return
   213  		}
   214  		reference = possiblyNormalizedName
   215  	}
   216  
   217  	platformSpecs := strings.Split(query.Platform, "/")
   218  	opts := entities.ImageImportOptions{
   219  		Source:    source,
   220  		Changes:   query.Changes,
   221  		Message:   query.Message,
   222  		Reference: reference,
   223  		OS:        platformSpecs[0],
   224  	}
   225  	if len(platformSpecs) > 1 {
   226  		opts.Architecture = platformSpecs[1]
   227  	}
   228  
   229  	imageEngine := abi.ImageEngine{Libpod: runtime}
   230  	report, err := imageEngine.Import(r.Context(), opts)
   231  	if err != nil {
   232  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to import tarball"))
   233  		return
   234  	}
   235  	// Success
   236  	utils.WriteResponse(w, http.StatusOK, struct {
   237  		Status         string            `json:"status"`
   238  		Progress       string            `json:"progress"`
   239  		ProgressDetail map[string]string `json:"progressDetail"`
   240  		Id             string            `json:"id"` // nolint
   241  	}{
   242  		Status:         report.Id,
   243  		ProgressDetail: map[string]string{},
   244  		Id:             report.Id,
   245  	})
   246  }
   247  
   248  type pullResult struct {
   249  	images []*libimage.Image
   250  	err    error
   251  }
   252  
   253  func CreateImageFromImage(w http.ResponseWriter, r *http.Request) {
   254  	// 200 no error
   255  	// 404 repo does not exist or no read access
   256  	// 500 internal
   257  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   258  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   259  
   260  	query := struct {
   261  		FromImage string `schema:"fromImage"`
   262  		Tag       string `schema:"tag"`
   263  		Platform  string `schema:"platform"`
   264  	}{
   265  		// This is where you can override the golang default value for one of fields
   266  	}
   267  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   268  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   269  		return
   270  	}
   271  
   272  	possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, mergeNameAndTagOrDigest(query.FromImage, query.Tag))
   273  	if err != nil {
   274  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
   275  		return
   276  	}
   277  
   278  	authConf, authfile, err := auth.GetCredentials(r)
   279  	if err != nil {
   280  		utils.Error(w, http.StatusBadRequest, err)
   281  		return
   282  	}
   283  	defer auth.RemoveAuthfile(authfile)
   284  
   285  	pullOptions := &libimage.PullOptions{}
   286  	pullOptions.AuthFilePath = authfile
   287  	if authConf != nil {
   288  		pullOptions.Username = authConf.Username
   289  		pullOptions.Password = authConf.Password
   290  		pullOptions.IdentityToken = authConf.IdentityToken
   291  	}
   292  	pullOptions.Writer = os.Stderr // allows for debugging on the server
   293  
   294  	// Handle the platform.
   295  	platformSpecs := strings.Split(query.Platform, "/")
   296  	pullOptions.OS = platformSpecs[0] // may be empty
   297  	if len(platformSpecs) > 1 {
   298  		pullOptions.Architecture = platformSpecs[1]
   299  		if len(platformSpecs) > 2 {
   300  			pullOptions.Variant = platformSpecs[2]
   301  		}
   302  	}
   303  
   304  	progress := make(chan types.ProgressProperties)
   305  	pullOptions.Progress = progress
   306  
   307  	pullResChan := make(chan pullResult)
   308  	go func() {
   309  		pulledImages, err := runtime.LibimageRuntime().Pull(r.Context(), possiblyNormalizedName, config.PullPolicyAlways, pullOptions)
   310  		pullResChan <- pullResult{images: pulledImages, err: err}
   311  	}()
   312  
   313  	flush := func() {
   314  		if flusher, ok := w.(http.Flusher); ok {
   315  			flusher.Flush()
   316  		}
   317  	}
   318  
   319  	w.WriteHeader(http.StatusOK)
   320  	w.Header().Set("Content-Type", "application/json")
   321  	flush()
   322  
   323  	enc := json.NewEncoder(w)
   324  	enc.SetEscapeHTML(true)
   325  
   326  loop: // break out of for/select infinite loop
   327  	for {
   328  		var report struct {
   329  			Stream   string `json:"stream,omitempty"`
   330  			Status   string `json:"status,omitempty"`
   331  			Progress struct {
   332  				Current uint64 `json:"current,omitempty"`
   333  				Total   int64  `json:"total,omitempty"`
   334  			} `json:"progressDetail,omitempty"`
   335  			Error string `json:"error,omitempty"`
   336  			Id    string `json:"id,omitempty"` // nolint
   337  		}
   338  		select {
   339  		case e := <-progress:
   340  			switch e.Event {
   341  			case types.ProgressEventNewArtifact:
   342  				report.Status = "Pulling fs layer"
   343  			case types.ProgressEventRead:
   344  				report.Status = "Downloading"
   345  				report.Progress.Current = e.Offset
   346  				report.Progress.Total = e.Artifact.Size
   347  			case types.ProgressEventSkipped:
   348  				report.Status = "Already exists"
   349  			case types.ProgressEventDone:
   350  				report.Status = "Download complete"
   351  			}
   352  			report.Id = e.Artifact.Digest.Encoded()[0:12]
   353  			if err := enc.Encode(report); err != nil {
   354  				logrus.Warnf("Failed to json encode error %q", err.Error())
   355  			}
   356  			flush()
   357  		case pullRes := <-pullResChan:
   358  			err := pullRes.err
   359  			pulledImages := pullRes.images
   360  			if err != nil {
   361  				report.Error = err.Error()
   362  			} else {
   363  				if len(pulledImages) > 0 {
   364  					img := pulledImages[0].ID()
   365  					if utils.IsLibpodRequest(r) {
   366  						report.Status = "Pull complete"
   367  					} else {
   368  						report.Status = "Download complete"
   369  					}
   370  					report.Id = img[0:12]
   371  				} else {
   372  					report.Error = "internal error: no images pulled"
   373  				}
   374  			}
   375  			if err := enc.Encode(report); err != nil {
   376  				logrus.Warnf("Failed to json encode error %q", err.Error())
   377  			}
   378  			flush()
   379  			break loop // break out of for/select infinite loop
   380  		}
   381  	}
   382  }
   383  
   384  func GetImage(w http.ResponseWriter, r *http.Request) {
   385  	// 200 no error
   386  	// 404 no such
   387  	// 500 internal
   388  	name := utils.GetName(r)
   389  	possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
   390  	if err != nil {
   391  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
   392  		return
   393  	}
   394  
   395  	newImage, err := utils.GetImage(r, possiblyNormalizedName)
   396  	if err != nil {
   397  		// Here we need to fiddle with the error message because docker-py is looking for "No
   398  		// such image" to determine on how to raise the correct exception.
   399  		errMsg := strings.ReplaceAll(err.Error(), "image not known", "No such image")
   400  		utils.Error(w, http.StatusNotFound, errors.Errorf("failed to find image %s: %s", name, errMsg))
   401  		return
   402  	}
   403  	inspect, err := handlers.ImageDataToImageInspect(r.Context(), newImage)
   404  	if err != nil {
   405  		utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "failed to convert ImageData to ImageInspect '%s'", inspect.ID))
   406  		return
   407  	}
   408  	utils.WriteResponse(w, http.StatusOK, inspect)
   409  }
   410  
   411  func GetImages(w http.ResponseWriter, r *http.Request) {
   412  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   413  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   414  	query := struct {
   415  		All     bool
   416  		Digests bool
   417  		Filter  string // Docker 1.24 compatibility
   418  	}{
   419  		// This is where you can override the golang default value for one of fields
   420  	}
   421  
   422  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   423  		utils.Error(w, http.StatusBadRequest,
   424  			errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   425  		return
   426  	}
   427  	if _, found := r.URL.Query()["digests"]; found && query.Digests {
   428  		utils.UnSupportedParameter("digests")
   429  		return
   430  	}
   431  
   432  	filterList, err := filters.FiltersFromRequest(r)
   433  	if err != nil {
   434  		utils.Error(w, http.StatusInternalServerError, err)
   435  		return
   436  	}
   437  	if !utils.IsLibpodRequest(r) {
   438  		if len(query.Filter) > 0 { // Docker 1.24 compatibility
   439  			filterList = append(filterList, "reference="+query.Filter)
   440  		}
   441  		filterList = append(filterList, "manifest=false")
   442  	}
   443  
   444  	imageEngine := abi.ImageEngine{Libpod: runtime}
   445  
   446  	listOptions := entities.ImageListOptions{All: query.All, Filter: filterList}
   447  	summaries, err := imageEngine.List(r.Context(), listOptions)
   448  	if err != nil {
   449  		utils.Error(w, http.StatusInternalServerError, err)
   450  		return
   451  	}
   452  
   453  	if !utils.IsLibpodRequest(r) {
   454  		// docker adds sha256: in front of the ID
   455  		for _, s := range summaries {
   456  			s.ID = "sha256:" + s.ID
   457  		}
   458  	}
   459  	utils.WriteResponse(w, http.StatusOK, summaries)
   460  }
   461  
   462  func LoadImages(w http.ResponseWriter, r *http.Request) {
   463  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   464  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   465  
   466  	query := struct {
   467  		Changes map[string]string `json:"changes"` // Ignored
   468  		Message string            `json:"message"` // Ignored
   469  		Quiet   bool              `json:"quiet"`   // Ignored
   470  	}{
   471  		// This is where you can override the golang default value for one of fields
   472  	}
   473  
   474  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   475  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   476  		return
   477  	}
   478  
   479  	// First write the body to a temporary file that we can later attempt
   480  	// to load.
   481  	f, err := ioutil.TempFile("", "api_load.tar")
   482  	if err != nil {
   483  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile"))
   484  		return
   485  	}
   486  	defer func() {
   487  		err := os.Remove(f.Name())
   488  		if err != nil {
   489  			logrus.Errorf("Failed to remove temporary file: %v.", err)
   490  		}
   491  	}()
   492  	if err := SaveFromBody(f, r); err != nil {
   493  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file"))
   494  		return
   495  	}
   496  
   497  	imageEngine := abi.ImageEngine{Libpod: runtime}
   498  
   499  	loadOptions := entities.ImageLoadOptions{Input: f.Name()}
   500  	loadReport, err := imageEngine.Load(r.Context(), loadOptions)
   501  	if err != nil {
   502  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to load image"))
   503  		return
   504  	}
   505  
   506  	if len(loadReport.Names) < 1 {
   507  		utils.Error(w, http.StatusInternalServerError, errors.Errorf("one or more images are required"))
   508  		return
   509  	}
   510  
   511  	utils.WriteResponse(w, http.StatusOK, struct {
   512  		Stream string `json:"stream"`
   513  	}{
   514  		Stream: fmt.Sprintf("Loaded image: %s", strings.Join(loadReport.Names, ",")),
   515  	})
   516  }
   517  
   518  func ExportImages(w http.ResponseWriter, r *http.Request) {
   519  	// 200 OK
   520  	// 500 Error
   521  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   522  	runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
   523  
   524  	query := struct {
   525  		Names []string `schema:"names"`
   526  	}{
   527  		// This is where you can override the golang default value for one of fields
   528  	}
   529  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   530  		utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   531  		return
   532  	}
   533  	if len(query.Names) == 0 {
   534  		utils.Error(w, http.StatusBadRequest, fmt.Errorf("no images to download"))
   535  		return
   536  	}
   537  
   538  	images := make([]string, len(query.Names))
   539  	for i, img := range query.Names {
   540  		possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, img)
   541  		if err != nil {
   542  			utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
   543  			return
   544  		}
   545  		images[i] = possiblyNormalizedName
   546  	}
   547  
   548  	tmpfile, err := ioutil.TempFile("", "api.tar")
   549  	if err != nil {
   550  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))
   551  		return
   552  	}
   553  	defer os.Remove(tmpfile.Name())
   554  	if err := tmpfile.Close(); err != nil {
   555  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile"))
   556  		return
   557  	}
   558  
   559  	imageEngine := abi.ImageEngine{Libpod: runtime}
   560  
   561  	saveOptions := entities.ImageSaveOptions{Format: "docker-archive", Output: tmpfile.Name(), MultiImageArchive: true}
   562  	if err := imageEngine.Save(r.Context(), images[0], images[1:], saveOptions); err != nil {
   563  		utils.InternalServerError(w, err)
   564  		return
   565  	}
   566  
   567  	rdr, err := os.Open(tmpfile.Name())
   568  	if err != nil {
   569  		utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile"))
   570  		return
   571  	}
   572  	defer rdr.Close()
   573  	utils.WriteResponse(w, http.StatusOK, rdr)
   574  }