compat API: allow enforcing short-names resolution to Docker Hub

The Docker-compatible REST API has historically behaved just as the rest
of Podman and Buildah (and the atomic Docker in older RHEL/Fedora) where
`containers-registries.conf` is centrally controlling which registries
a short name may resolve to during pull or local image lookups.  Please
refer to a blog for more details [1].

Docker, however, is only resolving short names to docker.io which has
been reported (see #12320) to break certain clients who rely on this
behavior.  In order to support this scenario, `containers.conf(5)`
received a new option to control whether Podman's compat API resolves
to docker.io only or behaves as before.

Most endpoints allow for directly normalizing parameters that represent
an image.  If set in containers.conf, Podman will then normalize the
references directly to docker.io.  The build endpoint is an outlier
since images are also referenced in Dockerfiles.  The Buildah API,
however, supports specifying a custom `types.SystemContext` in which
we can set a field that enforces short-name resolution to docker.io
in `c/image/pkg/shortnames`.

Notice that this a "hybrid" approach of doing the normalization directly
in the compat endpoints *and* in `pkg/shortnames` by passing a system
context.  Doing such a hybrid approach is neccessary since the compat
and the libpod endpoints share the same `libimage.Runtime` which makes
a global enforcement via the `libimage.Runtime.systemContext`
impossible.  Having two separate runtimes for the compat and the libpod
endpoints seems risky and not generally applicable to all endpoints.

[1] https://www.redhat.com/sysadmin/container-image-short-names

Fixes: #12320
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2021-11-23 11:08:31 +01:00
parent 8de68b1707
commit 5bdd571b1e
21 changed files with 486 additions and 118 deletions

View File

@ -512,7 +512,7 @@ validate.completions:
.PHONY: run-docker-py-tests .PHONY: run-docker-py-tests
run-docker-py-tests: run-docker-py-tests:
touch test/__init__.py touch test/__init__.py
pytest test/python/docker/ env CONTAINERS_CONF=$(CURDIR)/test/apiv2/containers.conf pytest test/python/docker/
-rm test/__init__.py -rm test/__init__.py
.PHONY: localunit .PHONY: localunit
@ -594,8 +594,8 @@ remotesystem:
.PHONY: localapiv2 .PHONY: localapiv2
localapiv2: localapiv2:
env PODMAN=./bin/podman ./test/apiv2/test-apiv2 env PODMAN=./bin/podman ./test/apiv2/test-apiv2
env PODMAN=./bin/podman ${PYTHON} -m unittest discover -v ./test/apiv2/python env CONTAINERS_CONF=$(CURDIR)/test/apiv2/containers.conf PODMAN=./bin/podman ${PYTHON} -m unittest discover -v ./test/apiv2/python
env PODMAN=./bin/podman ${PYTHON} -m unittest discover -v ./test/python/docker env CONTAINERS_CONF=$(CURDIR)/test/apiv2/containers.conf PODMAN=./bin/podman ${PYTHON} -m unittest discover -v ./test/python/docker
.PHONY: remoteapiv2 .PHONY: remoteapiv2
remoteapiv2: remoteapiv2:

4
go.mod
View File

@ -12,9 +12,9 @@ require (
github.com/containernetworking/cni v1.0.1 github.com/containernetworking/cni v1.0.1
github.com/containernetworking/plugins v1.0.1 github.com/containernetworking/plugins v1.0.1
github.com/containers/buildah v1.23.1 github.com/containers/buildah v1.23.1
github.com/containers/common v0.46.1-0.20211122213330-d4e7724a0c58 github.com/containers/common v0.46.1-0.20211125160015-ccf46abecd91
github.com/containers/conmon v2.0.20+incompatible github.com/containers/conmon v2.0.20+incompatible
github.com/containers/image/v5 v5.17.0 github.com/containers/image/v5 v5.17.1-0.20211129144953-4f6d0b45be6c
github.com/containers/ocicrypt v1.1.2 github.com/containers/ocicrypt v1.1.2
github.com/containers/psgo v1.7.1 github.com/containers/psgo v1.7.1
github.com/containers/storage v1.37.1-0.20211122214631-59ba58582415 github.com/containers/storage v1.37.1-0.20211122214631-59ba58582415

9
go.sum
View File

@ -199,7 +199,6 @@ github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo
github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
github.com/containerd/containerd v1.5.4/go.mod h1:sx18RgvW6ABJ4iYUw7Q5x7bgFOAB9B6G7+yO0XBc4zw=
github.com/containerd/containerd v1.5.5/go.mod h1:oSTh0QpT1w6jYcGmbiSbxv9OSQYaa88mPyWIuU79zyo= github.com/containerd/containerd v1.5.5/go.mod h1:oSTh0QpT1w6jYcGmbiSbxv9OSQYaa88mPyWIuU79zyo=
github.com/containerd/containerd v1.5.7 h1:rQyoYtj4KddB3bxG6SAqd4+08gePNyJjRqvOIfV3rkM= github.com/containerd/containerd v1.5.7 h1:rQyoYtj4KddB3bxG6SAqd4+08gePNyJjRqvOIfV3rkM=
github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
@ -263,14 +262,14 @@ github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNB
github.com/containers/buildah v1.23.1 h1:Tpc9DsRuU+0Oofewpxb6OJVNQjCu7yloN/obUqzfDTY= github.com/containers/buildah v1.23.1 h1:Tpc9DsRuU+0Oofewpxb6OJVNQjCu7yloN/obUqzfDTY=
github.com/containers/buildah v1.23.1/go.mod h1:4WnrN0yrA7ab0ppgunixu2WM1rlD2rG8QLJAKbEkZlQ= github.com/containers/buildah v1.23.1/go.mod h1:4WnrN0yrA7ab0ppgunixu2WM1rlD2rG8QLJAKbEkZlQ=
github.com/containers/common v0.44.2/go.mod h1:7sdP4vmI5Bm6FPFxb3lvAh1Iktb6tiO1MzjUzhxdoGo= github.com/containers/common v0.44.2/go.mod h1:7sdP4vmI5Bm6FPFxb3lvAh1Iktb6tiO1MzjUzhxdoGo=
github.com/containers/common v0.46.1-0.20211122213330-d4e7724a0c58 h1:d99ZfYePYt1gU5dPvtIdnORNtv/7mkAZUHhCJzR5D5k= github.com/containers/common v0.46.1-0.20211125160015-ccf46abecd91 h1:h9SrSLSQkvluH/sEJ8X1rlBqCoGJtLvSOu4OGK0Qtuw=
github.com/containers/common v0.46.1-0.20211122213330-d4e7724a0c58/go.mod h1:GrXYaGvQtdKA+fCQLudCTOSGRwZ06MVmRnC7KlI+syY= github.com/containers/common v0.46.1-0.20211125160015-ccf46abecd91/go.mod h1:PHwsa3UBgbvn2/MwpTQvyHXvVpuwfBrlDBx3GpIRPDQ=
github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg= github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg=
github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I= github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I=
github.com/containers/image/v5 v5.16.0/go.mod h1:XgTpfAPLRGOd1XYyCU5cISFr777bLmOerCSpt/v7+Q4= github.com/containers/image/v5 v5.16.0/go.mod h1:XgTpfAPLRGOd1XYyCU5cISFr777bLmOerCSpt/v7+Q4=
github.com/containers/image/v5 v5.16.1/go.mod h1:mCvIFdzyyP1B0NBcZ80OIuaYqFn/OpFpaOMOMn1kU2M=
github.com/containers/image/v5 v5.17.0 h1:KS5pro80CCsSp5qDBTMmSAWQo+xcBX19zUPExmYX2OQ=
github.com/containers/image/v5 v5.17.0/go.mod h1:GnYVusVRFPMMTAAUkrcS8NNSpBp8oyrjOUe04AAmRr4= github.com/containers/image/v5 v5.17.0/go.mod h1:GnYVusVRFPMMTAAUkrcS8NNSpBp8oyrjOUe04AAmRr4=
github.com/containers/image/v5 v5.17.1-0.20211129144953-4f6d0b45be6c h1:WfMOQlq3CDvVe5ONUGfj9/MajskqUHnbo24j24Xg2ZM=
github.com/containers/image/v5 v5.17.1-0.20211129144953-4f6d0b45be6c/go.mod h1:boW5ckkT0wu9obDEiOIxrtWQmz1znMuHiVMQPcpHnk0=
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE= github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE=
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=

View File

@ -52,6 +52,13 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) {
return return
} }
imageName, err := utils.NormalizeToDockerHub(r, body.Config.Image)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
body.Config.Image = imageName
newImage, resolvedName, err := runtime.LibimageRuntime().LookupImage(body.Config.Image, nil) newImage, resolvedName, err := runtime.LibimageRuntime().LookupImage(body.Config.Image, nil)
if err != nil { if err != nil {
if errors.Cause(err) == storage.ErrImageUnknown { if errors.Cause(err) == storage.ErrImageUnknown {

View File

@ -12,7 +12,6 @@ import (
"github.com/containers/common/libimage" "github.com/containers/common/libimage"
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
"github.com/containers/image/v5/manifest" "github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/pkg/shortnames"
"github.com/containers/image/v5/types" "github.com/containers/image/v5/types"
"github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod"
"github.com/containers/podman/v3/pkg/api/handlers" "github.com/containers/podman/v3/pkg/api/handlers"
@ -56,6 +55,12 @@ func ExportImage(w http.ResponseWriter, r *http.Request) {
defer os.Remove(tmpfile.Name()) defer os.Remove(tmpfile.Name())
name := utils.GetName(r) name := utils.GetName(r)
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
imageEngine := abi.ImageEngine{Libpod: runtime} imageEngine := abi.ImageEngine{Libpod: runtime}
saveOptions := entities.ImageSaveOptions{ saveOptions := entities.ImageSaveOptions{
@ -63,7 +68,7 @@ func ExportImage(w http.ResponseWriter, r *http.Request) {
Output: tmpfile.Name(), Output: tmpfile.Name(),
} }
if err := imageEngine.Save(r.Context(), name, nil, saveOptions); err != nil { if err := imageEngine.Save(r.Context(), possiblyNormalizedName, nil, saveOptions); err != nil {
if errors.Cause(err) == storage.ErrImageUnknown { if errors.Cause(err) == storage.ErrImageUnknown {
utils.ImageNotFound(w, name, errors.Wrapf(err, "failed to find image %s", name)) utils.ImageNotFound(w, name, errors.Wrapf(err, "failed to find image %s", name))
return return
@ -87,9 +92,6 @@ func ExportImage(w http.ResponseWriter, r *http.Request) {
} }
func CommitContainer(w http.ResponseWriter, r *http.Request) { func CommitContainer(w http.ResponseWriter, r *http.Request) {
var (
destImage string
)
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
@ -98,12 +100,12 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) {
Changes string `schema:"changes"` Changes string `schema:"changes"`
Comment string `schema:"comment"` Comment string `schema:"comment"`
Container string `schema:"container"` Container string `schema:"container"`
// fromSrc string # fromSrc is currently unused
Pause bool `schema:"pause"` Pause bool `schema:"pause"`
Repo string `schema:"repo"` Repo string `schema:"repo"`
Tag string `schema:"tag"` Tag string `schema:"tag"`
// fromSrc string # fromSrc is currently unused
}{ }{
// This is where you can override the golang default value for one of fields Tag: "latest",
} }
if err := decoder.Decode(&query, r.URL.Query()); err != nil { if err := decoder.Decode(&query, r.URL.Query()); err != nil {
@ -116,7 +118,6 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) {
return return
} }
sc := runtime.SystemContext() sc := runtime.SystemContext()
tag := "latest"
options := libpod.ContainerCommitOptions{ options := libpod.ContainerCommitOptions{
Pause: true, Pause: true,
} }
@ -133,9 +134,6 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) {
return return
} }
if len(query.Tag) > 0 {
tag = query.Tag
}
options.Message = query.Comment options.Message = query.Comment
options.Author = query.Author options.Author = query.Author
options.Pause = query.Pause options.Pause = query.Pause
@ -146,9 +144,15 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) {
return return
} }
// I know mitr hates this ... but doing for now var destImage string
if len(query.Repo) > 1 { if len(query.Repo) > 1 {
destImage = fmt.Sprintf("%s:%s", query.Repo, tag) destImage = fmt.Sprintf("%s:%s", query.Repo, query.Tag)
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, destImage)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
destImage = possiblyNormalizedName
} }
commitImage, err := ctr.Commit(r.Context(), destImage, options) commitImage, err := ctr.Commit(r.Context(), destImage, options)
@ -156,7 +160,7 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "CommitFailure")) utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "CommitFailure"))
return return
} }
utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: commitImage.ID()}) // nolint utils.WriteResponse(w, http.StatusCreated, handlers.IDResponse{ID: commitImage.ID()}) // nolint
} }
func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) {
@ -195,12 +199,22 @@ func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) {
} }
} }
reference := query.Repo
if query.Repo != "" {
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, reference)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
reference = possiblyNormalizedName
}
platformSpecs := strings.Split(query.Platform, "/") platformSpecs := strings.Split(query.Platform, "/")
opts := entities.ImageImportOptions{ opts := entities.ImageImportOptions{
Source: source, Source: source,
Changes: query.Changes, Changes: query.Changes,
Message: query.Message, Message: query.Message,
Reference: query.Repo, Reference: reference,
OS: platformSpecs[0], OS: platformSpecs[0],
} }
if len(platformSpecs) > 1 { if len(platformSpecs) > 1 {
@ -250,13 +264,9 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) {
return return
} }
fromImage := mergeNameAndTagOrDigest(query.FromImage, query.Tag) possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, mergeNameAndTagOrDigest(query.FromImage, query.Tag))
// without this early check this function would return 200 but reported error via body stream soon after
// it's better to let caller know early via HTTP status code that request cannot be processed
_, err := shortnames.Resolve(runtime.SystemContext(), fromImage)
if err != nil { if err != nil {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrap(err, "failed to resolve image name")) utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return return
} }
@ -291,7 +301,7 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) {
pullResChan := make(chan pullResult) pullResChan := make(chan pullResult)
go func() { go func() {
pulledImages, err := runtime.LibimageRuntime().Pull(r.Context(), fromImage, config.PullPolicyAlways, pullOptions) pulledImages, err := runtime.LibimageRuntime().Pull(r.Context(), possiblyNormalizedName, config.PullPolicyAlways, pullOptions)
pullResChan <- pullResult{images: pulledImages, err: err} pullResChan <- pullResult{images: pulledImages, err: err}
}() }()
@ -371,7 +381,13 @@ func GetImage(w http.ResponseWriter, r *http.Request) {
// 404 no such // 404 no such
// 500 internal // 500 internal
name := utils.GetName(r) name := utils.GetName(r)
newImage, err := utils.GetImage(r, name) possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
newImage, err := utils.GetImage(r, possiblyNormalizedName)
if err != nil { if err != nil {
// Here we need to fiddle with the error message because docker-py is looking for "No // Here we need to fiddle with the error message because docker-py is looking for "No
// such image" to determine on how to raise the correct exception. // such image" to determine on how to raise the correct exception.
@ -483,7 +499,16 @@ func ExportImages(w http.ResponseWriter, r *http.Request) {
return return
} }
images := query.Names images := make([]string, len(query.Names))
for i, img := range query.Names {
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, img)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
images[i] = possiblyNormalizedName
}
tmpfile, err := ioutil.TempFile("", "api.tar") tmpfile, err := ioutil.TempFile("", "api.tar")
if err != nil { if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile"))

View File

@ -118,7 +118,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
SecurityOpt string `schema:"securityopt"` SecurityOpt string `schema:"securityopt"`
ShmSize int `schema:"shmsize"` ShmSize int `schema:"shmsize"`
Squash bool `schema:"squash"` Squash bool `schema:"squash"`
Tag []string `schema:"t"` Tags []string `schema:"t"`
Target string `schema:"target"` Target string `schema:"target"`
Timestamp int64 `schema:"timestamp"` Timestamp int64 `schema:"timestamp"`
Ulimits string `schema:"ulimits"` Ulimits string `schema:"ulimits"`
@ -144,6 +144,9 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
} }
} }
// convert tag formats
tags := query.Tags
// convert addcaps formats // convert addcaps formats
var addCaps = []string{} var addCaps = []string{}
if _, found := r.URL.Query()["addcaps"]; found { if _, found := r.URL.Query()["addcaps"]; found {
@ -240,8 +243,13 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
} }
var output string var output string
if len(query.Tag) > 0 { if len(tags) > 0 {
output = query.Tag[0] possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, tags[0])
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
output = possiblyNormalizedName
} }
format := buildah.Dockerv2ImageManifest format := buildah.Dockerv2ImageManifest
registry := query.Registry registry := query.Registry
@ -257,9 +265,14 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
} }
} }
} }
var additionalTags []string var additionalTags []string // nolint
if len(query.Tag) > 1 { for i := 1; i < len(tags); i++ {
additionalTags = query.Tag[1:] possiblyNormalizedTag, err := utils.NormalizeToDockerHub(r, tags[i])
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
additionalTags = append(additionalTags, possiblyNormalizedTag)
} }
var buildArgs = map[string]string{} var buildArgs = map[string]string{}
@ -404,6 +417,22 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
} }
defer auth.RemoveAuthfile(authfile) defer auth.RemoveAuthfile(authfile)
fromImage := query.From
if fromImage != "" {
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, fromImage)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
fromImage = possiblyNormalizedName
}
systemContext := &types.SystemContext{
AuthFilePath: authfile,
DockerAuthConfig: creds,
}
utils.PossiblyEnforceDockerHub(r, systemContext)
// Channels all mux'ed in select{} below to follow API build protocol // Channels all mux'ed in select{} below to follow API build protocol
stdout := channel.NewWriter(make(chan []byte)) stdout := channel.NewWriter(make(chan []byte))
defer stdout.Close() defer stdout.Close()
@ -458,7 +487,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
Err: auxout, Err: auxout,
Excludes: excludes, Excludes: excludes,
ForceRmIntermediateCtrs: query.ForceRm, ForceRmIntermediateCtrs: query.ForceRm,
From: query.From, From: fromImage,
IgnoreUnrecognizedInstructions: query.Ignore, IgnoreUnrecognizedInstructions: query.Ignore,
Isolation: isolation, Isolation: isolation,
Jobs: &jobs, Jobs: &jobs,
@ -481,10 +510,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
RusageLogFile: query.RusageLogFile, RusageLogFile: query.RusageLogFile,
Squash: query.Squash, Squash: query.Squash,
Target: query.Target, Target: query.Target,
SystemContext: &types.SystemContext{ SystemContext: systemContext,
AuthFilePath: authfile,
DockerAuthConfig: creds,
},
} }
for _, platformSpec := range query.Platform { for _, platformSpec := range query.Platform {
@ -590,7 +616,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
logrus.Warnf("Failed to json encode error %v", err) logrus.Warnf("Failed to json encode error %v", err)
} }
flush() flush()
for _, tag := range query.Tag { for _, tag := range tags {
m.Stream = fmt.Sprintf("Successfully tagged %s\n", tag) m.Stream = fmt.Sprintf("Successfully tagged %s\n", tag)
if err := enc.Encode(m); err != nil { if err := enc.Encode(m); err != nil {
logrus.Warnf("Failed to json encode error %v", err) logrus.Warnf("Failed to json encode error %v", err)

View File

@ -14,9 +14,15 @@ func HistoryImage(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
name := utils.GetName(r) name := utils.GetName(r)
newImage, _, err := runtime.LibimageRuntime().LookupImage(name, nil) possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
if err != nil { if err != nil {
utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "failed to find image %s", name)) utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
newImage, _, err := runtime.LibimageRuntime().LookupImage(possiblyNormalizedName, nil)
if err != nil {
utils.ImageNotFound(w, possiblyNormalizedName, errors.Wrapf(err, "failed to find image %s", possiblyNormalizedName))
return return
} }
history, err := newImage.History(r.Context()) history, err := newImage.History(r.Context())

View File

@ -61,12 +61,24 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
if query.Tag != "" { if query.Tag != "" {
imageName += ":" + query.Tag imageName += ":" + query.Tag
} }
if _, err := utils.ParseStorageReference(imageName); err != nil { if _, err := utils.ParseStorageReference(imageName); err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "image source %q is not a containers-storage-transport reference", imageName)) errors.Wrapf(err, "image source %q is not a containers-storage-transport reference", imageName))
return return
} }
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, imageName)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
imageName = possiblyNormalizedName
if _, _, err := runtime.LibimageRuntime().LookupImage(possiblyNormalizedName, nil); err != nil {
utils.ImageNotFound(w, imageName, errors.Wrapf(err, "failed to find image %s", imageName))
return
}
authconf, authfile, key, err := auth.GetCredentials(r) authconf, authfile, key, err := auth.GetCredentials(r)
if err != nil { if err != nil {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))

