Skip to content

Commit 6f0430f

Browse files
committed
(feat) block cluster upgrade when installed incompatible operators
1 parent 1aa009c commit 6f0430f

File tree

8,667 files changed

+2222238
-69
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

8,667 files changed

+2222238
-69
lines changed

cmd/cluster-olm-operator/main.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,12 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
9797
controllerList = append(controllerList, controller)
9898
}
9999

100-
upgradeableConditionController := controller.NewStaticUpgradeableConditionController(
101-
"OLMStaticUpgradeableConditionController",
100+
dynamicUpgradeableConditionController := controller.NewDynamicUpgradeableConditionController(
101+
cl.KubeClient,
102+
cl.DynamicClient,
103+
"OLMDynamicUpgradeableConditionController",
102104
cl.OperatorClient,
103-
cc.EventRecorder.ForComponent("OLMStaticUpgradeableConditionController"),
105+
cc.EventRecorder.ForComponent("OLMDynamicUpgradeableConditionController"),
104106
controllerNames,
105107
)
106108

@@ -119,7 +121,7 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
119121

120122
cl.StartInformers(ctx)
121123

122-
for _, c := range append(controllerList, upgradeableConditionController, clusterOperatorController) {
124+
for _, c := range append(controllerList, dynamicUpgradeableConditionController, clusterOperatorController) {
123125
go func(c factory.Controller) {
124126
defer runtime.HandleCrash()
125127
c.Run(ctx, 1)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ toolchain go1.22.7
66

77
require (
88
github.com/blang/semver/v4 v4.0.0
9+
github.com/go-logr/logr v1.4.2
910
github.com/openshift/api v0.0.0-20240527133614-ba11c1587003
1011
github.com/openshift/build-machinery-go v0.0.0-20240419090851-af9c868bcf52
1112
github.com/openshift/client-go v0.0.0-20240528061634-b054aa794d87
@@ -68,7 +69,6 @@ require (
6869
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
6970
github.com/go-errors/errors v1.4.2 // indirect
7071
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
71-
github.com/go-logr/logr v1.4.2 // indirect
7272
github.com/go-logr/stdr v1.2.2 // indirect
7373
github.com/go-openapi/jsonpointer v0.21.0 // indirect
7474
github.com/go-openapi/jsonreference v0.21.0 // indirect
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"os"
9+
"sort"
10+
"strings"
11+
"sync"
12+
"time"
13+
14+
semver "github.com/blang/semver/v4"
15+
"github.com/go-logr/logr"
16+
operatorv1 "github.com/openshift/api/operator/v1"
17+
"github.com/openshift/cluster-olm-operator/pkg/clients"
18+
"github.com/openshift/library-go/pkg/controller/factory"
19+
"github.com/openshift/library-go/pkg/operator/events"
20+
"github.com/openshift/library-go/pkg/operator/v1helpers"
21+
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
22+
storage "github.com/operator-framework/helm-operator-plugins/pkg/storage"
23+
ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
24+
"github.com/operator-framework/operator-registry/alpha/property"
25+
helm "helm.sh/helm/v3/pkg/storage"
26+
corev1 "k8s.io/api/core/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
29+
"k8s.io/client-go/dynamic"
30+
"k8s.io/client-go/dynamic/dynamicinformer"
31+
"k8s.io/client-go/informers"
32+
"k8s.io/client-go/kubernetes"
33+
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
34+
"k8s.io/client-go/tools/cache"
35+
"k8s.io/klog/v2"
36+
)
37+
38+
const (
39+
incompatibleOperatorsInstalled = "IncompatibleOperatorsInstalled"
40+
MaxOpenShiftVersionProperty = "olm.maxOpenShiftVersion"
41+
OwnerKindKey = "olm.operatorframework.io/owner-kind"
42+
OwnerNameKey = "olm.operatorframework.io/owner-name"
43+
PackageNameKey = "olm.operatorframework.io/package-name"
44+
BundleNameKey = "olm.operatorframework.io/bundle-name"
45+
BundleVersionKey = "olm.operatorframework.io/bundle-version"
46+
)
47+
48+
type dynamicUpgradeableConditionController struct {
49+
kubeclient kubernetes.Interface
50+
informer informers.GenericInformer
51+
client dynamic.Interface
52+
name string
53+
operatorClient *clients.OperatorClient
54+
prefixes []string
55+
logger logr.Logger
56+
}
57+
58+
type ChartMetadata struct {
59+
Metadata struct {
60+
Annotations map[string]string `yaml:"annotations"`
61+
} `yaml:"metadata"`
62+
}
63+
64+
type SecretData struct {
65+
Chart struct {
66+
ChartMetadata
67+
} `yaml:"chart"`
68+
}
69+
70+
func NewDynamicUpgradeableConditionController(kubeclient kubernetes.Interface, client dynamic.Interface, name string, operatorClient *clients.OperatorClient, eventRecorder events.Recorder, prefixes []string) factory.Controller {
71+
infFact := dynamicinformer.NewDynamicSharedInformerFactory(client, 30*time.Minute)
72+
73+
clusterExtensionGVR := schema.GroupVersionResource{
74+
Group: ocv1alpha1.ClusterExtensionGVK.Group,
75+
Version: ocv1alpha1.ClusterExtensionGVK.Version,
76+
Resource: "clusterextensions",
77+
}
78+
79+
inf := infFact.ForResource(clusterExtensionGVR)
80+
81+
c := &dynamicUpgradeableConditionController{
82+
informer: inf,
83+
client: client,
84+
kubeclient: kubeclient,
85+
name: name,
86+
operatorClient: operatorClient,
87+
prefixes: prefixes,
88+
logger: klog.FromContext(context.TODO()).WithName(name),
89+
}
90+
91+
_, err := inf.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
92+
AddFunc: c.handleClusterExtension,
93+
UpdateFunc: func(oldObj, newObj interface{}) { c.handleClusterExtension(newObj) },
94+
DeleteFunc: c.handleClusterExtension,
95+
})
96+
if err != nil {
97+
c.logger.V(4).Error(nil, "could not add event handler to informer")
98+
return nil
99+
}
100+
101+
return factory.New().WithBareInformers(inf.Informer()).ToController(name, eventRecorder)
102+
}
103+
104+
func (c *dynamicUpgradeableConditionController) handleClusterExtension(obj interface{}) {
105+
_, ok := obj.(*ocv1alpha1.ClusterExtension)
106+
if !ok {
107+
c.logger.V(4).Error(nil, "could not cast to ClusterExtension object")
108+
return
109+
}
110+
111+
c.sync()
112+
}
113+
114+
func (c *dynamicUpgradeableConditionController) sync() {
115+
c.logger.V(4).Info("sync started")
116+
defer c.logger.V(4).Info("done syncing")
117+
updateStatusFuncs, err := c.ensureControllersManaged()
118+
if err != nil {
119+
c.logger.V(4).Error(err, "error verifying OLM state")
120+
return
121+
}
122+
123+
current, err := getCurrentOpenShiftVersion()
124+
if err != nil {
125+
c.logger.V(4).Error(err, "Error getting current OCP release")
126+
return
127+
}
128+
129+
if current == nil {
130+
// Note: This shouldn't happen
131+
err = fmt.Errorf("failed to determine current OpenShift Y-stream release")
132+
c.logger.V(4).Error(err, "current release is nil")
133+
return
134+
}
135+
136+
next, err := nextY(*current)
137+
if err != nil {
138+
c.logger.V(4).Error(err, "Error finding next OCP Y-stream release")
139+
return
140+
}
141+
142+
var incompatibleOperators []string
143+
144+
ceList, err := c.informer.Lister().List(nil)
145+
if err != nil {
146+
c.logger.V(4).Error(err, "Error listing cluster extensions")
147+
return
148+
}
149+
150+
// Get all ClusterExtensions incompatible with next Y-stream
151+
for _, obj := range ceList {
152+
ce, ok := obj.(*ocv1alpha1.ClusterExtension)
153+
if !ok {
154+
c.logger.V(4).Error(nil, "Error: could not cast to ClusterExtension object")
155+
return
156+
}
157+
store := buildHelmStore(ce, c.kubeclient.CoreV1().Secrets(ce.Namespace))
158+
159+
rel, _ := store.Deployed(ce.Name)
160+
props, err := propertyListFromPropertiesAnnotation(rel.Chart.Metadata.Annotations["olm.properties"])
161+
if err != nil {
162+
c.logger.V(4).Error(err, "Error converting olm.properties")
163+
return
164+
}
165+
for _, p := range props {
166+
if p.Type == MaxOpenShiftVersionProperty {
167+
version := string(p.Value)
168+
maxOCPVersion, err := toSemver(version)
169+
if err != nil {
170+
c.logger.V(4).Info(fmt.Sprintf("Error converting to semver for version %s: %v", version, err))
171+
continue
172+
}
173+
if maxOCPVersion != nil && !maxOCPVersion.GTE(next) {
174+
// Incompatible
175+
incompatibleOperators = append(incompatibleOperators, rel.Labels[BundleNameKey])
176+
}
177+
}
178+
}
179+
}
180+
181+
// deterministic ordering
182+
sort.Strings(incompatibleOperators)
183+
184+
if len(incompatibleOperators) > 0 {
185+
updateStatusFuncs = append(updateStatusFuncs, v1helpers.UpdateConditionFn(operatorv1.OperatorCondition{
186+
Type: "Upgradeable",
187+
Status: operatorv1.ConditionFalse,
188+
Reason: incompatibleOperatorsInstalled,
189+
Message: strings.Join(incompatibleOperators, ","),
190+
}))
191+
192+
if _, _, updateErr := v1helpers.UpdateStatus(context.TODO(), c.operatorClient, updateStatusFuncs...); updateErr != nil {
193+
log.Printf("Error listing secrets: %v", err)
194+
return
195+
}
196+
} else {
197+
updateStatusFuncs = append(updateStatusFuncs, v1helpers.UpdateConditionFn(operatorv1.OperatorCondition{
198+
Type: "Upgradeable",
199+
Status: operatorv1.ConditionTrue,
200+
}))
201+
202+
if _, _, updateErr := v1helpers.UpdateStatus(context.TODO(), c.operatorClient, updateStatusFuncs...); updateErr != nil {
203+
log.Printf("Error listing secrets: %v", err)
204+
return
205+
}
206+
}
207+
}
208+
209+
func (c *dynamicUpgradeableConditionController) ensureControllersManaged() ([]v1helpers.UpdateStatusFunc, error) {
210+
c.logger.V(4).Info("verifying state of olm controllers")
211+
defer c.logger.V(4).Info("done verifying state of olm controllers")
212+
opSpec, _, _, err := c.operatorClient.GetOperatorState()
213+
if err != nil {
214+
return nil, err
215+
}
216+
if opSpec.ManagementState != operatorv1.Managed {
217+
return nil, nil
218+
}
219+
220+
updateStatusFuncs := make([]v1helpers.UpdateStatusFunc, 0, len(c.prefixes))
221+
for _, prefix := range c.prefixes {
222+
updateStatusFuncs = append(updateStatusFuncs, v1helpers.UpdateConditionFn(operatorv1.OperatorCondition{
223+
Type: fmt.Sprintf("%sUpgradeable", prefix),
224+
Status: operatorv1.ConditionTrue,
225+
}))
226+
}
227+
return updateStatusFuncs, nil
228+
}
229+
230+
func propertyListFromPropertiesAnnotation(raw string) ([]property.Property, error) {
231+
var props []property.Property
232+
if err := json.Unmarshal([]byte(raw), &props); err != nil {
233+
return nil, fmt.Errorf("failed to unmarshal properties annotation: %w", err)
234+
}
235+
return props, nil
236+
}
237+
238+
func buildHelmStore(ce *ocv1alpha1.ClusterExtension, secretClient v1.SecretInterface) helm.Storage {
239+
csConfig := storage.ChunkedSecretsConfig{
240+
ChunkSize: 0,
241+
MaxReadChunks: 0,
242+
MaxWriteChunks: 0,
243+
Log: nil,
244+
}
245+
246+
owner := ce.Name
247+
ownerRefs := []metav1.OwnerReference{*metav1.NewControllerRef(ce, ce.GetObjectKind().GroupVersionKind())}
248+
ownerRefSecretClient := helmclient.NewOwnerRefSecretClient(secretClient, ownerRefs, func(secret *corev1.Secret) bool {
249+
return secret.Type == storage.SecretTypeChunkedIndex
250+
})
251+
252+
return helm.Storage{
253+
Driver: storage.NewChunkedSecrets(ownerRefSecretClient, owner, csConfig),
254+
MaxHistory: 0,
255+
Log: log.Printf,
256+
}
257+
}
258+
259+
type openshiftRelease struct {
260+
version *semver.Version
261+
mu sync.Mutex
262+
}
263+
264+
var (
265+
currentRelease = &openshiftRelease{}
266+
)
267+
268+
const (
269+
releaseEnvVar = "RELEASE_VERSION" // OpenShift's env variable for defining the current release
270+
)
271+
272+
func getCurrentOpenShiftVersion() (*semver.Version, error) {
273+
currentRelease.mu.Lock()
274+
defer currentRelease.mu.Unlock()
275+
276+
if currentRelease.version != nil {
277+
/*
278+
If the version is already set, we don't want to set it again as the currentRelease
279+
is designed to be a singleton. If a new version is set, we are making an assumption
280+
that this controller will be restarted and thus pull in the new version from the
281+
environment into memory.
282+
283+
Note: sync.Once is not used here as it was difficult to reliably test without hitting
284+
race conditions.
285+
*/
286+
return currentRelease.version, nil
287+
}
288+
289+
// Get the raw version from the releaseEnvVar environment variable
290+
raw, ok := os.LookupEnv(releaseEnvVar)
291+
if !ok || raw == "" {
292+
// No env var set, try again later
293+
return nil, fmt.Errorf("desired release version missing from %v env variable", releaseEnvVar)
294+
}
295+
296+
release, err := semver.ParseTolerant(raw)
297+
if err != nil {
298+
return nil, fmt.Errorf("cluster version has invalid desired release version: %w", err)
299+
}
300+
301+
currentRelease.version = &release
302+
303+
return currentRelease.version, nil
304+
}
305+
306+
func nextY(v semver.Version) (semver.Version, error) {
307+
v.Build = nil // Builds are irrelevant
308+
309+
if len(v.Pre) > 0 {
310+
// Dropping pre-releases is equivalent to incrementing Y
311+
v.Pre = nil
312+
v.Patch = 0
313+
314+
return v, nil
315+
}
316+
317+
return v, v.IncrementMinor() // Sets Y=Y+1 and Z=0
318+
}
319+
320+
func toSemver(max string) (*semver.Version, error) {
321+
value := strings.Trim(max, "\"")
322+
if value == "" {
323+
// Handle "" separately, so parse doesn't treat it as a zero
324+
return nil, fmt.Errorf(`value cannot be "" (an empty string)`)
325+
}
326+
327+
version, err := semver.ParseTolerant(value)
328+
if err != nil {
329+
return nil, fmt.Errorf(`failed to parse "%q" as semver: %w`, value, err)
330+
}
331+
332+
truncatedVersion := semver.Version{Major: version.Major, Minor: version.Minor}
333+
if !version.EQ(truncatedVersion) {
334+
return nil, fmt.Errorf("property %s must specify only <major>.<minor> version, got invalid value %s", MaxOpenShiftVersionProperty, version)
335+
}
336+
return &truncatedVersion, nil
337+
}

0 commit comments

Comments
 (0)