diff --git a/alpha/declcfg/declcfg_to_model.go b/alpha/declcfg/declcfg_to_model.go index 62dfd13ca..68e04b0f2 100644 --- a/alpha/declcfg/declcfg_to_model.go +++ b/alpha/declcfg/declcfg_to_model.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/blang/semver/v4" + "go.podman.io/image/v5/docker/reference" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" @@ -128,6 +129,15 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) { return nil, fmt.Errorf("package %q does not match %q property %q", b.Package, property.TypePackage, props.Packages[0].PackageName) } + if err := validateImagePullSpec(b.Image, "package %q bundle %q image", b.Package, b.Name); err != nil { + return nil, err + } + for i, rel := range b.RelatedImages { + if err := validateImagePullSpec(rel.Image, "package %q bundle %q relatedImages[%d].image", b.Package, b.Name, i); err != nil { + return nil, err + } + } + // Parse version from the package property. rawVersion := props.Packages[0].Version ver, err := semver.Parse(rawVersion) @@ -269,3 +279,15 @@ func relatedImagesToModelRelatedImages(in []RelatedImage) []model.RelatedImage { } return out } + +// validateImagePullSpec checks that a non-empty image pull spec is valid +// Empty pull specs are not validated. +func validateImagePullSpec(pullSpec, errFormat string, errArgs ...interface{}) error { + if pullSpec == "" { + return nil + } + if _, err := reference.ParseNormalizedNamed(pullSpec); err != nil { + return fmt.Errorf(errFormat+": invalid image pull spec %q: %w", append(errArgs, pullSpec, err)...) + } + return nil +} diff --git a/alpha/declcfg/declcfg_to_model_test.go b/alpha/declcfg/declcfg_to_model_test.go index 7a5b36377..2e176ba24 100644 --- a/alpha/declcfg/declcfg_to_model_test.go +++ b/alpha/declcfg/declcfg_to_model_test.go @@ -506,6 +506,71 @@ func TestConvertToModel(t *testing.T) { })}, }, }, + { + name: "Error/BundleImageInvalidPullSpecUnsupportedDigestSsha256", + assertion: hasErrorContaining("invalid image pull spec"), + cfg: DeclarativeConfig{ + Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)}, + Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: testBundleName("foo", "0.1.0")})}, + Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) { + // Misspelled digest algorithm: ssha256 instead of sha256 (unsupported hash type) + b.Image = "quay.io/operator-framework/foo-bundle@ssha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" + })}, + }, + }, + { + name: "Error/BundleImageInvalidPullSpecUnsupportedDigestMd5", + assertion: hasErrorContaining("invalid image pull spec"), + cfg: DeclarativeConfig{ + Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)}, + Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: testBundleName("foo", "0.1.0")})}, + Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) { + b.Image = "quay.io/operator-framework/foo-bundle@md5:abcd1234abcd1234abcd1234abcd1234" + })}, + }, + }, + { + name: "Error/BundleRelatedImageInvalidPullSpecSsha256", + assertion: hasErrorContaining("invalid image pull spec"), + cfg: DeclarativeConfig{ + Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)}, + Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: testBundleName("foo", "0.1.0")})}, + Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) { + b.RelatedImages = []RelatedImage{ + {Name: "bundle", Image: testBundleImage("foo", "0.1.0")}, + {Name: "operator", Image: "quay.io/operator-framework/my-operator@ssha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}, + } + })}, + }, + }, + { + name: "Success/BundleImageValidSha256Digest", + assertion: require.NoError, + cfg: DeclarativeConfig{ + Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)}, + Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: testBundleName("foo", "0.1.0")})}, + Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) { + b.Image = "quay.io/operator-framework/foo-bundle@sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" + b.RelatedImages = []RelatedImage{ + {Name: "bundle", Image: "quay.io/operator-framework/foo-bundle@sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}, + } + })}, + }, + }, + { + name: "Success/BundleImageValidTagWithDigest", + assertion: require.NoError, + cfg: DeclarativeConfig{ + Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)}, + Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: testBundleName("foo", "0.1.0")})}, + Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) { + b.Image = "quay.io/operator-framework/foo-bundle:v0.1.0@sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" + b.RelatedImages = []RelatedImage{ + {Name: "bundle", Image: "quay.io/operator-framework/foo-bundle:v0.1.0@sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}, + } + })}, + }, + }, } for _, s := range specs { @@ -577,3 +642,14 @@ func hasError(expectedError string) require.ErrorAssertionFunc { t.FailNow() } } + +// hasErrorContaining returns an ErrorAssertionFunc that passes when the error message contains the given substring. +func hasErrorContaining(substring string) require.ErrorAssertionFunc { + return func(t require.TestingT, actualError error, args ...interface{}) { + if stdt, ok := t.(*testing.T); ok { + stdt.Helper() + } + require.Error(t, actualError) + require.Contains(t, actualError.Error(), substring, "expected error to contain %q", substring) + } +}