View File

@ -34,12 +34,18 @@ func RemoveImage(w http.ResponseWriter, r *http.Request) {
} }
} }
name := utils.GetName(r) name := utils.GetName(r)
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
imageEngine := abi.ImageEngine{Libpod: runtime} imageEngine := abi.ImageEngine{Libpod: runtime}
options := entities.ImageRemoveOptions{ options := entities.ImageRemoveOptions{
Force: query.Force, Force: query.Force,
} }
report, rmerrors := imageEngine.Remove(r.Context(), []string{name}, options) report, rmerrors := imageEngine.Remove(r.Context(), []string{possiblyNormalizedName}, options)
if len(rmerrors) > 0 && rmerrors[0] != nil { if len(rmerrors) > 0 && rmerrors[0] != nil {
err := rmerrors[0] err := rmerrors[0]
if errors.Cause(err) == storage.ErrImageUnknown { if errors.Cause(err) == storage.ErrImageUnknown {

View File

@ -14,12 +14,16 @@ import (
func TagImage(w http.ResponseWriter, r *http.Request) { func TagImage(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
// /v1.xx/images/(name)/tag
name := utils.GetName(r) name := utils.GetName(r)
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
// Allow tagging manifest list instead of resolving instances from manifest // Allow tagging manifest list instead of resolving instances from manifest
lookupOptions := &libimage.LookupImageOptions{ManifestList: true} lookupOptions := &libimage.LookupImageOptions{ManifestList: true}
newImage, _, err := runtime.LibimageRuntime().LookupImage(name, lookupOptions) newImage, _, err := runtime.LibimageRuntime().LookupImage(possiblyNormalizedName, lookupOptions)
if err != nil { if err != nil {
utils.ImageNotFound(w, name, errors.Wrapf(err, "failed to find image %s", name)) utils.ImageNotFound(w, name, errors.Wrapf(err, "failed to find image %s", name))
return return
@ -35,7 +39,14 @@ func TagImage(w http.ResponseWriter, r *http.Request) {
} }
repo := r.Form.Get("repo") repo := r.Form.Get("repo")
tagName := fmt.Sprintf("%s:%s", repo, tag) tagName := fmt.Sprintf("%s:%s", repo, tag)
if err := newImage.Tag(tagName); err != nil {
possiblyNormalizedTag, err := utils.NormalizeToDockerHub(r, tagName)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
if err := newImage.Tag(possiblyNormalizedTag); err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return return
} }

View File

@ -3,19 +3,61 @@ package utils
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/containers/common/libimage" "github.com/containers/common/libimage"
"github.com/containers/common/pkg/filters" "github.com/containers/common/pkg/filters"
"github.com/containers/image/v5/docker" "github.com/containers/image/v5/docker"
"github.com/containers/image/v5/storage" storageTransport "github.com/containers/image/v5/storage"
"github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types" "github.com/containers/image/v5/types"
"github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod"
api "github.com/containers/podman/v3/pkg/api/types" api "github.com/containers/podman/v3/pkg/api/types"
"github.com/containers/podman/v3/pkg/util"
"github.com/containers/storage"
"github.com/docker/distribution/reference"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// NormalizeToDockerHub normalizes the specified nameOrID to Docker Hub if the
// request is for the compat API and if containers.conf set the specific mode.
// If nameOrID is a (short) ID for a local image, the full ID will be returned.
func NormalizeToDockerHub(r *http.Request, nameOrID string) (string, error) {
if IsLibpodRequest(r) || !util.DefaultContainerConfig().Engine.CompatAPIEnforceDockerHub {
return nameOrID, nil
}
// Try to lookup the input to figure out if it was an ID or not.
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
img, _, err := runtime.LibimageRuntime().LookupImage(nameOrID, nil)
if err != nil {
if errors.Cause(err) != storage.ErrImageUnknown {
return "", fmt.Errorf("normalizing name for compat API: %v", err)
}
} else if strings.HasPrefix(img.ID(), nameOrID) {
return img.ID(), nil
}
// No ID, so we can normalize.
named, err := reference.ParseNormalizedNamed(nameOrID)
if err != nil {
return "", fmt.Errorf("normalizing name for compat API: %v", err)
}
return named.String(), nil
}
// PossiblyEnforceDockerHub sets fields in the system context to enforce
// resolving short names to Docker Hub if the request is for the compat API and
// if containers.conf set the specific mode.
func PossiblyEnforceDockerHub(r *http.Request, sys *types.SystemContext) {
if IsLibpodRequest(r) || !util.DefaultContainerConfig().Engine.CompatAPIEnforceDockerHub {
return
}
sys.PodmanOnlyShortNamesIgnoreRegistriesConfAndForceDockerHub = true
}
// IsRegistryReference checks if the specified name points to the "docker://" // IsRegistryReference checks if the specified name points to the "docker://"
// transport. If it points to no supported transport, we'll assume a // transport. If it points to no supported transport, we'll assume a
// non-transport reference pointing to an image (e.g., "fedora:latest"). // non-transport reference pointing to an image (e.g., "fedora:latest").
@ -35,13 +77,13 @@ func IsRegistryReference(name string) error {
// `types.ImageReference` and enforces it to refer to a // `types.ImageReference` and enforces it to refer to a
// containers-storage-transport reference. // containers-storage-transport reference.
func ParseStorageReference(name string) (types.ImageReference, error) { func ParseStorageReference(name string) (types.ImageReference, error) {
storagePrefix := fmt.Sprintf("%s:", storage.Transport.Name()) storagePrefix := storageTransport.Transport.Name()
imageRef, err := alltransports.ParseImageName(name) imageRef, err := alltransports.ParseImageName(name)
if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { if err == nil && imageRef.Transport().Name() != docker.Transport.Name() {
return nil, errors.Errorf("reference %q must be a storage reference", name) return nil, errors.Errorf("reference %q must be a storage reference", name)
} else if err != nil { } else if err != nil {
origErr := err origErr := err
imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", storagePrefix, name)) imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s:%s", storagePrefix, name))
if err != nil { if err != nil {
return nil, errors.Wrapf(origErr, "reference %q must be a storage reference", name) return nil, errors.Wrapf(origErr, "reference %q must be a storage reference", name)
} }

View File

@ -47,8 +47,7 @@ t POST "images/localhost:5000/myrepo/push?tlsVerify=false&tag=mytag" 200
t POST "libpod/images/$iid/untag?repo=localhost:5000/myrepo&tag=mytag" 201 t POST "libpod/images/$iid/untag?repo=localhost:5000/myrepo&tag=mytag" 201
# Try to push non-existing image # Try to push non-existing image
t POST "images/localhost:5000/idonotexist/push?tlsVerify=false" 200 t POST "images/localhost:5000/idonotexist/push?tlsVerify=false" 404
jq -re 'select(.errorDetail)' <<<"$output" &>/dev/null || echo -e "${red}not ok: error message not found in output${nc}" 1>&2
t GET libpod/images/$IMAGE/json 200 \ t GET libpod/images/$IMAGE/json 200 \
.RepoTags[-1]=$IMAGE .RepoTags[-1]=$IMAGE

View File

@ -0,0 +1,148 @@
# -*- sh -*-
#
# Tests for exercising short-name resolution in the compat API.
#
# Pull the libpod/quay image which is used in all tests below.
t POST "images/create?fromImage=quay.io/libpod/alpine:latest" 200 .error~null .status~".*Download complete.*"
########## TAG
t POST "images/quay.io/libpod/alpine/tag?repo=foo" 201
t DELETE "images/foo" 200
########## BUILD
function test_build {
from=$1
tag=$2
fqn=$3
TMPD=$(mktemp -d podman-apiv2-test.build.XXXXXXXX)
CONTAINERFILE_TAR="${TMPD}/containerfile.tar"
cat > $TMPD/containerfile << EOF
FROM $from
RUN touch /foo
EOF
tar --format=posix -C $TMPD -cvf ${CONTAINERFILE_TAR} containerfile &> /dev/null
curl -XPOST --data-binary @<(cat $CONTAINERFILE_TAR) \
-H "content-type: application/x-tar" \
--dump-header "${TMPD}/headers.txt" \
-o "${TMPD}/response.txt" \
"http://$HOST:$PORT/build?dockerfile=containerfile&t=$tag" &> /dev/null
if ! grep -q '200 OK' "${TMPD}/headers.txt"; then
cat "${TMPD}/headers.txt"
cat "${TMPD}/response.txt"
echo -e "${red}NOK: Image build from tar failed response was not 200 OK (application/x-tar)"
exit 1
fi
rm -rf $TMPD
t DELETE "images/$fqn" 200
}
t POST "images/quay.io/libpod/alpine/tag?repo=foo" 201
test_build foo bar "docker.io/library/bar:latest"
t DELETE "images/foo" 200
########## TAG
# Looking up 'alpine' will fail as it gets normalized to docker.io.
t POST "images/alpine/tag?repo=foo" 404 .cause="image not known"
# The libpod endpoint will resolve to it without issues.
t GET "libpod/images/alpine/exists" 204
# Now let's tag the image with 'foo'. Remember, it will be normalized to
# docker.io/library/foo.
t GET "libpod/images/docker.io/library/foo/exists" 404
t POST "images/quay.io/libpod/alpine/tag?repo=foo" 201
t GET "libpod/images/docker.io/library/foo/exists" 204
########## REMOVE
t DELETE "images/alpine" 404 .cause="image not known" # fails since docker.io/library/alpine does not exist
t DELETE "images/foo" 200 # removes the previously tagged image
########## GET
# Same procedure as above but with the /get endpoint.
t GET "images/alpine/get" 404 .cause="image not known"
t POST "images/quay.io/libpod/alpine/tag?repo=foo" 201
t GET "images/foo/get" 200 '[POSIX tar archive]'
t DELETE "images/foo" 200
########## HISTORY
t GET "images/alpine/history" 404 .cause="image not known"
t GET "images/quay.io/libpod/alpine/history" 200
t POST "images/quay.io/libpod/alpine/tag?repo=foo" 201
t GET "libpod/images/foo/history" 200
t DELETE "images/foo" 200
########## PUSH
t POST "images/alpine/push?destination=localhost:9999/do/not:exist" 404 .cause="image not known"
t POST "images/quay.io/libpod/alpine/push?destination=localhost:9999/do/not:exist" 200 # Error is in the response
t POST "images/quay.io/libpod/alpine/tag?repo=foo" 201
t POST "images/foo/push?destination=localhost:9999/do/not:exist" 200 # Error is in the response
t DELETE "images/foo" 200
########## CREATE A CONTAINER
t POST "containers/create" Image=alpine 404 .cause="image not known"
t POST "containers/create" Image=quay.io/libpod/alpine:latest 201
cid=$(jq -r '.Id' <<<"$output")
t POST "images/quay.io/libpod/alpine/tag?repo=foo" 201
t POST "containers/create" Image=foo 201
cid=$(jq -r '.Id' <<<"$output")
t DELETE "images/foo" 200
t DELETE "containers/$cid" 204
########## COMMIT CONTAINER
t POST "containers/create" Image=quay.io/libpod/alpine:latest 201
cid=$(jq -r '.Id' <<<"$output")
t GET "images/alpine/get" 404 .cause="image not known"
t POST "commit?container=$cid&repo=foo&tag=tag" 201
t GET "images/foo/get" 404 .cause="image not known"
t GET "images/foo:tag/get" 200
t DELETE "images/docker.io/library/foo:tag" 200
t DELETE "containers/$cid" 204
######### SMOKE TESTS WITHOUT DOCKER.IO ENFORCEMENT
# Note that we need to restart the service with a custom containers.conf to
# disable the docker.io enforcement.
stop_service
CONTAINERS_CONF=$(pwd)/test/apiv2/containers.conf start_service
t POST "images/create?fromImage=quay.io/libpod/alpine:latest" 200 .error~null .status~".*Download complete.*"
t POST "images/alpine/tag?repo=foo" 201
t GET "images/localhost/foo:latest/get" 200
t DELETE "images/foo" 200
t GET "images/alpine/history" 200
t POST "images/alpine/push?destination=localhost:9999/do/not:exist" 200 # Error is in the response
t POST "containers/create" Image=alpine 201
cid=$(jq -r '.Id' <<<"$output")
t POST "commit?container=$cid&repo=foo&tag=tag" 201
t DELETE "images/localhost/foo:tag" 200
t DELETE "containers/$cid" 204
test_build alpine bar "localhost/bar:latest"
stop_service
start_service

View File

@ -0,0 +1,8 @@
# This containers.conf file is used to test enforcing short-name resolution to
# docker.io for Podman's *compat* API. By default, the compat API defaults to
# resolving to docker.io only. The behavior can be altered by configuring the
# containers.conf as done below in which case short names are subject to aliases,
# "localhost/" and the unqualified-search registries.
[engine]
compat_api_enforce_docker_hub=false

View File

@ -58,6 +58,10 @@ type SearchOptions struct {
InsecureSkipTLSVerify types.OptionalBool InsecureSkipTLSVerify types.OptionalBool
// ListTags returns the search result with available tags // ListTags returns the search result with available tags
ListTags bool ListTags bool
// Registries to search if the specified term does not include a
// registry. If set, the unqualified-search registries in
// containers-registries.conf(5) are ignored.
Registries []string
} }
// SearchFilter allows filtering images while searching. // SearchFilter allows filtering images while searching.
@ -105,6 +109,10 @@ func ParseSearchFilter(filter []string) (*SearchFilter, error) {
return sFilter, nil return sFilter, nil
} }
// Search searches term. If term includes a registry, only this registry will
// be used for searching. Otherwise, the unqualified-search registries in
// containers-registries.conf(5) or the ones specified in the options will be
// used.
func (r *Runtime) Search(ctx context.Context, term string, options *SearchOptions) ([]SearchResult, error) { func (r *Runtime) Search(ctx context.Context, term string, options *SearchOptions) ([]SearchResult, error) {
if options == nil { if options == nil {
options = &SearchOptions{} options = &SearchOptions{}
@ -117,10 +125,14 @@ func (r *Runtime) Search(ctx context.Context, term string, options *SearchOption
// that we cannot use the reference parser from the containers/image // that we cannot use the reference parser from the containers/image
// library as the search term may container arbitrary input such as // library as the search term may container arbitrary input such as
// wildcards. See bugzilla.redhat.com/show_bug.cgi?id=1846629. // wildcards. See bugzilla.redhat.com/show_bug.cgi?id=1846629.
if spl := strings.SplitN(term, "/", 2); len(spl) > 1 { spl := strings.SplitN(term, "/", 2)
searchRegistries = append(searchRegistries, spl[0]) switch {
case len(spl) > 1:
searchRegistries = []string{spl[0]}
term = spl[1] term = spl[1]
} else { case len(options.Registries) > 0:
searchRegistries = options.Registries
default:
regs, err := sysregistriesv2.UnqualifiedSearchRegistries(r.systemContextCopy()) regs, err := sysregistriesv2.UnqualifiedSearchRegistries(r.systemContextCopy())
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -84,7 +84,7 @@ type imageCopier struct {
manifestUpdates *types.ManifestUpdateOptions manifestUpdates *types.ManifestUpdateOptions
src types.Image src types.Image
diffIDsAreNeeded bool diffIDsAreNeeded bool
canModifyManifest bool cannotModifyManifestReason string // The reason the manifest cannot be modified, or an empty string if it can
canSubstituteBlobs bool canSubstituteBlobs bool
ociEncryptLayers *[]int ociEncryptLayers *[]int
} }
@ -129,10 +129,14 @@ type Options struct {
DestinationCtx *types.SystemContext DestinationCtx *types.SystemContext
ProgressInterval time.Duration // time to wait between reports to signal the progress channel ProgressInterval time.Duration // time to wait between reports to signal the progress channel
Progress chan types.ProgressProperties // Reported to when ProgressInterval has arrived for a single artifact+offset. Progress chan types.ProgressProperties // Reported to when ProgressInterval has arrived for a single artifact+offset.
// Preserve digests, and fail if we cannot.
PreserveDigests bool
// manifest MIME type of image set by user. "" is default and means use the autodetection to the the manifest MIME type // manifest MIME type of image set by user. "" is default and means use the autodetection to the the manifest MIME type
ForceManifestMIMEType string ForceManifestMIMEType string
ImageListSelection ImageListSelection // set to either CopySystemImage (the default), CopyAllImages, or CopySpecificImages to control which instances we copy when the source reference is a list; ignored if the source reference is not a list ImageListSelection ImageListSelection // set to either CopySystemImage (the default), CopyAllImages, or CopySpecificImages to control which instances we copy when the source reference is a list; ignored if the source reference is not a list
Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances and the list itself Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances and the list itself
// If OciEncryptConfig is non-nil, it indicates that an image should be encrypted. // If OciEncryptConfig is non-nil, it indicates that an image should be encrypted.
// The encryption options is derived from the construction of EncryptConfig object. // The encryption options is derived from the construction of EncryptConfig object.
// Note: During initial encryption process of a layer, the resultant digest is not known // Note: During initial encryption process of a layer, the resultant digest is not known
@ -410,7 +414,36 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur
return nil, errors.Wrapf(err, "Can not copy signatures to %s", transports.ImageName(c.dest.Reference())) return nil, errors.Wrapf(err, "Can not copy signatures to %s", transports.ImageName(c.dest.Reference()))
} }
} }
canModifyManifestList := (len(sigs) == 0)
// If the destination is a digested reference, make a note of that, determine what digest value we're
// expecting, and check that the source manifest matches it.
destIsDigestedReference := false
if named := c.dest.Reference().DockerReference(); named != nil {
if digested, ok := named.(reference.Digested); ok {
destIsDigestedReference = true
matches, err := manifest.MatchesDigest(manifestList, digested.Digest())
if err != nil {
return nil, errors.Wrapf(err, "computing digest of source image's manifest")
}
if !matches {
return nil, errors.New("Digest of source image's manifest would not match destination reference")
}
}
}
// Determine if we're allowed to modify the manifest list.
// If we can, set to the empty string. If we can't, set to the reason why.
// Compare, and perhaps keep in sync with, the version in copyOneImage.
cannotModifyManifestListReason := ""
if len(sigs) > 0 {
cannotModifyManifestListReason = "Would invalidate signatures"
}
if destIsDigestedReference {
cannotModifyManifestListReason = "Destination specifies a digest"
}
if options.PreserveDigests {
cannotModifyManifestListReason = "Instructed to preserve digests"
}
// Determine if we'll need to convert the manifest list to a different format. // Determine if we'll need to convert the manifest list to a different format.
forceListMIMEType := options.ForceManifestMIMEType forceListMIMEType := options.ForceManifestMIMEType
@ -425,8 +458,8 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur
return nil, errors.Wrapf(err, "determining manifest list type to write to destination") return nil, errors.Wrapf(err, "determining manifest list type to write to destination")
} }
if selectedListType != originalList.MIMEType() { if selectedListType != originalList.MIMEType() {
if !canModifyManifestList { if cannotModifyManifestListReason != "" {
return nil, errors.Errorf("manifest list must be converted to type %q to be written to destination, but that would invalidate signatures", selectedListType) return nil, errors.Errorf("Manifest list must be converted to type %q to be written to destination, but we cannot modify it: %q", selectedListType, cannotModifyManifestListReason)
} }
} }
@ -510,8 +543,8 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur
// If we can't just use the original value, but we have to change it, flag an error. // If we can't just use the original value, but we have to change it, flag an error.
if !bytes.Equal(attemptedManifestList, originalManifestList) { if !bytes.Equal(attemptedManifestList, originalManifestList) {
if !canModifyManifestList { if cannotModifyManifestListReason != "" {
return nil, errors.Errorf(" manifest list must be converted to type %q to be written to destination, but that would invalidate signatures", thisListType) return nil, errors.Errorf("Manifest list must be converted to type %q to be written to destination, but we cannot modify it: %q", thisListType, cannotModifyManifestListReason)
} }
logrus.Debugf("Manifest list has been updated") logrus.Debugf("Manifest list has been updated")
} else { } else {
@ -629,12 +662,26 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
} }
} }
// Determine if we're allowed to modify the manifest.
// If we can, set to the empty string. If we can't, set to the reason why.
// Compare, and perhaps keep in sync with, the version in copyMultipleImages.
cannotModifyManifestReason := ""
if len(sigs) > 0 {
cannotModifyManifestReason = "Would invalidate signatures"
}
if destIsDigestedReference {
cannotModifyManifestReason = "Destination specifies a digest"
}
if options.PreserveDigests {
cannotModifyManifestReason = "Instructed to preserve digests"
}
ic := imageCopier{ ic := imageCopier{
c: c, c: c,
manifestUpdates: &types.ManifestUpdateOptions{InformationOnly: types.ManifestUpdateInformation{Destination: c.dest}}, manifestUpdates: &types.ManifestUpdateOptions{InformationOnly: types.ManifestUpdateInformation{Destination: c.dest}},
src: src, src: src,
// diffIDsAreNeeded is computed later // diffIDsAreNeeded is computed later
canModifyManifest: len(sigs) == 0 && !destIsDigestedReference, cannotModifyManifestReason: cannotModifyManifestReason,
ociEncryptLayers: options.OciEncryptLayers, ociEncryptLayers: options.OciEncryptLayers,
} }
// Ensure _this_ copy sees exactly the intended data when either processing a signed image or signing it. // Ensure _this_ copy sees exactly the intended data when either processing a signed image or signing it.
@ -643,7 +690,7 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
// We do intend the RecordDigestUncompressedPair calls to only work with reliable data, but at least theres a risk // We do intend the RecordDigestUncompressedPair calls to only work with reliable data, but at least theres a risk
// that the compressed version coming from a third party may be designed to attack some other decompressor implementation, // that the compressed version coming from a third party may be designed to attack some other decompressor implementation,
// and we would reuse and sign it. // and we would reuse and sign it.
ic.canSubstituteBlobs = ic.canModifyManifest && options.SignBy == "" ic.canSubstituteBlobs = ic.cannotModifyManifestReason == "" && options.SignBy == ""
if err := ic.updateEmbeddedDockerReference(); err != nil { if err := ic.updateEmbeddedDockerReference(); err != nil {
return nil, "", "", err return nil, "", "", err
@ -710,10 +757,10 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli
} }
// If the original MIME type is acceptable, determineManifestConversion always uses it as preferredManifestMIMEType. // If the original MIME type is acceptable, determineManifestConversion always uses it as preferredManifestMIMEType.
// So if we are here, we will definitely be trying to convert the manifest. // So if we are here, we will definitely be trying to convert the manifest.
// With !ic.canModifyManifest, that would just be a string of repeated failures for the same reason, // With ic.cannotModifyManifestReason != "", that would just be a string of repeated failures for the same reason,
// so lets bail out early and with a better error message. // so lets bail out early and with a better error message.
if !ic.canModifyManifest { if ic.cannotModifyManifestReason != "" {
return nil, "", "", errors.Wrap(err, "Writing manifest failed (and converting it is not possible, image is signed or the destination specifies a digest)") return nil, "", "", errors.Wrapf(err, "Writing manifest failed and we cannot try conversions: %q", cannotModifyManifestReason)
} }
// errs is a list of errors when trying various manifest types. Also serves as an "upload succeeded" flag when set to nil. // errs is a list of errors when trying various manifest types. Also serves as an "upload succeeded" flag when set to nil.
@ -813,9 +860,9 @@ func (ic *imageCopier) updateEmbeddedDockerReference() error {
return nil // No reference embedded in the manifest, or it matches destRef already. return nil // No reference embedded in the manifest, or it matches destRef already.
} }
if !ic.canModifyManifest { if ic.cannotModifyManifestReason != "" {
return errors.Errorf("Copying a schema1 image with an embedded Docker reference to %s (Docker reference %s) would change the manifest, which is not possible (image is signed or the destination specifies a digest)", return errors.Errorf("Copying a schema1 image with an embedded Docker reference to %s (Docker reference %s) would change the manifest, which we cannot do: %q",
transports.ImageName(ic.c.dest.Reference()), destRef.String()) transports.ImageName(ic.c.dest.Reference()), destRef.String(), ic.cannotModifyManifestReason)
} }
ic.manifestUpdates.EmbeddedDockerReference = destRef ic.manifestUpdates.EmbeddedDockerReference = destRef
return nil return nil
@ -833,7 +880,7 @@ func isTTY(w io.Writer) bool {
return false return false
} }
// copyLayers copies layers from ic.src/ic.c.rawSource to dest, using and updating ic.manifestUpdates if necessary and ic.canModifyManifest. // copyLayers copies layers from ic.src/ic.c.rawSource to dest, using and updating ic.manifestUpdates if necessary and ic.cannotModifyManifestReason == "".
func (ic *imageCopier) copyLayers(ctx context.Context) error { func (ic *imageCopier) copyLayers(ctx context.Context) error {
srcInfos := ic.src.LayerInfos() srcInfos := ic.src.LayerInfos()
numLayers := len(srcInfos) numLayers := len(srcInfos)
@ -844,8 +891,8 @@ func (ic *imageCopier) copyLayers(ctx context.Context) error {
srcInfosUpdated := false srcInfosUpdated := false
// If we only need to check authorization, no updates required. // If we only need to check authorization, no updates required.
if updatedSrcInfos != nil && !reflect.DeepEqual(srcInfos, updatedSrcInfos) { if updatedSrcInfos != nil && !reflect.DeepEqual(srcInfos, updatedSrcInfos) {
if !ic.canModifyManifest { if ic.cannotModifyManifestReason != "" {
return errors.Errorf("Copying this image requires changing layer representation, which is not possible (image is signed or the destination specifies a digest)") return errors.Errorf("Copying this image would require changing layer representation, which we cannot do: %q", ic.cannotModifyManifestReason)
} }
srcInfos = updatedSrcInfos srcInfos = updatedSrcInfos
srcInfosUpdated = true srcInfosUpdated = true
@ -975,8 +1022,8 @@ func layerDigestsDiffer(a, b []types.BlobInfo) bool {
func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, digest.Digest, error) { func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, digest.Digest, error) {
pendingImage := ic.src pendingImage := ic.src
if !ic.noPendingManifestUpdates() { if !ic.noPendingManifestUpdates() {
if !ic.canModifyManifest { if ic.cannotModifyManifestReason != "" {
return nil, "", errors.Errorf("Internal error: copy needs an updated manifest but that was known to be forbidden") return nil, "", errors.Errorf("Internal error: copy needs an updated manifest but that was known to be forbidden: %q", ic.cannotModifyManifestReason)
} }
if !ic.diffIDsAreNeeded && ic.src.UpdatedImageNeedsLayerDiffIDs(*ic.manifestUpdates) { if !ic.diffIDsAreNeeded && ic.src.UpdatedImageNeedsLayerDiffIDs(*ic.manifestUpdates) {
// We have set ic.diffIDsAreNeeded based on the preferred MIME type returned by determineManifestConversion. // We have set ic.diffIDsAreNeeded based on the preferred MIME type returned by determineManifestConversion.
@ -1359,7 +1406,7 @@ func (ic *imageCopier) copyLayerFromStream(ctx context.Context, srcStream io.Rea
} }
} }
blobInfo, err := ic.c.copyBlobFromStream(ctx, srcStream, srcInfo, getDiffIDRecorder, ic.canModifyManifest, false, toEncrypt, bar, layerIndex, emptyLayer) // Sets err to nil on success blobInfo, err := ic.c.copyBlobFromStream(ctx, srcStream, srcInfo, getDiffIDRecorder, ic.cannotModifyManifestReason == "", false, toEncrypt, bar, layerIndex, emptyLayer) // Sets err to nil on success
return blobInfo, diffIDChan, err return blobInfo, diffIDChan, err
// We need the defer … pipeWriter.CloseWithError() to happen HERE so that the caller can block on reading from diffIDChan // We need the defer … pipeWriter.CloseWithError() to happen HERE so that the caller can block on reading from diffIDChan
} }

View File

@ -79,10 +79,10 @@ func (ic *imageCopier) determineManifestConversion(ctx context.Context, destSupp
if _, ok := supportedByDest[srcType]; ok { if _, ok := supportedByDest[srcType]; ok {
prioritizedTypes.append(srcType) prioritizedTypes.append(srcType)
} }
if !ic.canModifyManifest { if ic.cannotModifyManifestReason != "" {
// We could also drop the !ic.canModifyManifest check and have the caller // We could also drop this check and have the caller
// make the choice; it is already doing that to an extent, to improve error // make the choice; it is already doing that to an extent, to improve error
// messages. But it is nice to hide the “if !ic.canModifyManifest, do no conversion” // messages. But it is nice to hide the “if we can't modify, do no conversion”
// special case in here; the caller can then worry (or not) only about a good UI. // special case in here; the caller can then worry (or not) only about a good UI.
logrus.Debugf("We can't modify the manifest, hoping for the best...") logrus.Debugf("We can't modify the manifest, hoping for the best...")
return srcType, []string{}, nil // Take our chances - FIXME? Or should we fail without trying? return srcType, []string{}, nil // Take our chances - FIXME? Or should we fail without trying?

View File

@ -118,6 +118,7 @@ type Resolved struct {
} }
func (r *Resolved) addCandidate(named reference.Named) { func (r *Resolved) addCandidate(named reference.Named) {
named = reference.TagNameOnly(named) // Make sure to add ":latest" if needed
r.PullCandidates = append(r.PullCandidates, PullCandidate{named, false, r}) r.PullCandidates = append(r.PullCandidates, PullCandidate{named, false, r})
} }
@ -138,6 +139,8 @@ const (
rationaleUSR rationaleUSR
// Resolved value has been selected by the user (via the prompt). // Resolved value has been selected by the user (via the prompt).
rationaleUserSelection rationaleUserSelection
// Resolved value has been enforced to use Docker Hub (via SystemContext).
rationaleEnforcedDockerHub
) )
// Description returns a human-readable description about the resolution // Description returns a human-readable description about the resolution
@ -152,6 +155,8 @@ func (r *Resolved) Description() string {
return fmt.Sprintf("Resolved %q as an alias (%s)", r.userInput, r.originDescription) return fmt.Sprintf("Resolved %q as an alias (%s)", r.userInput, r.originDescription)
case rationaleUSR: case rationaleUSR:
return fmt.Sprintf("Resolving %q using unqualified-search registries (%s)", r.userInput, r.originDescription) return fmt.Sprintf("Resolving %q using unqualified-search registries (%s)", r.userInput, r.originDescription)
case rationaleEnforcedDockerHub:
return fmt.Sprintf("Resolving %q to docker.io (%s)", r.userInput, r.originDescription)
case rationaleUserSelection, rationaleNone: case rationaleUserSelection, rationaleNone:
fallthrough fallthrough
default: default:
@ -265,8 +270,20 @@ func Resolve(ctx *types.SystemContext, name string) (*Resolved, error) {
return nil, err return nil, err
} }
if !isShort { // no short name if !isShort { // no short name
named := reference.TagNameOnly(shortRef) // Make sure to add ":latest" if needed resolved.addCandidate(shortRef)
return resolved, nil
}
// Resolve to docker.io only if enforced by the caller (e.g., Podman's
// Docker-compatible REST API).
if ctx != nil && ctx.PodmanOnlyShortNamesIgnoreRegistriesConfAndForceDockerHub {
named, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, errors.Wrapf(err, "cannot normalize input: %q", name)
}
resolved.addCandidate(named) resolved.addCandidate(named)
resolved.rationale = rationaleEnforcedDockerHub
resolved.originDescription = "enforced by caller"
return resolved, nil return resolved, nil
} }
@ -295,9 +312,6 @@ func Resolve(ctx *types.SystemContext, name string) (*Resolved, error) {
return nil, err return nil, err
} }
} }
// Make sure to add ":latest" if needed
namedAlias = reference.TagNameOnly(namedAlias)
resolved.addCandidate(namedAlias) resolved.addCandidate(namedAlias)
resolved.rationale = rationaleAlias resolved.rationale = rationaleAlias
resolved.originDescription = aliasOriginDescription resolved.originDescription = aliasOriginDescription
@ -325,9 +339,6 @@ func Resolve(ctx *types.SystemContext, name string) (*Resolved, error) {
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "creating reference with unqualified-search registry %q", reg) return nil, errors.Wrapf(err, "creating reference with unqualified-search registry %q", reg)
} }
// Make sure to add ":latest" if needed
named = reference.TagNameOnly(named)
resolved.addCandidate(named) resolved.addCandidate(named)
} }
@ -412,6 +423,23 @@ func ResolveLocally(ctx *types.SystemContext, name string) ([]reference.Named, e
var candidates []reference.Named var candidates []reference.Named
// Complete the candidates with the specified registries.
completeCandidates := func(registries []string) ([]reference.Named, error) {
for _, reg := range registries {
named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name))
if err != nil {
return nil, errors.Wrapf(err, "creating reference with unqualified-search registry %q", reg)
}
named = reference.TagNameOnly(named) // Make sure to add ":latest" if needed
candidates = append(candidates, named)
}
return candidates, nil
}
if ctx != nil && ctx.PodmanOnlyShortNamesIgnoreRegistriesConfAndForceDockerHub {
return completeCandidates([]string{"docker.io"})
}
// Strip off the tag to normalize the short name for looking it up in // Strip off the tag to normalize the short name for looking it up in
// the config files. // the config files.
isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef) isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef)
@ -434,9 +462,7 @@ func ResolveLocally(ctx *types.SystemContext, name string) ([]reference.Named, e
return nil, err return nil, err
} }
} }
// Make sure to add ":latest" if needed namedAlias = reference.TagNameOnly(namedAlias) // Make sure to add ":latest" if needed
namedAlias = reference.TagNameOnly(namedAlias)
candidates = append(candidates, namedAlias) candidates = append(candidates, namedAlias)
} }
@ -447,16 +473,5 @@ func ResolveLocally(ctx *types.SystemContext, name string) ([]reference.Named, e
} }
// Note that "localhost" has precedence over the unqualified-search registries. // Note that "localhost" has precedence over the unqualified-search registries.
for _, reg := range append([]string{"localhost"}, unqualifiedSearchRegistries...) { return completeCandidates(append([]string{"localhost"}, unqualifiedSearchRegistries...))
named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name))
if err != nil {
return nil, errors.Wrapf(err, "creating reference with unqualified-search registry %q", reg)
}
// Make sure to add ":latest" if needed
named = reference.TagNameOnly(named)
candidates = append(candidates, named)
}
return candidates, nil
} }

