1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
|
//go:build !remote
package utils
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/containers/common/libimage"
"github.com/containers/common/pkg/config"
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/docker/reference"
storageTransport "github.com/containers/image/v5/storage"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/libpod"
api "github.com/containers/podman/v5/pkg/api/types"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/containers/storage"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/sirupsen/logrus"
)
// NormalizeToDockerHub normalizes the specified nameOrID to Docker Hub if the
// request is for the compat API and if containers.conf set the specific mode.
// If nameOrID is a (short) ID for a local image, the full ID will be returned.
func NormalizeToDockerHub(r *http.Request, nameOrID string) (string, error) {
cfg, err := config.Default()
if err != nil {
return "", err
}
if IsLibpodRequest(r) || !cfg.Engine.CompatAPIEnforceDockerHub {
return nameOrID, nil
}
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
// The candidate may resolve to a local non-Docker Hub image, such as
// 'busybox' -> 'registry.com/busybox'.
img, candidate, err := runtime.LibimageRuntime().LookupImage(nameOrID, nil)
if err != nil {
if !errors.Is(err, storage.ErrImageUnknown) {
return "", fmt.Errorf("normalizing name for compat API: %v", err)
}
// If the image could not be resolved locally, set the
// candidate back to the input.
candidate = nameOrID
} else if strings.HasPrefix(img.ID(), strings.TrimPrefix(nameOrID, "sha256:")) {
return img.ID(), nil
}
// No ID, so we can normalize.
named, err := reference.ParseNormalizedNamed(candidate)
if err != nil {
return "", fmt.Errorf("normalizing name %q (orig: %q) for compat API: %v", candidate, nameOrID, err)
}
return named.String(), nil
}
// PossiblyEnforceDockerHub sets fields in the system context to enforce
// resolving short names to Docker Hub if the request is for the compat API and
// if containers.conf set the specific mode.
func PossiblyEnforceDockerHub(r *http.Request, sys *types.SystemContext) error {
cfg, err := config.Default()
if err != nil {
return err
}
if IsLibpodRequest(r) || !cfg.Engine.CompatAPIEnforceDockerHub {
return nil
}
sys.PodmanOnlyShortNamesIgnoreRegistriesConfAndForceDockerHub = true
return nil
}
// IsRegistryReference checks if the specified name points to the "docker://"
// transport. If it points to no supported transport, we'll assume a
// non-transport reference pointing to an image (e.g., "fedora:latest").
func IsRegistryReference(name string) error {
imageRef, err := alltransports.ParseImageName(name)
if err != nil {
// No supported transport -> assume a docker-stype reference.
return nil //nolint: nilerr
}
if imageRef.Transport().Name() == docker.Transport.Name() {
return nil
}
return fmt.Errorf("unsupported transport %s in %q: only docker transport is supported", imageRef.Transport().Name(), name)
}
// ParseStorageReference parses the specified image name to a
// `types.ImageReference` and enforces it to refer to a
// containers-storage-transport reference.
func ParseStorageReference(name string) (types.ImageReference, error) {
storagePrefix := storageTransport.Transport.Name()
imageRef, err := alltransports.ParseImageName(name)
if err == nil && imageRef.Transport().Name() != docker.Transport.Name() {
return nil, fmt.Errorf("reference %q must be a storage reference", name)
} else if err != nil {
origErr := err
imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s:%s", storagePrefix, name))
if err != nil {
return nil, fmt.Errorf("reference %q must be a storage reference: %w", name, origErr)
}
}
return imageRef, nil
}
func GetImage(r *http.Request, name string) (*libimage.Image, error) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
image, _, err := runtime.LibimageRuntime().LookupImage(name, nil)
if err != nil {
return nil, err
}
return image, err
}
type pullResult struct {
images []*libimage.Image
err error
}
func CompatPull(ctx context.Context, w http.ResponseWriter, runtime *libpod.Runtime, reference string, pullPolicy config.PullPolicy, pullOptions *libimage.PullOptions) {
progress := make(chan types.ProgressProperties)
pullOptions.Progress = progress
pullResChan := make(chan pullResult)
go func() {
pulledImages, err := runtime.LibimageRuntime().Pull(ctx, reference, pullPolicy, pullOptions)
pullResChan <- pullResult{images: pulledImages, err: err}
}()
enc := json.NewEncoder(w)
enc.SetEscapeHTML(true)
flush := func() {
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
statusWritten := false
writeStatusCode := func(code int) {
if !statusWritten {
w.WriteHeader(code)
w.Header().Set("Content-Type", "application/json")
flush()
statusWritten = true
}
}
progressSent := false
loop: // break out of for/select infinite loop
for {
report := jsonmessage.JSONMessage{}
report.Progress = &jsonmessage.JSONProgress{}
select {
case e := <-progress:
writeStatusCode(http.StatusOK)
progressSent = true
switch e.Event {
case types.ProgressEventNewArtifact:
report.Status = "Pulling fs layer"
case types.ProgressEventRead:
report.Status = "Downloading"
report.Progress.Current = int64(e.Offset)
report.Progress.Total = e.Artifact.Size
//nolint:staticcheck // Deprecated field, but because consumers might still read it keep it.
report.ProgressMessage = report.Progress.String()
case types.ProgressEventSkipped:
report.Status = "Already exists"
case types.ProgressEventDone:
report.Status = "Download complete"
}
report.ID = e.Artifact.Digest.Encoded()[0:12]
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
}
flush()
case pullRes := <-pullResChan:
err := pullRes.err
if err != nil {
var errcd errcode.ErrorCoder
if errors.As(err, &errcd) {
writeStatusCode(errcd.ErrorCode().Descriptor().HTTPStatusCode)
} else {
writeStatusCode(http.StatusInternalServerError)
}
msg := err.Error()
report.Error = &jsonmessage.JSONError{
Message: msg,
}
//nolint:staticcheck // Deprecated field, but because consumers might still read it keep it.
report.ErrorMessage = msg
} else {
pulledImages := pullRes.images
if len(pulledImages) > 0 {
img := pulledImages[0].ID()
report.Status = "Download complete"
report.ID = img[0:12]
} else {
msg := "internal error: no images pulled"
report.Error = &jsonmessage.JSONError{
Message: msg,
}
//nolint:staticcheck // Deprecated field, but because consumers might still read it keep it.
report.ErrorMessage = msg
writeStatusCode(http.StatusInternalServerError)
}
}
// We need to check if no progress was sent previously. In that case, we should only return the base error message.
// This is necessary for compatibility with the Docker API.
if err != nil && !progressSent {
msg := errorhandling.Cause(err).Error()
message := jsonmessage.JSONError{
Message: msg,
}
if err := enc.Encode(message); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
}
} else {
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
}
}
flush()
break loop // break out of for/select infinite loop
}
}
}
|