mirror of
https://github.com/containers/podman.git
synced 2025-05-20 16:47:39 +08:00

Option defaults in API must be the same as in CLI. ``` % podman image push --help % podman image pull --help % podman manifest push --help % podman image search --help ``` All of these CLI commands them have --tls-verify=true by default: ``` --tls-verify require HTTPS and verify certificates when accessing the registry (default true) ``` As for `podman image build`, it doesn't have any means to control `tlsVerify` parameter but it must be true by default. Signed-off-by: Vladimir Kochnev <hashtable@yandex.ru>
480 lines
14 KiB
Go
480 lines
14 KiB
Go
package libpod
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/containers/image/v5/docker/reference"
|
|
"github.com/containers/image/v5/manifest"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/podman/v4/libpod"
|
|
"github.com/containers/podman/v4/pkg/api/handlers"
|
|
"github.com/containers/podman/v4/pkg/api/handlers/utils"
|
|
api "github.com/containers/podman/v4/pkg/api/types"
|
|
"github.com/containers/podman/v4/pkg/auth"
|
|
"github.com/containers/podman/v4/pkg/domain/entities"
|
|
"github.com/containers/podman/v4/pkg/domain/infra/abi"
|
|
"github.com/containers/podman/v4/pkg/errorhandling"
|
|
"github.com/gorilla/mux"
|
|
"github.com/gorilla/schema"
|
|
"github.com/opencontainers/go-digest"
|
|
)
|
|
|
|
func ManifestCreate(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
|
query := struct {
|
|
Name string `schema:"name"`
|
|
Images []string `schema:"images"`
|
|
All bool `schema:"all"`
|
|
}{
|
|
// Add defaults here once needed.
|
|
}
|
|
|
|
// Support 3.x API calls, alias image to images
|
|
if image, ok := r.URL.Query()["image"]; ok {
|
|
query.Images = image
|
|
}
|
|
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
utils.Error(w, http.StatusBadRequest,
|
|
fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
|
return
|
|
}
|
|
|
|
// Support 4.x API calls, map query parameter to path
|
|
if name, ok := mux.Vars(r)["name"]; ok {
|
|
n, err := url.QueryUnescape(name)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest,
|
|
fmt.Errorf("failed to parse name parameter %q: %w", name, err))
|
|
return
|
|
}
|
|
query.Name = n
|
|
}
|
|
|
|
if _, err := reference.ParseNormalizedNamed(query.Name); err != nil {
|
|
utils.Error(w, http.StatusBadRequest,
|
|
fmt.Errorf("invalid image name %s: %w", query.Name, err))
|
|
return
|
|
}
|
|
|
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
|
|
createOptions := entities.ManifestCreateOptions{All: query.All}
|
|
manID, err := imageEngine.ManifestCreate(r.Context(), query.Name, query.Images, createOptions)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
status := http.StatusOK
|
|
if _, err := utils.SupportedVersion(r, "< 4.0.0"); err == utils.ErrVersionNotSupported {
|
|
status = http.StatusCreated
|
|
}
|
|
|
|
buffer, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
// Treat \r\n as empty body
|
|
if len(buffer) < 3 {
|
|
utils.WriteResponse(w, status, entities.IDResponse{ID: manID})
|
|
return
|
|
}
|
|
|
|
body := new(entities.ManifestModifyOptions)
|
|
if err := json.Unmarshal(buffer, body); err != nil {
|
|
utils.InternalServerError(w, fmt.Errorf("Decode(): %w", err))
|
|
return
|
|
}
|
|
|
|
// gather all images for manifest list
|
|
var images []string
|
|
if len(query.Images) > 0 {
|
|
images = query.Images
|
|
}
|
|
if len(body.Images) > 0 {
|
|
images = body.Images
|
|
}
|
|
|
|
id, err := imageEngine.ManifestAdd(r.Context(), query.Name, images, body.ManifestAddOptions)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
utils.WriteResponse(w, status, entities.IDResponse{ID: id})
|
|
}
|
|
|
|
// ManifestExists return true if manifest list exists.
|
|
func ManifestExists(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
name := utils.GetName(r)
|
|
|
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
report, err := imageEngine.ManifestExists(r.Context(), name)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if !report.Value {
|
|
utils.Error(w, http.StatusNotFound, errors.New("manifest not found"))
|
|
return
|
|
}
|
|
utils.WriteResponse(w, http.StatusNoContent, "")
|
|
}
|
|
|
|
func ManifestInspect(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
name := utils.GetName(r)
|
|
|
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
rawManifest, err := imageEngine.ManifestInspect(r.Context(), name)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
|
|
var schema2List manifest.Schema2List
|
|
if err := json.Unmarshal(rawManifest, &schema2List); err != nil {
|
|
utils.Error(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
utils.WriteResponse(w, http.StatusOK, schema2List)
|
|
}
|
|
|
|
// ManifestAddV3 remove digest from manifest list
|
|
//
|
|
// As of 4.0.0 use ManifestModify instead
|
|
func ManifestAddV3(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
|
|
// Wrapper to support 3.x with 4.x libpod
|
|
query := struct {
|
|
entities.ManifestAddOptions
|
|
TLSVerify bool `schema:"tlsVerify"`
|
|
}{}
|
|
if err := json.NewDecoder(r.Body).Decode(&query); err != nil {
|
|
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("Decode(): %w", err))
|
|
return
|
|
}
|
|
|
|
authconf, authfile, err := auth.GetCredentials(r)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
defer auth.RemoveAuthfile(authfile)
|
|
var username, password string
|
|
if authconf != nil {
|
|
username = authconf.Username
|
|
password = authconf.Password
|
|
}
|
|
query.ManifestAddOptions.Authfile = authfile
|
|
query.ManifestAddOptions.Username = username
|
|
query.ManifestAddOptions.Password = password
|
|
if sys := runtime.SystemContext(); sys != nil {
|
|
query.ManifestAddOptions.CertDir = sys.DockerCertPath
|
|
}
|
|
if _, found := r.URL.Query()["tlsVerify"]; found {
|
|
query.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
|
|
}
|
|
|
|
name := utils.GetName(r)
|
|
if _, err := runtime.LibimageRuntime().LookupManifestList(name); err != nil {
|
|
utils.Error(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
|
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
newID, err := imageEngine.ManifestAdd(r.Context(), name, query.Images, query.ManifestAddOptions)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
utils.WriteResponse(w, http.StatusOK, entities.IDResponse{ID: newID})
|
|
}
|
|
|
|
// ManifestRemoveDigestV3 remove digest from manifest list
|
|
//
|
|
// As of 4.0.0 use ManifestModify instead
|
|
func ManifestRemoveDigestV3(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
|
query := struct {
|
|
Digest string `schema:"digest"`
|
|
}{
|
|
// Add defaults here once needed.
|
|
}
|
|
name := utils.GetName(r)
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
utils.Error(w, http.StatusBadRequest,
|
|
fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
|
return
|
|
}
|
|
manifestList, err := runtime.LibimageRuntime().LookupManifestList(name)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
d, err := digest.Parse(query.Digest)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
if err := manifestList.RemoveInstance(d); err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
utils.WriteResponse(w, http.StatusOK, entities.IDResponse{ID: manifestList.ID()})
|
|
}
|
|
|
|
// ManifestPushV3 push image to registry
|
|
//
|
|
// As of 4.0.0 use ManifestPush instead
|
|
func ManifestPushV3(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
|
query := struct {
|
|
All bool `schema:"all"`
|
|
Destination string `schema:"destination"`
|
|
RemoveSignatures bool `schema:"removeSignatures"`
|
|
TLSVerify bool `schema:"tlsVerify"`
|
|
}{
|
|
// Add defaults here once needed.
|
|
}
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
utils.Error(w, http.StatusBadRequest,
|
|
fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
|
return
|
|
}
|
|
if err := utils.IsRegistryReference(query.Destination); err != nil {
|
|
utils.Error(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
source := utils.GetName(r)
|
|
authconf, authfile, err := auth.GetCredentials(r)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
defer auth.RemoveAuthfile(authfile)
|
|
var username, password string
|
|
if authconf != nil {
|
|
username = authconf.Username
|
|
password = authconf.Password
|
|
}
|
|
options := entities.ImagePushOptions{
|
|
All: query.All,
|
|
Authfile: authfile,
|
|
Password: password,
|
|
RemoveSignatures: query.RemoveSignatures,
|
|
Username: username,
|
|
}
|
|
if sys := runtime.SystemContext(); sys != nil {
|
|
options.CertDir = sys.DockerCertPath
|
|
}
|
|
if _, found := r.URL.Query()["tlsVerify"]; found {
|
|
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
|
|
}
|
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
digest, err := imageEngine.ManifestPush(context.Background(), source, query.Destination, options)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest, fmt.Errorf("error pushing image %q: %w", query.Destination, err))
|
|
return
|
|
}
|
|
utils.WriteResponse(w, http.StatusOK, entities.IDResponse{ID: digest})
|
|
}
|
|
|
|
// ManifestPush push image to registry
|
|
//
|
|
// As of 4.0.0
|
|
func ManifestPush(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
|
|
|
query := struct {
|
|
All bool `schema:"all"`
|
|
TLSVerify bool `schema:"tlsVerify"`
|
|
}{
|
|
// Add defaults here once needed.
|
|
TLSVerify: true,
|
|
}
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
utils.Error(w, http.StatusBadRequest,
|
|
fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
|
return
|
|
}
|
|
|
|
destination := utils.GetVar(r, "destination")
|
|
if err := utils.IsRegistryReference(destination); err != nil {
|
|
utils.Error(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
authconf, authfile, err := auth.GetCredentials(r)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse registry header for %s: %w", r.URL.String(), err))
|
|
return
|
|
}
|
|
defer auth.RemoveAuthfile(authfile)
|
|
var username, password string
|
|
if authconf != nil {
|
|
username = authconf.Username
|
|
password = authconf.Password
|
|
}
|
|
options := entities.ImagePushOptions{
|
|
Authfile: authfile,
|
|
Username: username,
|
|
Password: password,
|
|
All: query.All,
|
|
}
|
|
if sys := runtime.SystemContext(); sys != nil {
|
|
options.CertDir = sys.DockerCertPath
|
|
}
|
|
if _, found := r.URL.Query()["tlsVerify"]; found {
|
|
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
|
|
}
|
|
|
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
source := utils.GetName(r)
|
|
digest, err := imageEngine.ManifestPush(context.Background(), source, destination, options)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest, fmt.Errorf("error pushing image %q: %w", destination, err))
|
|
return
|
|
}
|
|
utils.WriteResponse(w, http.StatusOK, entities.IDResponse{ID: digest})
|
|
}
|
|
|
|
// ManifestModify efficiently updates the named manifest list
|
|
func ManifestModify(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
|
|
body := new(entities.ManifestModifyOptions)
|
|
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
|
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("Decode(): %w", err))
|
|
return
|
|
}
|
|
|
|
name := utils.GetName(r)
|
|
if _, err := runtime.LibimageRuntime().LookupManifestList(name); err != nil {
|
|
utils.Error(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
|
|
if tlsVerify, ok := r.URL.Query()["tlsVerify"]; ok {
|
|
tls, err := strconv.ParseBool(tlsVerify[len(tlsVerify)-1])
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest, fmt.Errorf("tlsVerify param is not a bool: %w", err))
|
|
return
|
|
}
|
|
body.SkipTLSVerify = types.NewOptionalBool(!tls)
|
|
}
|
|
|
|
authconf, authfile, err := auth.GetCredentials(r)
|
|
if err != nil {
|
|
utils.Error(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
defer auth.RemoveAuthfile(authfile)
|
|
var username, password string
|
|
if authconf != nil {
|
|
username = authconf.Username
|
|
password = authconf.Password
|
|
}
|
|
body.ManifestAddOptions.Authfile = authfile
|
|
body.ManifestAddOptions.Username = username
|
|
body.ManifestAddOptions.Password = password
|
|
if sys := runtime.SystemContext(); sys != nil {
|
|
body.ManifestAddOptions.CertDir = sys.DockerCertPath
|
|
}
|
|
|
|
var report entities.ManifestModifyReport
|
|
switch {
|
|
case strings.EqualFold("update", body.Operation):
|
|
id, err := imageEngine.ManifestAdd(r.Context(), name, body.Images, body.ManifestAddOptions)
|
|
if err != nil {
|
|
report.Errors = append(report.Errors, err)
|
|
break
|
|
}
|
|
report = entities.ManifestModifyReport{
|
|
ID: id,
|
|
Images: body.Images,
|
|
}
|
|
case strings.EqualFold("remove", body.Operation):
|
|
for _, image := range body.Images {
|
|
id, err := imageEngine.ManifestRemoveDigest(r.Context(), name, image)
|
|
if err != nil {
|
|
report.Errors = append(report.Errors, err)
|
|
continue
|
|
}
|
|
report.ID = id
|
|
report.Images = append(report.Images, image)
|
|
}
|
|
case strings.EqualFold("annotate", body.Operation):
|
|
options := entities.ManifestAnnotateOptions{
|
|
Annotation: body.Annotation,
|
|
Arch: body.Arch,
|
|
Features: body.Features,
|
|
OS: body.OS,
|
|
OSFeatures: body.OSFeatures,
|
|
OSVersion: body.OSVersion,
|
|
Variant: body.Variant,
|
|
}
|
|
for _, image := range body.Images {
|
|
id, err := imageEngine.ManifestAnnotate(r.Context(), name, image, options)
|
|
if err != nil {
|
|
report.Errors = append(report.Errors, err)
|
|
continue
|
|
}
|
|
report.ID = id
|
|
report.Images = append(report.Images, image)
|
|
}
|
|
default:
|
|
utils.Error(w, http.StatusBadRequest, fmt.Errorf("illegal operation %q for %q", body.Operation, r.URL.String()))
|
|
return
|
|
}
|
|
|
|
statusCode := http.StatusOK
|
|
switch {
|
|
case len(report.Errors) > 0 && len(report.Images) > 0:
|
|
statusCode = http.StatusConflict
|
|
case len(report.Errors) > 0:
|
|
statusCode = http.StatusBadRequest
|
|
}
|
|
utils.WriteResponse(w, statusCode, report)
|
|
}
|
|
|
|
// ManifestDelete removes a manifest list from storage
|
|
func ManifestDelete(w http.ResponseWriter, r *http.Request) {
|
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
|
|
name := utils.GetName(r)
|
|
if _, err := runtime.LibimageRuntime().LookupManifestList(name); err != nil {
|
|
utils.Error(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
|
|
results, errs := imageEngine.ManifestRm(r.Context(), []string{name})
|
|
errsString := errorhandling.ErrorsToStrings(errs)
|
|
report := handlers.LibpodImagesRemoveReport{
|
|
ImageRemoveReport: *results,
|
|
Errors: errsString,
|
|
}
|
|
utils.WriteResponse(w, http.StatusOK, report)
|
|
}
|