diff --git a/go.mod b/go.mod index 7c64585c4..3e618eb19 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/onsi/ginkgo v1.12.0 // indirect github.com/onsi/gomega v1.9.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect - github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/opencontainers/image-spec v1.0.1 github.com/sirupsen/logrus v1.6.0 // indirect github.com/spf13/cobra v1.0.0 github.com/vdemeester/k8s-pkg-credentialprovider v1.18.1-0.20201019120933-f1d16962a4db diff --git a/pkg/v1/match/match.go b/pkg/v1/match/match.go new file mode 100644 index 000000000..39fae5e6d --- /dev/null +++ b/pkg/v1/match/match.go @@ -0,0 +1,73 @@ +// Copyright 2020 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 match provides functionality for conveniently matching a v1.Descriptor. +package match + +import ( + v1 "github.com/google/go-containerregistry/pkg/v1" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Matcher function that is given a v1.Descriptor, and returns whether or +// not it matches a given rule. Can match on anything it wants in the Descriptor. +type Matcher func(desc v1.Descriptor) bool + +// Name returns a match.Matcher that matches based on the value of the +// "org.opencontainers.image.ref.name" annotation: +// github.com/opencontainers/image-spec/blob/v1.0.1/annotations.md#pre-defined-annotation-keys +func Name(name string) Matcher { + return Annotation(imagespec.AnnotationRefName, name) +} + +// Annotation returns a match.Matcher that matches based on the provided annotation. +func Annotation(key, value string) Matcher { + return func(desc v1.Descriptor) bool { + if desc.Annotations == nil { + return false + } + if aValue, ok := desc.Annotations[key]; ok && aValue == value { + return true + } + return false + } +} + +// Platform returns a match.Matcher that matches on the provided platform. +// Ignores any descriptors that do not have a platform. +func Platform(platform v1.Platform) Matcher { + return func(desc v1.Descriptor) bool { + if desc.Platform == nil { + return false + } + return desc.Platform.Equals(platform) + } +} + +// MediaTypes returns a match.Matcher that matches at least one of the provided media types. +func MediaTypes(mediaTypes []string) Matcher { + mts := map[string]bool{} + for _, media := range mediaTypes { + mts[media] = true + } + return func(desc v1.Descriptor) bool { + if desc.MediaType == "" { + return false + } + if _, ok := mts[string(desc.MediaType)]; ok { + return true + } + return false + } +} diff --git a/pkg/v1/match/match_test.go b/pkg/v1/match/match_test.go new file mode 100644 index 000000000..cbdee93d5 --- /dev/null +++ b/pkg/v1/match/match_test.go @@ -0,0 +1,106 @@ +// Copyright 2020 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 match_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/types" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestName(t *testing.T) { + tests := []struct { + desc v1.Descriptor + name string + match bool + }{ + {v1.Descriptor{Annotations: map[string]string{imagespec.AnnotationRefName: "foo"}}, "foo", true}, + {v1.Descriptor{Annotations: map[string]string{imagespec.AnnotationRefName: "foo"}}, "bar", false}, + {v1.Descriptor{Annotations: map[string]string{}}, "bar", false}, + {v1.Descriptor{Annotations: nil}, "bar", false}, + {v1.Descriptor{}, "bar", false}, + } + for i, tt := range tests { + f := match.Name(tt.name) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v name %s", i, match, tt.match, tt.desc, tt.name) + } + } +} + +func TestAnnotation(t *testing.T) { + tests := []struct { + desc v1.Descriptor + key string + value string + match bool + }{ + {v1.Descriptor{Annotations: map[string]string{"foo": "bar"}}, "foo", "bar", true}, + {v1.Descriptor{Annotations: map[string]string{"foo": "bar"}}, "bar", "foo", false}, + {v1.Descriptor{Annotations: map[string]string{}}, "foo", "bar", false}, + {v1.Descriptor{Annotations: nil}, "foo", "bar", false}, + {v1.Descriptor{}, "foo", "bar", false}, + } + for i, tt := range tests { + f := match.Annotation(tt.key, tt.value) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v annotation %s:%s", i, match, tt.match, tt.desc, tt.key, tt.value) + } + } +} + +func TestPlatform(t *testing.T) { + tests := []struct { + desc v1.Descriptor + platform v1.Platform + match bool + }{ + {v1.Descriptor{Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}}, v1.Platform{Architecture: "amd64", OS: "linux"}, true}, + {v1.Descriptor{Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}}, v1.Platform{Architecture: "arm64", OS: "linux"}, false}, + {v1.Descriptor{Platform: &v1.Platform{OS: "linux"}}, v1.Platform{Architecture: "arm64", OS: "linux"}, false}, + {v1.Descriptor{Platform: &v1.Platform{}}, v1.Platform{Architecture: "arm64", OS: "linux"}, false}, + {v1.Descriptor{Platform: nil}, v1.Platform{Architecture: "arm64", OS: "linux"}, false}, + {v1.Descriptor{}, v1.Platform{Architecture: "arm64", OS: "linux"}, false}, + } + for i, tt := range tests { + f := match.Platform(tt.platform) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v platform %#v", i, match, tt.match, tt.desc, tt.platform) + } + } +} + +func TestMediaTypes(t *testing.T) { + tests := []struct { + desc v1.Descriptor + mediaTypes []string + match bool + }{ + {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIImageIndex)}, true}, + {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIManifestSchema1)}, false}, + {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIManifestSchema1), string(types.OCIImageIndex)}, true}, + {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{"a", "b"}, false}, + {v1.Descriptor{}, []string{string(types.OCIManifestSchema1), string(types.OCIImageIndex)}, false}, + } + for i, tt := range tests { + f := match.MediaTypes(tt.mediaTypes) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v mediaTypes %#v", i, match, tt.match, tt.desc, tt.mediaTypes) + } + } +} diff --git a/pkg/v1/platform.go b/pkg/v1/platform.go index bb9886433..a586ab367 100644 --- a/pkg/v1/platform.go +++ b/pkg/v1/platform.go @@ -14,6 +14,10 @@ package v1 +import ( + "sort" +) + // Platform represents the target os/arch for an image. type Platform struct { Architecture string `json:"architecture"` @@ -23,3 +27,33 @@ type Platform struct { Variant string `json:"variant,omitempty"` Features []string `json:"features,omitempty"` } + +// Equals returns true if the given platform is semantically equivalent to this one. +// The order of Features and OSFeatures is not important. +func (p Platform) Equals(o Platform) bool { + return p.OS == o.OS && p.Architecture == o.Architecture && p.Variant == o.Variant && p.OSVersion == o.OSVersion && + stringSliceEqualIgnoreOrder(p.OSFeatures, o.OSFeatures) && stringSliceEqualIgnoreOrder(p.Features, o.Features) +} + +// stringSliceEqual compares 2 string slices and returns if their contents are identical. +func stringSliceEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, elm := range a { + if elm != b[i] { + return false + } + } + return true +} + +// stringSliceEqualIgnoreOrder compares 2 string slices and returns if their contents are identical, ignoring order +func stringSliceEqualIgnoreOrder(a, b []string) bool { + a1, b1 := a[:], b[:] + if a1 != nil && b1 != nil { + sort.Strings(a1) + sort.Strings(b1) + } + return stringSliceEqual(a1, b1) +} diff --git a/pkg/v1/platform_test.go b/pkg/v1/platform_test.go new file mode 100644 index 000000000..98c24150f --- /dev/null +++ b/pkg/v1/platform_test.go @@ -0,0 +1,53 @@ +// Copyright 2020 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 v1_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +func TestPlatformEquals(t *testing.T) { + tests := []struct { + a v1.Platform + b v1.Platform + equal bool + }{ + {v1.Platform{Architecture: "amd64", OS: "linux"}, v1.Platform{Architecture: "amd64", OS: "linux"}, true}, + {v1.Platform{Architecture: "amd64", OS: "linux"}, v1.Platform{Architecture: "arm64", OS: "linux"}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux"}, v1.Platform{Architecture: "amd64", OS: "darwin"}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"}, v1.Platform{Architecture: "amd64", OS: "linux"}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"}, v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "3.6"}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, v1.Platform{Architecture: "amd64", OS: "linux"}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, v1.Platform{Architecture: "amd64", OS: "linux", Variant: "ubuntu"}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, true}, + {v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, v1.Platform{Architecture: "amd64", OS: "linux"}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, true}, + {v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"ac", "bd"}}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"b", "a"}}, true}, + + {v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, v1.Platform{Architecture: "amd64", OS: "linux"}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, true}, + {v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"ac", "bd"}}, false}, + {v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"b", "a"}}, true}, + } + for i, tt := range tests { + if equal := tt.a.Equals(tt.b); equal != tt.equal { + t.Errorf("%d: mismatched was %v expected %v; original (-want +got) %s", i, equal, tt.equal, cmp.Diff(tt.a, tt.b)) + } + } +}