github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/operators/openshift/helpers.go (about) 1 package openshift 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "strings" 9 "sync" 10 11 semver "github.com/blang/semver/v4" 12 configv1 "github.com/openshift/api/config/v1" 13 "k8s.io/apimachinery/pkg/labels" 14 "k8s.io/apimachinery/pkg/selection" 15 utilerrors "k8s.io/apimachinery/pkg/util/errors" 16 "sigs.k8s.io/controller-runtime/pkg/client" 17 "sigs.k8s.io/controller-runtime/pkg/predicate" 18 19 operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" 20 "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/projection" 21 ) 22 23 func stripObject(obj client.Object) { 24 if obj == nil { 25 return 26 } 27 28 obj.SetResourceVersion("") 29 obj.SetUID("") 30 } 31 32 func watchName(name *string) predicate.Funcs { 33 return predicate.NewPredicateFuncs(func(object client.Object) bool { 34 return object.GetName() == *name 35 }) 36 } 37 38 func conditionsEqual(a, b *configv1.ClusterOperatorStatusCondition) bool { 39 if a == b { 40 return true 41 } 42 43 if a == nil || b == nil { 44 return false 45 } 46 47 return a.Type == b.Type && a.Status == b.Status && a.Message == b.Message && a.Reason == b.Reason 48 } 49 50 func versionsMatch(a []configv1.OperandVersion, b []configv1.OperandVersion) bool { 51 if len(a) != len(b) { 52 return false 53 } 54 55 counts := map[configv1.OperandVersion]int{} 56 for _, av := range a { 57 counts[av]++ 58 } 59 60 for _, bv := range b { 61 remaining, ok := counts[bv] 62 if !ok { 63 return false 64 } 65 66 if remaining == 1 { 67 delete(counts, bv) 68 continue 69 } 70 71 counts[bv]-- 72 } 73 74 return len(counts) < 1 75 } 76 77 type skews []skew 78 79 func (s skews) String() string { 80 msg := make([]string, len(s)) 81 i, j := 0, len(s)-1 82 for _, sk := range s { 83 m := sk.String() 84 // Partial order: error skews first 85 if sk.err != nil { 86 msg[i] = m 87 i++ 88 continue 89 } 90 msg[j] = m 91 j-- 92 } 93 94 // it is safe to ignore the error here, with the assumption 95 // that we build each skew object only after verifying that the 96 // version string is parseable safely. 97 maxOCPVersion, _ := semver.ParseTolerant(s[0].maxOpenShiftVersion) 98 nextY := nextY(maxOCPVersion).String() 99 return fmt.Sprintf("ClusterServiceVersions blocking minor version upgrades to %s or higher:\n%s", nextY, strings.Join(msg, "\n")) 100 } 101 102 type skew struct { 103 namespace string 104 name string 105 maxOpenShiftVersion string 106 err error 107 } 108 109 func (s skew) String() string { 110 if s.err != nil { 111 return fmt.Sprintf("- %s/%s has invalid %s properties: %s", s.namespace, s.name, MaxOpenShiftVersionProperty, s.err) 112 } 113 return fmt.Sprintf("- maximum supported OCP version for %s/%s is %s", s.namespace, s.name, s.maxOpenShiftVersion) 114 } 115 116 type transientError struct { 117 error 118 } 119 120 // transientErrors returns the result of stripping all wrapped errors not of type transientError from the given error. 121 func transientErrors(err error) error { 122 return utilerrors.FilterOut(err, func(e error) bool { 123 return !errors.As(e, new(transientError)) 124 }) 125 } 126 127 func incompatibleOperators(ctx context.Context, cli client.Client) (skews, error) { 128 current, err := getCurrentRelease() 129 if err != nil { 130 return nil, err 131 } 132 133 if current == nil { 134 // Note: This shouldn't happen 135 return nil, fmt.Errorf("failed to determine current OpenShift Y-stream release") 136 } 137 138 csvList := &operatorsv1alpha1.ClusterServiceVersionList{} 139 if err := cli.List(ctx, csvList); err != nil { 140 return nil, &transientError{fmt.Errorf("failed to list ClusterServiceVersions: %w", err)} 141 } 142 143 var incompatible skews 144 for _, csv := range csvList.Items { 145 if csv.IsCopied() { 146 continue 147 } 148 149 s := skew{ 150 name: csv.GetName(), 151 namespace: csv.GetNamespace(), 152 } 153 max, err := maxOpenShiftVersion(&csv) 154 if err != nil { 155 s.err = err 156 incompatible = append(incompatible, s) 157 continue 158 } 159 160 if max == nil || max.GTE(nextY(*current)) { 161 continue 162 } 163 164 s.maxOpenShiftVersion = fmt.Sprintf("%d.%d", max.Major, max.Minor) 165 166 incompatible = append(incompatible, s) 167 } 168 169 return incompatible, nil 170 } 171 172 type openshiftRelease struct { 173 version *semver.Version 174 mu sync.Mutex 175 } 176 177 var ( 178 currentRelease = &openshiftRelease{} 179 ) 180 181 const ( 182 releaseEnvVar = "RELEASE_VERSION" // OpenShift's env variable for defining the current release 183 ) 184 185 // getCurrentRelease thread safely retrieves the current version of OCP at the time of this operator starting. 186 // This is defined by an environment variable that our release manifests define (and get dynamically updated) 187 // by OCP. For the purposes of this package, that environment variable is a constant under the name of releaseEnvVar. 188 // 189 // Note: currentRelease is designed to be a singleton that only gets updated the first time that this function 190 // is called. As a result, all calls to this will return the same value even if the releaseEnvVar gets 191 // changed during runtime somehow. 192 func getCurrentRelease() (*semver.Version, error) { 193 currentRelease.mu.Lock() 194 defer currentRelease.mu.Unlock() 195 196 if currentRelease.version != nil { 197 /* 198 If the version is already set, we don't want to set it again as the currentRelease 199 is designed to be a singleton. If a new version is set, we are making an assumption 200 that this controller will be restarted and thus pull in the new version from the 201 environment into memory. 202 203 Note: sync.Once is not used here as it was difficult to reliably test without hitting 204 race conditions. 205 */ 206 return currentRelease.version, nil 207 } 208 209 // Get the raw version from the releaseEnvVar environment variable 210 raw, ok := os.LookupEnv(releaseEnvVar) 211 if !ok || raw == "" { 212 // No env var set, try again later 213 return nil, fmt.Errorf("desired release version missing from %v env variable", releaseEnvVar) 214 } 215 216 release, err := semver.ParseTolerant(raw) 217 if err != nil { 218 return nil, fmt.Errorf("cluster version has invalid desired release version: %w", err) 219 } 220 221 currentRelease.version = &release 222 223 return currentRelease.version, nil 224 } 225 226 func nextY(v semver.Version) semver.Version { 227 return semver.Version{Major: v.Major, Minor: v.Minor + 1} // Sets Y=Y+1 228 } 229 230 const ( 231 MaxOpenShiftVersionProperty = "olm.maxOpenShiftVersion" 232 ) 233 234 func maxOpenShiftVersion(csv *operatorsv1alpha1.ClusterServiceVersion) (*semver.Version, error) { 235 // Extract the property from the CSV's annotations if possible 236 annotation, ok := csv.GetAnnotations()[projection.PropertiesAnnotationKey] 237 if !ok { 238 return nil, nil 239 } 240 241 properties, err := projection.PropertyListFromPropertiesAnnotation(annotation) 242 if err != nil { 243 return nil, err 244 } 245 246 var max *string 247 for _, property := range properties { 248 if property.Type != MaxOpenShiftVersionProperty { 249 continue 250 } 251 252 if max != nil { 253 return nil, fmt.Errorf(`defining more than one "%s" property is not allowed`, MaxOpenShiftVersionProperty) 254 } 255 256 max = &property.Value 257 } 258 259 if max == nil { 260 return nil, nil 261 } 262 263 // Account for any additional quoting 264 value := strings.Trim(*max, "\"") 265 if value == "" { 266 // Handle "" separately, so parse doesn't treat it as a zero 267 return nil, fmt.Errorf(`value cannot be "" (an empty string)`) 268 } 269 270 version, err := semver.ParseTolerant(value) 271 if err != nil { 272 return nil, fmt.Errorf(`failed to parse "%s" as semver: %w`, value, err) 273 } 274 275 truncatedVersion := semver.Version{Major: version.Major, Minor: version.Minor} 276 if !version.EQ(truncatedVersion) { 277 return nil, fmt.Errorf("property %s must specify only <major>.<minor> version, got invalid value %s", MaxOpenShiftVersionProperty, version) 278 } 279 return &truncatedVersion, nil 280 } 281 282 func notCopiedSelector() (labels.Selector, error) { 283 requirement, err := labels.NewRequirement(operatorsv1alpha1.CopiedLabelKey, selection.DoesNotExist, nil) 284 if err != nil { 285 return nil, err 286 } 287 return labels.NewSelector().Add(*requirement), nil 288 } 289 290 func olmOperatorRelatedObjects(ctx context.Context, cli client.Client, namespace string) ([]configv1.ObjectReference, error) { 291 selector, err := notCopiedSelector() 292 if err != nil { 293 return nil, err 294 } 295 296 csvList := &operatorsv1alpha1.ClusterServiceVersionList{} 297 if err := cli.List(ctx, csvList, client.InNamespace(namespace), client.MatchingLabelsSelector{Selector: selector}); err != nil { 298 return nil, err 299 } 300 301 var refs []configv1.ObjectReference 302 for _, csv := range csvList.Items { 303 if csv.IsCopied() { 304 // Filter out copied CSVs that the label selector missed 305 continue 306 } 307 308 // TODO: Generalize ObjectReference generation 309 refs = append(refs, configv1.ObjectReference{ 310 Group: operatorsv1alpha1.GroupName, 311 Resource: "clusterserviceversions", 312 Namespace: csv.GetNamespace(), 313 Name: csv.GetName(), 314 }) 315 } 316 317 return refs, nil 318 }