Skip to content

Commit

Permalink
Add annotations to BundleDeployment
Browse files Browse the repository at this point in the history
This adds annotations to `BundleDeployment` objects created by
operator-controller in response to `Operator` objects.

This makes it easier to filter bundles during resolution and
makes us less dependant on image bundle format.

Signed-off-by: Mikalai Radchuk <mradchuk@redhat.com>
  • Loading branch information
m1kola committed Oct 12, 2023
1 parent f178f75 commit 67ccd0f
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 69 deletions.
12 changes: 6 additions & 6 deletions internal/catalogmetadata/filter/bundle_predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ func WithPackageName(packageName string) Predicate[catalogmetadata.Bundle] {
}
}

func WithName(bundleName string) Predicate[catalogmetadata.Bundle] {
return func(bundle *catalogmetadata.Bundle) bool {
return bundle.Name == bundleName
}
}

func InMastermindsSemverRange(semverRange *mmsemver.Constraints) Predicate[catalogmetadata.Bundle] {
return func(bundle *catalogmetadata.Bundle) bool {
bVersion, err := bundle.Version()
Expand Down Expand Up @@ -52,12 +58,6 @@ func InChannel(channelName string) Predicate[catalogmetadata.Bundle] {
}
}

func WithBundleImage(bundleImage string) Predicate[catalogmetadata.Bundle] {
return func(bundle *catalogmetadata.Bundle) bool {
return bundle.Image == bundleImage
}
}

func Replaces(bundleName string) Predicate[catalogmetadata.Bundle] {
return func(bundle *catalogmetadata.Bundle) bool {
for _, ch := range bundle.InChannels {
Expand Down
24 changes: 12 additions & 12 deletions internal/catalogmetadata/filter/bundle_predicates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ func TestWithPackageName(t *testing.T) {
assert.False(t, f(b3))
}

func TestWithName(t *testing.T) {
b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Name: "package1.v1"}}
b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Name: "package2.v1"}}
b3 := &catalogmetadata.Bundle{}

f := filter.WithName("package1.v1")

assert.True(t, f(b1))
assert.False(t, f(b2))
assert.False(t, f(b3))
}

func TestInMastermindsSemverRange(t *testing.T) {
b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{
Properties: []property.Property{
Expand Down Expand Up @@ -114,18 +126,6 @@ func TestInChannel(t *testing.T) {
assert.False(t, f(b3))
}

func TestWithBundleImage(t *testing.T) {
b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Image: "fake-image-uri-1"}}
b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Image: "fake-image-uri-2"}}
b3 := &catalogmetadata.Bundle{}

f := filter.WithBundleImage("fake-image-uri-1")

assert.True(t, f(b1))
assert.False(t, f(b2))
assert.False(t, f(b3))
}

func TestReplaces(t *testing.T) {
fakeChannel := &catalogmetadata.Channel{
Channel: declcfg.Channel{
Expand Down
25 changes: 21 additions & 4 deletions internal/controllers/operator_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/operator-framework/operator-controller/internal/catalogmetadata"
"github.com/operator-framework/operator-controller/internal/controllers/validators"
olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables"
"github.com/operator-framework/operator-controller/internal/resolution/variablesources"
)

// OperatorReconciler reconciles a Operator object
Expand Down Expand Up @@ -179,9 +180,15 @@ func (r *OperatorReconciler) reconcile(ctx context.Context, op *operatorsv1alpha
setInstalledStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration())
return ctrl.Result{}, err
}

// Ensure a BundleDeployment exists with its bundle source from the bundle
// image we just looked up in the solution.
dep := r.generateExpectedBundleDeployment(*op, bundle.Image, bundleProvisioner)
dep, err := r.generateExpectedBundleDeployment(*op, bundle, bundleProvisioner)
if err != nil {
op.Status.InstalledBundleResource = ""
setInstalledStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration())
return ctrl.Result{}, err
}
if err := r.ensureBundleDeployment(ctx, dep); err != nil {
// originally Reason: operatorsv1alpha1.ReasonInstallationFailed
op.Status.InstalledBundleResource = ""
Expand Down Expand Up @@ -264,18 +271,28 @@ func (r *OperatorReconciler) bundleFromSolution(solution *solver.Solution, packa
return nil, fmt.Errorf("bundle for package %q not found in solution", packageName)
}

func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundlePath string, bundleProvisioner string) *unstructured.Unstructured {
func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundle *catalogmetadata.Bundle, bundleProvisioner string) (*unstructured.Unstructured, error) {
// We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver.
// If you use a typed object, any default values from that struct get serialized into the JSON patch, which could
// cause unrelated fields to be patched back to the default value even though that isn't the intention. Using an
// unstructured ensures that the patch contains only what is specified. Using unstructured like this is basically
// identical to "kubectl apply -f"

bundleVersion, err := bundle.Version()
if err != nil {
return nil, err
}

bd := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": rukpakv1alpha1.GroupVersion.String(),
"kind": rukpakv1alpha1.BundleDeploymentKind,
"metadata": map[string]interface{}{
"name": o.GetName(),
"labels": map[string]string{
variablesources.LabelPackageName: bundle.Package,
variablesources.LabelBundleName: bundle.Name,
variablesources.LabelBundleVersion: bundleVersion.String(),
},
},
"spec": map[string]interface{}{
// TODO: Don't assume plain provisioner
Expand All @@ -287,7 +304,7 @@ func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha
// TODO: Don't assume image type
"type": string(rukpakv1alpha1.SourceTypeImage),
"image": map[string]interface{}{
"ref": bundlePath,
"ref": bundle.Image,
},
},
},
Expand All @@ -304,7 +321,7 @@ func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha
BlockOwnerDeletion: pointer.Bool(true),
},
})
return bd
return bd, nil
}

