Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add annotations to BundleDeployment #442

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member Author

@m1kola m1kola Oct 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to care about bundle deployments created directly? These annotations will only be present on bundle deployments reconciled by operator-controller.

Edit: I think we do need to care so this whole thing might not fly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I don't think we need to care, at least in this context.

I sorta think this context is: is there a BD for this operator already? (And maybe that means we should have a label on the BD like operators.operatorframework.io/operator-name whose value is Operator.metadata.name?

Perhaps in the future, there's a separate feature that helps the resolver become aware of other BDs, but I don't think we need to do that now.

}
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.
Comment on lines -37 to -39
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is no longer relevant given that we filter by package name, bundle name and bundle version.

sort.SliceStable(resultSet, func(i, j int) bool {
return catalogsort.ByVersion(resultSet[i], resultSet[j])
})
Comment on lines -40 to -42
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we no longer need sorting by version since we filter bundles by bundle version amongst other things. I assume previously we were doing sorting beucase multiple bundles could in theory use the same image (.e.g some sort of fake version).

Copy link
Member

@joelanford joelanford Oct 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorting, I think, was to make sure the resolver favored high semver versions over low semver versions. Maybe different context though?

Copy link
Member Author

@m1kola m1kola Oct 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For upgrade edges there is a separate sorting below in this file (not in the diff) which is still needed.

This piece of code is where we look for a currently installed bundle by image ref. I suspect that there may be multiple bundles with the same image, but different versions 🤷‍♂️. So in the current implementation we need sorting to prefer higher version.

But when filtering bundles by of package name, bundle name and bundle version we should be able to uniquely identify a bundle. So I expect there always will be 1 or 0 bundles.

@jmprusi might be able to give some more context

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. It looks like we check the result set for 0 bundles, but not more than 1. Can we add another check that errors if there is more than 1 bundle?

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
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
Loading