Added `--layout-path` flag to save image in OCI layout.
Fixed #296. The output manifests may have `application/vnd.docker.distribution.manifest.v2+json` as their media types instead of `application/vnd.oci.image.manifest.v1+json`.
This commit is contained in:
parent
1d3cf8a67d
commit
730b8b77c8
|
|
@ -445,7 +445,7 @@
|
|||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:16c8837e951303ef6388132bc875337660a48ea2dedf1c941ca118ea92d2a3d2"
|
||||
digest = "1:5924704ec96f00247784c512cc57f45a595030376a7ff2ff993bf356793a2cb0"
|
||||
name = "github.com/google/go-containerregistry"
|
||||
packages = [
|
||||
"pkg/authn",
|
||||
|
|
@ -456,6 +456,7 @@
|
|||
"pkg/v1",
|
||||
"pkg/v1/daemon",
|
||||
"pkg/v1/empty",
|
||||
"pkg/v1/layout",
|
||||
"pkg/v1/mutate",
|
||||
"pkg/v1/partial",
|
||||
"pkg/v1/random",
|
||||
|
|
@ -467,7 +468,7 @@
|
|||
"pkg/v1/v1util",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "273af77a08b28b49cc2cff2dd8ae50a5094dac74"
|
||||
revision = "31e00cede111067bae48bfc2cbfc522b0b36207f"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f4f203acd8b11b8747bdcd91696a01dbc95ccb9e2ca2db6abf81c3a4f5e950ce"
|
||||
|
|
@ -1368,6 +1369,7 @@
|
|||
"github.com/google/go-containerregistry/pkg/v1",
|
||||
"github.com/google/go-containerregistry/pkg/v1/daemon",
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty",
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout",
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate",
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial",
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ required = [
|
|||
|
||||
[[constraint]]
|
||||
name = "github.com/google/go-containerregistry"
|
||||
revision = "273af77a08b28b49cc2cff2dd8ae50a5094dac74"
|
||||
revision = "31e00cede111067bae48bfc2cbfc522b0b36207f"
|
||||
|
||||
[[override]]
|
||||
name = "k8s.io/apimachinery"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ func addKanikoOptionsFlags(cmd *cobra.Command) {
|
|||
RootCmd.PersistentFlags().StringVarP(&opts.CacheRepo, "cache-repo", "", "", "Specify a repository to use as a cache, otherwise one will be inferred from the destination provided")
|
||||
RootCmd.PersistentFlags().StringVarP(&opts.CacheDir, "cache-dir", "", "/cache", "Specify a local directory to use as a cache.")
|
||||
RootCmd.PersistentFlags().StringVarP(&opts.DigestFile, "digest-file", "", "", "Specify a file to save the digest of the built image to.")
|
||||
RootCmd.PersistentFlags().StringVarP(&opts.LayoutPath, "layout-path", "", "", "Path to save the OCI image spec of the built image.")
|
||||
RootCmd.PersistentFlags().BoolVarP(&opts.Cache, "cache", "", false, "Use cache when building image")
|
||||
RootCmd.PersistentFlags().BoolVarP(&opts.Cleanup, "cleanup", "", false, "Clean the filesystem at the end")
|
||||
RootCmd.PersistentFlags().DurationVarP(&opts.CacheTTL, "cache-ttl", "", time.Hour*336, "Cache timeout in hours. Defaults to two weeks.")
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ type KanikoOptions struct {
|
|||
Target string
|
||||
CacheRepo string
|
||||
DigestFile string
|
||||
LayoutPath string
|
||||
Destinations multiArg
|
||||
BuildArgs multiArg
|
||||
Insecure bool
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import (
|
|||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
|
|
@ -101,6 +102,16 @@ func DoPush(image v1.Image, opts *config.KanikoOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
if opts.LayoutPath != "" {
|
||||
path, err := layout.Write(opts.LayoutPath, empty.Index)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "writing empty layout")
|
||||
}
|
||||
if err := path.AppendImage(image); err != nil {
|
||||
return errors.Wrap(err, "appending image")
|
||||
}
|
||||
}
|
||||
|
||||
destRefs := []name.Tag{}
|
||||
for _, destination := range opts.Destinations {
|
||||
destRef, err := name.NewTag(destination, name.WeakValidation)
|
||||
|
|
|
|||
38
vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go
generated
vendored
Normal file
38
vendor/github.com/google/go-containerregistry/pkg/v1/layout/blob.go
generated
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// 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"
|
||||
"io/ioutil"
|
||||
"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 ioutil.ReadFile(l.blobPath(h))
|
||||
}
|
||||
|
||||
func (l Path) blobPath(h v1.Hash) string {
|
||||
return l.path("blobs", h.Algorithm, h.Hex)
|
||||
}
|
||||
19
vendor/github.com/google/go-containerregistry/pkg/v1/layout/doc.go
generated
vendored
Normal file
19
vendor/github.com/google/go-containerregistry/pkg/v1/layout/doc.go
generated
vendored
Normal file
|
|
@ -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
|
||||
131
vendor/github.com/google/go-containerregistry/pkg/v1/layout/image.go
generated
vendored
Normal file
131
vendor/github.com/google/go-containerregistry/pkg/v1/layout/image.go
generated
vendored
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// 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"
|
||||
"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 partial.CompressedLayer(&compressedBlob{
|
||||
path: li.path,
|
||||
desc: manifest.Config,
|
||||
}), nil
|
||||
}
|
||||
|
||||
for _, desc := range manifest.Layers {
|
||||
if h == desc.Digest {
|
||||
switch desc.MediaType {
|
||||
case types.OCILayer, types.DockerLayer:
|
||||
return partial.CompressedToLayer(&compressedBlob{
|
||||
path: li.path,
|
||||
desc: desc,
|
||||
})
|
||||
default:
|
||||
// TODO: We assume everything is a compressed blob, but that might not be true.
|
||||
// TODO: Handle foreign layers.
|
||||
return nil, fmt.Errorf("unexpected media type: %v for layer: %v", desc.MediaType, desc.Digest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
146
vendor/github.com/google/go-containerregistry/pkg/v1/layout/index.go
generated
vendored
Normal file
146
vendor/github.com/google/go-containerregistry/pkg/v1/layout/index.go
generated
vendored
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// 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"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
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 {
|
||||
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 := ioutil.ReadFile(l.path("index.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idx := &layoutIndex{
|
||||
path: l,
|
||||
rawIndex: rawIndex,
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) MediaType() (types.MediaType, error) {
|
||||
return types.OCIImageIndex, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) Digest() (v1.Hash, error) {
|
||||
return partial.Digest(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{
|
||||
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
|
||||
}
|
||||
|
||||
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
vendor/github.com/google/go-containerregistry/pkg/v1/layout/layoutpath.go
generated
vendored
Normal file
25
vendor/github.com/google/go-containerregistry/pkg/v1/layout/layoutpath.go
generated
vendored
Normal file
|
|
@ -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...)...)
|
||||
}
|
||||
42
vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go
generated
vendored
Normal file
42
vendor/github.com/google/go-containerregistry/pkg/v1/layout/options.go
generated
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package layout
|
||||
|
||||
import v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
|
||||
// Option is a functional option for Layout.
|
||||
//
|
||||
// TODO: We'll need to change this signature to support Sparse/Thin images.
|
||||
// Or, alternatively, wrap it in a sparse.Image that returns an empty list for layers?
|
||||
type Option func(*v1.Descriptor) error
|
||||
|
||||
// WithAnnotations adds annotations to the artifact descriptor.
|
||||
func WithAnnotations(annotations map[string]string) Option {
|
||||
return func(desc *v1.Descriptor) error {
|
||||
if desc.Annotations == nil {
|
||||
desc.Annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range annotations {
|
||||
desc.Annotations[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithURLs adds urls to the artifact descriptor.
|
||||
func WithURLs(urls []string) Option {
|
||||
return func(desc *v1.Descriptor) error {
|
||||
if desc.URLs == nil {
|
||||
desc.URLs = []string{}
|
||||
}
|
||||
desc.URLs = append(desc.URLs, urls...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPlatform sets the platform of the artifact descriptor.
|
||||
func WithPlatform(platform v1.Platform) Option {
|
||||
return func(desc *v1.Descriptor) error {
|
||||
desc.Platform = &platform
|
||||
return nil
|
||||
}
|
||||
}
|
||||
32
vendor/github.com/google/go-containerregistry/pkg/v1/layout/read.go
generated
vendored
Normal file
32
vendor/github.com/google/go-containerregistry/pkg/v1/layout/read.go
generated
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
301
vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go
generated
vendored
Normal file
301
vendor/github.com/google/go-containerregistry/pkg/v1/layout/write.go
generated
vendored
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
// 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"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var layoutFile = `{
|
||||
"imageLayoutVersion": "1.0.0"
|
||||
}`
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
mt, err := img.MediaType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d, err := img.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifest, err := img.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc := v1.Descriptor{
|
||||
MediaType: mt,
|
||||
Size: int64(len(manifest)),
|
||||
Digest: d,
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
if err := opt(&desc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
mt, err := ii.MediaType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d, err := ii.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifest, err := ii.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc := v1.Descriptor{
|
||||
MediaType: mt,
|
||||
Size: int64(len(manifest)),
|
||||
Digest: d,
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
if err := opt(&desc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (l Path) writeFile(name string, data []byte) error {
|
||||
if err := os.MkdirAll(l.path(), os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(l.path(name), data, os.ModePerm)
|
||||
|
||||
}
|
||||
|
||||
// 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 {
|
||||
dir := l.path("blobs", hash.Algorithm)
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
file := filepath.Join(dir, hash.Hex)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
// Blob already exists, that's fine.
|
||||
return nil
|
||||
}
|
||||
w, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
_, err = io.Copy(w, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: A streaming version of WriteBlob so we don't have to know the hash
|
||||
// before we write it.
|
||||
|
||||
// TODO: For streaming layers we should write to a tmp file then Rename to the
|
||||
// final digest.
|
||||
func (l Path) writeLayer(layer v1.Layer) error {
|
||||
d, err := layer.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := layer.Compressed()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteBlob(d, r)
|
||||
}
|
||||
|
||||
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, ioutil.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, ioutil.NopCloser(bytes.NewReader(manifest)))
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
rawIndex, err := ii.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.writeFile(indexFile, rawIndex)
|
||||
}
|
||||
|
||||
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)); 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)); 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)
|
||||
}
|
||||
|
|
@ -165,11 +165,9 @@ func (i *image) compute() error {
|
|||
manifest := m.DeepCopy()
|
||||
manifestLayers := manifest.Layers
|
||||
for _, add := range i.adds {
|
||||
d := v1.Descriptor{
|
||||
MediaType: types.DockerLayer,
|
||||
}
|
||||
|
||||
d := v1.Descriptor{}
|
||||
var err error
|
||||
|
||||
if d.Size, err = add.Layer.Size(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -178,6 +176,10 @@ func (i *image) compute() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if d.MediaType, err = add.Layer.MediaType(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestLayers = append(manifestLayers, d)
|
||||
digestMap[d.Digest] = add.Layer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ package remote
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
|
@ -38,7 +37,20 @@ var defaultPlatform = v1.Platform{
|
|||
// ErrSchema1 indicates that we received a schema1 manifest from the registry.
|
||||
// This library doesn't have plans to support this legacy image format:
|
||||
// https://github.com/google/go-containerregistry/issues/377
|
||||
var ErrSchema1 = errors.New("unsupported MediaType: https://github.com/google/go-containerregistry/issues/377")
|
||||
type ErrSchema1 struct {
|
||||
schema string
|
||||
}
|
||||
|
||||
func NewErrSchema1(schema types.MediaType) error {
|
||||
return &ErrSchema1{
|
||||
schema: string(schema),
|
||||
}
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
func (e *ErrSchema1) Error() string {
|
||||
return fmt.Sprintf("unsupported MediaType: %q, see https://github.com/google/go-containerregistry/issues/377", e.schema)
|
||||
}
|
||||
|
||||
// Descriptor provides access to metadata about remote artifact and accessors
|
||||
// for efficiently converting it into a v1.Image or v1.ImageIndex.
|
||||
|
|
@ -111,7 +123,7 @@ func (d *Descriptor) Image() (v1.Image, error) {
|
|||
case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
|
||||
// We don't care to support schema 1 images:
|
||||
// https://github.com/google/go-containerregistry/issues/377
|
||||
return nil, ErrSchema1
|
||||
return nil, NewErrSchema1(d.MediaType)
|
||||
case types.OCIImageIndex, types.DockerManifestList:
|
||||
// We want an image but the registry has an index, resolve it to an image.
|
||||
return d.remoteIndex().imageByPlatform(d.platform)
|
||||
|
|
@ -141,7 +153,7 @@ func (d *Descriptor) ImageIndex() (v1.ImageIndex, error) {
|
|||
case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
|
||||
// We don't care to support schema 1 images:
|
||||
// https://github.com/google/go-containerregistry/issues/377
|
||||
return nil, ErrSchema1
|
||||
return nil, NewErrSchema1(d.MediaType)
|
||||
case types.OCIManifestSchema1, types.DockerManifestSchema2:
|
||||
// We want an index but the registry has an image, nothing we can do.
|
||||
return nil, fmt.Errorf("unexpected media type for ImageIndex(): %s; call Image() instead", d.MediaType)
|
||||
|
|
|
|||
20
vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go
generated
vendored
20
vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go
generated
vendored
|
|
@ -26,6 +26,10 @@ import (
|
|||
// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
|
||||
type Error struct {
|
||||
Errors []Diagnostic `json:"errors,omitempty"`
|
||||
// The http status code returned.
|
||||
StatusCode int
|
||||
// The raw body if we couldn't understand it.
|
||||
rawBody string
|
||||
}
|
||||
|
||||
// Check that Error implements error
|
||||
|
|
@ -35,7 +39,10 @@ var _ error = (*Error)(nil)
|
|||
func (e *Error) Error() string {
|
||||
switch len(e.Errors) {
|
||||
case 0:
|
||||
return "<empty transport.Error response>"
|
||||
if len(e.rawBody) == 0 {
|
||||
return fmt.Sprintf("unsupported status code %d", e.StatusCode)
|
||||
}
|
||||
return fmt.Sprintf("unsupported status code %d; body: %s", e.StatusCode, e.rawBody)
|
||||
case 1:
|
||||
return e.Errors[0].String()
|
||||
default:
|
||||
|
|
@ -115,11 +122,10 @@ func CheckError(resp *http.Response, codes ...int) error {
|
|||
}
|
||||
|
||||
// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
|
||||
var structuredError Error
|
||||
if err := json.Unmarshal(b, &structuredError); err != nil {
|
||||
// If the response isn't an unstructured error, then return some
|
||||
// reasonable error response containing the response body.
|
||||
return fmt.Errorf("unsupported status code %d; body: %s", resp.StatusCode, string(b))
|
||||
structuredError := &Error{}
|
||||
if err := json.Unmarshal(b, structuredError); err != nil {
|
||||
structuredError.rawBody = string(b)
|
||||
}
|
||||
return &structuredError
|
||||
structuredError.StatusCode = resp.StatusCode
|
||||
return structuredError
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue