Skip to content

Commit 676e1f3

Browse files
committed
(feat) block cluster upgrade when installed incompatible operators
1 parent 8d692e3 commit 676e1f3

File tree

7 files changed

+263
-11
lines changed

7 files changed

+263
-11
lines changed

cmd/cluster-olm-operator/main.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"k8s.io/component-base/cli"
2121
utilflag "k8s.io/component-base/cli/flag"
2222

23+
"github.com/openshift/cluster-olm-operator/internal/utils"
2324
"github.com/openshift/cluster-olm-operator/pkg/clients"
2425
"github.com/openshift/cluster-olm-operator/pkg/controller"
2526
"github.com/openshift/cluster-olm-operator/pkg/version"
@@ -105,13 +106,28 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
105106
deploymentControllerList = append(deploymentControllerList, controller)
106107
}
107108

109+
operatorImageVersion := status.VersionForOperatorFromEnv()
110+
nextOCPMinorVersion, err := utils.GetNextOCPMinorVersion(operatorImageVersion)
111+
if err != nil {
112+
return err
113+
}
114+
108115
upgradeableConditionController := controller.NewStaticUpgradeableConditionController(
109116
"OLMStaticUpgradeableConditionController",
110117
cl.OperatorClient,
111118
cc.EventRecorder.ForComponent("OLMStaticUpgradeableConditionController"),
112119
controllerNames,
113120
)
114121

122+
incompatibleOperatorController := controller.NewIncompatibleOperatorController(
123+
"OLMIncompatibleOperatorController",
124+
nextOCPMinorVersion,
125+
cl.KubeClient,
126+
cl.ClusterExtensionClient,
127+
cl.OperatorClient,
128+
cc.EventRecorder.ForComponent("OLMIncompatibleOperatorController"),
129+
)
130+
115131
versionGetter := status.NewVersionGetter()
116132
versionGetter.SetVersion("operator", status.VersionForOperatorFromEnv())
117133

@@ -129,7 +145,7 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
129145

130146
cl.StartInformers(ctx)
131147

