github.com/quay/claircore@v1.5.28/layer.go (about) 1 package claircore 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "io/fs" 10 "os" 11 "path/filepath" 12 "runtime" 13 14 "github.com/quay/claircore/pkg/tarfs" 15 ) 16 17 // LayerDescription is a description of a container layer. It should contain 18 // enough information to fetch the layer. 19 // 20 // Unlike the [Layer] type, this type does not have any extra state or access to 21 // the contents of the layer. 22 type LayerDescription struct { 23 // Digest is a content addressable checksum uniquely identifying this layer. 24 Digest string 25 // URI is a URI that can be used to fetch layer contents. 26 URI string 27 // MediaType is the [OCI Layer media type] for this layer. Any [Indexer] 28 // instance will support the OCI-defined media types, and may support others 29 // based on its configuration. 30 // 31 // [OCI Layer media type]: https://github.com/opencontainers/image-spec/blob/main/layer.md 32 MediaType string 33 // Headers is additional request headers for fetching layer contents. 34 Headers map[string][]string 35 } 36 37 // Layer is an internal representation of a container image file system layer. 38 // Layers are stacked on top of each other to create the final file system of 39 // the container image. 40 // 41 // This type being in the external API of the 42 // [github.com/quay/claircore/libindex.Libindex] type is a historical accident. 43 // 44 // Previously, it was OK to use Layer literals. This is no longer allowed and 45 // the [Layer.Init] method must be called. Any methods besides [Layer.Init] 46 // called on an uninitialized Layer will report errors and may panic. 47 type Layer struct { 48 noCopy noCopy 49 // Final is used to implement the panicking Finalizer. 50 // 51 // Without a unique allocation, we cannot set a Finalizer (so, when filling 52 // in a Layer in a slice). A pointer to a zero-sized type (like a *struct{}) 53 // is not unique. So this camps on a unique heap allocation made for the 54 // purpose of tracking the Closed state of this Layer. 55 final *string 56 57 // Hash is a content addressable hash uniquely identifying this layer. 58 // Libindex will treat layers with this same hash as identical. 59 Hash Digest `json:"hash"` 60 // URI is a URI that can be used to fetch layer contents. 61 // 62 // Deprecated: This is exported for historical reasons and may stop being 63 // populated in the future. 64 URI string `json:"uri"` 65 // Headers is additional request headers for fetching layer contents. 66 // 67 // Deprecated: This is exported for historical reasons and may stop being 68 // populated in the future. 69 Headers map[string][]string `json:"headers"` 70 71 cleanup []io.Closer 72 sys fs.FS 73 rd io.ReaderAt 74 closed bool // Used to catch double-closes. 75 init bool // Used to track initialization. 76 } 77 78 // Init initializes a Layer in-place. This is provided for flexibility when 79 // constructing a slice of Layers. 80 func (l *Layer) Init(ctx context.Context, desc *LayerDescription, r io.ReaderAt) error { 81 if l.init { 82 return fmt.Errorf("claircore: Init called on already initialized Layer") 83 } 84 var err error 85 l.Hash, err = ParseDigest(desc.Digest) 86 if err != nil { 87 return err 88 } 89 l.URI = desc.URI 90 l.Headers = desc.Headers 91 l.rd = r 92 defer func() { 93 if l.init { 94 return 95 } 96 for _, f := range l.cleanup { 97 f.Close() 98 } 99 }() 100 101 switch desc.MediaType { 102 case `application/vnd.oci.image.layer.v1.tar`, 103 `application/vnd.oci.image.layer.v1.tar+gzip`, 104 `application/vnd.oci.image.layer.v1.tar+zstd`, 105 `application/vnd.oci.image.layer.nondistributable.v1.tar`, 106 `application/vnd.oci.image.layer.nondistributable.v1.tar+gzip`, 107 `application/vnd.oci.image.layer.nondistributable.v1.tar+zstd`: 108 sys, err := tarfs.New(r) 109 switch { 110 case errors.Is(err, nil): 111 default: 112 return fmt.Errorf("claircore: layer %v: unable to create fs.FS: %w", desc.Digest, err) 113 } 114 l.sys = sys 115 default: 116 return fmt.Errorf("claircore: layer %v: unknown MediaType %q", desc.Digest, desc.MediaType) 117 } 118 119 _, file, line, _ := runtime.Caller(1) 120 fmsg := fmt.Sprintf("%s:%d: Layer not closed", file, line) 121 l.final = &fmsg 122 runtime.SetFinalizer(l.final, func(msg *string) { panic(*msg) }) 123 l.init = true 124 return nil 125 } 126 127 // Close releases held resources by this Layer. 128 // 129 // Not calling Close may cause the program to panic. 130 func (l *Layer) Close() error { 131 if !l.init { 132 return errors.New("claircore: Close: uninitialized Layer") 133 } 134 if l.closed { 135 _, file, line, _ := runtime.Caller(1) 136 panic(fmt.Sprintf("%s:%d: Layer closed twice", file, line)) 137 } 138 runtime.SetFinalizer(l.final, nil) 139 l.closed = true 140 errs := make([]error, len(l.cleanup)) 141 for i, c := range l.cleanup { 142 errs[i] = c.Close() 143 } 144 return errors.Join(errs...) 145 } 146 147 // SetLocal is a namespacing wart. 148 // 149 // Deprecated: This function unconditionally errors and does nothing. Use the 150 // [Layer.Init] method instead. 151 func (l *Layer) SetLocal(_ string) error { 152 // TODO(hank) Just wrap errors.ErrUnsupported when updating to go1.21 153 return errUnsupported 154 } 155 156 type unsupported struct{} 157 158 var errUnsupported = &unsupported{} 159 160 func (*unsupported) Error() string { 161 return "unsupported operation" 162 } 163 func (*unsupported) Is(tgt error) bool { 164 // Hack for forwards compatibility: In go1.21, [errors.ErrUnsupported] was 165 // added and ideally we'd just use that. However, we're supporting go1.20 166 // until it's out of upstream support. This hack will make constructions 167 // like: 168 // 169 // errors.Is(err, errors.ErrUnsupported) 170 // 171 // work as soon as a main module is built with go1.21. 172 return tgt.Error() == "unsupported operation" 173 } 174 175 // Fetched reports whether the layer blob has been fetched locally. 176 // 177 // Deprecated: Layers should now only be constructed by the code that does the 178 // fetching. That is, merely having a valid Layer indicates that the blob has 179 // been fetched. 180 func (l *Layer) Fetched() bool { 181 return l.init 182 } 183 184 // FS returns an [fs.FS] reading from an initialized layer. 185 func (l *Layer) FS() (fs.FS, error) { 186 if !l.init { 187 return nil, errors.New("claircore: unable to return FS: uninitialized Layer") 188 } 189 return l.sys, nil 190 } 191 192 // Reader returns a [ReadAtCloser] of the layer. 193 // 194 // It should also implement [io.Seeker], and should be a tar stream. 195 func (l *Layer) Reader() (ReadAtCloser, error) { 196 if !l.init { 197 return nil, errors.New("claircore: unable to return Reader: uninitialized Layer") 198 } 199 // Some hacks for making the returned ReadAtCloser implements as many 200 // interfaces as possible. 201 switch rd := l.rd.(type) { 202 case *os.File: 203 fi, err := rd.Stat() 204 if err != nil { 205 return nil, fmt.Errorf("claircore: unable to stat file: %w", err) 206 } 207 return &fileAdapter{ 208 SectionReader: io.NewSectionReader(rd, 0, fi.Size()), 209 File: rd, 210 }, nil 211 default: 212 } 213 // Doing this with no size breaks the "seek to the end trick". 214 // 215 // This could do additional interface testing to support the various sizing 216 // tricks we do elsewhere. 217 return &rac{io.NewSectionReader(l.rd, 0, -1)}, nil 218 } 219 220 // Rac implements [io.Closer] on an [io.SectionReader]. 221 type rac struct { 222 *io.SectionReader 223 } 224 225 // Close implements [io.Closer]. 226 func (*rac) Close() error { 227 return nil 228 } 229 230 // FileAdapter implements [ReadAtCloser] in such a way that most of the File's 231 // methods are promoted, but the [io.ReaderAt] and [io.ReadSeeker] interfaces 232 // are dispatched to the [io.SectionReader] so that there's an independent 233 // cursor. 234 type fileAdapter struct { 235 *io.SectionReader 236 *os.File 237 } 238 239 // Read implements [io.Reader]. 240 func (a *fileAdapter) Read(p []byte) (n int, err error) { 241 return a.SectionReader.Read(p) 242 } 243 244 // ReadAt implements [io.ReaderAt]. 245 func (a *fileAdapter) ReadAt(p []byte, off int64) (n int, err error) { 246 return a.SectionReader.ReadAt(p, off) 247 } 248 249 // Seek implements [io.Seeker]. 250 func (a *fileAdapter) Seek(offset int64, whence int) (int64, error) { 251 return a.SectionReader.Seek(offset, whence) 252 } 253 254 // Close implements [io.Closer]. 255 func (*fileAdapter) Close() error { 256 return nil 257 } 258 259 // ReadAtCloser is an [io.ReadCloser] and also an [io.ReaderAt]. 260 type ReadAtCloser interface { 261 io.ReadCloser 262 io.ReaderAt 263 } 264 265 // NormalizeIn is used to make sure paths are tar-root relative. 266 func normalizeIn(in, p string) string { 267 p = filepath.Clean(p) 268 if !filepath.IsAbs(p) { 269 p = filepath.Join(in, p) 270 } 271 if filepath.IsAbs(p) { 272 p = p[1:] 273 } 274 return p 275 } 276 277 // ErrNotFound is returned by [Layer.Files] if none of the requested files are 278 // found. 279 // 280 // Deprecated: The [Layer.Files] method is deprecated. 281 var ErrNotFound = errors.New("claircore: unable to find any requested files") 282 283 // Files retrieves specific files from the layer's tar archive. 284 // 285 // An error is returned only if none of the requested files are found. 286 // 287 // The returned map may contain more entries than the number of paths requested. 288 // All entries in the map are keyed by paths that are relative to the tar-root. 289 // For example, requesting paths of "/etc/os-release", "./etc/os-release", and 290 // "etc/os-release" will all result in any found content being stored with the 291 // key "etc/os-release". 292 // 293 // Deprecated: Callers should instead use [fs.WalkDir] with the [fs.FS] returned 294 // by [Layer.FS]. 295 func (l *Layer) Files(paths ...string) (map[string]*bytes.Buffer, error) { 296 // Clean the input paths. 297 want := make(map[string]struct{}) 298 for i, p := range paths { 299 p := normalizeIn("/", p) 300 paths[i] = p 301 want[p] = struct{}{} 302 } 303 304 f := make(map[string]*bytes.Buffer) 305 // Walk the fs. ReadFile will handle symlink resolution. 306 if err := fs.WalkDir(l.sys, ".", func(p string, d fs.DirEntry, err error) error { 307 switch { 308 case err != nil: 309 return err 310 case d.IsDir(): 311 return nil 312 } 313 if _, ok := want[p]; !ok { 314 return nil 315 } 316 delete(want, p) 317 b, err := fs.ReadFile(l.sys, p) 318 if err != nil { 319 return err 320 } 321 f[p] = bytes.NewBuffer(b) 322 return nil 323 }); err != nil { 324 return nil, err 325 } 326 327 // If there's nothing in the "f" map, we didn't find anything. 328 if len(f) == 0 { 329 return nil, ErrNotFound 330 } 331 return f, nil 332 } 333 334 // NoCopy is a marker to get `go vet` to complain about copying. 335 type noCopy struct{} 336 337 func (noCopy) Lock() {} 338 func (noCopy) Unlock() {}