diff --git a/internal/operator-controller/applier/boxcutter_test.go b/internal/operator-controller/applier/boxcutter_test.go index c44d978f9f..71187ec9cf 100644 --- a/internal/operator-controller/applier/boxcutter_test.go +++ b/internal/operator-controller/applier/boxcutter_test.go @@ -24,6 +24,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/operator-framework/api/pkg/operators/v1alpha1" + ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/applier" "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" @@ -31,6 +33,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" testutils "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/bundlefs" ) func Test_RegistryV1BundleRenderer_Render_Success(t *testing.T) { @@ -52,7 +55,9 @@ func Test_RegistryV1BundleRenderer_Render_Success(t *testing.T) { }, }, } - bundleFS := testutils.NewBundleFS() + bundleFS := bundlefs.Builder(). + WithPackageName("some-package"). + WithCSV(testutils.MakeCSV(testutils.WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces))).Build() objs, err := r.Render(bundleFS, &ocv1.ClusterExtension{ Spec: ocv1.ClusterExtensionSpec{ @@ -74,7 +79,9 @@ func Test_RegistryV1BundleRenderer_Render_Failure(t *testing.T) { }, }, } - bundleFS := testutils.NewBundleFS() + bundleFS := bundlefs.Builder(). + WithPackageName("some-package"). + WithCSV(testutils.MakeCSV(testutils.WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces))).Build() objs, err := r.Render(bundleFS, &ocv1.ClusterExtension{ Spec: ocv1.ClusterExtensionSpec{ diff --git a/internal/operator-controller/rukpak/bundle/source/source.go b/internal/operator-controller/rukpak/bundle/source/source.go index ad8570179e..0f5d3f185c 100644 --- a/internal/operator-controller/rukpak/bundle/source/source.go +++ b/internal/operator-controller/rukpak/bundle/source/source.go @@ -24,6 +24,10 @@ type BundleSource interface { GetBundle() (bundle.RegistryV1, error) } +type RegistryV1Properties struct { + Properties []property.Property `json:"properties"` +} + // identitySource is a bundle source that returns itself type identitySource bundle.RegistryV1 @@ -158,11 +162,7 @@ func copyMetadataPropertiesToCSV(csv *v1alpha1.ClusterServiceVersion, fsys fs.FS // Otherwise, we need to parse the properties.yaml file and // append its properties into the CSV annotation. - type registryV1Properties struct { - Properties []property.Property `json:"properties"` - } - - var metadataProperties registryV1Properties + var metadataProperties RegistryV1Properties if err := yaml.Unmarshal(metadataPropertiesJSON, &metadataProperties); err != nil { return fmt.Errorf("failed to unmarshal metadata/properties.yaml: %w", err) } diff --git a/internal/operator-controller/rukpak/bundle/source/source_test.go b/internal/operator-controller/rukpak/bundle/source/source_test.go index cf7b1cb90d..5acd9f0652 100644 --- a/internal/operator-controller/rukpak/bundle/source/source_test.go +++ b/internal/operator-controller/rukpak/bundle/source/source_test.go @@ -2,15 +2,19 @@ package source_test import ( "io/fs" - "strings" "testing" - "testing/fstest" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" - . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + testutils "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/bundlefs" ) const ( @@ -27,7 +31,18 @@ func Test_FromBundle_Success(t *testing.T) { } func Test_FromFS_Success(t *testing.T) { - rv1, err := source.FromFS(NewBundleFS()).GetBundle() + bundleFS := bundlefs.Builder(). + WithPackageName("test"). + WithBundleProperty("from-file-key", "from-file-value"). + WithBundleResource("csv.yaml", ptr.To(testutils.MakeCSV( + testutils.WithName("test.v1.0.0"), + testutils.WithAnnotations(map[string]string{ + "olm.properties": `[{"type":"from-csv-annotations-key", "value":"from-csv-annotations-value"}]`, + }), + testutils.WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + )).Build() + + rv1, err := source.FromFS(bundleFS).GetBundle() require.NoError(t, err) t.Log("Check package name is correctly taken from metadata/annotations.yaml") @@ -44,16 +59,30 @@ func Test_FromFS_Fails(t *testing.T) { }{ { name: "bundle missing ClusterServiceVersion manifest", - FS: removePaths(NewBundleFS(), BundlePathCSV), + FS: bundlefs.Builder(). + WithPackageName("test"). + WithBundleProperty("foo", "bar"). + WithBundleResource("service.yaml", &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + }).Build(), }, { name: "bundle missing metadata/annotations.yaml", - FS: removePaths(NewBundleFS(), BundlePathAnnotations), + FS: bundlefs.Builder(). + WithBundleProperty("foo", "bar"). + WithBundleResource("csv.yaml", ptr.To(testutils.MakeCSV())).Build(), }, { - name: "bundle missing metadata/ directory", - FS: removePaths(NewBundleFS(), "metadata/"), + name: "metadata/annotations.yaml missing package name annotation", + FS: bundlefs.Builder(). + WithBundleProperty("foo", "bar"). + WithBundleResource("csv.yaml", ptr.To(testutils.MakeCSV())).Build(), }, { - name: "bundle missing manifests/ directory", - FS: removePaths(NewBundleFS(), "manifests/"), + name: "bundle missing manifests directory", + FS: bundlefs.Builder(). + WithPackageName("test"). + WithBundleProperty("foo", "bar").Build(), }, } { t.Run(tt.name, func(t *testing.T) { @@ -62,14 +91,3 @@ func Test_FromFS_Fails(t *testing.T) { }) } } - -func removePaths(mapFs fstest.MapFS, paths ...string) fstest.MapFS { - for k := range mapFs { - for _, path := range paths { - if strings.HasPrefix(k, path) { - delete(mapFs, k) - } - } - } - return mapFs -} diff --git a/internal/operator-controller/rukpak/util/testing/bundlefs.go b/internal/operator-controller/rukpak/util/testing/bundlefs.go deleted file mode 100644 index 6c5f588373..0000000000 --- a/internal/operator-controller/rukpak/util/testing/bundlefs.go +++ /dev/null @@ -1,64 +0,0 @@ -package testing - -import ( - "fmt" - "path/filepath" - "strings" - "testing/fstest" - - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" -) - -const ( - BundlePathAnnotations = "metadata/annotations.yaml" - BundlePathProperties = "metadata/properties.yaml" - BundlePathManifests = "manifests" - BundlePathCSV = BundlePathManifests + "/csv.yaml" -) - -func NewBundleFS() fstest.MapFS { - annotationsYml := ` -annotations: - operators.operatorframework.io.bundle.mediatype.v1: registry+v1 - operators.operatorframework.io.bundle.package.v1: test -` - - propertiesYml := ` -properties: - - type: "from-file-key" - value: "from-file-value" -` - - csvYml := ` -apiVersion: operators.operatorframework.io/v1alpha1 -kind: ClusterServiceVersion -metadata: - name: test.v1.0.0 - annotations: - olm.properties: '[{"type":"from-csv-annotations-key", "value":"from-csv-annotations-value"}]' -spec: - installModes: - - type: AllNamespaces - supported: true -` - - return fstest.MapFS{ - BundlePathAnnotations: &fstest.MapFile{Data: []byte(strings.Trim(annotationsYml, "\n"))}, - BundlePathProperties: &fstest.MapFile{Data: []byte(strings.Trim(propertiesYml, "\n"))}, - BundlePathCSV: &fstest.MapFile{Data: []byte(strings.Trim(csvYml, "\n"))}, - } -} - -func AddManifest(bundleFS fstest.MapFS, obj client.Object) error { - gvk := obj.GetObjectKind().GroupVersionKind() - manifestName := fmt.Sprintf("%s%s_%s_%s%s.yaml", gvk.Group, gvk.Version, gvk.Kind, obj.GetNamespace(), obj.GetName()) - bytes, err := yaml.Marshal(obj) - if err != nil { - return err - } - bundleFS[filepath.Join(BundlePathManifests, manifestName)] = &fstest.MapFile{ - Data: bytes, - } - return nil -} diff --git a/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs.go b/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs.go new file mode 100644 index 0000000000..b9d2d8c25a --- /dev/null +++ b/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs.go @@ -0,0 +1,145 @@ +package bundlefs + +import ( + "fmt" + "path/filepath" + "strings" + "testing/fstest" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-registry/alpha/property" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" + registry "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/operator-registry" +) + +const ( + BundlePathAnnotations = "metadata/annotations.yaml" + BundlePathProperties = "metadata/properties.yaml" + BundlePathManifests = "manifests" +) + +// BundleFSBuilder builds a registry+v1 bundle filesystem +type BundleFSBuilder interface { + WithPackageName(packageName string) BundleFSBuilder + WithChannels(channels ...string) BundleFSBuilder + WithDefaultChannel(channel string) BundleFSBuilder + WithBundleProperty(propertyType string, value string) BundleFSBuilder + WithBundleResource(resourceName string, resource client.Object) BundleFSBuilder + WithCSV(csv v1alpha1.ClusterServiceVersion) BundleFSBuilder + Build() fstest.MapFS +} + +// bundleFSBuilder builds a registry+v1 bundle filesystem +type bundleFSBuilder struct { + annotations *registry.Annotations + properties []property.Property + resources map[string]client.Object +} + +func Builder() BundleFSBuilder { + return &bundleFSBuilder{} +} + +// WithPackageName is an option for NewBundleFS used to set the package name annotation in the +// bundle filesystem metadata/annotations.yaml file +func (b *bundleFSBuilder) WithPackageName(packageName string) BundleFSBuilder { + if b.annotations == nil { + b.annotations = ®istry.Annotations{} + } + b.annotations.PackageName = packageName + return b +} + +// WithChannels is an option for NewBundleFS used to set the channels annotation in the +// bundle filesystem metadata/annotations.yaml file +func (b *bundleFSBuilder) WithChannels(channels ...string) BundleFSBuilder { + if b.annotations == nil { + b.annotations = ®istry.Annotations{} + } + b.annotations.Channels = strings.Join(channels, ",") + return b +} + +// WithDefaultChannel is an option for NewBundleFS used to set the channel annotation in the +// bundle filesystem metadata/annotations.yaml file +func (b *bundleFSBuilder) WithDefaultChannel(channel string) BundleFSBuilder { + if b.annotations == nil { + b.annotations = ®istry.Annotations{} + } + b.annotations.DefaultChannelName = channel + return b +} + +// WithBundleProperty is an options for NewBundleFS used to add a property to the list of properties +// in the bundle filesystem metadata/properties.yaml file +func (b *bundleFSBuilder) WithBundleProperty(propertyType string, value string) BundleFSBuilder { + b.properties = append(b.properties, property.Property{ + Type: propertyType, + Value: []byte(`"` + value + `"`), + }) + return b +} + +// WithBundleResource is an option for NewBundleFS use to add the yaml representation of resource to the +// path manifests/.yaml on the bundles filesystem +func (b *bundleFSBuilder) WithBundleResource(resourceName string, resource client.Object) BundleFSBuilder { + if b.resources == nil { + b.resources = make(map[string]client.Object) + } + b.resources[resourceName] = resource + return b +} + +// WithCSV is an optiona for NewBundleFS used to add the yaml representation of csv to the +// path manifests/csv.yaml on the bundle filesystem +func (b *bundleFSBuilder) WithCSV(csv v1alpha1.ClusterServiceVersion) BundleFSBuilder { + if b.resources == nil { + b.resources = make(map[string]client.Object) + } + b.resources["csv.yaml"] = &csv + return b +} + +// Build creates a registry+v1 bundle filesystem with the applied options +// By default, an empty registry+v1 bundle filesystem will be returned +func (b *bundleFSBuilder) Build() fstest.MapFS { + bundleFS := fstest.MapFS{} + + // Add annotations metadata + if b.annotations != nil { + annotationsYml, err := yaml.Marshal(registry.AnnotationsFile{ + Annotations: *b.annotations, + }) + if err != nil { + panic(fmt.Errorf("error building bundle fs: %w", err)) + } + bundleFS[BundlePathAnnotations] = &fstest.MapFile{Data: annotationsYml} + } + + // Add property metadata + if len(b.properties) > 0 { + propertiesYml, err := yaml.Marshal(source.RegistryV1Properties{ + Properties: b.properties, + }) + if err != nil { + panic(fmt.Errorf("error building bundle fs: %w", err)) + } + bundleFS[BundlePathProperties] = &fstest.MapFile{Data: propertiesYml} + } + + // Add resources + for name, obj := range b.resources { + resourcePath := filepath.Join(BundlePathManifests, name) + resourceYml, err := yaml.Marshal(obj) + if err != nil { + panic(fmt.Errorf("error building bundle fs: %w", err)) + } + bundleFS[resourcePath] = &fstest.MapFile{Data: resourceYml} + } + + return bundleFS +} diff --git a/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs_test.go b/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs_test.go new file mode 100644 index 0000000000..2b323ced57 --- /dev/null +++ b/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs_test.go @@ -0,0 +1,110 @@ +package bundlefs_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + testutils "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/bundlefs" +) + +func Test_BundleFSBuilder(t *testing.T) { + t.Run("returns empty bundle file system by default", func(t *testing.T) { + bundleFs := bundlefs.Builder().Build() + assert.Empty(t, bundleFs) + }) + + t.Run("WithPackageName sets the bundle package annotation", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithPackageName("test").Build() + require.Contains(t, bundleFs, "metadata/annotations.yaml") + require.Equal(t, []byte(`annotations: + operators.operatorframework.io.bundle.channel.default.v1: "" + operators.operatorframework.io.bundle.channels.v1: "" + operators.operatorframework.io.bundle.package.v1: test +`), bundleFs["metadata/annotations.yaml"].Data) + }) + + t.Run("WithChannels sets the bundle channels annotation", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithChannels("alpha", "beta", "stable").Build() + require.Contains(t, bundleFs, "metadata/annotations.yaml") + require.Equal(t, []byte(`annotations: + operators.operatorframework.io.bundle.channel.default.v1: "" + operators.operatorframework.io.bundle.channels.v1: alpha,beta,stable + operators.operatorframework.io.bundle.package.v1: "" +`), bundleFs["metadata/annotations.yaml"].Data) + }) + + t.Run("WithDefaultChannel sets the bundle default channel annotation", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithDefaultChannel("stable").Build() + require.Contains(t, bundleFs, "metadata/annotations.yaml") + require.Equal(t, []byte(`annotations: + operators.operatorframework.io.bundle.channel.default.v1: stable + operators.operatorframework.io.bundle.channels.v1: "" + operators.operatorframework.io.bundle.package.v1: "" +`), bundleFs["metadata/annotations.yaml"].Data) + }) + + t.Run("WithBundleProperty sets the bundle properties", func(t *testing.T) { + bundleFs := bundlefs.Builder(). + WithBundleProperty("foo", "bar"). + WithBundleProperty("key", "value"). + Build() + + require.Contains(t, bundleFs, "metadata/properties.yaml") + require.Equal(t, []byte(`properties: +- type: foo + value: bar +- type: key + value: value +`), bundleFs["metadata/properties.yaml"].Data) + }) + + t.Run("WithBundleResource adds a resource to the manifests directory", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithBundleResource("service.yaml", &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + }).Build() + require.Contains(t, bundleFs, "manifests/service.yaml") + require.Equal(t, []byte(`apiVersion: v1 +kind: Service +metadata: + name: test +spec: {} +status: + loadBalancer: {} +`), bundleFs["manifests/service.yaml"].Data) + }) + + t.Run("WithCSV adds a csv to the manifests directory", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithCSV(testutils.MakeCSV(testutils.WithName("some-csv"))).Build() + require.Contains(t, bundleFs, "manifests/csv.yaml") + require.Equal(t, []byte(`apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: some-csv +spec: + apiservicedefinitions: {} + cleanup: + enabled: false + customresourcedefinitions: {} + displayName: "" + install: + spec: + deployments: null + strategy: "" + provider: {} + version: 0.0.0 +status: + cleanup: {} +`), bundleFs["manifests/csv.yaml"].Data) + }) +}