//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) }