Skip to content

Commit 4704d83

Browse files
🌱 Add support for compatible contracts to clusterctl (#12018)
* Add support for compatible contracts to clusterctl * Address feedback * More feedback * Improve documentation about compatible contracts * Fix new tests after rebase
1 parent 8523bb2 commit 4704d83

34 files changed

+1002
-447
lines changed

cmd/clusterctl/api/v1alpha3/metadata_type.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ type Metadata struct {
3232
// +optional
3333
metav1.ObjectMeta `json:"metadata,omitempty"`
3434

35-
// releaseSeries maps a provider release series (major/minor) with an API Version of Cluster API (contract).
35+
// releaseSeries maps a provider release series (major/minor) with a Cluster API contract version.
3636
// +optional
3737
ReleaseSeries []ReleaseSeries `json:"releaseSeries"`
3838
}
3939

40-
// ReleaseSeries maps a provider release series (major/minor) with a API Version of Cluster API (contract).
40+
// ReleaseSeries maps a provider release series (major/minor) with a Cluster API contract version.
4141
type ReleaseSeries struct {
4242
// major version of the release series
4343
// +optional

cmd/clusterctl/client/client.go

+24-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package client
1919
import (
2020
"context"
2121

22+
"k8s.io/apimachinery/pkg/util/sets"
23+
2224
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
2325
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/alpha"
2426
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
@@ -106,10 +108,12 @@ type YamlPrinter interface {
106108

107109
// clusterctlClient implements Client.
108110
type clusterctlClient struct {
109-
configClient config.Client
110-
repositoryClientFactory RepositoryClientFactory
111-
clusterClientFactory ClusterClientFactory
112-
alphaClient alpha.Client
111+
configClient config.Client
112+
repositoryClientFactory RepositoryClientFactory
113+
clusterClientFactory ClusterClientFactory
114+
alphaClient alpha.Client
115+
currentContractVersion string
116+
getCompatibleContractVersions func(string) sets.Set[string]
113117
}
114118

115119
// RepositoryClientFactoryInput represents the inputs required by the factory.
@@ -159,13 +163,24 @@ func InjectClusterClientFactory(factory ClusterClientFactory) Option {
159163
}
160164
}
161165

166+
// InjectCurrentContractVersion allows you to override the currentContractVersion that
167+
// cluster client uses. This option is intended for internal tests only.
168+
func InjectCurrentContractVersion(currentContractVersion string) Option {
169+
return func(c *clusterctlClient) {
170+
c.currentContractVersion = currentContractVersion
171+
}
172+
}
173+
162174
// New returns a configClient.
163175
func New(ctx context.Context, path string, options ...Option) (Client, error) {
164176
return newClusterctlClient(ctx, path, options...)
165177
}
166178

167179
func newClusterctlClient(ctx context.Context, path string, options ...Option) (*clusterctlClient, error) {
168-
client := &clusterctlClient{}
180+
client := &clusterctlClient{
181+
currentContractVersion: cluster.CurrentContractVersion,
182+
getCompatibleContractVersions: cluster.GetCompatibleContractVersions,
183+
}
169184
for _, o := range options {
170185
o(client)
171186
}
@@ -187,7 +202,7 @@ func newClusterctlClient(ctx context.Context, path string, options ...Option) (*
187202

188203
// if there is an injected ClusterFactory, use it, otherwise use a default one.
189204
if client.clusterClientFactory == nil {
190-
client.clusterClientFactory = defaultClusterFactory(client.configClient)
205+
client.clusterClientFactory = defaultClusterFactory(client.configClient, client.currentContractVersion, client.getCompatibleContractVersions)
191206
}
192207

193208
// if there is an injected alphaClient, use it, otherwise use a default one.
@@ -212,13 +227,15 @@ func defaultRepositoryFactory(configClient config.Client) RepositoryClientFactor
212227
}
213228

214229
// defaultClusterFactory is a ClusterClientFactory func the uses the default client provided by the cluster low level library.
215-
func defaultClusterFactory(configClient config.Client) ClusterClientFactory {
230+
func defaultClusterFactory(configClient config.Client, currentContractVersion string, getCompatibleContractVersions func(string) sets.Set[string]) ClusterClientFactory {
216231
return func(input ClusterClientFactoryInput) (cluster.Client, error) {
217232
return cluster.New(
218233
// Kubeconfig is a type alias to cluster.Kubeconfig
219234
cluster.Kubeconfig(input.Kubeconfig),
220235
configClient,
221236
cluster.InjectYamlProcessor(input.Processor),
237+
cluster.InjectCurrentContractVersion(currentContractVersion),
238+
cluster.InjectGetCompatibleContractVersionsFunc(getCompatibleContractVersions),
222239
), nil
223240
}
224241
}

cmd/clusterctl/client/client_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"time"
2424

2525
"github.com/pkg/errors"
26+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2628
"k8s.io/apimachinery/pkg/runtime"
2729
"k8s.io/apimachinery/pkg/runtime/serializer"
2830
"k8s.io/apimachinery/pkg/util/wait"
@@ -182,6 +184,7 @@ func newFakeClient(ctx context.Context, configClient config.Client) *fakeClient
182184
}
183185
return fake.repositories[input.Provider.ManifestLabel()], nil
184186
}),
187+
InjectCurrentContractVersion(currentContractVersion),
185188
)
186189

187190
return fake
@@ -225,6 +228,8 @@ func newFakeCluster(kubeconfig cluster.Kubeconfig, configClient config.Client) *
225228
}
226229
return fake.repositories[provider.Name()], nil
227230
}),
231+
cluster.InjectCurrentContractVersion(currentContractVersion),
232+
cluster.InjectGetCompatibleContractVersionsFunc(getCompatibleContractVersions),
228233
)
229234
return fake
230235
}
@@ -604,3 +609,19 @@ func (f *fakeComponentClient) getRawBytes(ctx context.Context, options *reposito
604609

605610
return f.fakeRepository.GetFile(ctx, options.Version, path)
606611
}
612+
613+
func fakeCAPISetupObjects() []client.Object {
614+
return []client.Object{
615+
&apiextensionsv1.CustomResourceDefinition{
616+
ObjectMeta: metav1.ObjectMeta{Name: "clusters.cluster.x-k8s.io"},
617+
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
618+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
619+
{
620+
Name: currentContractVersion,
621+
Storage: true,
622+
},
623+
},
624+
},
625+
},
626+
}
627+
}

