sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/upgrader_test.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cluster
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	. "github.com/onsi/gomega"
    25  
    26  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    27  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
    28  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
    29  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
    30  )
    31  
    32  func Test_providerUpgrader_Plan(t *testing.T) {
    33  	type fields struct {
    34  		reader     config.Reader
    35  		repository map[string]repository.Repository
    36  		proxy      Proxy
    37  	}
    38  	tests := []struct {
    39  		name    string
    40  		fields  fields
    41  		want    []UpgradePlan
    42  		wantErr bool
    43  	}{
    44  		{
    45  			name: "Upgrade within the current contract",
    46  			fields: fields{
    47  				// config for two providers
    48  				reader: test.NewFakeReader().
    49  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
    50  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
    51  				repository: map[string]repository.Repository{
    52  					"cluster-api": repository.NewMemoryRepository().
    53  						WithVersions("v1.0.0", "v1.0.1").
    54  						WithMetadata("v1.0.1", &clusterctlv1.Metadata{
    55  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
    56  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
    57  							},
    58  						}),
    59  					"infrastructure-infra": repository.NewMemoryRepository().
    60  						WithVersions("v2.0.0", "v2.0.1").
    61  						WithMetadata("v2.0.1", &clusterctlv1.Metadata{
    62  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
    63  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
    64  							},
    65  						}),
    66  				},
    67  				// two providers existing in the cluster
    68  				proxy: test.NewFakeProxy().
    69  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
    70  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
    71  			},
    72  			want: []UpgradePlan{
    73  				{ // one upgrade plan with the latest releases the current contract
    74  					Contract: test.CurrentCAPIContract,
    75  					Providers: []UpgradeItem{
    76  						{
    77  							Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
    78  							NextVersion: "v1.0.1",
    79  						},
    80  						{
    81  							Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
    82  							NextVersion: "v2.0.1",
    83  						},
    84  					},
    85  				},
    86  			},
    87  			wantErr: false,
    88  		},
    89  		{
    90  			name: "pre-releases should be ignored",
    91  			fields: fields{
    92  				// config for two providers
    93  				reader: test.NewFakeReader().
    94  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
    95  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
    96  				repository: map[string]repository.Repository{
    97  					"cluster-api": repository.NewMemoryRepository().
    98  						WithVersions("v1.0.0", "v1.0.1").
    99  						WithMetadata("v1.0.1", &clusterctlv1.Metadata{
   100  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   101  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   102  							},
   103  						}),
   104  					"infrastructure-infra": repository.NewMemoryRepository().
   105  						WithVersions("v2.0.0", "v2.0.1", "v3.0.0-alpha.0").
   106  						WithMetadata("v2.0.1", &clusterctlv1.Metadata{
   107  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   108  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   109  							},
   110  						}).
   111  						WithMetadata("v3.0.0-alpha.0", &clusterctlv1.Metadata{
   112  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   113  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   114  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   115  							},
   116  						}),
   117  				},
   118  				// two providers existing in the cluster
   119  				proxy: test.NewFakeProxy().
   120  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   121  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   122  			},
   123  			want: []UpgradePlan{
   124  				{ // one upgrade plan with the latest releases the current contract
   125  					Contract: test.CurrentCAPIContract,
   126  					Providers: []UpgradeItem{
   127  						{
   128  							Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   129  							NextVersion: "v1.0.1",
   130  						},
   131  						{
   132  							Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   133  							NextVersion: "v2.0.1",
   134  						},
   135  					},
   136  				},
   137  			},
   138  			wantErr: false,
   139  		},
   140  		{
   141  			name: "Upgrade for previous contract (not supported), current contract", // upgrade plan should report unsupported options
   142  			fields: fields{
   143  				// config for two providers
   144  				reader: test.NewFakeReader().
   145  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   146  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   147  				repository: map[string]repository.Repository{
   148  					"cluster-api": repository.NewMemoryRepository().
   149  						WithVersions("v1.0.0", "v1.0.1", "v2.0.0").
   150  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   151  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   152  								{Major: 1, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   153  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   154  							},
   155  						}),
   156  					"infrastructure-infra": repository.NewMemoryRepository().
   157  						WithVersions("v2.0.0", "v2.0.1", "v3.0.0").
   158  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   159  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   160  								{Major: 2, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   161  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   162  							},
   163  						}),
   164  				},
   165  				// two providers existing in the cluster
   166  				proxy: test.NewFakeProxy().
   167  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   168  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   169  			},
   170  			want: []UpgradePlan{
   171  				{ // one upgrade plan with the latest releases in the previous contract (not supported, but upgrade plan should report these options)
   172  					Contract: test.PreviousCAPIContractNotSupported,
   173  					Providers: []UpgradeItem{
   174  						{
   175  							Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   176  							NextVersion: "v1.0.1",
   177  						},
   178  						{
   179  							Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   180  							NextVersion: "v2.0.1",
   181  						},
   182  					},
   183  				},
   184  				{ // one upgrade plan with the latest releases in the current contract
   185  					Contract: test.CurrentCAPIContract,
   186  					Providers: []UpgradeItem{
   187  						{
   188  							Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   189  							NextVersion: "v2.0.0",
   190  						},
   191  						{
   192  							Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   193  							NextVersion: "v3.0.0",
   194  						},
   195  					},
   196  				},
   197  			},
   198  			wantErr: false,
   199  		},
   200  		{
   201  			name: "Upgrade for both current contract and next contract (not supported)", // upgrade plan should report unsupported options
   202  			fields: fields{
   203  				// config for two providers
   204  				reader: test.NewFakeReader().
   205  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   206  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   207  				repository: map[string]repository.Repository{
   208  					"cluster-api": repository.NewMemoryRepository().
   209  						WithVersions("v1.0.0", "v1.0.1", "v2.0.0").
   210  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   211  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   212  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   213  								{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   214  							},
   215  						}),
   216  					"infrastructure-infra": repository.NewMemoryRepository().
   217  						WithVersions("v2.0.0", "v2.0.1", "v3.0.0").
   218  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   219  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   220  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   221  								{Major: 3, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   222  							},
   223  						}),
   224  				},
   225  				// two providers existing in the cluster
   226  				proxy: test.NewFakeProxy().
   227  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   228  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   229  			},
   230  			want: []UpgradePlan{
   231  				{ // one upgrade plan with the latest releases in the current
   232  					Contract: test.CurrentCAPIContract,
   233  					Providers: []UpgradeItem{
   234  						{
   235  							Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   236  							NextVersion: "v1.0.1",
   237  						},
   238  						{
   239  							Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   240  							NextVersion: "v2.0.1",
   241  						},
   242  					},
   243  				},
   244  				{ // one upgrade plan with the latest releases in the next contract (not supported, but upgrade plan should report these options)
   245  					Contract: test.NextCAPIContractNotSupported,
   246  					Providers: []UpgradeItem{
   247  						{
   248  							Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   249  							NextVersion: "v2.0.0",
   250  						},
   251  						{
   252  							Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   253  							NextVersion: "v3.0.0",
   254  						},
   255  					},
   256  				},
   257  			},
   258  			wantErr: false,
   259  		},
   260  		{
   261  			name: "Partial upgrades for next contract", // upgrade plan should report unsupported options
   262  			fields: fields{
   263  				// config for two providers
   264  				reader: test.NewFakeReader().
   265  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   266  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   267  				repository: map[string]repository.Repository{
   268  					"cluster-api": repository.NewMemoryRepository().
   269  						WithVersions("v1.0.0", "v1.0.1", "v2.0.0").
   270  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   271  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   272  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   273  								{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   274  							},
   275  						}),
   276  					"infrastructure-infra": repository.NewMemoryRepository().
   277  						WithVersions("v2.0.0"). // no new releases available for the infra provider (only the current release exists)
   278  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   279  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   280  								{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   281  							},
   282  						}),
   283  				},
   284  				// two providers existing in the cluster
   285  				proxy: test.NewFakeProxy().
   286  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   287  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   288  			},
   289  			want: []UpgradePlan{
   290  				{ // one upgrade plan with the latest releases in the current contract
   291  					Contract: test.CurrentCAPIContract,
   292  					Providers: []UpgradeItem{
   293  						{
   294  							Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   295  							NextVersion: "v1.0.1",
   296  						},
   297  						{
   298  							Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   299  							NextVersion: "", // we are already to the latest version for the infra provider, but this is acceptable for the current contract
   300  						},
   301  					},
   302  				},
   303  				// the upgrade plan with the latest releases in the next contract should be dropped because all the provider are required to change the contract at the same time
   304  			},
   305  			wantErr: false,
   306  		},
   307  	}
   308  	for _, tt := range tests {
   309  		t.Run(tt.name, func(t *testing.T) {
   310  			g := NewWithT(t)
   311  
   312  			ctx := context.Background()
   313  
   314  			configClient, _ := config.New(ctx, "", config.InjectReader(tt.fields.reader))
   315  
   316  			u := &providerUpgrader{
   317  				configClient: configClient,
   318  				repositoryClientFactory: func(ctx context.Context, provider config.Provider, configClient config.Client, _ ...repository.Option) (repository.Client, error) {
   319  					return repository.New(ctx, provider, configClient, repository.InjectRepository(tt.fields.repository[provider.ManifestLabel()]))
   320  				},
   321  				providerInventory: newInventoryClient(tt.fields.proxy, nil),
   322  			}
   323  			got, err := u.Plan(ctx)
   324  			if tt.wantErr {
   325  				g.Expect(err).To(HaveOccurred())
   326  				return
   327  			}
   328  
   329  			g.Expect(err).ToNot(HaveOccurred())
   330  			g.Expect(got).To(BeComparableTo(tt.want), cmp.Diff(got, tt.want))
   331  		})
   332  	}
   333  }
   334  
   335  func Test_providerUpgrader_createCustomPlan(t *testing.T) {
   336  	type fields struct {
   337  		reader     config.Reader
   338  		repository map[string]repository.Repository
   339  		proxy      Proxy
   340  	}
   341  	type args struct {
   342  		coreProvider       clusterctlv1.Provider
   343  		providersToUpgrade []UpgradeItem
   344  	}
   345  	tests := []struct {
   346  		name    string
   347  		fields  fields
   348  		args    args
   349  		want    *UpgradePlan
   350  		wantErr bool
   351  	}{
   352  		{
   353  			name: "pass if upgrade infra provider, same contract",
   354  			fields: fields{
   355  				// config for two providers
   356  				reader: test.NewFakeReader().
   357  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   358  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   359  				repository: map[string]repository.Repository{
   360  					"cluster-api": repository.NewMemoryRepository().
   361  						WithVersions("v1.0.0", "v1.0.1").
   362  						WithMetadata("v1.0.1", &clusterctlv1.Metadata{
   363  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   364  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   365  							},
   366  						}),
   367  					"infra": repository.NewMemoryRepository().
   368  						WithVersions("v2.0.0", "v2.0.1").
   369  						WithMetadata("v2.0.1", &clusterctlv1.Metadata{
   370  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   371  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   372  							},
   373  						}),
   374  				},
   375  				// two providers existing in the cluster
   376  				proxy: test.NewFakeProxy().
   377  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   378  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   379  			},
   380  			args: args{
   381  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   382  				providersToUpgrade: []UpgradeItem{
   383  					{
   384  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   385  						NextVersion: "v2.0.1", // upgrade to next release in the current contract
   386  					},
   387  				},
   388  			},
   389  			want: &UpgradePlan{
   390  				Contract: test.CurrentCAPIContract,
   391  				Providers: []UpgradeItem{
   392  					{
   393  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   394  						NextVersion: "v2.0.1",
   395  					},
   396  				},
   397  			},
   398  			wantErr: false,
   399  		},
   400  		{
   401  			name: "pass if upgrade core provider, same contract",
   402  			fields: fields{
   403  				// config for two providers
   404  				reader: test.NewFakeReader().
   405  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   406  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   407  				repository: map[string]repository.Repository{
   408  					"cluster-api": repository.NewMemoryRepository().
   409  						WithVersions("v1.0.0", "v1.0.1").
   410  						WithMetadata("v1.0.1", &clusterctlv1.Metadata{
   411  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   412  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   413  							},
   414  						}),
   415  					"infra": repository.NewMemoryRepository().
   416  						WithVersions("v2.0.0", "v2.0.1").
   417  						WithMetadata("v2.0.1", &clusterctlv1.Metadata{
   418  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   419  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   420  							},
   421  						}),
   422  				},
   423  				// two providers existing in the cluster
   424  				proxy: test.NewFakeProxy().
   425  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   426  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   427  			},
   428  			args: args{
   429  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   430  				providersToUpgrade: []UpgradeItem{
   431  					{
   432  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   433  						NextVersion: "v1.0.1", // upgrade to next release in the current contract
   434  					},
   435  				},
   436  			},
   437  			want: &UpgradePlan{
   438  				Contract: test.CurrentCAPIContract,
   439  				Providers: []UpgradeItem{
   440  					{
   441  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   442  						NextVersion: "v1.0.1",
   443  					},
   444  				},
   445  			},
   446  			wantErr: false,
   447  		},
   448  		{
   449  			name: "pass if upgrade core and infra provider, same contract",
   450  			fields: fields{
   451  				// config for two providers
   452  				reader: test.NewFakeReader().
   453  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   454  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   455  				repository: map[string]repository.Repository{
   456  					"cluster-api": repository.NewMemoryRepository().
   457  						WithVersions("v1.0.0", "v2.0.0").
   458  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   459  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   460  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   461  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   462  							},
   463  						}),
   464  					"infra": repository.NewMemoryRepository().
   465  						WithVersions("v2.0.0", "v3.0.0").
   466  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   467  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   468  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   469  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   470  							},
   471  						}),
   472  				},
   473  				// two providers existing in the cluster
   474  				proxy: test.NewFakeProxy().
   475  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   476  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   477  			},
   478  			args: args{
   479  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   480  				providersToUpgrade: []UpgradeItem{
   481  					{
   482  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   483  						NextVersion: "v2.0.0", // upgrade to next release in the next contract; not supported in current clusterctl release.
   484  					},
   485  					{
   486  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   487  						NextVersion: "v3.0.0", // upgrade to next release in the next contract; not supported in current clusterctl release.
   488  					},
   489  				},
   490  			},
   491  			want: &UpgradePlan{
   492  				Contract: test.CurrentCAPIContract,
   493  				Providers: []UpgradeItem{
   494  					{
   495  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   496  						NextVersion: "v2.0.0",
   497  					},
   498  					{
   499  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   500  						NextVersion: "v3.0.0",
   501  					},
   502  				},
   503  			},
   504  			wantErr: false,
   505  		},
   506  		{
   507  			name: "fail if upgrade infra provider alone from current to the next contract", // not supported in current clusterctl release.
   508  			fields: fields{
   509  				// config for two providers
   510  				reader: test.NewFakeReader().
   511  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   512  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   513  				repository: map[string]repository.Repository{
   514  					"cluster-api": repository.NewMemoryRepository().
   515  						WithVersions("v1.0.0", "v2.0.0").
   516  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   517  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   518  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   519  								{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   520  							},
   521  						}),
   522  					"infra": repository.NewMemoryRepository().
   523  						WithVersions("v2.0.0", "v3.0.0").
   524  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   525  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   526  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   527  								{Major: 3, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   528  							},
   529  						}),
   530  				},
   531  				// two providers existing in the cluster
   532  				proxy: test.NewFakeProxy().
   533  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   534  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   535  			},
   536  			args: args{
   537  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   538  				providersToUpgrade: []UpgradeItem{
   539  					{
   540  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   541  						NextVersion: "v3.0.0", // upgrade to next release in the next contract; not supported in current clusterctl release.
   542  					},
   543  				},
   544  			},
   545  			want:    nil,
   546  			wantErr: true,
   547  		},
   548  		{
   549  			name: "fail if upgrade infra provider alone from previous to the current contract",
   550  			fields: fields{
   551  				// config for two providers
   552  				reader: test.NewFakeReader().
   553  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   554  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   555  				repository: map[string]repository.Repository{
   556  					"cluster-api": repository.NewMemoryRepository().
   557  						WithVersions("v1.0.0", "v2.0.0").
   558  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   559  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   560  								{Major: 1, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   561  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   562  							},
   563  						}),
   564  					"infra": repository.NewMemoryRepository().
   565  						WithVersions("v2.0.0", "v3.0.0").
   566  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   567  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   568  								{Major: 2, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   569  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   570  							},
   571  						}),
   572  				},
   573  				// two providers existing in the cluster
   574  				proxy: test.NewFakeProxy().
   575  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   576  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   577  			},
   578  			args: args{
   579  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   580  				providersToUpgrade: []UpgradeItem{
   581  					{
   582  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   583  						NextVersion: "v3.0.0", // upgrade to next release in the current contract.
   584  					},
   585  				},
   586  			},
   587  			want:    nil,
   588  			wantErr: true,
   589  		},
   590  		{
   591  			name: "fail if upgrade core provider alone from current to the next contract", // not supported in current clusterctl release.
   592  			fields: fields{
   593  				// config for two providers
   594  				reader: test.NewFakeReader().
   595  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   596  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   597  				repository: map[string]repository.Repository{
   598  					"cluster-api": repository.NewMemoryRepository().
   599  						WithVersions("v1.0.0", "v2.0.0").
   600  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   601  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   602  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   603  								{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   604  							},
   605  						}),
   606  					"infra": repository.NewMemoryRepository().
   607  						WithVersions("v2.0.0", "v3.0.0").
   608  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   609  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   610  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   611  								{Major: 3, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   612  							},
   613  						}),
   614  				},
   615  				// two providers existing in the cluster
   616  				proxy: test.NewFakeProxy().
   617  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   618  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   619  			},
   620  			args: args{
   621  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   622  				providersToUpgrade: []UpgradeItem{
   623  					{
   624  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   625  						NextVersion: "v2.0.0", // upgrade to next release in the next contract; not supported in current clusterctl release.
   626  					},
   627  				},
   628  			},
   629  			want:    nil,
   630  			wantErr: true,
   631  		},
   632  		{
   633  			name: "fail if upgrade core provider alone from previous to the current contract",
   634  			fields: fields{
   635  				// config for two providers
   636  				reader: test.NewFakeReader().
   637  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   638  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   639  				repository: map[string]repository.Repository{
   640  					"cluster-api": repository.NewMemoryRepository().
   641  						WithVersions("v1.0.0", "v2.0.0").
   642  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   643  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   644  								{Major: 1, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   645  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   646  							},
   647  						}),
   648  					"infra": repository.NewMemoryRepository().
   649  						WithVersions("v2.0.0", "v3.0.0").
   650  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   651  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   652  								{Major: 2, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   653  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   654  							},
   655  						}),
   656  				},
   657  				// two providers existing in the cluster
   658  				proxy: test.NewFakeProxy().
   659  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   660  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   661  			},
   662  			args: args{
   663  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   664  				providersToUpgrade: []UpgradeItem{
   665  					{
   666  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   667  						NextVersion: "v2.0.0", // upgrade to next release in the current contract
   668  					},
   669  				},
   670  			},
   671  			want:    nil,
   672  			wantErr: true,
   673  		},
   674  		{
   675  			name: "fail if upgrade core and infra provider to the next contract", // not supported in current clusterctl release
   676  			fields: fields{
   677  				// config for two providers
   678  				reader: test.NewFakeReader().
   679  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   680  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   681  				repository: map[string]repository.Repository{
   682  					"cluster-api": repository.NewMemoryRepository().
   683  						WithVersions("v1.0.0", "v2.0.0").
   684  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   685  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   686  								{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
   687  								{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   688  							},
   689  						}),
   690  					"infra": repository.NewMemoryRepository().
   691  						WithVersions("v2.0.0", "v3.0.0").
   692  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   693  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   694  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   695  								{Major: 3, Minor: 0, Contract: test.NextCAPIContractNotSupported},
   696  							},
   697  						}),
   698  				},
   699  				// two providers existing in the cluster
   700  				proxy: test.NewFakeProxy().
   701  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   702  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   703  			},
   704  			args: args{
   705  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   706  				providersToUpgrade: []UpgradeItem{
   707  					{
   708  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   709  						NextVersion: "v2.0.0", // upgrade to next release in the next contract; not supported in current clusterctl release.
   710  					},
   711  					{
   712  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   713  						NextVersion: "v3.0.0", // upgrade to next release in the next contract; not supported in current clusterctl release.
   714  					},
   715  				},
   716  			},
   717  			want:    nil,
   718  			wantErr: true,
   719  		},
   720  		{
   721  			name: "pass if upgrade core and infra provider from previous to current contract",
   722  			fields: fields{
   723  				// config for two providers
   724  				reader: test.NewFakeReader().
   725  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   726  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   727  				repository: map[string]repository.Repository{
   728  					"cluster-api": repository.NewMemoryRepository().
   729  						WithVersions("v1.0.0", "v2.0.0").
   730  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   731  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   732  								{Major: 1, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   733  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   734  							},
   735  						}),
   736  					"infra": repository.NewMemoryRepository().
   737  						WithVersions("v2.0.0", "v3.0.0").
   738  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   739  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   740  								{Major: 2, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   741  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   742  							},
   743  						}),
   744  				},
   745  				// two providers existing in the cluster
   746  				proxy: test.NewFakeProxy().
   747  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   748  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   749  			},
   750  			args: args{
   751  				coreProvider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "", "cluster-api-system"),
   752  				providersToUpgrade: []UpgradeItem{
   753  					{
   754  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   755  						NextVersion: "v2.0.0", // upgrade to next release in the next contract; not supported in current clusterctl release.
   756  					},
   757  					{
   758  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   759  						NextVersion: "v3.0.0", // upgrade to next release in the next contract; not supported in current clusterctl release.
   760  					},
   761  				},
   762  			},
   763  			want: &UpgradePlan{
   764  				Contract: test.CurrentCAPIContract,
   765  				Providers: []UpgradeItem{
   766  					{
   767  						Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   768  						NextVersion: "v2.0.0",
   769  					},
   770  					{
   771  						Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   772  						NextVersion: "v3.0.0",
   773  					},
   774  				},
   775  			},
   776  			wantErr: false,
   777  		},
   778  	}
   779  	for _, tt := range tests {
   780  		t.Run(tt.name, func(t *testing.T) {
   781  			g := NewWithT(t)
   782  
   783  			ctx := context.Background()
   784  
   785  			configClient, _ := config.New(ctx, "", config.InjectReader(tt.fields.reader))
   786  
   787  			u := &providerUpgrader{
   788  				configClient: configClient,
   789  				repositoryClientFactory: func(ctx context.Context, provider config.Provider, configClient config.Client, _ ...repository.Option) (repository.Client, error) {
   790  					return repository.New(ctx, provider, configClient, repository.InjectRepository(tt.fields.repository[provider.Name()]))
   791  				},
   792  				providerInventory: newInventoryClient(tt.fields.proxy, nil),
   793  			}
   794  			got, err := u.createCustomPlan(ctx, tt.args.providersToUpgrade)
   795  			if tt.wantErr {
   796  				g.Expect(err).To(HaveOccurred())
   797  				return
   798  			}
   799  
   800  			g.Expect(err).ToNot(HaveOccurred())
   801  			g.Expect(got).To(BeComparableTo(tt.want))
   802  		})
   803  	}
   804  }
   805  
   806  // TODO add tests  for success scenarios.
   807  func Test_providerUpgrader_ApplyPlan(t *testing.T) {
   808  	type fields struct {
   809  		reader     config.Reader
   810  		repository map[string]repository.Repository
   811  		proxy      Proxy
   812  	}
   813  
   814  	tests := []struct {
   815  		name     string
   816  		fields   fields
   817  		contract string
   818  		wantErr  bool
   819  		errorMsg string
   820  		opts     UpgradeOptions
   821  	}{
   822  		{
   823  			name: "fails to upgrade to current contract when there are multiple instances of the core provider",
   824  			fields: fields{
   825  				// config for two providers
   826  				reader: test.NewFakeReader().
   827  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   828  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   829  				// two provider repositories, with current v1alpha3 contract and new versions for v1alpha4 contract
   830  				repository: map[string]repository.Repository{
   831  					"cluster-api": repository.NewMemoryRepository().
   832  						WithVersions("v1.0.0", "v1.0.1", "v2.0.0").
   833  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   834  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   835  								{Major: 1, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   836  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   837  							},
   838  						}),
   839  					"infrastructure-infra": repository.NewMemoryRepository().
   840  						WithVersions("v2.0.0", "v2.0.1", "v3.0.0").
   841  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   842  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   843  								{Major: 2, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   844  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   845  							},
   846  						}),
   847  				},
   848  				// two providers with multiple instances existing in the cluster
   849  				proxy: test.NewFakeProxy().
   850  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   851  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system-1").
   852  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   853  			},
   854  			contract: test.CurrentCAPIContract,
   855  			wantErr:  true,
   856  			errorMsg: "detected multiple instances of the same provider",
   857  			opts:     UpgradeOptions{},
   858  		},
   859  		{
   860  			name: "fails to upgrade to current contract when there are multiple instances of the infra provider",
   861  			fields: fields{
   862  				// config for two providers
   863  				reader: test.NewFakeReader().
   864  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   865  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   866  				// two provider repositories, with current v1alpha3 contract and new versions for v1alpha4 contract
   867  				repository: map[string]repository.Repository{
   868  					"cluster-api": repository.NewMemoryRepository().
   869  						WithVersions("v1.0.0", "v1.0.1", "v2.0.0").
   870  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   871  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   872  								{Major: 1, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   873  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   874  							},
   875  						}),
   876  					"infrastructure-infra": repository.NewMemoryRepository().
   877  						WithVersions("v2.0.0", "v2.0.1", "v3.0.0").
   878  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   879  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   880  								{Major: 2, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   881  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   882  							},
   883  						}),
   884  				},
   885  				// two providers with multiple instances existing in the cluster
   886  				proxy: test.NewFakeProxy().
   887  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   888  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system").
   889  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system-1"),
   890  			},
   891  			contract: test.CurrentCAPIContract,
   892  			wantErr:  true,
   893  			errorMsg: "detected multiple instances of the same provider",
   894  			opts:     UpgradeOptions{},
   895  		},
   896  	}
   897  
   898  	for _, tt := range tests {
   899  		t.Run(tt.name, func(t *testing.T) {
   900  			g := NewWithT(t)
   901  
   902  			ctx := context.Background()
   903  
   904  			configClient, _ := config.New(ctx, "", config.InjectReader(tt.fields.reader))
   905  
   906  			u := &providerUpgrader{
   907  				configClient: configClient,
   908  				repositoryClientFactory: func(ctx context.Context, provider config.Provider, configClient config.Client, _ ...repository.Option) (repository.Client, error) {
   909  					return repository.New(ctx, provider, configClient, repository.InjectRepository(tt.fields.repository[provider.ManifestLabel()]))
   910  				},
   911  				providerInventory: newInventoryClient(tt.fields.proxy, nil),
   912  			}
   913  			err := u.ApplyPlan(ctx, tt.opts, tt.contract)
   914  			if tt.wantErr {
   915  				g.Expect(err).To(HaveOccurred())
   916  				g.Expect(err.Error()).Should(ContainSubstring(tt.errorMsg))
   917  				return
   918  			}
   919  
   920  			g.Expect(err).ToNot(HaveOccurred())
   921  		})
   922  	}
   923  }
   924  
   925  // TODO add tests  for success scenarios.
   926  func Test_providerUpgrader_ApplyCustomPlan(t *testing.T) {
   927  	type fields struct {
   928  		reader     config.Reader
   929  		repository map[string]repository.Repository
   930  		proxy      Proxy
   931  	}
   932  
   933  	tests := []struct {
   934  		name               string
   935  		fields             fields
   936  		providersToUpgrade []UpgradeItem
   937  		wantErr            bool
   938  		errorMsg           string
   939  		opts               UpgradeOptions
   940  	}{
   941  		{
   942  			name: "fails to upgrade to v1alpha4 when there are multiple instances of the core provider",
   943  			fields: fields{
   944  				// config for two providers
   945  				reader: test.NewFakeReader().
   946  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   947  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   948  				// two provider repositories, with current v1alpha3 contract and new versions for v1alpha4 contract
   949  				repository: map[string]repository.Repository{
   950  					"cluster-api": repository.NewMemoryRepository().
   951  						WithVersions("v1.0.0", "v1.0.1", "v2.0.0").
   952  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   953  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   954  								{Major: 1, Minor: 0, Contract: "v1alpha3"},
   955  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
   956  							},
   957  						}),
   958  					"infrastructure-infra": repository.NewMemoryRepository().
   959  						WithVersions("v2.0.0", "v2.0.1", "v3.0.0").
   960  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
   961  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
   962  								{Major: 2, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
   963  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
   964  							},
   965  						}),
   966  				},
   967  				// two providers with multiple instances existing in the cluster
   968  				proxy: test.NewFakeProxy().
   969  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
   970  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system-1").
   971  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   972  			},
   973  			providersToUpgrade: []UpgradeItem{
   974  				{
   975  					Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
   976  					NextVersion: "v2.0.0",
   977  				},
   978  				{
   979  					Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
   980  					NextVersion: "v3.0.0",
   981  				},
   982  			},
   983  			wantErr:  true,
   984  			errorMsg: "invalid management cluster: there should a core provider, found 2",
   985  			opts:     UpgradeOptions{},
   986  		},
   987  		{
   988  			name: "fails to upgrade to v1alpha4 when there are multiple instances of the infra provider",
   989  			fields: fields{
   990  				// config for two providers
   991  				reader: test.NewFakeReader().
   992  					WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
   993  					WithProvider("infra", clusterctlv1.InfrastructureProviderType, "https://somewhere.com"),
   994  				// two provider repositories, with current v1alpha3 contract and new versions for v1alpha4 contract
   995  				repository: map[string]repository.Repository{
   996  					"cluster-api": repository.NewMemoryRepository().
   997  						WithVersions("v1.0.0", "v1.0.1", "v2.0.0").
   998  						WithMetadata("v2.0.0", &clusterctlv1.Metadata{
   999  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
  1000  								{Major: 1, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
  1001  								{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
  1002  							},
  1003  						}),
  1004  					"infrastructure-infra": repository.NewMemoryRepository().
  1005  						WithVersions("v2.0.0", "v2.0.1", "v3.0.0").
  1006  						WithMetadata("v3.0.0", &clusterctlv1.Metadata{
  1007  							ReleaseSeries: []clusterctlv1.ReleaseSeries{
  1008  								{Major: 2, Minor: 0, Contract: test.PreviousCAPIContractNotSupported},
  1009  								{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
  1010  							},
  1011  						}),
  1012  				},
  1013  				// two providers with multiple instances existing in the cluster
  1014  				proxy: test.NewFakeProxy().
  1015  					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
  1016  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system").
  1017  					WithProviderInventory("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system-1"),
  1018  			},
  1019  			providersToUpgrade: []UpgradeItem{
  1020  				{
  1021  					Provider:    fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system"),
  1022  					NextVersion: "v2.0.0",
  1023  				},
  1024  				{
  1025  					Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system"),
  1026  					NextVersion: "v3.0.0",
  1027  				},
  1028  				{
  1029  					Provider:    fakeProvider("infra", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra-system-1"),
  1030  					NextVersion: "v3.0.0",
  1031  				},
  1032  			},
  1033  			wantErr:  true,
  1034  			errorMsg: "detected multiple instances of the same provider",
  1035  			opts:     UpgradeOptions{},
  1036  		},
  1037  	}
  1038  
  1039  	for _, tt := range tests {
  1040  		t.Run(tt.name, func(t *testing.T) {
  1041  			g := NewWithT(t)
  1042  
  1043  			ctx := context.Background()
  1044  
  1045  			configClient, _ := config.New(ctx, "", config.InjectReader(tt.fields.reader))
  1046  
  1047  			u := &providerUpgrader{
  1048  				configClient: configClient,
  1049  				repositoryClientFactory: func(ctx context.Context, provider config.Provider, configClient config.Client, _ ...repository.Option) (repository.Client, error) {
  1050  					return repository.New(ctx, provider, configClient, repository.InjectRepository(tt.fields.repository[provider.ManifestLabel()]))
  1051  				},
  1052  				providerInventory: newInventoryClient(tt.fields.proxy, nil),
  1053  			}
  1054  			err := u.ApplyCustomPlan(ctx, tt.opts, tt.providersToUpgrade...)
  1055  			if tt.wantErr {
  1056  				g.Expect(err).To(HaveOccurred())
  1057  				g.Expect(err.Error()).Should(ContainSubstring(tt.errorMsg))
  1058  				return
  1059  			}
  1060  
  1061  			g.Expect(err).ToNot(HaveOccurred())
  1062  		})
  1063  	}
  1064  }