github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/operators/openshift/helpers_test.go (about)

     1  package openshift
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	semver "github.com/blang/semver/v4"
     9  	configv1 "github.com/openshift/api/config/v1"
    10  	"github.com/stretchr/testify/require"
    11  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  	"k8s.io/apimachinery/pkg/labels"
    13  	"k8s.io/apimachinery/pkg/runtime"
    14  	"sigs.k8s.io/controller-runtime/pkg/client"
    15  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    16  
    17  	operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
    18  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/projection"
    19  	"github.com/operator-framework/operator-registry/pkg/api"
    20  )
    21  
    22  func TestConditionsEqual(t *testing.T) {
    23  	type args struct {
    24  		a, b *configv1.ClusterOperatorStatusCondition
    25  	}
    26  	for _, tt := range []struct {
    27  		description string
    28  		args        args
    29  		expect      bool
    30  	}{
    31  		{
    32  			description: "Nil/Both",
    33  			expect:      true,
    34  		},
    35  		{
    36  			description: "Nil/A",
    37  			args: args{
    38  				b: &configv1.ClusterOperatorStatusCondition{},
    39  			},
    40  			expect: false,
    41  		},
    42  		{
    43  			description: "Nil/B",
    44  			args: args{
    45  				a: &configv1.ClusterOperatorStatusCondition{},
    46  			},
    47  			expect: false,
    48  		},
    49  		{
    50  			description: "Same",
    51  			args: args{
    52  				a: &configv1.ClusterOperatorStatusCondition{},
    53  				b: &configv1.ClusterOperatorStatusCondition{},
    54  			},
    55  			expect: true,
    56  		},
    57  		{
    58  			description: "Different/LastTransitionTime",
    59  			args: args{
    60  				a: &configv1.ClusterOperatorStatusCondition{
    61  					LastTransitionTime: metav1.Now(),
    62  				},
    63  				b: &configv1.ClusterOperatorStatusCondition{},
    64  			},
    65  			expect: true,
    66  		},
    67  		{
    68  			description: "Different/Status",
    69  			args: args{
    70  				a: &configv1.ClusterOperatorStatusCondition{
    71  					Status: configv1.ConditionTrue,
    72  				},
    73  				b: &configv1.ClusterOperatorStatusCondition{},
    74  			},
    75  			expect: false,
    76  		},
    77  	} {
    78  		t.Run(tt.description, func(t *testing.T) {
    79  			require.Equal(t, tt.expect, conditionsEqual(tt.args.a, tt.args.b))
    80  		})
    81  	}
    82  }
    83  
    84  func TestVersionsMatch(t *testing.T) {
    85  	type in struct {
    86  		a, b []configv1.OperandVersion
    87  	}
    88  	for _, tt := range []struct {
    89  		description string
    90  		in          in
    91  		expect      bool
    92  	}{
    93  		{
    94  			description: "Different/Nil",
    95  			in: in{
    96  				a: []configv1.OperandVersion{
    97  					{Name: "weyland", Version: "1.0.0"},
    98  				},
    99  				b: nil,
   100  			},
   101  			expect: false,
   102  		},
   103  		{
   104  			description: "Different/Names",
   105  			in: in{
   106  				a: []configv1.OperandVersion{
   107  					{Name: "weyland", Version: "1.0.0"},
   108  				},
   109  				b: []configv1.OperandVersion{
   110  					{Name: "yutani", Version: "1.0.0"},
   111  				},
   112  			},
   113  			expect: false,
   114  		},
   115  		{
   116  			description: "Different/Versions",
   117  			in: in{
   118  				a: []configv1.OperandVersion{
   119  					{Name: "weyland", Version: "1.0.0"},
   120  				},
   121  				b: []configv1.OperandVersion{
   122  					{Name: "weyland", Version: "2.0.0"},
   123  				},
   124  			},
   125  			expect: false,
   126  		},
   127  		{
   128  			description: "Different/Lengths",
   129  			in: in{
   130  				a: []configv1.OperandVersion{
   131  					{Name: "weyland", Version: "1.0.0"},
   132  				},
   133  				b: []configv1.OperandVersion{
   134  					{Name: "weyland", Version: "1.0.0"},
   135  					{Name: "yutani", Version: "1.0.0"},
   136  				},
   137  			},
   138  			expect: false,
   139  		},
   140  		{
   141  			description: "Different/Elements",
   142  			in: in{
   143  				a: []configv1.OperandVersion{
   144  					{Name: "weyland", Version: "1.0.0"},
   145  					{Name: "weyland", Version: "1.0.0"},
   146  					{Name: "weyland", Version: "1.0.0"},
   147  					{Name: "yutani", Version: "1.0.0"},
   148  				},
   149  				b: []configv1.OperandVersion{
   150  					{Name: "weyland", Version: "1.0.0"},
   151  					{Name: "weyland", Version: "1.0.0"},
   152  					{Name: "yutani", Version: "1.0.0"},
   153  					{Name: "yutani", Version: "1.0.0"},
   154  				},
   155  			},
   156  			expect: false,
   157  		},
   158  		{
   159  			description: "Same/Nil",
   160  			in: in{
   161  				a: nil,
   162  				b: nil,
   163  			},
   164  			expect: true,
   165  		},
   166  		{
   167  			description: "Same/Empty",
   168  			in: in{
   169  				a: []configv1.OperandVersion{},
   170  				b: []configv1.OperandVersion{},
   171  			},
   172  			expect: true,
   173  		},
   174  		{
   175  			description: "Same/Empty/Nil",
   176  			in: in{
   177  				a: []configv1.OperandVersion{},
   178  				b: nil,
   179  			},
   180  			expect: true,
   181  		},
   182  		{
   183  			description: "Same",
   184  			in: in{
   185  				a: []configv1.OperandVersion{
   186  					{Name: "weyland", Version: "1.0.0"},
   187  					{Name: "yutani", Version: "1.0.0"},
   188  				},
   189  				b: []configv1.OperandVersion{
   190  					{Name: "weyland", Version: "1.0.0"},
   191  					{Name: "yutani", Version: "1.0.0"},
   192  				},
   193  			},
   194  			expect: true,
   195  		},
   196  		{
   197  			description: "Same/Unordered",
   198  			in: in{
   199  				a: []configv1.OperandVersion{
   200  					{Name: "weyland", Version: "1.0.0"},
   201  					{Name: "yutani", Version: "1.0.0"},
   202  				},
   203  				b: []configv1.OperandVersion{
   204  					{Name: "yutani", Version: "1.0.0"},
   205  					{Name: "weyland", Version: "1.0.0"},
   206  				},
   207  			},
   208  			expect: true,
   209  		},
   210  	} {
   211  		t.Run(tt.description, func(t *testing.T) {
   212  			require.Equal(t, tt.expect, versionsMatch(tt.in.a, tt.in.b))
   213  		})
   214  	}
   215  }
   216  
   217  func TestIncompatibleOperators(t *testing.T) {
   218  	type expect struct {
   219  		err          bool
   220  		incompatible skews
   221  	}
   222  	for _, tt := range []struct {
   223  		description string
   224  		version     string
   225  		in          skews
   226  		expect      expect
   227  	}{
   228  		{
   229  			description: "Compatible",
   230  			version:     "1.0.0",
   231  			in: skews{
   232  				{
   233  					name:                "almond",
   234  					namespace:           "default",
   235  					maxOpenShiftVersion: "1.1.0",
   236  				},
   237  				{
   238  					name:                "beech",
   239  					namespace:           "default",
   240  					maxOpenShiftVersion: "1.1.0+build",
   241  				},
   242  				{
   243  					name:                "chestnut",
   244  					namespace:           "default",
   245  					maxOpenShiftVersion: "2.0.0",
   246  				},
   247  			},
   248  			expect: expect{
   249  				err:          false,
   250  				incompatible: nil,
   251  			},
   252  		},
   253  		{
   254  			description: "Incompatible",
   255  			version:     "1.0.0",
   256  			in: skews{
   257  				{
   258  					name:                "almond",
   259  					namespace:           "default",
   260  					maxOpenShiftVersion: "1.0.0",
   261  				},
   262  				{
   263  					name:                "beech",
   264  					namespace:           "default",
   265  					maxOpenShiftVersion: "1.0.0+build",
   266  				},
   267  				{
   268  					name:                "chestnut",
   269  					namespace:           "default",
   270  					maxOpenShiftVersion: "1.1.0-pre",
   271  				},
   272  				{
   273  					name:                "drupe",
   274  					namespace:           "default",
   275  					maxOpenShiftVersion: "1.1.0-pre+build",
   276  				},
   277  				{
   278  					name:                "european-hazelnut",
   279  					namespace:           "default",
   280  					maxOpenShiftVersion: "0.1.0",
   281  				},
   282  			},
   283  			expect: expect{
   284  				err: false,
   285  				incompatible: skews{
   286  					{
   287  						name:                "almond",
   288  						namespace:           "default",
   289  						maxOpenShiftVersion: "1.0",
   290  					},
   291  					{
   292  						name:                "beech",
   293  						namespace:           "default",
   294  						maxOpenShiftVersion: "1.0",
   295  					},
   296  					{
   297  						name:      "chestnut",
   298  						namespace: "default",
   299  						err:       fmt.Errorf("property olm.maxOpenShiftVersion must specify only <major>.<minor> version, got invalid value 1.1.0-pre"),
   300  					},
   301  					{
   302  						name:      "drupe",
   303  						namespace: "default",
   304  						err:       fmt.Errorf("property olm.maxOpenShiftVersion must specify only <major>.<minor> version, got invalid value 1.1.0-pre+build"),
   305  					},
   306  					{
   307  						name:                "european-hazelnut",
   308  						namespace:           "default",
   309  						maxOpenShiftVersion: "0.1",
   310  					},
   311  				},
   312  			},
   313  		},
   314  		{
   315  			description: "Mixed",
   316  			version:     "1.0.0",
   317  			in: skews{
   318  				{
   319  					name:                "almond",
   320  					namespace:           "default",
   321  					maxOpenShiftVersion: "1.1.0",
   322  				},
   323  				{
   324  					name:                "beech",
   325  					namespace:           "default",
   326  					maxOpenShiftVersion: "1.0.0",
   327  				},
   328  				{
   329  					name:                "chestnut",
   330  					namespace:           "default",
   331  					maxOpenShiftVersion: "1.0",
   332  				},
   333  			},
   334  			expect: expect{
   335  				err: false,
   336  				incompatible: skews{
   337  					{
   338  						name:                "beech",
   339  						namespace:           "default",
   340  						maxOpenShiftVersion: "1.0",
   341  					},
   342  					{
   343  						name:                "chestnut",
   344  						namespace:           "default",
   345  						maxOpenShiftVersion: "1.0",
   346  					},
   347  				},
   348  			},
   349  		},
   350  		{
   351  			description: "Mixed/BadVersion",
   352  			version:     "1.0.0",
   353  			in: skews{
   354  				{
   355  					name:                "almond",
   356  					namespace:           "default",
   357  					maxOpenShiftVersion: "1.1.0",
   358  				},
   359  				{
   360  					name:                "beech",
   361  					namespace:           "default",
   362  					maxOpenShiftVersion: "1.0.0",
   363  				},
   364  				{
   365  					name:                "chestnut",
   366  					namespace:           "default",
   367  					maxOpenShiftVersion: "bad_version",
   368  				},
   369  			},
   370  			expect: expect{
   371  				err: false,
   372  				incompatible: skews{
   373  					{
   374  						name:                "beech",
   375  						namespace:           "default",
   376  						maxOpenShiftVersion: "1.0",
   377  					},
   378  					{
   379  						name:      "chestnut",
   380  						namespace: "default",
   381  						err: fmt.Errorf(`failed to parse "bad_version" as semver: %w`, func() error {
   382  							_, err := semver.ParseTolerant("bad_version")
   383  							return err
   384  						}()),
   385  					},
   386  				},
   387  			},
   388  		},
   389  		{
   390  			description: "EmptyVersion",
   391  			version:     "", // This should result in an transient error
   392  			in: skews{
   393  				{
   394  					name:                "almond",
   395  					namespace:           "default",
   396  					maxOpenShiftVersion: "1.1.0",
   397  				},
   398  				{
   399  					name:                "beech",
   400  					namespace:           "default",
   401  					maxOpenShiftVersion: "1.0.0",
   402  				},
   403  			},
   404  			expect: expect{
   405  				err:          true,
   406  				incompatible: nil,
   407  			},
   408  		},
   409  		{
   410  			description: "ClusterZ",
   411  			version:     "1.0.1", // Next Y-stream is 1.1.0, NOT 1.1.1
   412  			in: skews{
   413  				{
   414  					name:                "beech",
   415  					namespace:           "default",
   416  					maxOpenShiftVersion: "1.1",
   417  				},
   418  			},
   419  			expect: expect{
   420  				err:          false,
   421  				incompatible: nil,
   422  			},
   423  		},
   424  		{
   425  			description: "ClusterPre",
   426  			version:     "1.1.0-pre", // Next Y-stream is 1.2.0
   427  			in: skews{
   428  				{
   429  					name:                "almond",
   430  					namespace:           "default",
   431  					maxOpenShiftVersion: "1.1.0",
   432  				},
   433  			},
   434  			expect: expect{
   435  				err: false,
   436  				incompatible: skews{
   437  					{
   438  						name:                "almond",
   439  						namespace:           "default",
   440  						maxOpenShiftVersion: "1.1",
   441  					},
   442  				},
   443  			},
   444  		},
   445  	} {
   446  		t.Run(tt.description, func(t *testing.T) {
   447  			objs := []client.Object{}
   448  
   449  			resetCurrentReleaseTo(tt.version)
   450  
   451  			for _, s := range tt.in {
   452  				csv := &operatorsv1alpha1.ClusterServiceVersion{}
   453  				csv.SetName(s.name)
   454  				csv.SetNamespace(s.namespace)
   455  
   456  				maxProperty := &api.Property{
   457  					Type:  MaxOpenShiftVersionProperty,
   458  					Value: `"` + s.maxOpenShiftVersion + `"`, // Wrap in quotes so we don't break property marshaling
   459  				}
   460  				value, err := projection.PropertiesAnnotationFromPropertyList([]*api.Property{maxProperty})
   461  				require.NoError(t, err)
   462  
   463  				csv.SetAnnotations(map[string]string{
   464  					projection.PropertiesAnnotationKey: value,
   465  				})
   466  
   467  				objs = append(objs, csv)
   468  			}
   469  
   470  			scheme := runtime.NewScheme()
   471  			require.NoError(t, AddToScheme(scheme))
   472  
   473  			fcli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build()
   474  			incompatible, err := incompatibleOperators(context.Background(), fcli)
   475  			if tt.expect.err {
   476  				require.Error(t, err)
   477  			} else {
   478  				require.NoError(t, err)
   479  			}
   480  
   481  			require.ElementsMatch(t, tt.expect.incompatible, incompatible)
   482  		})
   483  	}
   484  }
   485  
   486  func TestMaxOpenShiftVersion(t *testing.T) {
   487  	mustParse := func(s string) *semver.Version {
   488  		version, err := semver.ParseTolerant(s)
   489  		if err != nil {
   490  			panic(fmt.Sprintf("bad version given for test case: %s", err))
   491  		}
   492  		return &version
   493  	}
   494  
   495  	type expect struct {
   496  		err bool
   497  		max *semver.Version
   498  	}
   499  	for _, tt := range []struct {
   500  		description string
   501  		in          []string
   502  		expect      expect
   503  	}{
   504  		{
   505  			description: "None",
   506  			expect: expect{
   507  				err: false,
   508  				max: nil,
   509  			},
   510  		},
   511  		{
   512  			description: "Nothing",
   513  			in:          []string{`""`},
   514  			expect: expect{
   515  				err: true,
   516  				max: nil,
   517  			},
   518  		},
   519  		{
   520  			description: "Garbage",
   521  			in:          []string{`"bad_version"`},
   522  			expect: expect{
   523  				err: true,
   524  				max: nil,
   525  			},
   526  		},
   527  		{
   528  			description: "Single",
   529  			in:          []string{`"1.0.0"`},
   530  			expect: expect{
   531  				err: false,
   532  				max: mustParse("1.0.0"),
   533  			},
   534  		},
   535  		{
   536  			description: "Multiple",
   537  			in: []string{
   538  				`"1.0.0"`,
   539  				`"2.0.0"`,
   540  			},
   541  			expect: expect{
   542  				err: true,
   543  				max: nil,
   544  			},
   545  		},
   546  		{
   547  			// Ensure unquoted short strings are accepted; e.g. X.Y
   548  			description: "Unquoted/Short",
   549  			in:          []string{"4.8"},
   550  			expect: expect{
   551  				err: false,
   552  				max: mustParse("4.8"),
   553  			},
   554  		},
   555  	} {
   556  		t.Run(tt.description, func(t *testing.T) {
   557  			var properties []*api.Property
   558  			for _, max := range tt.in {
   559  				properties = append(properties, &api.Property{
   560  					Type:  MaxOpenShiftVersionProperty,
   561  					Value: max,
   562  				})
   563  			}
   564  
   565  			value, err := projection.PropertiesAnnotationFromPropertyList(properties)
   566  			require.NoError(t, err)
   567  
   568  			csv := &operatorsv1alpha1.ClusterServiceVersion{}
   569  			csv.SetAnnotations(map[string]string{
   570  				projection.PropertiesAnnotationKey: value,
   571  			})
   572  
   573  			max, err := maxOpenShiftVersion(csv)
   574  			if tt.expect.err {
   575  				require.Error(t, err)
   576  			} else {
   577  				require.NoError(t, err)
   578  			}
   579  
   580  			require.Equal(t, tt.expect.max, max)
   581  		})
   582  	}
   583  }
   584  
   585  func TestNotCopiedSelector(t *testing.T) {
   586  	for _, tc := range []struct {
   587  		Labels  labels.Set
   588  		Matches bool
   589  	}{
   590  		{
   591  			Labels:  labels.Set{operatorsv1alpha1.CopiedLabelKey: ""},
   592  			Matches: false,
   593  		},
   594  		{
   595  			Labels:  labels.Set{},
   596  			Matches: true,
   597  		},
   598  	} {
   599  		t.Run(tc.Labels.String(), func(t *testing.T) {
   600  			selector, err := notCopiedSelector()
   601  			require.NoError(t, err)
   602  			require.Equal(t, tc.Matches, selector.Matches(tc.Labels))
   603  		})
   604  	}
   605  }
   606  
   607  func TestOCPVersionNextY(t *testing.T) {
   608  	for _, tc := range []struct {
   609  		description     string
   610  		inVersion       semver.Version
   611  		expectedVersion semver.Version
   612  	}{
   613  		{
   614  			description:     "Version: 4.16.0. Expected output: 4.17",
   615  			inVersion:       semver.MustParse("4.16.0"),
   616  			expectedVersion: semver.MustParse("4.17.0"),
   617  		},
   618  		{
   619  			description:     "Version: 4.16.0-rc1. Expected output: 4.17",
   620  			inVersion:       semver.MustParse("4.16.0-rc1"),
   621  			expectedVersion: semver.MustParse("4.17.0"),
   622  		},
   623  	} {
   624  		t.Run(tc.description, func(t *testing.T) {
   625  			outVersion := nextY(tc.inVersion)
   626  			require.Equal(t, outVersion, tc.expectedVersion)
   627  		})
   628  	}
   629  }