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  }