diff --git a/pkg/collection/imagetxt/imagetxt.go b/pkg/collection/imagetxt/imagetxt.go index ae4b113..b6b34d5 100644 --- a/pkg/collection/imagetxt/imagetxt.go +++ b/pkg/collection/imagetxt/imagetxt.go @@ -22,8 +22,8 @@ type ImageTxt struct { IncludeSources map[string]bool ExcludeSources map[string]bool - getter local.Opener lock *sync.Mutex + getter local.Opener computed bool contents map[name.Reference]artifact.OCI } @@ -34,23 +34,6 @@ type Option interface { Apply(*ImageTxt) error } -type withRef string - -func (o withRef) Apply(it *ImageTxt) error { - ref := string(o) - - if strings.HasPrefix(ref, "http") || strings.HasPrefix(ref, "https") { - it.getter = local.RemoteOpener(ref) - } else { - it.getter = local.LocalOpener(ref) - } - return nil -} - -func WithRef(ref string) Option { - return withRef(ref) -} - type withIncludeSources []string func (o withIncludeSources) Apply(it *ImageTxt) error { @@ -83,11 +66,19 @@ func WithExcludeSources(exclude ...string) Option { return withExcludeSources(exclude) } -func New(opts ...Option) (*ImageTxt, error) { +func New(ref string, opts ...Option) (*ImageTxt, error) { it := &ImageTxt{ + Ref: ref, + lock: &sync.Mutex{}, } + if strings.HasPrefix(ref, "http") || strings.HasPrefix(ref, "https") { + it.getter = local.RemoteOpener(ref) + } else { + it.getter = local.LocalOpener(ref) + } + for i, o := range opts { if err := o.Apply(it); err != nil { return nil, fmt.Errorf("invalid option %d: %v", i, err) @@ -139,7 +130,7 @@ func (it *ImageTxt) compute() error { } var pullAll bool - var targetSources map[string]bool + targetSources := make(map[string]bool) if len(foundSources) == 0 || (len(it.IncludeSources) == 0 && len(it.ExcludeSources) == 0) { // pull all found images diff --git a/pkg/collection/imagetxt/imagetxt_test.go b/pkg/collection/imagetxt/imagetxt_test.go new file mode 100644 index 0000000..556a85a --- /dev/null +++ b/pkg/collection/imagetxt/imagetxt_test.go @@ -0,0 +1,216 @@ +package imagetxt + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + + artifacts "github.com/rancherfederal/hauler/pkg/artifact" + "github.com/rancherfederal/hauler/pkg/content/image" +) + +var ( + ErrInvalidRef = errors.New("invalid reference") + ErrRefNotFound = errors.New("ref not found") + ErrRefNotImage = errors.New("ref is not image") + ErrExtraRefsFound = errors.New("extra refs found in contents") +) + +var ( + testServer *httptest.Server +) + +func TestMain(m *testing.M) { + setup() + code := m.Run() + teardown() + os.Exit(code) +} + +func setup() { + dir := http.Dir("./testdata/http/") + h := http.FileServer(dir) + testServer = httptest.NewServer(h) +} + +func teardown() { + if testServer != nil { + testServer.Close() + } +} + +type failKind string + +const ( + failKindNew = failKind("New") + failKindContents = failKind("Contents") +) + +func checkError(checkedFailKind failKind) func(*testing.T, error, bool, failKind) { + return func(cet *testing.T, err error, testShouldFail bool, testFailKind failKind) { + if err != nil { + // if error should not have happened at all OR error should have happened + // at a different point, test failed + if !testShouldFail || testFailKind != checkedFailKind { + cet.Fatalf("unexpected error at %s: %v", checkedFailKind, err) + } + // test should fail at this point, test passed + return + } + // if no error occurred but error should have happened at this point, test + // failed + if testShouldFail && testFailKind == checkedFailKind { + cet.Fatalf("unexpected nil error at %s", checkedFailKind) + } + } +} + +func TestImageTxtCollection(t *testing.T) { + type testEntry struct { + Name string + Ref string + IncludeSources []string + ExcludeSources []string + ExpectedImages []string + ShouldFail bool + FailKind failKind + } + tt := []testEntry{ + { + Name: "http ref basic", + Ref: fmt.Sprintf("%s/images-http.txt", testServer.URL), + ExpectedImages: []string{ + "busybox", + "nginx:1.19", + "rancher/hyperkube:v1.21.7-rancher1", + "docker.io/rancher/klipper-lb:v0.3.4", + "quay.io/jetstack/cert-manager-controller:v1.6.1", + }, + }, + { + Name: "http ref sources format pull all", + Ref: fmt.Sprintf("%s/images-src-http.txt", testServer.URL), + ExpectedImages: []string{ + "busybox", + "nginx:1.19", + "rancher/hyperkube:v1.21.7-rancher1", + "docker.io/rancher/klipper-lb:v0.3.4", + "quay.io/jetstack/cert-manager-controller:v1.6.1", + }, + }, + { + Name: "http ref sources format include sources A", + Ref: fmt.Sprintf("%s/images-src-http.txt", testServer.URL), + IncludeSources: []string{ + "core", "rke", + }, + ExpectedImages: []string{ + "busybox", + "nginx:1.19", + "rancher/hyperkube:v1.21.7-rancher1", + }, + }, + { + Name: "http ref sources format include sources B", + Ref: fmt.Sprintf("%s/images-src-http.txt", testServer.URL), + IncludeSources: []string{ + "nginx", "rancher", "cert-manager", + }, + ExpectedImages: []string{ + "nginx:1.19", + "rancher/hyperkube:v1.21.7-rancher1", + "docker.io/rancher/klipper-lb:v0.3.4", + "quay.io/jetstack/cert-manager-controller:v1.6.1", + }, + }, + { + Name: "http ref sources format exclude sources A", + Ref: fmt.Sprintf("%s/images-src-http.txt", testServer.URL), + ExcludeSources: []string{ + "cert-manager", + }, + ExpectedImages: []string{ + "busybox", + "nginx:1.19", + "rancher/hyperkube:v1.21.7-rancher1", + "docker.io/rancher/klipper-lb:v0.3.4", + }, + }, + { + Name: "http ref sources format exclude sources B", + Ref: fmt.Sprintf("%s/images-src-http.txt", testServer.URL), + ExcludeSources: []string{ + "core", + }, + ExpectedImages: []string{ + "nginx:1.19", + "rancher/hyperkube:v1.21.7-rancher1", + "docker.io/rancher/klipper-lb:v0.3.4", + "quay.io/jetstack/cert-manager-controller:v1.6.1", + }, + }, + { + Name: "local file ref", + Ref: "./testdata/images-file.txt", + ExpectedImages: []string{ + "busybox", + "nginx:1.19", + "rancher/hyperkube:v1.21.7-rancher1", + "docker.io/rancher/klipper-lb:v0.3.4", + "quay.io/jetstack/cert-manager-controller:v1.6.1", + }, + }, + } + + checkErrorNew := checkError(failKindNew) + checkErrorContents := checkError(failKindContents) + + for _, curTest := range tt { + t.Run(curTest.Name, func(innerT *testing.T) { + curImageTxt, err := New(curTest.Ref, + WithIncludeSources(curTest.IncludeSources...), + WithExcludeSources(curTest.ExcludeSources...), + ) + checkErrorNew(innerT, err, curTest.ShouldFail, curTest.FailKind) + + ociContents, err := curImageTxt.Contents() + checkErrorContents(innerT, err, curTest.ShouldFail, curTest.FailKind) + + if err := checkImages(ociContents, curTest.ExpectedImages); err != nil { + innerT.Fatal(err) + } + }) + } +} + +func checkImages(content map[name.Reference]artifacts.OCI, refs []string) error { + contentCopy := make(map[name.Reference]artifacts.OCI, len(content)) + for k, v := range content { + contentCopy[k] = v + } + for _, ref := range refs { + nameRef, err := name.ParseReference(ref) + if err != nil { + return fmt.Errorf("ref %s: %w", ref, ErrInvalidRef) + } + target, ok := content[nameRef] + if !ok { + return fmt.Errorf("ref %s: %w", ref, ErrRefNotFound) + } + if _, ok := target.(*image.Image); !ok { + return fmt.Errorf("got underlying type %T: %w", target, ErrRefNotImage) + } + delete(contentCopy, nameRef) + } + + if len(contentCopy) != 0 { + return ErrExtraRefsFound + } + + return nil +} diff --git a/pkg/collection/imagetxt/testdata/http/images-http.txt b/pkg/collection/imagetxt/testdata/http/images-http.txt new file mode 100644 index 0000000..0ae5fa8 --- /dev/null +++ b/pkg/collection/imagetxt/testdata/http/images-http.txt @@ -0,0 +1,5 @@ +busybox +nginx:1.19 +rancher/hyperkube:v1.21.7-rancher1 +docker.io/rancher/klipper-lb:v0.3.4 +quay.io/jetstack/cert-manager-controller:v1.6.1 diff --git a/pkg/collection/imagetxt/testdata/http/images-src-http.txt b/pkg/collection/imagetxt/testdata/http/images-src-http.txt new file mode 100644 index 0000000..013abe6 --- /dev/null +++ b/pkg/collection/imagetxt/testdata/http/images-src-http.txt @@ -0,0 +1,5 @@ +busybox core +nginx:1.19 core,nginx +rancher/hyperkube:v1.21.7-rancher1 rancher,rke +docker.io/rancher/klipper-lb:v0.3.4 rancher,k3s +quay.io/jetstack/cert-manager-controller:v1.6.1 cert-manager diff --git a/pkg/collection/imagetxt/testdata/images-file.txt b/pkg/collection/imagetxt/testdata/images-file.txt new file mode 100644 index 0000000..0ae5fa8 --- /dev/null +++ b/pkg/collection/imagetxt/testdata/images-file.txt @@ -0,0 +1,5 @@ +busybox +nginx:1.19 +rancher/hyperkube:v1.21.7-rancher1 +docker.io/rancher/klipper-lb:v0.3.4 +quay.io/jetstack/cert-manager-controller:v1.6.1 diff --git a/pkg/content/image/image.go b/pkg/content/image/image.go index 4ac8ab8..d7c70a9 100644 --- a/pkg/content/image/image.go +++ b/pkg/content/image/image.go @@ -8,9 +8,9 @@ import ( "github.com/rancherfederal/hauler/pkg/artifact" ) -var _ artifact.OCI = (*image)(nil) +var _ artifact.OCI = (*Image)(nil) -func (i *image) MediaType() string { +func (i *Image) MediaType() string { mt, err := i.Image.MediaType() if err != nil { return "" @@ -18,15 +18,15 @@ func (i *image) MediaType() string { return string(mt) } -func (i *image) RawConfig() ([]byte, error) { +func (i *Image) RawConfig() ([]byte, error) { return i.RawConfigFile() } -type image struct { +type Image struct { gv1.Image } -func NewImage(ref string) (*image, error) { +func NewImage(ref string) (*Image, error) { r, err := name.ParseReference(ref) if err != nil { return nil, err @@ -37,7 +37,7 @@ func NewImage(ref string) (*image, error) { return nil, err } - return &image{ + return &Image{ Image: img, }, nil }