// SetupWithManager sets up the controller with the Manager.
Expand Down
25 changes: 13 additions & 12 deletions internal/resolution/variablesources/bundle_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
LabelPackageName = "operators.operatorframework.io/package-name"
LabelBundleName = "operators.operatorframework.io/bundle-name"
LabelBundleVersion = "operators.operatorframework.io/bundle-version"
)

var _ input.VariableSource = &BundleDeploymentVariableSource{}

type BundleDeploymentVariableSource struct {
Expand Down Expand Up @@ -36,20 +42,15 @@ func (o *BundleDeploymentVariableSource) GetVariables(ctx context.Context) ([]de
return nil, err
}

processed := map[string]struct{}{}
for _, bundleDeployment := range bundleDeployments.Items {
sourceImage := bundleDeployment.Spec.Template.Spec.Source.Image
if sourceImage != nil && sourceImage.Ref != "" {
if _, ok := processed[sourceImage.Ref]; ok {
continue
}
processed[sourceImage.Ref] = struct{}{}
ips, err := NewInstalledPackageVariableSource(o.catalogClient, bundleDeployment.Spec.Template.Spec.Source.Image.Ref)
if err != nil {
return nil, err
}
variableSources = append(variableSources, ips)
pkgName := bundleDeployment.Labels[LabelPackageName]
bundleName := bundleDeployment.Labels[LabelBundleName]
bundleVersion := bundleDeployment.Labels[LabelBundleVersion]
if pkgName == "" || bundleName == "" || bundleVersion == "" {
continue
}
ips := NewInstalledPackageVariableSource(o.catalogClient, pkgName, bundleName, bundleVersion)
variableSources = append(variableSources, ips)
}

return variableSources.GetVariables(ctx)
Expand Down
31 changes: 23 additions & 8 deletions internal/resolution/variablesources/bundle_deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ func BundleDeploymentFakeClient(objects ...client.Object) client.Client {
return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
}

func bundleDeployment(name, image string) *rukpakv1alpha1.BundleDeployment {
func bundleDeployment(pkgName, bundleName, bundleVersion, image string) *rukpakv1alpha1.BundleDeployment {
return &rukpakv1alpha1.BundleDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Name: pkgName,
Labels: map[string]string{
variablesources.LabelPackageName: pkgName,
variablesources.LabelBundleName: bundleName,
variablesources.LabelBundleVersion: bundleVersion,
},
},
Spec: rukpakv1alpha1.BundleDeploymentSpec{
ProvisionerClassName: "core-rukpak-io-plain",
Expand Down Expand Up @@ -115,10 +120,10 @@ var _ = Describe("BundleDeploymentVariableSource", func() {
fakeCatalogClient = testutil.NewFakeCatalogClient(testBundleList)
})

It("should produce RequiredPackage variables", func() {
cl := BundleDeploymentFakeClient(bundleDeployment("prometheus", "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"))
It("should produce InstalledPackage variables", func() {
cl := BundleDeploymentFakeClient(bundleDeployment("prometheus", "operatorhub/prometheus/0.37.0", "0.37.0", "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"))

bdVariableSource := variablesources.NewBundleDeploymentVariableSource(cl, &fakeCatalogClient, &MockRequiredPackageSource{})
bdVariableSource := variablesources.NewBundleDeploymentVariableSource(cl, &fakeCatalogClient, &MockVariableSource{})
variables, err := bdVariableSource.GetVariables(context.Background())
Expect(err).ToNot(HaveOccurred())

Expand All @@ -136,11 +141,21 @@ var _ = Describe("BundleDeploymentVariableSource", func() {
deppy.IdentifierFromString("installed package prometheus"): 2,
})))
})
It("should not produce InstalledPackage variables when annotations on BundleDeployment are not set", func() {
cl := BundleDeploymentFakeClient(bundleDeployment("prometheus", "", "", "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"))

bdVariableSource := variablesources.NewBundleDeploymentVariableSource(cl, &fakeCatalogClient, &MockVariableSource{})
variables, err := bdVariableSource.GetVariables(context.Background())
Expect(err).ToNot(HaveOccurred())

installedPackageVariable := filterVariables[*olmvariables.InstalledPackageVariable](variables)
Expect(installedPackageVariable).To(BeEmpty())
})
It("should return an error if the bundleDeployment image doesn't match any operator resource", func() {
cl := BundleDeploymentFakeClient(bundleDeployment("prometheus", "quay.io/operatorhubio/prometheus@sha256:nonexistent"))
cl := BundleDeploymentFakeClient(bundleDeployment("prometheus", "operatorhub/prometheus/9.9.9", "9.9.9", "quay.io/operatorhubio/prometheus@sha256:nonexistent"))

bdVariableSource := variablesources.NewBundleDeploymentVariableSource(cl, &fakeCatalogClient, &MockRequiredPackageSource{})
bdVariableSource := variablesources.NewBundleDeploymentVariableSource(cl, &fakeCatalogClient, &MockVariableSource{})
_, err := bdVariableSource.GetVariables(context.Background())
Expect(err.Error()).To(Equal("bundleImage \"quay.io/operatorhubio/prometheus@sha256:nonexistent\" not found"))
Expect(err).To(MatchError(`bundle for package "prometheus" with name "operatorhub/prometheus/9.9.9" at version "9.9.9" not found`))
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ var _ = Describe("BundlesAndDepsVariableSource", func() {
fakeCatalogClient = testutil.NewFakeCatalogClient(testBundleList)
bdvs = variablesources.NewBundlesAndDepsVariableSource(
&fakeCatalogClient,
&MockRequiredPackageSource{
&MockVariableSource{
ResultSet: []deppy.Variable{
// must match data in fakeCatalogClient
olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{
Expand Down Expand Up @@ -257,7 +257,7 @@ var _ = Describe("BundlesAndDepsVariableSource", func() {
}),
},
},
&MockRequiredPackageSource{
&MockVariableSource{
ResultSet: []deppy.Variable{
// must match data in fakeCatalogClient
olmvariables.NewRequiredPackageVariable("test-package-2", []*catalogmetadata.Bundle{
Expand Down Expand Up @@ -339,7 +339,7 @@ var _ = Describe("BundlesAndDepsVariableSource", func() {

bdvs = variablesources.NewBundlesAndDepsVariableSource(
&emptyCatalogClient,
&MockRequiredPackageSource{
&MockVariableSource{
ResultSet: []deppy.Variable{
// must match data in fakeCatalogClient
olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{
Expand Down Expand Up @@ -380,20 +380,20 @@ var _ = Describe("BundlesAndDepsVariableSource", func() {
It("should return error if an inner variable source returns an error", func() {
bdvs = variablesources.NewBundlesAndDepsVariableSource(
&fakeCatalogClient,
&MockRequiredPackageSource{Error: errors.New("fake error")},
&MockVariableSource{Error: errors.New("fake error")},
)
_, err := bdvs.GetVariables(context.TODO())
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("fake error"))
})
})

type MockRequiredPackageSource struct {
type MockVariableSource struct {
ResultSet []deppy.Variable
Error error
}

func (m *MockRequiredPackageSource) GetVariables(_ context.Context) ([]deppy.Variable, error) {
func (m *MockVariableSource) GetVariables(_ context.Context) ([]deppy.Variable, error) {
return m.ResultSet, m.Error
}

Expand Down
40 changes: 23 additions & 17 deletions internal/resolution/variablesources/installed_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"sort"

mmsemver "github.com/Masterminds/semver/v3"
"github.com/operator-framework/deppy/pkg/deppy"
"github.com/operator-framework/deppy/pkg/deppy/input"

Expand All @@ -19,7 +20,9 @@ var _ input.VariableSource = &InstalledPackageVariableSource{}
type InstalledPackageVariableSource struct {
catalogClient BundleProvider
successors successorsFunc
bundleImage string
pkgName string
bundleName string
bundleVersion string
}

func (r *InstalledPackageVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) {
Expand All @@ -28,18 +31,23 @@ func (r *InstalledPackageVariableSource) GetVariables(ctx context.Context) ([]de
return nil, err
}

vr, err := mmsemver.NewConstraint(r.bundleVersion)
if err != nil {
return nil, err
}

// find corresponding bundle for the installed content
resultSet := catalogfilter.Filter(allBundles, catalogfilter.WithBundleImage(r.bundleImage))
resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(
catalogfilter.WithPackageName(r.pkgName),
catalogfilter.WithName(r.bundleName),
catalogfilter.InMastermindsSemverRange(vr),
))
if len(resultSet) == 0 {
return nil, r.notFoundError()
return nil, fmt.Errorf("bundle for package %q with name %q at version %q not found", r.pkgName, r.bundleName, r.bundleVersion)
}
if len(resultSet) > 1 {
return nil, fmt.Errorf("more than one bundle for package %q with name %q at version %q found", r.pkgName, r.bundleName, r.bundleVersion)
}

// TODO: fast follow - we should check whether we are already supporting the channel attribute in the operator spec.
// if so, we should take the value from spec of the operator CR in the owner ref of the bundle deployment.
// If that channel is set, we need to update the filter above to filter by channel as well.
sort.SliceStable(resultSet, func(i, j int) bool {
return catalogsort.ByVersion(resultSet[i], resultSet[j])
})
installedBundle := resultSet[0]

upgradeEdges, err := r.successors(allBundles, installedBundle)
Expand All @@ -54,16 +62,14 @@ func (r *InstalledPackageVariableSource) GetVariables(ctx context.Context) ([]de
}, nil
}

func (r *InstalledPackageVariableSource) notFoundError() error {
return fmt.Errorf("bundleImage %q not found", r.bundleImage)
}

func NewInstalledPackageVariableSource(catalogClient BundleProvider, bundleImage string) (*InstalledPackageVariableSource, error) {
func NewInstalledPackageVariableSource(catalogClient BundleProvider, pkgName, bundleName, bundleVersion string) *InstalledPackageVariableSource {
return &InstalledPackageVariableSource{
catalogClient: catalogClient,
bundleImage: bundleImage,
successors: legacySemanticsSuccessors,
}, nil
pkgName: pkgName,
bundleName: bundleName,
bundleVersion: bundleVersion,
}
}

// successorsFunc must return successors of a currently installed bundle
Expand Down
7 changes: 4 additions & 3 deletions internal/resolution/variablesources/installed_package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,15 @@ func TestInstalledPackageVariableSource(t *testing.T) {
},
}

const bundleImage = "registry.io/repo/test-package@v2.0.0"
fakeCatalogClient := testutil.NewFakeCatalogClient(bundleList)

t.Run("with ForceSemverUpgradeConstraints feature gate disabled", func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, false)()

ipvs, err := variablesources.NewInstalledPackageVariableSource(&fakeCatalogClient, bundleImage)
require.NoError(t, err)
pkgName := "test-package"
bundleName := "test-package.v2.0.0"
bundleVersion := "2.0.0"
ipvs := variablesources.NewInstalledPackageVariableSource(&fakeCatalogClient, pkgName, bundleName, bundleVersion)

variables, err := ipvs.GetVariables(context.TODO())
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion internal/resolution/variablesources/operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ var _ = Describe("OperatorVariableSource", func() {
It("should produce RequiredPackage variables", func() {
cl := FakeClient(operator("prometheus"), operator("packageA"))
fakeCatalogClient := testutil.NewFakeCatalogClient(testBundleList)
opVariableSource := variablesources.NewOperatorVariableSource(cl, &fakeCatalogClient, &MockRequiredPackageSource{})
opVariableSource := variablesources.NewOperatorVariableSource(cl, &fakeCatalogClient, &MockVariableSource{})
variables, err := opVariableSource.GetVariables(context.Background())
Expect(err).ToNot(HaveOccurred())

Expand Down

0 comments on commit 67ccd0f

Please sign in to comment.