Skip to content

Commit 49be998

Browse files
Filter out bundle versions lower than installed (operator-framework#711)
Adds an annotation to the App created for each extension indicating the bundle version, which can then be used on future reconciles to filter out older versions. This can be overridden by setting the UpgradeConstraintPolicy to 'Ignore'. Signed-off-by: dtfranz <[email protected]> Co-authored-by: Varsha Prasad Narsing <[email protected]>
1 parent b621279 commit 49be998

File tree

7 files changed

+995
-59
lines changed

7 files changed

+995
-59
lines changed

cmd/manager/main.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,17 @@ package main
1818

1919
import (
2020
"flag"
21-
"fmt"
2221
"net/http"
2322
"os"
2423
"time"
2524

2625
"github.com/spf13/pflag"
2726
carvelv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1"
2827
"go.uber.org/zap/zapcore"
29-
"k8s.io/apimachinery/pkg/api/errors"
3028
"k8s.io/apimachinery/pkg/runtime"
3129
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
32-
"k8s.io/client-go/discovery"
3330
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
3431
_ "k8s.io/client-go/plugin/pkg/client/auth"
35-
"k8s.io/client-go/rest"
3632
ctrl "sigs.k8s.io/controller-runtime"
3733
"sigs.k8s.io/controller-runtime/pkg/healthz"
3834
"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -131,16 +127,9 @@ func main() {
131127
os.Exit(1)
132128
}
133129

134-
hasKappApis, err := hasKappApis(mgr.GetConfig())
135-
if err != nil {
136-
setupLog.Error(err, "unable to evaluate if App needs to be created")
137-
os.Exit(1)
138-
}
139-
140130
if err = (&controllers.ExtensionReconciler{
141131
Client: cl,
142132
BundleProvider: catalogClient,
143-
HasKappApis: hasKappApis,
144133
}).SetupWithManager(mgr); err != nil {
145134
setupLog.Error(err, "unable to create controller", "controller", "Extension")
146135
os.Exit(1)
@@ -162,27 +151,3 @@ func main() {
162151
os.Exit(1)
163152
}
164153
}
165-
166-
// hasKappApis checks whether the cluster has Kapp APIs installed in the cluster.
167-
// This does not guarantee that the controller is present to reconcile the App CRs.
168-
func hasKappApis(config *rest.Config) (bool, error) {
169-
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
170-
if err != nil {
171-
return false, fmt.Errorf("creating discovery client: %v", err)
172-
}
173-
apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(carvelv1alpha1.SchemeGroupVersion.String())
174-
if err != nil && !errors.IsNotFound(err) {
175-
return false, fmt.Errorf("listing resource APIs: %v", err)
176-
}
177-
178-
if apiResourceList == nil {
179-
return false, nil
180-
}
181-
182-
for _, resource := range apiResourceList.APIResources {
183-
if resource.Kind == "App" {
184-
return true, nil
185-
}
186-
}
187-
return false, nil
188-
}

internal/catalogmetadata/filter/bundle_predicates.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ func InMastermindsSemverRange(semverRange *mmsemver.Constraints) Predicate[catal
3333
}
3434
}
3535

