Files
podman/pkg/api/handlers/libpod/manifests.go
Paul Holzinger 942f789a88 set !remote build tags where needed
The new golangci-lint version 1.60.1 has problems with typecheck when
linting remote files. We have certain pakcages that should never be
inlcuded in remote but the typecheck tries to compile all of them but
this never works and it seems to ignore the exclude files we gave it.

To fix this the proper way is to mark all packages we only use locally
with !remote tags. This is a bit ugly but more correct. I also moved the
DecodeChanges() code around as it is called from the client so the
handles package which should only be remote doesn't really fit anyway.

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2024-08-19 11:41:28 +02:00

759 lines
24 KiB
Go

//go:build !remote
package libpod
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/libpod"
"github.com/containers/podman/v5/pkg/api/handlers"
"github.com/containers/podman/v5/pkg/api/handlers/utils"
"github.com/containers/podman/v5/pkg/api/handlers/utils/apiutil"
api "github.com/containers/podman/v5/pkg/api/types"
"github.com/containers/podman/v5/pkg/auth"
"github.com/containers/podman/v5/pkg/channel"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/infra/abi"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)
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"`
Amend bool `schema:"amend"`
Annotation []string `schema:"annotation"`
Annotations map[string]string `schema:"annotations"`
}{
// 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}
annotations := maps.Clone(query.Annotations)
for _, annotation := range query.Annotation {
k, v, ok := strings.Cut(annotation, "=")
if !ok {
utils.Error(w, http.StatusBadRequest,
fmt.Errorf("invalid annotation %s", annotation))
return
}
if annotations == nil {
annotations = make(map[string]string)
}
annotations[k] = v
}
createOptions := entities.ManifestCreateOptions{All: query.All, Amend: query.Amend, Annotations: annotations}
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 == apiutil.ErrVersionNotSupported {
status = http.StatusCreated
}
buffer, err := io.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("decoding modifications in request: %w", err))
return
}
if len(body.IndexAnnotation) != 0 || len(body.IndexAnnotations) != 0 || body.IndexSubject != "" {
manifestAnnotateOptions := entities.ManifestAnnotateOptions{
IndexAnnotation: body.IndexAnnotation,
IndexAnnotations: body.IndexAnnotations,
IndexSubject: body.IndexSubject,
}
if _, err := imageEngine.ManifestAnnotate(r.Context(), manID, "", manifestAnnotateOptions); err != nil {
utils.InternalServerError(w, err)
return
}
}
if len(body.Images) > 0 {
if _, err := imageEngine.ManifestAdd(r.Context(), manID, body.Images, body.ManifestAddOptions); err != nil {
utils.InternalServerError(w, err)
return
}
}
utils.WriteResponse(w, status, entities.IDResponse{ID: manID})
}
// 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)
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
name := utils.GetName(r)
// Wrapper to support 3.x with 4.x libpod
query := struct {
TLSVerify bool `schema:"tlsVerify"`
}{}
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
}
_, authfile, err := auth.GetCredentials(r)
if err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
opts := entities.ManifestInspectOptions{Authfile: authfile}
if _, found := r.URL.Query()["tlsVerify"]; found {
opts.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
}
imageEngine := abi.ImageEngine{Libpod: runtime}
manifest, err := imageEngine.ManifestInspect(r.Context(), name, opts)
if err != nil {
utils.Error(w, http.StatusNotFound, err)
return
}
utils.WriteResponse(w, http.StatusOK, manifest)
}
// 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("decoding AddV3 query: %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(r.Context(), source, query.Destination, options)
if err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("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"`
CompressionFormat string `schema:"compressionFormat"`
CompressionLevel *int `schema:"compressionLevel"`
ForceCompressionFormat bool `schema:"forceCompressionFormat"`
Format string `schema:"format"`
RemoveSignatures bool `schema:"removeSignatures"`
TLSVerify bool `schema:"tlsVerify"`
Quiet bool `schema:"quiet"`
AddCompression []string `schema:"addCompression"`
}{
// Add defaults here once needed.
TLSVerify: true,
// #15210: older versions did not sent *any* data, so we need
// to be quiet by default to remain backwards compatible
Quiet: 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{
All: query.All,
Authfile: authfile,
AddCompression: query.AddCompression,
CompressionFormat: query.CompressionFormat,
CompressionLevel: query.CompressionLevel,
ForceCompressionFormat: query.ForceCompressionFormat,
Format: query.Format,
Password: password,
Quiet: true,
RemoveSignatures: query.RemoveSignatures,
Username: username,
}
if _, found := r.URL.Query()["compressionFormat"]; found {
if _, foundForceCompression := r.URL.Query()["forceCompressionFormat"]; !foundForceCompression {
// If `compressionFormat` is set and no value for `forceCompressionFormat`
// is selected then default has to be `true`.
options.ForceCompressionFormat = true
}
}
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)
// Let's keep thing simple when running in quiet mode and push directly.
if query.Quiet {
digest, err := imageEngine.ManifestPush(r.Context(), source, destination, options)
if err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("pushing image %q: %w", destination, err))
return
}
utils.WriteResponse(w, http.StatusOK, entities.ManifestPushReport{ID: digest})
return
}
writer := channel.NewWriter(make(chan []byte))
defer writer.Close()
options.Writer = writer
pushCtx, pushCancel := context.WithCancel(r.Context())
var digest string
var pushError error
go func() {
defer pushCancel()
digest, pushError = imageEngine.ManifestPush(pushCtx, source, destination, options)
}()
flush := func() {
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
flush()
enc := json.NewEncoder(w)
enc.SetEscapeHTML(true)
for {
var report entities.ManifestPushReport
select {
case s := <-writer.Chan():
report.Stream = string(s)
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to encode json: %v", err)
}
flush()
case <-pushCtx.Done():
if pushError != nil {
report.Error = pushError.Error()
} else {
report.ID = digest
}
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to encode json: %v", err)
}
flush()
return
case <-r.Context().Done():
// Client has closed connection
return
}
}
}
// 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)
multireader, err := r.MultipartReader()
if err != nil {
multireader = nil
// not multipart - request is just encoded JSON, nothing else
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decoding modify request: %w", err))
return
}
} else {
// multipart - request is encoded JSON in the first part, each artifact is its own part
bodyPart, err := multireader.NextPart()
if err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("reading first part of multipart request: %w", err))
return
}
err = json.NewDecoder(bodyPart).Decode(body)
bodyPart.Close()
if err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decoding modify request in multipart request: %w", err))
return
}
}
name := utils.GetName(r)
manifestList, err := runtime.LibimageRuntime().LookupManifestList(name)
if err != nil {
utils.Error(w, http.StatusNotFound, err)
return
}
annotationsFromAnnotationSlice := func(annotation []string) map[string]string {
annotations := make(map[string]string)
for _, annotationSpec := range annotation {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("no value given for annotation %q", key))
return nil
}
annotations[key] = val
}
return annotations
}
if len(body.ManifestAddOptions.Annotation) != 0 {
if len(body.ManifestAddOptions.Annotations) != 0 {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both Annotation and Annotations"))
return
}
body.ManifestAddOptions.Annotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.Annotation)
body.ManifestAddOptions.Annotation = nil
}
if len(body.ManifestAddOptions.IndexAnnotation) != 0 {
if len(body.ManifestAddOptions.IndexAnnotations) != 0 {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both IndexAnnotation and IndexAnnotations"))
return
}
body.ManifestAddOptions.IndexAnnotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.IndexAnnotation)
body.ManifestAddOptions.IndexAnnotation = nil
}
var artifactExtractionError error
var artifactExtraction sync.WaitGroup
if multireader != nil {
// If the data was multipart, then save items from it into a
// directory that will be removed along with this list,
// whenever that happens.
artifactExtraction.Add(1)
go func() {
defer artifactExtraction.Done()
storageConfig := runtime.StorageConfig()
// FIXME: knowing that this is the location of the
// per-image-record-stuff directory is a little too
// "inside storage"
fileDir, err := os.MkdirTemp(filepath.Join(runtime.GraphRoot(), storageConfig.GraphDriverName+"-images", manifestList.ID()), "")
if err != nil {
artifactExtractionError = err
return
}
// We'll be building a list of the names of files we
// received as part of the request and setting it in
// the request body before we're done.
var contentFiles []string
part, err := multireader.NextPart()
if err != nil {
artifactExtractionError = err
return
}
for part != nil {
partName := part.FormName()
if filename := part.FileName(); filename != "" {
partName = filename
}
if partName != "" {
partName = path.Base(partName)
}
// Write the file in a scope that lets us close it as quickly
// as possible.
if err = func() error {
defer part.Close()
var f *os.File
// Create the file.
if partName != "" {
// Try to use the supplied name.
f, err = os.OpenFile(filepath.Join(fileDir, partName), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
} else {
// No supplied name means they don't care.
f, err = os.CreateTemp(fileDir, "upload")
}
if err != nil {
return err
}
defer f.Close()
// Write the file's contents.
if _, err = io.Copy(f, part); err != nil {
return err
}
contentFiles = append(contentFiles, f.Name())
return nil
}(); err != nil {
break
}
part, err = multireader.NextPart()
}
// If we stowed all of the uploaded files without issue, we're all good.
if err != nil && !errors.Is(err, io.EOF) {
artifactExtractionError = err
return
}
// Save the list of files that we created.
body.ArtifactFiles = contentFiles
}()
}
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
}
report := entities.ManifestModifyReport{ID: manifestList.ID()}
switch {
case strings.EqualFold("update", body.Operation):
if len(body.Images) > 0 {
id, err := imageEngine.ManifestAdd(r.Context(), name, body.Images, body.ManifestAddOptions)
if err != nil {
report.Errors = append(report.Errors, err)
break
}
report.ID = id
report.Images = body.Images
}
if multireader != nil {
// Wait for the extraction goroutine to finish
// processing the message in the request body, so that
// we know whether or not everything looked alright.
artifactExtraction.Wait()
if artifactExtractionError != nil {
report.Errors = append(report.Errors, artifactExtractionError)
artifactExtractionError = nil
break
}
// Reconstruct a ManifestAddArtifactOptions from the corresponding
// fields in the entities.ManifestModifyOptions that we decoded
// the request struct into and then supplemented with the files list.
// We waited until after the extraction goroutine finished to ensure
// that we'd pick up its changes to the ArtifactFiles list.
manifestAddArtifactOptions := entities.ManifestAddArtifactOptions{
Type: body.ArtifactType,
LayerType: body.ArtifactLayerType,
ConfigType: body.ArtifactConfigType,
Config: body.ArtifactConfig,
ExcludeTitles: body.ArtifactExcludeTitles,
Annotations: body.ArtifactAnnotations,
Subject: body.ArtifactSubject,
Files: body.ArtifactFiles,
}
id, err := imageEngine.ManifestAddArtifact(r.Context(), name, body.ArtifactFiles, manifestAddArtifactOptions)
if err != nil {
report.Errors = append(report.Errors, err)
break
}
report.ID = id
report.Files = body.ArtifactFiles
}
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 := body.ManifestAnnotateOptions
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
}
// In case something weird happened, don't just let the goroutine go; make the
// client at least wait for it.
artifactExtraction.Wait()
if artifactExtractionError != nil {
report.Errors = append(report.Errors, artifactExtractionError)
}
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)
}