View File

@ -561,6 +561,11 @@ type SystemContext struct {
UserShortNameAliasConfPath string UserShortNameAliasConfPath string
// If set, short-name resolution in pkg/shortnames must follow the specified mode // If set, short-name resolution in pkg/shortnames must follow the specified mode
ShortNameMode *ShortNameMode ShortNameMode *ShortNameMode
// If set, short names will resolve in pkg/shortnames to docker.io only, and unqualified-search registries and
// short-name aliases in registries.conf are ignored. Note that this field is only intended to help enforce
// resolving to Docker Hub in the Docker-compatible REST API of Podman; it should never be used outside this
// specific context.
PodmanOnlyShortNamesIgnoreRegistriesConfAndForceDockerHub bool
// If not "", overrides the default path for the authentication file, but only new format files // If not "", overrides the default path for the authentication file, but only new format files
AuthFilePath string AuthFilePath string
// if not "", overrides the default path for the authentication file, but with the legacy format; // if not "", overrides the default path for the authentication file, but with the legacy format;

View File

@ -8,10 +8,10 @@ const (
// VersionMinor is for functionality in a backwards-compatible manner // VersionMinor is for functionality in a backwards-compatible manner
VersionMinor = 17 VersionMinor = 17
// VersionPatch is for backwards-compatible bug fixes // VersionPatch is for backwards-compatible bug fixes
VersionPatch = 0 VersionPatch = 1
// VersionDev indicates development branch. Releases will be empty string. // VersionDev indicates development branch. Releases will be empty string.
VersionDev = "" VersionDev = "-dev"
) )
// Version is the specification version that the package types support. // Version is the specification version that the package types support.

