add new libpod/images/$name/resolve endpoint

Podman Desktop [1] is looking into improving the user experience which
requires to know the source of an image.  Consider the user triggers an
image pull and Podman Desktop wants to figure out whether the image name
refers to a Red Hat registry, for instance, to prompt installing the RH
auth extension.

Since the input values of images may be a short name [2], Podman Desktop
has no means to figure out the (potential) source of the image.  Hence,
add a new `/resolve` endpoint to allow external callers to figure out
the (potential) fully-qualified image name of a given value.

With the new endpoint, Podman Desktop can ask Podman directly to resolve
the image name and then make an informed decision whether to prompt the
user to perform certain tasks or not.  This for sure can also be used
for any other registry (e.g., Quay, Docker Hub).

[1] https://github.com/containers/podman-desktop/issues/5771
[2] https://www.redhat.com/sysadmin/container-image-short-names

Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2024-01-31 14:34:24 +01:00
parent 0655bf3d34
commit b028f6aa75
6 changed files with 87 additions and 1 deletions

View File

@ -15,6 +15,8 @@ import (
"github.com/containers/common/libimage"
"github.com/containers/common/pkg/ssh"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/pkg/shortnames"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v4/libpod"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/api/handlers"
@ -720,3 +722,36 @@ func ImageScp(w http.ResponseWriter, r *http.Request) {
utils.WriteResponse(w, http.StatusOK, &reports.ScpReport{Id: rep.Names[0]})
}
// Resolve the passed (short) name to one more candidates it may resolve to.
// See https://www.redhat.com/sysadmin/container-image-short-names.
//
// One user of this endpoint is Podman Desktop which needs to figure out where
// an image may resolve to.
func ImageResolve(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
name := utils.GetName(r)
mode := types.ShortNameModeDisabled
sys := runtime.SystemContext()
sys.ShortNameMode = &mode
resolved, err := shortnames.Resolve(sys, name)
if err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("resolving %q: %w", name, err))
return
}
if len(resolved.PullCandidates) == 0 { // Should never happen but let's be defensive.
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("name %q did not resolve to any candidate", name))
return
}
names := make([]string, 0, len(resolved.PullCandidates))
for _, candidate := range resolved.PullCandidates {
names = append(names, candidate.Value.String())
}
report := handlers.LibpodImagesResolveReport{Names: names}
utils.WriteResponse(w, http.StatusOK, report)
}

View File

@ -34,6 +34,12 @@ type LibpodImagesRemoveReport struct {
Errors []string
}
// LibpodImagesResolveReport includes a list of fully-qualified image references.
type LibpodImagesResolveReport struct {
// Fully-qualified image references.
Names []string
}
type ContainersPruneReport struct {
docker.ContainersPruneReport
}

View File

@ -1690,5 +1690,27 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
// 500:
// $ref: '#/responses/internalError'
r.Handle(VersionedPath("/libpod/images/scp/{name:.*}"), s.APIHandler(libpod.ImageScp)).Methods(http.MethodPost)
// swagger:operation GET /libpod/images/{name}/resolve libpod ImageResolveLibpod
// ---
// tags:
// - images
// summary: Resolve an image (short) name
// description: Resolve the passed image name to a list of fully-qualified images referring to container registries.
// parameters:
// - in: path
// name: name
// type: string
// required: true
// description: the (short) name to resolve
// produces:
// - application/json
// responses:
// 204:
// description: resolved image names
// 400:
// $ref: "#/responses/badParamError"
// 500:
// $ref: '#/responses/internalError'
r.Handle(VersionedPath("/libpod/images/{name:.*}/resolve"), s.APIHandler(libpod.ImageResolve)).Methods(http.MethodGet)
return nil
}

View File

@ -325,4 +325,22 @@ t DELETE images/$iid_test2?noprune=false 200
t GET libpod/images/$iid_test1/exists 404
t GET libpod/images/$iid_test2/exists 404
# If the /resolve tests fail, make sure to use ../registries.conf for the
# podman-service.
# With an alias, we only get one item back.
t GET libpod/images/podman-desktop-test123:this/resolve 200 \
.Names[0]="florent.fr/will/like:this"
# If no alias matches, we will get a candidate for each unqualified-search
# registry.
t GET libpod/images/no-alias-for-sure/resolve 200 \
.Names[0]="docker.io/library/no-alias-for-sure:latest" \
.Names[1]="quay.io/no-alias-for-sure:latest" \
.Names[2]="registry.fedoraproject.org/no-alias-for-sure:latest"
# Test invalid input.
t GET libpod/images/noCAPITALcharAllowed/resolve 400 \
.cause="repository name must be lowercase"
# vim: filetype=sh

View File

@ -448,7 +448,8 @@ function start_service() {
$PODMAN_BIN unshare true
fi
$PODMAN_BIN \
CONTAINERS_REGISTRIES_CONF=$TESTS_DIR/../registries.conf \
$PODMAN_BIN \
--root $WORKDIR/server_root --syslog=true \
system service \
--time 0 \

View File

@ -21,3 +21,7 @@ location="quay.io/libpod"
[[registry]]
location="localhost:5000"
insecure=true
# Alias used in tests.
[aliases]
"podman-desktop-test123"="florent.fr/will/like"