cmd/clusterctl/client/cluster/client.go

+54-12
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,34 @@ import (
2121
"time"
2222

2323
"github.com/pkg/errors"
24+
"k8s.io/apimachinery/pkg/util/sets"
2425
"k8s.io/apimachinery/pkg/util/wait"
2526

27+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2628
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
2729
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
2830
yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor"
2931
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
3032
)
3133

34+
var (
35+
// CurrentContractVersion is the contract version supported by this Cluster API version.
36+
// Note: Each Cluster API version supports one contract version, and by convention the contract version matches the current API version.
37+
CurrentContractVersion = clusterv1.GroupVersion.Version
38+
)
39+
40+
// GetCompatibleContractVersions return the list of contract version compatible with a given contract version.
41+
// NOTE: A contract version might be temporarily compatible with older contract versions e.g. to allow users time to transition to the new API.
42+
// NOTE: The return value must include also the contract version received in input.
43+
func GetCompatibleContractVersions(contract string) sets.Set[string] {
44+
compatibleContracts := sets.New(contract)
45+
// v1beta2 contract is temporarily be compatible with v1beta1 (until v1beta1 is EOL).
46+
if contract == "v1beta2" {
47+
compatibleContracts.Insert("v1beta1")
48+
}
49+
return compatibleContracts
50+
}
51+
3252
// Kubeconfig is a type that specifies inputs related to the actual
3353
// kubeconfig.
3454
type Kubeconfig struct {
@@ -89,12 +109,14 @@ type PollImmediateWaiter func(ctx context.Context, interval, timeout time.Durati
89109

90110
// clusterClient implements Client.
91111
type clusterClient struct {
92-
configClient config.Client
93-
kubeconfig Kubeconfig
94-
proxy Proxy
95-
repositoryClientFactory RepositoryClientFactory
96-
pollImmediateWaiter PollImmediateWaiter
97-
processor yaml.Processor
112+
configClient config.Client
113+
kubeconfig Kubeconfig
114+
proxy Proxy
115+
repositoryClientFactory RepositoryClientFactory
116+
pollImmediateWaiter PollImmediateWaiter
117+
processor yaml.Processor
118+
currentContractVersion string
119+
getCompatibleContractVersions func(string) sets.Set[string]
98120
}
99121

100122
// RepositoryClientFactory defines a function that returns a new repository.Client.
@@ -120,19 +142,19 @@ func (c *clusterClient) ProviderComponents() ComponentsClient {
120142
}
121143

122144
func (c *clusterClient) ProviderInventory() InventoryClient {
123-
return newInventoryClient(c.proxy, c.pollImmediateWaiter)
145+
return newInventoryClient(c.proxy, c.pollImmediateWaiter, c.currentContractVersion)
124146
}
125147

126148
func (c *clusterClient) ProviderInstaller() ProviderInstaller {
127-
return newProviderInstaller(c.configClient, c.repositoryClientFactory, c.proxy, c.ProviderInventory(), c.ProviderComponents())
149+
return newProviderInstaller(c.configClient, c.repositoryClientFactory, c.proxy, c.ProviderInventory(), c.ProviderComponents(), c.currentContractVersion, c.getCompatibleContractVersions)
128150
}
129151

130152
func (c *clusterClient) ObjectMover() ObjectMover {
131153
return newObjectMover(c.proxy, c.ProviderInventory())
132154
}
133155

134156
func (c *clusterClient) ProviderUpgrader() ProviderUpgrader {
135-
return newProviderUpgrader(c.configClient, c.proxy, c.repositoryClientFactory, c.ProviderInventory(), c.ProviderComponents())
157+
return newProviderUpgrader(c.configClient, c.proxy, c.repositoryClientFactory, c.ProviderInventory(), c.ProviderComponents(), c.currentContractVersion, c.getCompatibleContractVersions)
136158
}
137159

138160
func (c *clusterClient) Template() TemplateClient {
@@ -183,16 +205,36 @@ func InjectYamlProcessor(p yaml.Processor) Option {
183205
}
184206
}
185207

208+
// InjectGetCompatibleContractVersionsFunc allows you to override the getCompatibleContractVersions function that
209+
// cluster client uses. This option is intended for internal tests only.
210+
func InjectGetCompatibleContractVersionsFunc(getCompatibleContractVersions func(string) sets.Set[string]) Option {
211+
return func(c *clusterClient) {
212+
if getCompatibleContractVersions != nil {
213+
c.getCompatibleContractVersions = getCompatibleContractVersions
214+
}
215+
}
216+
}
217+
218+
// InjectCurrentContractVersion allows you to override the currentContractVersion that
219+
// cluster client uses. This option is intended for internal tests only.
220+
func InjectCurrentContractVersion(currentContractVersion string) Option {
221+
return func(c *clusterClient) {
222+
c.currentContractVersion = currentContractVersion
223+
}
224+
}
225+
186226
// New returns a cluster.Client.
187227
func New(kubeconfig Kubeconfig, configClient config.Client, options ...Option) Client {
188228
return newClusterClient(kubeconfig, configClient, options...)
189229
}
190230

191231
func newClusterClient(kubeconfig Kubeconfig, configClient config.Client, options ...Option) *clusterClient {
192232
client := &clusterClient{
193-
configClient: configClient,
194-
kubeconfig: kubeconfig,
195-
processor: yaml.NewSimpleProcessor(),
233+
configClient: configClient,
234+
kubeconfig: kubeconfig,
235+
processor: yaml.NewSimpleProcessor(),
236+
currentContractVersion: CurrentContractVersion,
237+
getCompatibleContractVersions: GetCompatibleContractVersions,
196238
}
197239
for _, o := range options {
198240
o(client)

cmd/clusterctl/client/cluster/components.go

-4
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ type ComponentsClient interface {
6666
// and for the deletion of the provider's CRDs.
6767
Delete(ctx context.Context, options DeleteOptions) error
6868

69-
// DeleteWebhookNamespace deletes the core provider webhook namespace (eg. capi-webhook-system).
70-
// This is required when upgrading to v1alpha4 where webhooks are included in the controller itself.
71-
DeleteWebhookNamespace(ctx context.Context) error
72-
7369
// ValidateNoObjectsExist checks if custom resources of the custom resource definitions exist and returns an error if so.
7470
ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error
7571
}

cmd/clusterctl/client/cluster/installer.go

+29-24
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434
"k8s.io/apimachinery/pkg/util/wait"
3535
"sigs.k8s.io/controller-runtime/pkg/client"
3636

37-
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3837
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
3938
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
4039
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
@@ -57,7 +56,7 @@ type ProviderInstaller interface {
5756
// Validate performs steps to validate a management cluster by looking at the current state and the providers in the queue.
5857
// The following checks are performed in order to ensure a fully operational cluster:
5958
// - There must be only one instance of the same provider
60-
// - All the providers in must support the same API Version of Cluster API (contract)
59+
// - All providers must support the contract version or one of its compatible versions
6160
// - All provider CRDs that are referenced in core Cluster API CRDs must comply with the CRD naming scheme,
6261
// otherwise a warning is logged.
6362
Validate(context.Context) error
@@ -74,12 +73,14 @@ type InstallOptions struct {
7473

7574
// providerInstaller implements ProviderInstaller.
7675
type providerInstaller struct {
77-
configClient config.Client
78-
repositoryClientFactory RepositoryClientFactory
79-
proxy Proxy
80-
providerComponents ComponentsClient
81-
providerInventory InventoryClient
82-
installQueue []repository.Components
76+
configClient config.Client
77+
repositoryClientFactory RepositoryClientFactory
78+
proxy Proxy
79+
providerComponents ComponentsClient
80+
providerInventory InventoryClient
81+
installQueue []repository.Components
82+
currentContractVersion string
83+
getCompatibleContractVersions func(string) sets.Set[string]
8384
}
8485

8586
var _ ProviderInstaller = &providerInstaller{}
@@ -188,32 +189,33 @@ func (i *providerInstaller) Validate(ctx context.Context) error {
188189
}
189190
}
190191

191-
// Gets the API Version of Cluster API (contract) all the providers in the management cluster must support,
192-
// which is the same of the core provider.
192+
// Gets the contract version that all the providers in the management cluster must support,
193+
// which is the same of the core provider (or a compatible one).
193194
providerInstanceContracts := map[string]string{}
194195

195196
coreProviders := providerList.FilterCore()
196197
if len(coreProviders) != 1 {
197-
return errors.Errorf("invalid management cluster: there should a core provider, found %d", len(coreProviders))
198+
return errors.Errorf("invalid management cluster: there must be one core provider, found %d", len(coreProviders))
198199
}
199200
coreProvider := coreProviders[0]
200201

201202
managementClusterContract, err := i.getProviderContract(ctx, providerInstanceContracts, coreProvider)
202203
if err != nil {
203204
return err
204205
}
206+
compatibleContracts := i.getCompatibleContractVersions(managementClusterContract)
205207

206-
// Checks if all the providers supports the same API Version of Cluster API (contract).
208+
// Checks if all the providers supports the same Cluster API contract or compatible contracts.
207209
for _, components := range i.installQueue {
208210
provider := components.InventoryObject()
209211

210-
// Gets the API Version of Cluster API (contract) the provider support and compare it with the management cluster contract.
212+
// Gets the contract version supported by the provider and compare it with the contract versions the management cluster is compatible with.
211213
providerContract, err := i.getProviderContract(ctx, providerInstanceContracts, provider)
212214
if err != nil {
213215
return err
214216
}
215-
if providerContract != managementClusterContract {
216-
return errors.Errorf("installing provider %q can lead to a non functioning management cluster: the target version for the provider supports the %s API Version of Cluster API (contract), while the management cluster is using %s", components.ManifestLabel(), providerContract, managementClusterContract)
217+
if !compatibleContracts.Has(providerContract) {
218+
return errors.Errorf("installing provider %q could lead to a non functioning management cluster: the target version for the provider implements the %s contract version, while the core provider is compatible with %s contract versions", components.ManifestLabel(), providerContract, strings.Join(compatibleContracts.UnsortedList(), ","))
217219
}
218220
}
219221

@@ -285,7 +287,7 @@ func validateCRDName(obj unstructured.Unstructured, gk *schema.GroupKind) error
285287
"CRDs. If not, this warning can be hidden by setting the %q' annotation.", obj.GetName(), correctCRDName, clusterctlv1.SkipCRDNamePreflightCheckAnnotation)
286288
}
287289

288-
// getProviderContract returns the API Version of Cluster API (contract) for a provider instance.
290+
// getProviderContract returns the contract versions supported by a provider instance.
289291
func (i *providerInstaller) getProviderContract(ctx context.Context, providerInstanceContracts map[string]string, provider clusterctlv1.Provider) (string, error) {
290292
// If the contract for the provider instance is already known, return it.
291293
if contract, ok := providerInstanceContracts[provider.InstanceName()]; ok {
@@ -321,8 +323,9 @@ func (i *providerInstaller) getProviderContract(ctx context.Context, providerIns
321323
return "", errors.Errorf("invalid provider metadata: version %s for the provider %s does not match any release series", provider.Version, provider.InstanceName())
322324
}
323325

324-
if releaseSeries.Contract != clusterv1.GroupVersion.Version {
325-
return "", errors.Errorf("current version of clusterctl is only compatible with %s providers, detected %s for provider %s", clusterv1.GroupVersion.Version, releaseSeries.Contract, provider.ManifestLabel())
326+
compatibleContracts := i.getCompatibleContractVersions(i.currentContractVersion)
327+
if !compatibleContracts.Has(releaseSeries.Contract) {
328+
return "", errors.Errorf("current version of clusterctl is only compatible with providers implementing the %s contract versions, detected contract version %s for provider %s", strings.Join(compatibleContracts.UnsortedList(), ", "), releaseSeries.Contract, provider.ManifestLabel())
326329
}
327330

328331
providerInstanceContracts[provider.InstanceName()] = releaseSeries.Contract
@@ -357,12 +360,14 @@ func (i *providerInstaller) Images() []string {
357360
return sets.List(ret)
358361
}
359362

360-
func newProviderInstaller(configClient config.Client, repositoryClientFactory RepositoryClientFactory, proxy Proxy, providerMetadata InventoryClient, providerComponents ComponentsClient) *providerInstaller {
363+
func newProviderInstaller(configClient config.Client, repositoryClientFactory RepositoryClientFactory, proxy Proxy, providerMetadata InventoryClient, providerComponents ComponentsClient, currentContractVersion string, getCompatibleContractVersions func(string) sets.Set[string]) *providerInstaller {
361364
return &providerInstaller{
362-
configClient: configClient,
363-
repositoryClientFactory: repositoryClientFactory,
364-
proxy: proxy,
365-
providerComponents: providerComponents,
366-
providerInventory: providerMetadata,
365+
configClient: configClient,
366+
repositoryClientFactory: repositoryClientFactory,
367+
proxy: proxy,
368+
providerComponents: providerComponents,
369+
providerInventory: providerMetadata,
370+
currentContractVersion: currentContractVersion,
371+
getCompatibleContractVersions: getCompatibleContractVersions,
367372
}
368373
}

0 commit comments

Comments
 (0)