132-
for _, c := range append(staticResourceControllerList, upgradeableConditionController, clusterOperatorController, operatorLoggingController) {
148+
for _, c := range append(staticResourceControllerList, upgradeableConditionController, incompatibleOperatorController, clusterOperatorController, operatorLoggingController) {
133149
go func(c factory.Controller) {
134150
defer runtime.HandleCrash()
135151
c.Run(ctx, 1)

internal/utils/utils.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
semver "github.com/blang/semver/v4"
8+
)
9+
10+
func GetNextOCPMinorVersion(versionString string) (*semver.Version, error) {
11+
v, err := ToSemver(versionString)
12+
if err != nil {
13+
return v, err
14+
}
15+
v.Build = nil // Builds are irrelevant
16+
v.Pre = nil // Next Y release
17+
return v, v.IncrementMinor() // Sets Y=Y+1 and Z=0
18+
}
19+
20+
func ToSemver(max string) (*semver.Version, error) {
21+
value := strings.Trim(max, "\"")
22+
if value == "" {
23+
// Handle "" separately, so parse doesn't treat it as a zero
24+
return nil, fmt.Errorf(`value cannot be "" (an empty string)`)
25+
}
26+
27+
version, err := semver.ParseTolerant(value)
28+
if err != nil {
29+
return nil, fmt.Errorf(`failed to parse "%q" as semver: %w`, value, err)
30+
}
31+
32+
truncatedVersion := semver.Version{Major: version.Major, Minor: version.Minor}
33+
if !version.EQ(truncatedVersion) {
34+
return nil, fmt.Errorf("expected <major>.<minor> version, but got invalid value %q", version)
35+
}
36+
return &truncatedVersion, nil
37+
}

manifests/0000_51_olm_02_operator_clusterrole.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ rules:
7272
- get
7373
- list
7474
- watch
75+
- apiGroups:
76+
- ""
77+
resources:
78+
- secrets
79+
verbs:
80+
- get
81+
- list
82+
- watch
7583
- apiGroups:
7684
- ""
7785
resources:
@@ -210,6 +218,7 @@ rules:
210218
verbs:
211219
- get
212220
- list
221+
- watch
213222
- apiGroups:
214223
- ""
215224
resources:

manifests/0000_51_olm_06_deployment.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ spec:
5858
- /cluster-olm-operator
5959
args:
6060
- start
61-
- -v=2
6261
imagePullPolicy: IfNotPresent
6362
env:
6463
- name: OPERATOR_NAME

pkg/clients/clients.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/openshift/library-go/pkg/controller/controllercmd"
1818
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
1919
"github.com/openshift/library-go/pkg/operator/v1helpers"
20+
ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
2021
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
2122
"k8s.io/apimachinery/pkg/api/meta"
2223
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -26,6 +27,7 @@ import (
2627
"k8s.io/apimachinery/pkg/types"
2728
"k8s.io/apimachinery/pkg/util/sets"
2829
"k8s.io/client-go/dynamic"
30+
"k8s.io/client-go/dynamic/dynamicinformer"
2931
"k8s.io/client-go/informers"
3032
"k8s.io/client-go/kubernetes"
3133
"k8s.io/client-go/rest"
@@ -43,6 +45,7 @@ type Clients struct {
4345
RESTMapper meta.RESTMapper
4446
OperatorClient *OperatorClient
4547
OperatorInformers operatorinformers.SharedInformerFactory
48+
ClusterExtensionClient *ClusterExtensionClient
4649
ConfigClient configclient.Interface
4750
KubeInformerFactory informers.SharedInformerFactory
4851
ConfigInformerFactory configinformer.SharedInformerFactory
@@ -91,23 +94,34 @@ func New(cc *controllercmd.ControllerContext) (*Clients, error) {
9194
return nil, err
9295
}
9396

97+
infFact := dynamicinformer.NewDynamicSharedInformerFactory(dynClient, defaultResyncPeriod)
98+
clusterExtensionGVR := ocv1alpha1.GroupVersion.WithResource("clusterextensions")
99+
inf := infFact.ForResource(clusterExtensionGVR)
100+
101+
ceClient := &ClusterExtensionClient{
102+
factory: infFact,
103+
informer: inf,
104+
}
105+
94106
return &Clients{
95-
KubeClient: kubeClient,
96-
APIExtensionsClient: apiExtensionsClient,
97-
DynamicClient: dynClient,
98-
RESTMapper: rm,
99-
OperatorClient: opClient,
100-
OperatorInformers: operatorInformersFactory,
101-
ConfigClient: configClient,
102-
KubeInformerFactory: informers.NewSharedInformerFactory(kubeClient, defaultResyncPeriod),
103-
ConfigInformerFactory: configinformer.NewSharedInformerFactory(configClient, defaultResyncPeriod),
107+
KubeClient: kubeClient,
108+
APIExtensionsClient: apiExtensionsClient,
109+
DynamicClient: dynClient,
110+
RESTMapper: rm,
111+
OperatorClient: opClient,
112+
OperatorInformers: operatorInformersFactory,
113+
ClusterExtensionClient: ceClient,
114+
ConfigClient: configClient,
115+
KubeInformerFactory: informers.NewSharedInformerFactory(kubeClient, defaultResyncPeriod),
116+
ConfigInformerFactory: configinformer.NewSharedInformerFactory(configClient, defaultResyncPeriod),
104117
}, nil
105118
}
106119

107120
func (c *Clients) StartInformers(ctx context.Context) {
108121
c.KubeInformerFactory.Start(ctx.Done())
109122
c.ConfigInformerFactory.Start(ctx.Done())
110123
c.OperatorInformers.Start(ctx.Done())
124+
c.ClusterExtensionClient.factory.Start(ctx.Done())
111125
if c.KubeInformersForNamespaces != nil {
112126
c.KubeInformersForNamespaces.Start(ctx.Done())
113127
}
@@ -131,6 +145,15 @@ const (
131145
fieldManager = "cluster-olm-operator"
132146
)
133147

148+
type ClusterExtensionClient struct {
149+
factory dynamicinformer.DynamicSharedInformerFactory
150+
informer informers.GenericInformer
151+
}
152+
153+
func (ce ClusterExtensionClient) Informer() informers.GenericInformer {
154+
return ce.informer
155+
}
156+
134157
type OperatorClient struct {
135158
clientset operatorclient.Interface
136159
informers operatorinformers.SharedInformerFactory
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"sort"
9+
"strings"
10+
11+
semver "github.com/blang/semver/v4"
12+
"github.com/go-logr/logr"
13+
operatorv1 "github.com/openshift/api/operator/v1"
14+
"github.com/openshift/cluster-olm-operator/internal/utils"
15+
"github.com/openshift/cluster-olm-operator/pkg/clients"
16+
"github.com/openshift/library-go/pkg/controller/factory"
17+
"github.com/openshift/library-go/pkg/operator/events"
18+
"github.com/openshift/library-go/pkg/operator/v1helpers"
19+
storage "github.com/operator-framework/helm-operator-plugins/pkg/storage"
20+
"github.com/operator-framework/operator-registry/alpha/property"
21+
helm "helm.sh/helm/v3/pkg/storage"
22+
"helm.sh/helm/v3/pkg/storage/driver"
23+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
24+
"k8s.io/apimachinery/pkg/labels"
25+
"k8s.io/client-go/kubernetes"
26+
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
27+
"k8s.io/klog/v2"
28+
)
29+
30+
const (
31+
reasonIncompatibleOperatorsInstalled = "IncompatibleOperatorsInstalled"
32+
maxOpenShiftVersionProperty = "olm.maxOpenShiftVersion"
33+
ownerKindKey = "olm.operatorframework.io/owner-kind"
34+
ownerNameKey = "olm.operatorframework.io/owner-name"
35+
packageNameKey = "olm.operatorframework.io/package-name"
36+
bundleNameKey = "olm.operatorframework.io/bundle-name"
37+
bundleVersionKey = "olm.operatorframework.io/bundle-version"
38+
)
39+
40+
type incompatibleOperatorController struct {
41+
name string
42+
nextOCPMinorVersion *semver.Version
43+
kubeclient kubernetes.Interface
44+
clusterExtensionClient *clients.ClusterExtensionClient
45+
operatorClient *clients.OperatorClient
46+
logger logr.Logger
47+
}
48+
49+
func NewIncompatibleOperatorController(name string, nextOCPMinorVersion *semver.Version, kubeclient kubernetes.Interface, clusterExtensionClient *clients.ClusterExtensionClient, operatorClient *clients.OperatorClient, eventRecorder events.Recorder) factory.Controller {
50+
c := &incompatibleOperatorController{
51+
name: name,
52+
nextOCPMinorVersion: nextOCPMinorVersion,
53+
kubeclient: kubeclient,
54+
clusterExtensionClient: clusterExtensionClient,
55+
operatorClient: operatorClient,
56+
logger: klog.NewKlogr().WithName(name),
57+
}
58+
59+
return factory.New().WithSync(c.sync).WithSyncDegradedOnError(operatorClient).WithInformers(operatorClient.Informer(), clusterExtensionClient.Informer().Informer()).ToController(name, eventRecorder)
60+
}
61+
62+
func (c *incompatibleOperatorController) sync(ctx context.Context, _ factory.SyncContext) error {
63+
c.logger.Info("sync started")
64+
defer c.logger.Info("sync finished")
65+
66+
incompatibleOperators, err := c.getIncompatibleOperators()
67+
if err != nil {
68+
c.logger.V(4).Error(err, "error checking incompatible operators")
69+
return err
70+
}
71+
72+
var updateStatusFn v1helpers.UpdateStatusFunc
73+
if len(incompatibleOperators) > 0 {
74+
updateStatusFn = v1helpers.UpdateConditionFn(operatorv1.OperatorCondition{
75+
Type: "InstalledOLMOperatorsUpgradeable",
76+
Status: operatorv1.ConditionFalse,
77+
Reason: reasonIncompatibleOperatorsInstalled,
78+
Message: strings.Join(incompatibleOperators, ","),
79+
})
80+
} else {
81+
updateStatusFn = v1helpers.UpdateConditionFn(operatorv1.OperatorCondition{
82+
Type: "InstalledOLMOperatorsUpgradeable",
83+
Status: operatorv1.ConditionTrue,
84+
})
85+
}
86+
87+
if _, _, updateErr := v1helpers.UpdateStatus(ctx, c.operatorClient, updateStatusFn); updateErr != nil {
88+
c.logger.V(4).Info(fmt.Sprintf("Error updating operator condition status: %v", updateErr))
89+
return updateErr
90+
}
91+
return nil
92+
}
93+
94+
func (c *incompatibleOperatorController) getIncompatibleOperators() ([]string, error) {
95+
var incompatibleOperators []string
96+
97+
ceList, err := c.clusterExtensionClient.Informer().Lister().List(labels.NewSelector())
98+
if err != nil {
99+
c.logger.V(4).Error(err, "Error listing cluster extensions")
100+
return nil, err
101+
}
102+
103+
store := c.buildHelmStore(c.kubeclient.CoreV1().Secrets("openshift-operator-controller"))
104+
105+
// Get all ClusterExtensions incompatible with next Y-stream
106+
for _, obj := range ceList {
107+
ce, ok := obj.(*unstructured.Unstructured)
108+
if !ok {
109+
c.logger.V(4).Error(nil, fmt.Sprintf("Unable to Cast Object to unstructured.Unstructured. Object is of type %T", obj))
110+
return nil, nil
111+
}
112+
113+
rel, err := store.Deployed(ce.GetName())
114+
if errors.Is(err, driver.ErrNoDeployedReleases) {
115+
c.logger.Info("Cluster Extension not yet deployed - will check again later")
116+
continue
117+
}
118+
if err != nil {
119+
c.logger.V(4).Error(err, "Error returning the last deployed release")
120+
return nil, err
121+
}
122+
123+
if rel.Chart == nil || rel.Chart.Metadata == nil {
124+
continue
125+
}
126+
props, err := propertyListFromPropertiesAnnotation(rel.Chart.Metadata.Annotations["olm.properties"])
127+
if err != nil {
128+
c.logger.V(4).Error(err, "Error converting olm.properties")
129+
return nil, err
130+
}
131+
for _, p := range props {
132+
if p.Type == maxOpenShiftVersionProperty {
133+
version := string(p.Value)
134+
maxOCPVersion, err := utils.ToSemver(version)
135+
if err != nil {
136+
c.logger.V(4).Info(fmt.Sprintf("Error converting to semver for version %s: %v", version, err))
137+
continue
138+
}
139+
if maxOCPVersion != nil && !maxOCPVersion.GTE(*c.nextOCPMinorVersion) {
140+
// Incompatible
141+
incompatibleOperators = append(incompatibleOperators, rel.Labels[bundleNameKey])
142+
}
143+
}
144+
}
145+
}
146+
// deterministic ordering
147+
sort.Strings(incompatibleOperators)
148+
149+
return incompatibleOperators, nil
150+
}
151+
152+
func propertyListFromPropertiesAnnotation(raw string) ([]property.Property, error) {
153+
var props []property.Property
154+
if err := json.Unmarshal([]byte(raw), &props); err != nil {
155+
return nil, fmt.Errorf("failed to unmarshal properties annotation: %w", err)
156+
}
157+
return props, nil
158+
}
159+
160+
func (c *incompatibleOperatorController) buildHelmStore(secretClient v1.SecretInterface) helm.Storage {
161+
log := func(s string, args ...interface{}) { c.logger.V(4).Info(fmt.Sprintf(s, args...)) }
162+
csConfig := storage.ChunkedSecretsConfig{Log: log}
163+
164+
return helm.Storage{
165+
Driver: storage.NewChunkedSecrets(secretClient, "operator-controller", csConfig),
166+
Log: log,
167+
}
168+
}

0 commit comments

Comments
 (0)