|
| 1 | +package registry |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "net/url" |
| 9 | + "os" |
| 10 | + "unicode" |
| 11 | + |
| 12 | + // thanks, go-digest... |
| 13 | + _ "crypto/sha256" |
| 14 | + _ "crypto/sha512" |
| 15 | + |
| 16 | + "github.com/containerd/containerd/images" |
| 17 | + "github.com/containerd/containerd/reference/docker" |
| 18 | + "github.com/containerd/containerd/remotes" |
| 19 | + dockerremote "github.com/containerd/containerd/remotes/docker" |
| 20 | + ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 21 | +) |
| 22 | + |
| 23 | +type ResolvedObject struct { |
| 24 | + Ref string |
| 25 | + Desc ocispec.Descriptor |
| 26 | + |
| 27 | + resolver remotes.Resolver |
| 28 | + fetcher remotes.Fetcher |
| 29 | +} |
| 30 | + |
| 31 | +func (obj ResolvedObject) FetchJSON(ctx context.Context, v interface{}) error { |
| 32 | + // prevent go-digest panics later |
| 33 | + if err := obj.Desc.Digest.Validate(); err != nil { |
| 34 | + return err |
| 35 | + } |
| 36 | + |
| 37 | + r, err := obj.fetcher.Fetch(ctx, obj.Desc) |
| 38 | + if err != nil { |
| 39 | + return err |
| 40 | + } |
| 41 | + defer r.Close() |
| 42 | + |
| 43 | + // make sure we can't possibly read (much) more than we're supposed to |
| 44 | + limited := &io.LimitedReader{ |
| 45 | + R: r, |
| 46 | + N: obj.Desc.Size + 1, // +1 to allow us to detect if we read too much (see verification below) |
| 47 | + } |
| 48 | + |
| 49 | + // copy all read data into the digest verifier so we can validate afterwards |
| 50 | + verifier := obj.Desc.Digest.Verifier() |
| 51 | + tee := io.TeeReader(limited, verifier) |
| 52 | + |
| 53 | + // decode directly! (mostly avoids double memory hit for big objects) |
| 54 | + // (TODO protect against malicious objects somehow?) |
| 55 | + if err := json.NewDecoder(tee).Decode(v); err != nil { |
| 56 | + return err |
| 57 | + } |
| 58 | + |
| 59 | + // read anything leftover ... |
| 60 | + bs, err := io.ReadAll(tee) |
| 61 | + if err != nil { |
| 62 | + return err |
| 63 | + } |
| 64 | + // ... and make sure it was just whitespace, if anything |
| 65 | + for _, b := range bs { |
| 66 | + if !unicode.IsSpace(rune(b)) { |
| 67 | + return fmt.Errorf("unexpected non-whitespace at the end of %q: %+v\n", obj.Desc.Digest.String(), rune(b)) |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + // after reading *everything*, we should have exactly one byte left in our LimitedReader (anything else is an error) |
| 72 | + if limited.N < 1 { |
| 73 | + return fmt.Errorf("size of %q is bigger than it should be (%d)", obj.Desc.Digest.String(), obj.Desc.Size) |
| 74 | + } else if limited.N > 1 { |
| 75 | + return fmt.Errorf("size of %q is %d bytes smaller than it should be (%d)", obj.Desc.Digest.String(), limited.N-1, obj.Desc.Size) |
| 76 | + } |
| 77 | + |
| 78 | + // and finally, let's verify our checksum |
| 79 | + if !verifier.Verified() { |
| 80 | + return fmt.Errorf("digest of %q not correct", obj.Desc.Digest.String()) |
| 81 | + } |
| 82 | + |
| 83 | + return nil |
| 84 | +} |
| 85 | + |
| 86 | +func (obj ResolvedObject) Manifests(ctx context.Context) ([]ocispec.Descriptor, error) { |
| 87 | + if obj.IsImageManifest() { |
| 88 | + return []ocispec.Descriptor{obj.Desc}, nil |
| 89 | + } |
| 90 | + |
| 91 | + if !obj.IsImageIndex() { |
| 92 | + return nil, fmt.Errorf("unknown media type: %q", obj.Desc.MediaType) |
| 93 | + } |
| 94 | + |
| 95 | + // (perhaps use a containerd content store??) |
| 96 | + var index ocispec.Index |
| 97 | + if err := obj.FetchJSON(ctx, &index); err != nil { |
| 98 | + return nil, err |
| 99 | + } |
| 100 | + return index.Manifests, nil |
| 101 | +} |
| 102 | + |
| 103 | +func (obj ResolvedObject) Manifest(ctx context.Context, desc ocispec.Descriptor) (*ocispec.Manifest, error) { |
| 104 | + obj.Desc = desc // swap our object to point at this manifest |
| 105 | + if !obj.IsImageManifest() { |
| 106 | + return nil, fmt.Errorf("unknown media type: %q", obj.Desc.MediaType) |
| 107 | + } |
| 108 | + |
| 109 | + // (perhaps use a containerd content store??) |
| 110 | + var manifest ocispec.Manifest |
| 111 | + if err := obj.FetchJSON(ctx, &manifest); err != nil { |
| 112 | + return nil, err |
| 113 | + } |
| 114 | + return &manifest, nil |
| 115 | +} |
| 116 | + |
| 117 | +func (obj ResolvedObject) IsImageManifest() bool { |
| 118 | + return obj.Desc.MediaType == ocispec.MediaTypeImageManifest || obj.Desc.MediaType == images.MediaTypeDockerSchema2Manifest |
| 119 | +} |
| 120 | + |
| 121 | +func (obj ResolvedObject) IsImageIndex() bool { |
| 122 | + return obj.Desc.MediaType == ocispec.MediaTypeImageIndex || obj.Desc.MediaType == images.MediaTypeDockerSchema2ManifestList |
| 123 | +} |
| 124 | + |
| 125 | +func Resolve(ctx context.Context, image string) (*ResolvedObject, error) { |
| 126 | + var ( |
| 127 | + obj = ResolvedObject{ |
| 128 | + Ref: image, |
| 129 | + } |
| 130 | + err error |
| 131 | + ) |
| 132 | + |
| 133 | + obj.Ref, obj.resolver, err = resolverHelper(obj.Ref) |
| 134 | + if err != nil { |
| 135 | + return nil, err |
| 136 | + } |
| 137 | + |
| 138 | + obj.Ref, obj.Desc, err = obj.resolver.Resolve(ctx, obj.Ref) |
| 139 | + if err != nil { |
| 140 | + return nil, err |
| 141 | + } |
| 142 | + |
| 143 | + obj.fetcher, err = obj.resolver.Fetcher(ctx, obj.Ref) |
| 144 | + if err != nil { |
| 145 | + return nil, err |
| 146 | + } |
| 147 | + |
| 148 | + return &obj, nil |
| 149 | +} |
| 150 | + |
| 151 | +func resolverHelper(image string) (string, remotes.Resolver, error) { |
| 152 | + ref, err := docker.ParseAnyReference(image) |
| 153 | + if err != nil { |
| 154 | + return "", nil, err |
| 155 | + } |
| 156 | + if namedRef, ok := ref.(docker.Named); ok { |
| 157 | + // add ":latest" if necessary |
| 158 | + namedRef = docker.TagNameOnly(namedRef) |
| 159 | + ref = namedRef |
| 160 | + } |
| 161 | + return ref.String(), dockerremote.NewResolver(dockerremote.ResolverOptions{ |
| 162 | + // TODO port this to "Hosts:" (especially so we can return Scheme correctly) but requires reimplementing some of https://github.com/containerd/containerd/blob/v1.6.9/remotes/docker/resolver.go#L161-L184 😞 |
| 163 | + Host: func(host string) (string, error) { |
| 164 | + if host == "docker.io" { |
| 165 | + if publicProxy := os.Getenv("DOCKERHUB_PUBLIC_PROXY"); publicProxy != "" { |
| 166 | + if publicProxyURL, err := url.Parse(publicProxy); err == nil { |
| 167 | + // TODO Scheme (also not sure if "host:port" will be satisfactory to containerd here, but 🤷) |
| 168 | + return publicProxyURL.Host, nil |
| 169 | + } else { |
| 170 | + return "", err |
| 171 | + } |
| 172 | + } |
| 173 | + return "registry-1.docker.io", nil // https://github.com/containerd/containerd/blob/1c90a442489720eec95342e1789ee8a5e1b9536f/remotes/docker/registry.go#L193 |
| 174 | + } |
| 175 | + return host, nil |
| 176 | + }, |
| 177 | + }), nil |
| 178 | +} |
0 commit comments