36+
func HigherBundleVersion(currentVersion *bsemver.Version) Predicate[catalogmetadata.Bundle] {
37+
return func(bundle *catalogmetadata.Bundle) bool {
38+
if currentVersion == nil {
39+
return false
40+
}
41+
bundleVersion, err := bundle.Version()
42+
if err != nil {
43+
return false
44+
}
45+
return bundleVersion.GTE(*currentVersion)
46+
}
47+
}
48+
3649
func InBlangSemverRange(semverRange bsemver.Range) Predicate[catalogmetadata.Bundle] {
3750
return func(bundle *catalogmetadata.Bundle) bool {
3851
bundleVersion, err := bundle.Version()

internal/catalogmetadata/filter/bundle_predicates_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package filter_test
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"testing"
67

78
mmsemver "github.com/Masterminds/semver/v3"
@@ -63,6 +64,67 @@ func TestInMastermindsSemverRange(t *testing.T) {
6364
assert.False(t, f(b3))
6465
}
6566

67+
func TestHigherBundleVersion(t *testing.T) {
68+
testCases := []struct {
69+
name string
70+
requestedVersion string
71+
comparedVersion string
72+
wantResult bool
73+
}{
74+
{
75+
name: "includes equal version",
76+
requestedVersion: "1.0.0",
77+
comparedVersion: "1.0.0",
78+
wantResult: true,
79+
},
80+
{
81+
name: "filters out older version",
82+
requestedVersion: "1.0.0",
83+
comparedVersion: "0.0.1",
84+
wantResult: false,
85+
},
86+
{
87+
name: "includes higher version",
88+
requestedVersion: "1.0.0",
89+
comparedVersion: "2.0.0",
90+
wantResult: true,
91+
},
92+
{
93+
name: "filters out broken version",
94+
requestedVersion: "1.0.0",
95+
comparedVersion: "broken",
96+
wantResult: false,
97+
},
98+
{
99+
name: "filter returns false with nil version",
100+
requestedVersion: "",
101+
comparedVersion: "1.0.0",
102+
wantResult: false,
103+
},
104+
}
105+
106+
for _, tc := range testCases {
107+
t.Run(tc.name, func(t *testing.T) {
108+
bundle := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{
109+
Properties: []property.Property{
110+
{
111+
Type: property.TypePackage,
112+
Value: json.RawMessage(fmt.Sprintf(`{"packageName": "package1", "version": "%s"}`, tc.comparedVersion)),
113+
},
114+
},
115+
}}
116+
var version *bsemver.Version
117+
if tc.requestedVersion != "" {
118+
parsedVersion := bsemver.MustParse(tc.requestedVersion)
119+
// to test with nil requested version
120+
version = &parsedVersion
121+
}
122+
f := filter.HigherBundleVersion(version)
123+
assert.Equal(t, tc.wantResult, f(bundle))
124+
})
125+
}
126+
}
127+
66128
func TestInBlangSemverRange(t *testing.T) {
67129
b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{
68130
Properties: []property.Property{

internal/controllers/extension_controller.go

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package controllers
1818

1919
import (
2020
"context"
21-
"errors"
2221
"fmt"
2322
"sort"
2423
"strings"
@@ -29,6 +28,7 @@ import (
2928
kappctrlv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1"
3029
corev1 "k8s.io/api/core/v1"
3130
"k8s.io/apimachinery/pkg/api/equality"
31+
apierrors "k8s.io/apimachinery/pkg/api/errors"
3232
apimeta "k8s.io/apimachinery/pkg/api/meta"
3333
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3434
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -56,10 +56,11 @@ import (
5656
type ExtensionReconciler struct {
5757
client.Client
5858
BundleProvider BundleProvider
59-
HasKappApis bool
6059
}
6160

62-
var errkappAPIUnavailable = errors.New("kapp-controller apis unavailable on cluster")
61+
var (
62+
bundleVersionKey = "olm.operatorframework.io/bundleVersion"
63+
)
6364

6465
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions,verbs=get;list;watch;create;update;patch;delete
6566
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions/status,verbs=update;patch
@@ -146,17 +147,6 @@ func (r *ExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.Ext
146147
return ctrl.Result{}, nil
147148
}
148149

149-
if !r.HasKappApis {
150-
ext.Status.InstalledBundle = nil
151-
setInstalledStatusConditionFailed(&ext.Status.Conditions, errkappAPIUnavailable.Error(), ext.GetGeneration())
152-
153-
ext.Status.ResolvedBundle = nil
154-
setResolvedStatusConditionUnknown(&ext.Status.Conditions, "kapp apis are unavailable", ext.GetGeneration())
155-
156-
setDeprecationStatusesUnknown(&ext.Status.Conditions, "kapp apis are unavailable", ext.GetGeneration())
157-
return ctrl.Result{}, errkappAPIUnavailable
158-
}
159-
160150
// TODO: Improve the resolution logic.
161151
bundle, err := r.resolve(ctx, *ext)
162152
if err != nil {
@@ -190,7 +180,13 @@ func (r *ExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.Ext
190180
return ctrl.Result{}, nil
191181
}
192182

193-
app := r.GenerateExpectedApp(*ext, bundle.Image)
183+
app, err := r.GenerateExpectedApp(*ext, bundle)
184+
if err != nil {
185+
setInstalledStatusConditionUnknown(&ext.Status.Conditions, err.Error(), ext.GetGeneration())
186+
setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration())
187+
return ctrl.Result{}, err
188+
}
189+
194190
if err := r.ensureApp(ctx, app); err != nil {
195191
// originally Reason: ocv1alpha1.ReasonInstallationFailed
196192
ext.Status.InstalledBundle = nil
@@ -403,7 +399,13 @@ func extensionRequestsForCatalog(c client.Reader, logger logr.Logger) handler.Ma
403399
}
404400
}
405401

406-
func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundlePath string) *unstructured.Unstructured {
402+
func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundle *catalogmetadata.Bundle) (*unstructured.Unstructured, error) {
403+
bundleVersion, err := bundle.Version()
404+
if err != nil {
405+
return nil, fmt.Errorf("failed to generate App from Extension %q with bundle %q: %w", o.GetName(), bundle.Name, err)
406+
}
407+
bundlePath := bundle.Image
408+
407409
// We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver.
408410
// If you use a typed object, any default values from that struct get serialized into the JSON patch, which could
409411
// cause unrelated fields to be patched back to the default value even though that isn't the intention. Using an
@@ -437,6 +439,9 @@ func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundle
437439
"metadata": map[string]interface{}{
438440
"name": o.GetName(),
439441
"namespace": o.GetNamespace(),
442+
"annotations": map[string]string{
443+
bundleVersionKey: bundleVersion.String(),
444+
},
440445
},
441446
"spec": spec,
442447
},
@@ -452,7 +457,24 @@ func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundle
452457
BlockOwnerDeletion: ptr.To(true),
453458
},
454459
})
455-
return app
460+
return app, nil
461+
}
462+
463+
func (r *ExtensionReconciler) getInstalledVersion(ctx context.Context, namespacedName types.NamespacedName) (*bsemver.Version, error) {
464+
existingApp, err := r.existingAppUnstructured(ctx, namespacedName.Name, namespacedName.Namespace)
465+
if err != nil {
466+
return nil, err
467+
}
468+
existingVersion, ok := existingApp.GetAnnotations()[bundleVersionKey]
469+
if !ok {
470+
return nil, fmt.Errorf("existing App %q in Namespace %q missing bundle version", namespacedName.Name, namespacedName.Namespace)
471+
}
472+
473+
existingVersionSemver, err := bsemver.New(existingVersion)
474+
if err != nil {
475+
return nil, fmt.Errorf("could not determine bundle version of existing App %q in Namespace %q: %w", namespacedName.Name, namespacedName.Namespace, err)
476+
}
477+
return existingVersionSemver, nil
456478
}
457479

458480
func (r *ExtensionReconciler) resolve(ctx context.Context, extension ocv1alpha1.Extension) (*catalogmetadata.Bundle, error) {
@@ -481,19 +503,33 @@ func (r *ExtensionReconciler) resolve(ctx context.Context, extension ocv1alpha1.
481503
predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr))
482504
}
483505

