added vendor/
This commit is contained in:
+5
@@ -0,0 +1,5 @@
|
||||
# `layout`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout)
|
||||
|
||||
The `layout` package implements support for interacting with an [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md).
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
// Blob returns a blob with the given hash from the Path.
|
||||
func (l Path) Blob(h v1.Hash) (io.ReadCloser, error) {
|
||||
return os.Open(l.blobPath(h))
|
||||
}
|
||||
|
||||
// Bytes is a convenience function to return a blob from the Path as
|
||||
// a byte slice.
|
||||
func (l Path) Bytes(h v1.Hash) ([]byte, error) {
|
||||
return os.ReadFile(l.blobPath(h))
|
||||
}
|
||||
|
||||
func (l Path) blobPath(h v1.Hash) string {
|
||||
return l.path("blobs", h.Algorithm, h.Hex)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package layout provides facilities for reading/writing artifacts from/to
|
||||
// an OCI image layout on disk, see:
|
||||
//
|
||||
// https://github.com/opencontainers/image-spec/blob/master/image-layout.md
|
||||
package layout
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This is an EXPERIMENTAL package, and may change in arbitrary ways without notice.
|
||||
package layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
// GarbageCollect removes unreferenced blobs from the oci-layout
|
||||
//
|
||||
// This is an experimental api, and not subject to any stability guarantees
|
||||
// We may abandon it at any time, without prior notice.
|
||||
// Deprecated: Use it at your own risk!
|
||||
func (l Path) GarbageCollect() ([]v1.Hash, error) {
|
||||
idx, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobsToKeep := map[string]bool{}
|
||||
if err := l.garbageCollectImageIndex(idx, blobsToKeep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobsDir := l.path("blobs")
|
||||
removedBlobs := []v1.Hash{}
|
||||
|
||||
err = filepath.WalkDir(blobsDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(blobsDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hashString := strings.Replace(rel, "/", ":", 1)
|
||||
if present := blobsToKeep[hashString]; !present {
|
||||
h, err := v1.NewHash(hashString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
removedBlobs = append(removedBlobs, h)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removedBlobs, nil
|
||||
}
|
||||
|
||||
func (l Path) garbageCollectImageIndex(index v1.ImageIndex, blobsToKeep map[string]bool) error {
|
||||
idxm, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h, err := index.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blobsToKeep[h.String()] = true
|
||||
|
||||
for _, descriptor := range idxm.Manifests {
|
||||
if descriptor.MediaType.IsImage() {
|
||||
img, err := index.Image(descriptor.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.garbageCollectImage(img, blobsToKeep); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if descriptor.MediaType.IsIndex() {
|
||||
idx, err := index.ImageIndex(descriptor.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.garbageCollectImageIndex(idx, blobsToKeep); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("gc: unknown media type: %s", descriptor.MediaType)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Path) garbageCollectImage(image v1.Image, blobsToKeep map[string]bool) error {
|
||||
h, err := image.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobsToKeep[h.String()] = true
|
||||
|
||||
h, err = image.ConfigName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobsToKeep[h.String()] = true
|
||||
|
||||
ls, err := image.Layers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, l := range ls {
|
||||
h, err := l.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobsToKeep[h.String()] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
type layoutImage struct {
|
||||
path Path
|
||||
desc v1.Descriptor
|
||||
manifestLock sync.Mutex // Protects rawManifest
|
||||
rawManifest []byte
|
||||
}
|
||||
|
||||
var _ partial.CompressedImageCore = (*layoutImage)(nil)
|
||||
|
||||
// Image reads a v1.Image with digest h from the Path.
|
||||
func (l Path) Image(h v1.Hash) (v1.Image, error) {
|
||||
ii, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ii.Image(h)
|
||||
}
|
||||
|
||||
func (li *layoutImage) MediaType() (types.MediaType, error) {
|
||||
return li.desc.MediaType, nil
|
||||
}
|
||||
|
||||
// Implements WithManifest for partial.Blobset.
|
||||
func (li *layoutImage) Manifest() (*v1.Manifest, error) {
|
||||
return partial.Manifest(li)
|
||||
}
|
||||
|
||||
func (li *layoutImage) RawManifest() ([]byte, error) {
|
||||
li.manifestLock.Lock()
|
||||
defer li.manifestLock.Unlock()
|
||||
if li.rawManifest != nil {
|
||||
return li.rawManifest, nil
|
||||
}
|
||||
|
||||
b, err := li.path.Bytes(li.desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
li.rawManifest = b
|
||||
return li.rawManifest, nil
|
||||
}
|
||||
|
||||
func (li *layoutImage) RawConfigFile() ([]byte, error) {
|
||||
manifest, err := li.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return li.path.Bytes(manifest.Config.Digest)
|
||||
}
|
||||
|
||||
func (li *layoutImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
|
||||
manifest, err := li.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h == manifest.Config.Digest {
|
||||
return &compressedBlob{
|
||||
path: li.path,
|
||||
desc: manifest.Config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
for _, desc := range manifest.Layers {
|
||||
if h == desc.Digest {
|
||||
return &compressedBlob{
|
||||
path: li.path,
|
||||
desc: desc,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find layer in image: %s", h)
|
||||
}
|
||||
|
||||
type compressedBlob struct {
|
||||
path Path
|
||||
desc v1.Descriptor
|
||||
}
|
||||
|
||||
func (b *compressedBlob) Digest() (v1.Hash, error) {
|
||||
return b.desc.Digest, nil
|
||||
}
|
||||
|
||||
func (b *compressedBlob) Compressed() (io.ReadCloser, error) {
|
||||
return b.path.Blob(b.desc.Digest)
|
||||
}
|
||||
|
||||
func (b *compressedBlob) Size() (int64, error) {
|
||||
return b.desc.Size, nil
|
||||
}
|
||||
|
||||
func (b *compressedBlob) MediaType() (types.MediaType, error) {
|
||||
return b.desc.MediaType, nil
|
||||
}
|
||||
|
||||
// Descriptor implements partial.withDescriptor.
|
||||
func (b *compressedBlob) Descriptor() (*v1.Descriptor, error) {
|
||||
return &b.desc, nil
|
||||
}
|
||||
|
||||
// See partial.Exists.
|
||||
func (b *compressedBlob) Exists() (bool, error) {
|
||||
_, err := os.Stat(b.path.blobPath(b.desc.Digest))
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
var _ v1.ImageIndex = (*layoutIndex)(nil)
|
||||
|
||||
type layoutIndex struct {
|
||||
mediaType types.MediaType
|
||||
path Path
|
||||
rawIndex []byte
|
||||
}
|
||||
|
||||
// ImageIndexFromPath is a convenience function which constructs a Path and returns its v1.ImageIndex.
|
||||
func ImageIndexFromPath(path string) (v1.ImageIndex, error) {
|
||||
lp, err := FromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lp.ImageIndex()
|
||||
}
|
||||
|
||||
// ImageIndex returns a v1.ImageIndex for the Path.
|
||||
func (l Path) ImageIndex() (v1.ImageIndex, error) {
|
||||
rawIndex, err := os.ReadFile(l.path("index.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idx := &layoutIndex{
|
||||
mediaType: types.OCIImageIndex,
|
||||
path: l,
|
||||
rawIndex: rawIndex,
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) MediaType() (types.MediaType, error) {
|
||||
return i.mediaType, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) Digest() (v1.Hash, error) {
|
||||
return partial.Digest(i)
|
||||
}
|
||||
|
||||
func (i *layoutIndex) Size() (int64, error) {
|
||||
return partial.Size(i)
|
||||
}
|
||||
|
||||
func (i *layoutIndex) IndexManifest() (*v1.IndexManifest, error) {
|
||||
var index v1.IndexManifest
|
||||
err := json.Unmarshal(i.rawIndex, &index)
|
||||
return &index, err
|
||||
}
|
||||
|
||||
func (i *layoutIndex) RawManifest() ([]byte, error) {
|
||||
return i.rawIndex, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) Image(h v1.Hash) (v1.Image, error) {
|
||||
// Look up the digest in our manifest first to return a better error.
|
||||
desc, err := i.findDescriptor(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isExpectedMediaType(desc.MediaType, types.OCIManifestSchema1, types.DockerManifestSchema2) {
|
||||
return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType)
|
||||
}
|
||||
|
||||
img := &layoutImage{
|
||||
path: i.path,
|
||||
desc: *desc,
|
||||
}
|
||||
return partial.CompressedToImage(img)
|
||||
}
|
||||
|
||||
func (i *layoutIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
|
||||
// Look up the digest in our manifest first to return a better error.
|
||||
desc, err := i.findDescriptor(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isExpectedMediaType(desc.MediaType, types.OCIImageIndex, types.DockerManifestList) {
|
||||
return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType)
|
||||
}
|
||||
|
||||
rawIndex, err := i.path.Bytes(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &layoutIndex{
|
||||
mediaType: desc.MediaType,
|
||||
path: i.path,
|
||||
rawIndex: rawIndex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) Blob(h v1.Hash) (io.ReadCloser, error) {
|
||||
return i.path.Blob(h)
|
||||
}
|
||||
|
||||
func (i *layoutIndex) findDescriptor(h v1.Hash) (*v1.Descriptor, error) {
|
||||
im, err := i.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h == (v1.Hash{}) {
|
||||
if len(im.Manifests) != 1 {
|
||||
return nil, errors.New("oci layout must contain only a single image to be used with layout.Image")
|
||||
}
|
||||
return &(im.Manifests)[0], nil
|
||||
}
|
||||
|
||||
for _, desc := range im.Manifests {
|
||||
if desc.Digest == h {
|
||||
return &desc, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find descriptor in index: %s", h)
|
||||
}
|
||||
|
||||
// TODO: Pull this out into methods on types.MediaType? e.g. instead, have:
|
||||
// * mt.IsIndex()
|
||||
// * mt.IsImage()
|
||||
func isExpectedMediaType(mt types.MediaType, expected ...types.MediaType) bool {
|
||||
for _, allowed := range expected {
|
||||
if mt == allowed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2019 The original author or authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layout
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// Path represents an OCI image layout rooted in a file system path
|
||||
type Path string
|
||||
|
||||
func (l Path) path(elem ...string) string {
|
||||
complete := []string{string(l)}
|
||||
return filepath.Join(append(complete, elem...)...)
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// Copyright 2019 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layout
|
||||
|
||||
import v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
|
||||
// Option is a functional option for Layout.
|
||||
type Option func(*options)
|
||||
|
||||
type options struct {
|
||||
descOpts []descriptorOption
|
||||
}
|
||||
|
||||
func makeOptions(opts ...Option) *options {
|
||||
o := &options{
|
||||
descOpts: []descriptorOption{},
|
||||
}
|
||||
for _, apply := range opts {
|
||||
apply(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
type descriptorOption func(*v1.Descriptor)
|
||||
|
||||
// WithAnnotations adds annotations to the artifact descriptor.
|
||||
func WithAnnotations(annotations map[string]string) Option {
|
||||
return func(o *options) {
|
||||
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
|
||||
if desc.Annotations == nil {
|
||||
desc.Annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range annotations {
|
||||
desc.Annotations[k] = v
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithURLs adds urls to the artifact descriptor.
|
||||
func WithURLs(urls []string) Option {
|
||||
return func(o *options) {
|
||||
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
|
||||
if desc.URLs == nil {
|
||||
desc.URLs = []string{}
|
||||
}
|
||||
desc.URLs = append(desc.URLs, urls...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithPlatform sets the platform of the artifact descriptor.
|
||||
func WithPlatform(platform v1.Platform) Option {
|
||||
return func(o *options) {
|
||||
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
|
||||
desc.Platform = &platform
|
||||
})
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright 2019 The original author or authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FromPath reads an OCI image layout at path and constructs a layout.Path.
|
||||
func FromPath(path string) (Path, error) {
|
||||
// TODO: check oci-layout exists
|
||||
|
||||
_, err := os.Stat(filepath.Join(path, "index.json"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return Path(path), nil
|
||||
}
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/stream"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var layoutFile = `{
|
||||
"imageLayoutVersion": "1.0.0"
|
||||
}`
|
||||
|
||||
// renameMutex guards os.Rename calls in AppendImage on Windows only.
|
||||
var renameMutex sync.Mutex
|
||||
|
||||
// AppendImage writes a v1.Image to the Path and updates
|
||||
// the index.json to reference it.
|
||||
func (l Path) AppendImage(img v1.Image, options ...Option) error {
|
||||
if err := l.WriteImage(img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := partial.Descriptor(img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o := makeOptions(options...)
|
||||
for _, opt := range o.descOpts {
|
||||
opt(desc)
|
||||
}
|
||||
|
||||
return l.AppendDescriptor(*desc)
|
||||
}
|
||||
|
||||
// AppendIndex writes a v1.ImageIndex to the Path and updates
|
||||
// the index.json to reference it.
|
||||
func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error {
|
||||
if err := l.WriteIndex(ii); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := partial.Descriptor(ii)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o := makeOptions(options...)
|
||||
for _, opt := range o.descOpts {
|
||||
opt(desc)
|
||||
}
|
||||
|
||||
return l.AppendDescriptor(*desc)
|
||||
}
|
||||
|
||||
// AppendDescriptor adds a descriptor to the index.json of the Path.
|
||||
func (l Path) AppendDescriptor(desc v1.Descriptor) error {
|
||||
ii, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index, err := ii.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index.Manifests = append(index.Manifests, desc)
|
||||
|
||||
rawIndex, err := json.MarshalIndent(index, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile("index.json", rawIndex, os.ModePerm)
|
||||
}
|
||||
|
||||
// ReplaceImage writes a v1.Image to the Path and updates
|
||||
// the index.json to reference it, replacing any existing one that matches matcher, if found.
|
||||
func (l Path) ReplaceImage(img v1.Image, matcher match.Matcher, options ...Option) error {
|
||||
if err := l.WriteImage(img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.replaceDescriptor(img, matcher, options...)
|
||||
}
|
||||
|
||||
// ReplaceIndex writes a v1.ImageIndex to the Path and updates
|
||||
// the index.json to reference it, replacing any existing one that matches matcher, if found.
|
||||
func (l Path) ReplaceIndex(ii v1.ImageIndex, matcher match.Matcher, options ...Option) error {
|
||||
if err := l.WriteIndex(ii); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.replaceDescriptor(ii, matcher, options...)
|
||||
}
|
||||
|
||||
// replaceDescriptor adds a descriptor to the index.json of the Path, replacing
|
||||
// any one matching matcher, if found.
|
||||
func (l Path) replaceDescriptor(appendable mutate.Appendable, matcher match.Matcher, options ...Option) error {
|
||||
ii, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := partial.Descriptor(appendable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o := makeOptions(options...)
|
||||
for _, opt := range o.descOpts {
|
||||
opt(desc)
|
||||
}
|
||||
|
||||
add := mutate.IndexAddendum{
|
||||
Add: appendable,
|
||||
Descriptor: *desc,
|
||||
}
|
||||
ii = mutate.AppendManifests(mutate.RemoveManifests(ii, matcher), add)
|
||||
|
||||
index, err := ii.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawIndex, err := json.MarshalIndent(index, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile("index.json", rawIndex, os.ModePerm)
|
||||
}
|
||||
|
||||
// RemoveDescriptors removes any descriptors that match the match.Matcher from the index.json of the Path.
|
||||
func (l Path) RemoveDescriptors(matcher match.Matcher) error {
|
||||
ii, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ii = mutate.RemoveManifests(ii, matcher)
|
||||
|
||||
index, err := ii.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawIndex, err := json.MarshalIndent(index, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile("index.json", rawIndex, os.ModePerm)
|
||||
}
|
||||
|
||||
// WriteFile write a file with arbitrary data at an arbitrary location in a v1
|
||||
// layout. Used mostly internally to write files like "oci-layout" and
|
||||
// "index.json", also can be used to write other arbitrary files. Do *not* use
|
||||
// this to write blobs. Use only WriteBlob() for that.
|
||||
func (l Path) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||
if err := os.MkdirAll(l.path(), os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(l.path(name), data, perm)
|
||||
}
|
||||
|
||||
// WriteBlob copies a file to the blobs/ directory in the Path from the given ReadCloser at
|
||||
// blobs/{hash.Algorithm}/{hash.Hex}.
|
||||
func (l Path) WriteBlob(hash v1.Hash, r io.ReadCloser) error {
|
||||
return l.writeBlob(hash, -1, r, nil)
|
||||
}
|
||||
|
||||
func (l Path) writeBlob(hash v1.Hash, size int64, rc io.ReadCloser, renamer func() (v1.Hash, error)) error {
|
||||
defer rc.Close()
|
||||
if hash.Hex == "" && renamer == nil {
|
||||
panic("writeBlob called an invalid hash and no renamer")
|
||||
}
|
||||
|
||||
dir := l.path("blobs", hash.Algorithm)
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if blob already exists and is the correct size
|
||||
file := filepath.Join(dir, hash.Hex)
|
||||
if s, err := os.Stat(file); err == nil && !s.IsDir() && (s.Size() == size || size == -1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a renamer func was provided write to a temporary file
|
||||
open := func() (*os.File, error) { return os.Create(file) }
|
||||
if renamer != nil {
|
||||
open = func() (*os.File, error) { return os.CreateTemp(dir, hash.Hex) }
|
||||
}
|
||||
w, err := open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if renamer != nil {
|
||||
// Delete temp file if an error is encountered before renaming
|
||||
defer func() {
|
||||
if err := os.Remove(w.Name()); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
logs.Warn.Printf("error removing temporary file after encountering an error while writing blob: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
// Write to file and exit if not renaming
|
||||
if n, err := io.Copy(w, rc); err != nil || renamer == nil {
|
||||
return err
|
||||
} else if size != -1 && n != size {
|
||||
return fmt.Errorf("expected blob size %d, but only wrote %d", size, n)
|
||||
}
|
||||
|
||||
// Always close reader before renaming, since Close computes the digest in
|
||||
// the case of streaming layers. If Close is not called explicitly, it will
|
||||
// occur in a goroutine that is not guaranteed to succeed before renamer is
|
||||
// called. When renamer is the layer's Digest method, it can return
|
||||
// ErrNotComputed.
|
||||
if err := rc.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Always close file before renaming
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename file based on the final hash
|
||||
finalHash, err := renamer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting final digest of layer: %w", err)
|
||||
}
|
||||
|
||||
renamePath := l.path("blobs", finalHash.Algorithm, finalHash.Hex)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
renameMutex.Lock()
|
||||
defer renameMutex.Unlock()
|
||||
}
|
||||
return os.Rename(w.Name(), renamePath)
|
||||
}
|
||||
|
||||
// writeLayer writes the compressed layer to a blob. Unlike WriteBlob it will
|
||||
// write to a temporary file (suffixed with .tmp) within the layout until the
|
||||
// compressed reader is fully consumed and written to disk. Also unlike
|
||||
// WriteBlob, it will not skip writing and exit without error when a blob file
|
||||
// exists, but does not have the correct size. (The blob hash is not
|
||||
// considered, because it may be expensive to compute.)
|
||||
func (l Path) writeLayer(layer v1.Layer) error {
|
||||
d, err := layer.Digest()
|
||||
if errors.Is(err, stream.ErrNotComputed) {
|
||||
// Allow digest errors, since streams may not have calculated the hash
|
||||
// yet. Instead, use an empty value, which will be transformed into a
|
||||
// random file name with `os.CreateTemp` and the final digest will be
|
||||
// calculated after writing to a temp file and before renaming to the
|
||||
// final path.
|
||||
d = v1.Hash{Algorithm: "sha256", Hex: ""}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := layer.Size()
|
||||
if errors.Is(err, stream.ErrNotComputed) {
|
||||
// Allow size errors, since streams may not have calculated the size
|
||||
// yet. Instead, use zero as a sentinel value meaning that no size
|
||||
// comparison can be done and any sized blob file should be considered
|
||||
// valid and not overwritten.
|
||||
//
|
||||
// TODO: Provide an option to always overwrite blobs.
|
||||
s = -1
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := layer.Compressed()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := l.writeBlob(d, s, r, layer.Digest); err != nil {
|
||||
return fmt.Errorf("error writing layer: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveBlob removes a file from the blobs directory in the Path
|
||||
// at blobs/{hash.Algorithm}/{hash.Hex}
|
||||
// It does *not* remove any reference to it from other manifests or indexes, or
|
||||
// from the root index.json.
|
||||
func (l Path) RemoveBlob(hash v1.Hash) error {
|
||||
dir := l.path("blobs", hash.Algorithm)
|
||||
err := os.Remove(filepath.Join(dir, hash.Hex))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteImage writes an image, including its manifest, config and all of its
|
||||
// layers, to the blobs directory. If any blob already exists, as determined by
|
||||
// the hash filename, does not write it.
|
||||
// This function does *not* update the `index.json` file. If you want to write the
|
||||
// image and also update the `index.json`, call AppendImage(), which wraps this
|
||||
// and also updates the `index.json`.
|
||||
func (l Path) WriteImage(img v1.Image) error {
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the layers concurrently.
|
||||
var g errgroup.Group
|
||||
for _, layer := range layers {
|
||||
layer := layer
|
||||
g.Go(func() error {
|
||||
return l.writeLayer(layer)
|
||||
})
|
||||
}
|
||||
if err := g.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the config.
|
||||
cfgName, err := img.ConfigName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgBlob, err := img.RawConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.WriteBlob(cfgName, io.NopCloser(bytes.NewReader(cfgBlob))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the img manifest.
|
||||
d, err := img.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest, err := img.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteBlob(d, io.NopCloser(bytes.NewReader(manifest)))
|
||||
}
|
||||
|
||||
type withLayer interface {
|
||||
Layer(v1.Hash) (v1.Layer, error)
|
||||
}
|
||||
|
||||
type withBlob interface {
|
||||
Blob(v1.Hash) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
|
||||
index, err := ii.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walk the descriptors and write any v1.Image or v1.ImageIndex that we find.
|
||||
// If we come across something we don't expect, just write it as a blob.
|
||||
for _, desc := range index.Manifests {
|
||||
switch desc.MediaType {
|
||||
case types.OCIImageIndex, types.DockerManifestList:
|
||||
ii, err := ii.ImageIndex(desc.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.WriteIndex(ii); err != nil {
|
||||
return err
|
||||
}
|
||||
case types.OCIManifestSchema1, types.DockerManifestSchema2:
|
||||
img, err := ii.Image(desc.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.WriteImage(img); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// TODO: The layout could reference arbitrary things, which we should
|
||||
// probably just pass through.
|
||||
|
||||
var blob io.ReadCloser
|
||||
// Workaround for #819.
|
||||
if wl, ok := ii.(withLayer); ok {
|
||||
layer, lerr := wl.Layer(desc.Digest)
|
||||
if lerr != nil {
|
||||
return lerr
|
||||
}
|
||||
blob, err = layer.Compressed()
|
||||
} else if wb, ok := ii.(withBlob); ok {
|
||||
blob, err = wb.Blob(desc.Digest)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.WriteBlob(desc.Digest, blob); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rawIndex, err := ii.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile(indexFile, rawIndex, os.ModePerm)
|
||||
}
|
||||
|
||||
// WriteIndex writes an index to the blobs directory. Walks down the children,
|
||||
// including its children manifests and/or indexes, and down the tree until all of
|
||||
// config and all layers, have been written. If any blob already exists, as determined by
|
||||
// the hash filename, does not write it.
|
||||
// This function does *not* update the `index.json` file. If you want to write the
|
||||
// index and also update the `index.json`, call AppendIndex(), which wraps this
|
||||
// and also updates the `index.json`.
|
||||
func (l Path) WriteIndex(ii v1.ImageIndex) error {
|
||||
// Always just write oci-layout file, since it's small.
|
||||
if err := l.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h, err := ii.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexFile := filepath.Join("blobs", h.Algorithm, h.Hex)
|
||||
return l.writeIndexToFile(indexFile, ii)
|
||||
}
|
||||
|
||||
// Write constructs a Path at path from an ImageIndex.
|
||||
//
|
||||
// The contents are written in the following format:
|
||||
// At the top level, there is:
|
||||
//
|
||||
// One oci-layout file containing the version of this image-layout.
|
||||
// One index.json file listing descriptors for the contained images.
|
||||
//
|
||||
// Under blobs/, there is, for each image:
|
||||
//
|
||||
// One file for each layer, named after the layer's SHA.
|
||||
// One file for each config blob, named after its SHA.
|
||||
// One file for each manifest blob, named after its SHA.
|
||||
func Write(path string, ii v1.ImageIndex) (Path, error) {
|
||||
lp := Path(path)
|
||||
// Always just write oci-layout file, since it's small.
|
||||
if err := lp.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TODO create blobs/ in case there is a blobs file which would prevent the directory from being created
|
||||
|
||||
return lp, lp.writeIndexToFile("index.json", ii)
|
||||
}
|
||||
Reference in New Issue
Block a user