4
vendor/modules.txt vendored
View File

@ -106,7 +106,7 @@ github.com/containers/buildah/pkg/rusage
github.com/containers/buildah/pkg/sshagent github.com/containers/buildah/pkg/sshagent
github.com/containers/buildah/pkg/util github.com/containers/buildah/pkg/util
github.com/containers/buildah/util github.com/containers/buildah/util
# github.com/containers/common v0.46.1-0.20211122213330-d4e7724a0c58 # github.com/containers/common v0.46.1-0.20211125160015-ccf46abecd91
## explicit ## explicit
github.com/containers/common/libimage github.com/containers/common/libimage
github.com/containers/common/libimage/manifests github.com/containers/common/libimage/manifests
@ -142,7 +142,7 @@ github.com/containers/common/version
# github.com/containers/conmon v2.0.20+incompatible # github.com/containers/conmon v2.0.20+incompatible
## explicit ## explicit
github.com/containers/conmon/runner/config github.com/containers/conmon/runner/config
# github.com/containers/image/v5 v5.17.0 # github.com/containers/image/v5 v5.17.1-0.20211129144953-4f6d0b45be6c
## explicit ## explicit
github.com/containers/image/v5/copy github.com/containers/image/v5/copy
github.com/containers/image/v5/directory github.com/containers/image/v5/directory