354 lines
9.6 KiB
Go
354 lines
9.6 KiB
Go
/*
|
|
Copyright 2022 The Crossplane 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 store implements OCI container storage.
|
|
package store
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
ociv1 "github.com/google/go-containerregistry/pkg/v1"
|
|
|
|
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
|
"github.com/crossplane/crossplane-runtime/pkg/test"
|
|
)
|
|
|
|
type MockImage struct {
|
|
ociv1.Image
|
|
|
|
MockDigest func() (ociv1.Hash, error)
|
|
MockRawConfigFile func() ([]byte, error)
|
|
MockLayers func() ([]ociv1.Layer, error)
|
|
}
|
|
|
|
func (i *MockImage) Digest() (ociv1.Hash, error) { return i.MockDigest() }
|
|
func (i *MockImage) RawConfigFile() ([]byte, error) { return i.MockRawConfigFile() }
|
|
func (i *MockImage) Layers() ([]ociv1.Layer, error) { return i.MockLayers() }
|
|
|
|
type MockLayer struct {
|
|
ociv1.Layer
|
|
|
|
MockDiffID func() (ociv1.Hash, error)
|
|
MockUncompressed func() (io.ReadCloser, error)
|
|
}
|
|
|
|
func (l *MockLayer) DiffID() (ociv1.Hash, error) { return l.MockDiffID() }
|
|
func (l *MockLayer) Uncompressed() (io.ReadCloser, error) { return l.MockUncompressed() }
|
|
|
|
func TestHash(t *testing.T) {
|
|
type args struct {
|
|
r name.Reference
|
|
}
|
|
type want struct {
|
|
h ociv1.Hash
|
|
err error
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
files map[string][]byte
|
|
args args
|
|
want want
|
|
}{
|
|
"ReadError": {
|
|
reason: "We should return any error encountered reading the stored hash.",
|
|
args: args{
|
|
r: name.MustParseReference("example.org/image"),
|
|
},
|
|
want: want{
|
|
// Note we're matching with cmpopts.EquateErrors, which only
|
|
// cares that the returned error errors.Is() this one.
|
|
err: os.ErrNotExist,
|
|
},
|
|
},
|
|
"ParseError": {
|
|
reason: "We should return any error encountered reading the stored hash.",
|
|
files: map[string][]byte{
|
|
"276640b463239572f62edd97253f05e0de082e9888f57dac0b83d2149efa59e0": []byte("wat"),
|
|
},
|
|
args: args{
|
|
r: name.MustParseReference("example.org/image"),
|
|
},
|
|
want: want{
|
|
err: cmpopts.AnyError,
|
|
},
|
|
},
|
|
"SuccessfulRead": {
|
|
reason: "We should return the stored hash.",
|
|
files: map[string][]byte{
|
|
"276640b463239572f62edd97253f05e0de082e9888f57dac0b83d2149efa59e0": []byte("sha256:c34045c1a1db8d1b3fca8a692198466952daae07eaf6104b4c87ed3b55b6af1b"),
|
|
},
|
|
args: args{
|
|
r: name.MustParseReference("example.org/image"),
|
|
},
|
|
want: want{
|
|
h: ociv1.Hash{
|
|
Algorithm: "sha256",
|
|
Hex: "c34045c1a1db8d1b3fca8a692198466952daae07eaf6104b4c87ed3b55b6af1b",
|
|
},
|
|
err: nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
|
|
if err != nil {
|
|
t.Fatal(err.Error())
|
|
}
|
|
t.Cleanup(func() {
|
|
os.RemoveAll(tmp)
|
|
})
|
|
|
|
for name, data := range tc.files {
|
|
path := filepath.Join(tmp, DirDigests, "sha256", name)
|
|
_ = os.MkdirAll(filepath.Dir(path), 0700)
|
|
_ = os.WriteFile(path, data, 0600)
|
|
}
|
|
|
|
c, err := NewDigest(tmp)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
h, err := c.Hash(tc.args.r)
|
|
if diff := cmp.Diff(tc.want.h, h); diff != "" {
|
|
t.Errorf("\n%s\nHash(...): -want, +got:\n%s", tc.reason, diff)
|
|
}
|
|
// Note cmpopts.EquateErrors, not the usual testing.EquateErrors
|
|
// from crossplane-runtime. We need this to support cmpopts.AnyError.
|
|
if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
|
|
t.Errorf("\n%s\nHash(...): -want error, +got error:\n%s", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteImage(t *testing.T) {
|
|
errBoom := errors.New("boom")
|
|
|
|
type args struct {
|
|
i ociv1.Image
|
|
}
|
|
type want struct {
|
|
err error
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
files map[string][]byte
|
|
args args
|
|
want want
|
|
}{
|
|
"DigestError": {
|
|
reason: "We should return an error if we can't get the image's digest.",
|
|
args: args{
|
|
i: &MockImage{
|
|
MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
|
|
},
|
|
},
|
|
want: want{
|
|
err: errors.Wrap(errBoom, errGetDigest),
|
|
},
|
|
},
|
|
"RawConfigFileError": {
|
|
reason: "We should return an error if we can't access the image's raw config file.",
|
|
args: args{
|
|
i: &MockImage{
|
|
MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
|
|
MockRawConfigFile: func() ([]byte, error) { return nil, errBoom },
|
|
},
|
|
},
|
|
want: want{
|
|
err: errors.Wrap(errBoom, errGetRawConfigFile),
|
|
},
|
|
},
|
|
"WriteLayerError": {
|
|
reason: "We should return an error if we can't write a layer to the store.",
|
|
args: args{
|
|
i: &MockImage{
|
|
MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
|
|
MockRawConfigFile: func() ([]byte, error) { return nil, nil },
|
|
MockLayers: func() ([]ociv1.Layer, error) {
|
|
return []ociv1.Layer{
|
|
&MockLayer{
|
|
// To cause WriteLayer to fail.
|
|
MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
},
|
|
want: want{
|
|
err: errors.Wrap(errors.Wrap(errBoom, errGetDigest), errWriteLayers),
|
|
},
|
|
},
|
|
"SuccessfulWrite": {
|
|
reason: "We should not return an error if we successfully wrote an image to the store.",
|
|
args: args{
|
|
i: &MockImage{
|
|
MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
|
|
MockRawConfigFile: func() ([]byte, error) { return []byte(`{"variant":"cool"}`), nil },
|
|
MockLayers: func() ([]ociv1.Layer, error) { return nil, nil },
|
|
},
|
|
},
|
|
want: want{
|
|
err: nil,
|
|
},
|
|
},
|
|
"SuccessfulNoOp": {
|
|
reason: "We should return early if the supplied image is already stored.",
|
|
files: map[string][]byte{
|
|
// The minimum valid config file required by validate.Image.
|
|
"cool": []byte(`{"rootfs":{"type":"layers"}}`),
|
|
},
|
|
args: args{
|
|
i: &MockImage{
|
|
MockDigest: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
|
|
},
|
|
},
|
|
want: want{
|
|
err: nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
|
|
if err != nil {
|
|
t.Fatal(err.Error())
|
|
}
|
|
t.Cleanup(func() {
|
|
os.RemoveAll(tmp)
|
|
})
|
|
|
|
for name, data := range tc.files {
|
|
path := filepath.Join(tmp, DirImages, name)
|
|
_ = os.MkdirAll(filepath.Dir(path), 0700)
|
|
_ = os.WriteFile(path, data, 0600)
|
|
}
|
|
|
|
c := NewImage(tmp)
|
|
err = c.WriteImage(tc.args.i)
|
|
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
|
t.Errorf("\n%s\nWriteImage(...): -want error, +got error:\n%s", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteLayer(t *testing.T) {
|
|
errBoom := errors.New("boom")
|
|
|
|
type args struct {
|
|
l ociv1.Layer
|
|
}
|
|
type want struct {
|
|
err error
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
files map[string][]byte
|
|
args args
|
|
want want
|
|
}{
|
|
"DiffIDError": {
|
|
reason: "We should return an error if we can't get the layer's (diff) digest.",
|
|
args: args{
|
|
l: &MockLayer{
|
|
MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{}, errBoom },
|
|
},
|
|
},
|
|
want: want{
|
|
err: errors.Wrap(errBoom, errGetDigest),
|
|
},
|
|
},
|
|
"Uncompressed": {
|
|
reason: "We should return an error if we can't get the layer's uncompressed tarball reader.",
|
|
args: args{
|
|
l: &MockLayer{
|
|
MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{}, nil },
|
|
MockUncompressed: func() (io.ReadCloser, error) { return nil, errBoom },
|
|
},
|
|
},
|
|
want: want{
|
|
err: errors.Wrap(errBoom, errReadLayer),
|
|
},
|
|
},
|
|
"SuccessfulWrite": {
|
|
reason: "We should not return an error if we successfully wrote a layer to the store.",
|
|
args: args{
|
|
l: &MockLayer{
|
|
MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
|
|
MockUncompressed: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("")), nil },
|
|
},
|
|
},
|
|
want: want{
|
|
err: nil,
|
|
},
|
|
},
|
|
"SuccessfulNoOp": {
|
|
reason: "We should return early if the supplied layer is already stored.",
|
|
files: map[string][]byte{
|
|
"cool": nil, // This file just has to exist.
|
|
},
|
|
args: args{
|
|
l: &MockLayer{
|
|
MockDiffID: func() (ociv1.Hash, error) { return ociv1.Hash{Hex: "cool"}, nil },
|
|
MockUncompressed: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("")), nil },
|
|
},
|
|
},
|
|
want: want{
|
|
err: nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
tmp, err := os.MkdirTemp(os.TempDir(), strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
|
|
if err != nil {
|
|
t.Fatal(err.Error())
|
|
}
|
|
t.Cleanup(func() {
|
|
os.RemoveAll(tmp)
|
|
})
|
|
|
|
for name, data := range tc.files {
|
|
path := filepath.Join(tmp, DirImages, name)
|
|
_ = os.MkdirAll(filepath.Dir(path), 0700)
|
|
_ = os.WriteFile(path, data, 0600)
|
|
}
|
|
|
|
c := NewImage(tmp)
|
|
err = c.WriteLayer(tc.args.l)
|
|
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
|
t.Errorf("\n%s\nWriteLayer(...): -want error, +got error:\n%s", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|