mirror of
https://github.com/containers/podman.git
synced 2025-08-06 19:44:14 +08:00

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>
759 lines
24 KiB
Go
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)
|
|
}
|