506+
var installedVersion string
507+
// Do not include bundle versions older than currently installed unless UpgradeConstraintPolicy = 'Ignore'
508+
if extension.Spec.Source.Package.UpgradeConstraintPolicy != ocv1alpha1.UpgradeConstraintPolicyIgnore {
509+
installedVersionSemver, err := r.getInstalledVersion(ctx, types.NamespacedName{Name: extension.GetName(), Namespace: extension.GetNamespace()})
510+
if err != nil && !apierrors.IsNotFound(err) {
511+
return nil, err
512+
}
513+
if installedVersionSemver != nil {
514+
installedVersion = installedVersionSemver.String()
515+
predicates = append(predicates, catalogfilter.HigherBundleVersion(installedVersionSemver))
516+
}
517+
}
518+
484519
resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(predicates...))
485520

486521
if len(resultSet) == 0 {
487-
if versionRange != "" && channelName != "" {
488-
return nil, fmt.Errorf("no package %q matching version %q found in channel %q", packageName, versionRange, channelName)
489-
}
522+
var versionError, channelError, existingVersionError string
490523
if versionRange != "" {
491-
return nil, fmt.Errorf("no package %q matching version %q found", packageName, versionRange)
524+
versionError = fmt.Sprintf(" matching version %q", versionRange)
492525
}
493526
if channelName != "" {
494-
return nil, fmt.Errorf("no package %q found in channel %q", packageName, channelName)
527+
channelError = fmt.Sprintf(" in channel %q", channelName)
528+
}
529+
if installedVersion != "" {
530+
existingVersionError = fmt.Sprintf(" which upgrades currently installed version %q", installedVersion)
495531
}
496-
return nil, fmt.Errorf("no package %q found", packageName)
532+
return nil, fmt.Errorf("no package %q%s%s%s found", packageName, versionError, channelError, existingVersionError)
497533
}
498534

499535
sort.SliceStable(resultSet, func(i, j int) bool {

0 commit comments

Comments
 (0)