Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌱 Add support for compatible contracts to clusterctl #12018

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/clusterctl/api/v1alpha3/metadata_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ type Metadata struct {
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`

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

// ReleaseSeries maps a provider release series (major/minor) with a API Version of Cluster API (contract).
// ReleaseSeries maps a provider release series (major/minor) with a Cluster API contract version.
type ReleaseSeries struct {
// major version of the release series
// +optional
Expand Down
31 changes: 24 additions & 7 deletions cmd/clusterctl/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package client
import (
"context"

"k8s.io/apimachinery/pkg/util/sets"

clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/alpha"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
Expand Down Expand Up @@ -106,10 +108,12 @@ type YamlPrinter interface {

// clusterctlClient implements Client.
type clusterctlClient struct {
configClient config.Client
repositoryClientFactory RepositoryClientFactory
clusterClientFactory ClusterClientFactory
alphaClient alpha.Client
configClient config.Client
repositoryClientFactory RepositoryClientFactory
clusterClientFactory ClusterClientFactory
alphaClient alpha.Client
currentContractVersion string
getCompatibleContractVersions func(string) sets.Set[string]
}

// RepositoryClientFactoryInput represents the inputs required by the factory.
Expand Down Expand Up @@ -159,13 +163,24 @@ func InjectClusterClientFactory(factory ClusterClientFactory) Option {
}
}

// InjectCurrentContractVersion allows you to override the currentContractVersion that
// cluster client uses. This option is intended for internal tests only.
func InjectCurrentContractVersion(currentContractVersion string) Option {
return func(c *clusterctlClient) {
c.currentContractVersion = currentContractVersion
}
}

// New returns a configClient.
func New(ctx context.Context, path string, options ...Option) (Client, error) {
return newClusterctlClient(ctx, path, options...)
}

func newClusterctlClient(ctx context.Context, path string, options ...Option) (*clusterctlClient, error) {
client := &clusterctlClient{}
client := &clusterctlClient{
currentContractVersion: cluster.CurrentContractVersion,
getCompatibleContractVersions: cluster.GetCompatibleContractVersions,
}
for _, o := range options {
o(client)
}
Expand All @@ -187,7 +202,7 @@ func newClusterctlClient(ctx context.Context, path string, options ...Option) (*

// if there is an injected ClusterFactory, use it, otherwise use a default one.
if client.clusterClientFactory == nil {
client.clusterClientFactory = defaultClusterFactory(client.configClient)
client.clusterClientFactory = defaultClusterFactory(client.configClient, client.currentContractVersion, client.getCompatibleContractVersions)
}

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

// defaultClusterFactory is a ClusterClientFactory func the uses the default client provided by the cluster low level library.
func defaultClusterFactory(configClient config.Client) ClusterClientFactory {
func defaultClusterFactory(configClient config.Client, currentContractVersion string, getCompatibleContractVersions func(string) sets.Set[string]) ClusterClientFactory {
return func(input ClusterClientFactoryInput) (cluster.Client, error) {
return cluster.New(
// Kubeconfig is a type alias to cluster.Kubeconfig
cluster.Kubeconfig(input.Kubeconfig),
configClient,
cluster.InjectYamlProcessor(input.Processor),
cluster.InjectCurrentContractVersion(currentContractVersion),
cluster.InjectGetCompatibleContractVersionsFunc(getCompatibleContractVersions),
), nil
}
}
21 changes: 21 additions & 0 deletions cmd/clusterctl/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"time"

"github.com/pkg/errors"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/wait"
Expand Down Expand Up @@ -182,6 +184,7 @@ func newFakeClient(ctx context.Context, configClient config.Client) *fakeClient
}
return fake.repositories[input.Provider.ManifestLabel()], nil
}),
InjectCurrentContractVersion(currentContractVersion),
)

return fake
Expand Down Expand Up @@ -225,6 +228,8 @@ func newFakeCluster(kubeconfig cluster.Kubeconfig, configClient config.Client) *
}
return fake.repositories[provider.Name()], nil
}),
cluster.InjectCurrentContractVersion(currentContractVersion),
cluster.InjectGetCompatibleContractVersionsFunc(getCompatibleContractVersions),
)
return fake
}
Expand Down Expand Up @@ -604,3 +609,19 @@ func (f *fakeComponentClient) getRawBytes(ctx context.Context, options *reposito

return f.fakeRepository.GetFile(ctx, options.Version, path)
}

func fakeCAPISetupObjects() []client.Object {
return []client.Object{
&apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "clusters.cluster.x-k8s.io"},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: currentContractVersion,
Storage: true,
},
},
},
},
}
}
66 changes: 54 additions & 12 deletions cmd/clusterctl/client/cluster/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,34 @@ import (
"time"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor"
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
)

var (
// CurrentContractVersion is the contract version supported by this Cluster API version.
// Note: Each Cluster API version supports one contract version, and by convention the contract version matches the current API version.
CurrentContractVersion = clusterv1.GroupVersion.Version
)

// GetCompatibleContractVersions return the list of contract version compatible with a given contract version.
// NOTE: A contract version might be temporarily compatible with older contract versions e.g. to allow users time to transition to the new API.
// NOTE: The return value must include also the contract version received in input.
func GetCompatibleContractVersions(contract string) sets.Set[string] {
compatibleContracts := sets.New(contract)
// v1beta2 contract is temporarily be compatible with v1beta1 (until v1beta1 is EOL).
if contract == "v1beta2" {
compatibleContracts.Insert("v1beta1")
}
return compatibleContracts
}

// Kubeconfig is a type that specifies inputs related to the actual
// kubeconfig.
type Kubeconfig struct {
Expand Down Expand Up @@ -89,12 +109,14 @@ type PollImmediateWaiter func(ctx context.Context, interval, timeout time.Durati

// clusterClient implements Client.
type clusterClient struct {
configClient config.Client
kubeconfig Kubeconfig
proxy Proxy
repositoryClientFactory RepositoryClientFactory
pollImmediateWaiter PollImmediateWaiter
processor yaml.Processor
configClient config.Client
kubeconfig Kubeconfig
proxy Proxy
repositoryClientFactory RepositoryClientFactory
pollImmediateWaiter PollImmediateWaiter
processor yaml.Processor
currentContractVersion string
getCompatibleContractVersions func(string) sets.Set[string]
}

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

func (c *clusterClient) ProviderInventory() InventoryClient {
return newInventoryClient(c.proxy, c.pollImmediateWaiter)
return newInventoryClient(c.proxy, c.pollImmediateWaiter, c.currentContractVersion)
}

func (c *clusterClient) ProviderInstaller() ProviderInstaller {
return newProviderInstaller(c.configClient, c.repositoryClientFactory, c.proxy, c.ProviderInventory(), c.ProviderComponents())
return newProviderInstaller(c.configClient, c.repositoryClientFactory, c.proxy, c.ProviderInventory(), c.ProviderComponents(), c.currentContractVersion, c.getCompatibleContractVersions)
}

func (c *clusterClient) ObjectMover() ObjectMover {
return newObjectMover(c.proxy, c.ProviderInventory())
}

func (c *clusterClient) ProviderUpgrader() ProviderUpgrader {
return newProviderUpgrader(c.configClient, c.proxy, c.repositoryClientFactory, c.ProviderInventory(), c.ProviderComponents())
return newProviderUpgrader(c.configClient, c.proxy, c.repositoryClientFactory, c.ProviderInventory(), c.ProviderComponents(), c.currentContractVersion, c.getCompatibleContractVersions)
}

func (c *clusterClient) Template() TemplateClient {
Expand Down Expand Up @@ -183,16 +205,36 @@ func InjectYamlProcessor(p yaml.Processor) Option {
}
}

// InjectGetCompatibleContractVersionsFunc allows you to override the getCompatibleContractVersions function that
// cluster client uses. This option is intended for internal tests only.
func InjectGetCompatibleContractVersionsFunc(getCompatibleContractVersions func(string) sets.Set[string]) Option {
return func(c *clusterClient) {
if getCompatibleContractVersions != nil {
c.getCompatibleContractVersions = getCompatibleContractVersions
}
}
}

// InjectCurrentContractVersion allows you to override the currentContractVersion that
// cluster client uses. This option is intended for internal tests only.
func InjectCurrentContractVersion(currentContractVersion string) Option {
return func(c *clusterClient) {
c.currentContractVersion = currentContractVersion
}
}

// New returns a cluster.Client.
func New(kubeconfig Kubeconfig, configClient config.Client, options ...Option) Client {
return newClusterClient(kubeconfig, configClient, options...)
}

func newClusterClient(kubeconfig Kubeconfig, configClient config.Client, options ...Option) *clusterClient {
client := &clusterClient{
configClient: configClient,
kubeconfig: kubeconfig,
processor: yaml.NewSimpleProcessor(),
configClient: configClient,
kubeconfig: kubeconfig,
processor: yaml.NewSimpleProcessor(),
currentContractVersion: CurrentContractVersion,
getCompatibleContractVersions: GetCompatibleContractVersions,
}
for _, o := range options {
o(client)
Expand Down
4 changes: 0 additions & 4 deletions cmd/clusterctl/client/cluster/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ type ComponentsClient interface {
// and for the deletion of the provider's CRDs.
Delete(ctx context.Context, options DeleteOptions) error

// DeleteWebhookNamespace deletes the core provider webhook namespace (eg. capi-webhook-system).
// This is required when upgrading to v1alpha4 where webhooks are included in the controller itself.
DeleteWebhookNamespace(ctx context.Context) error

// ValidateNoObjectsExist checks if custom resources of the custom resource definitions exist and returns an error if so.
ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error
}
Expand Down
53 changes: 29 additions & 24 deletions cmd/clusterctl/client/cluster/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"

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

// providerInstaller implements ProviderInstaller.
type providerInstaller struct {
configClient config.Client
repositoryClientFactory RepositoryClientFactory
proxy Proxy
providerComponents ComponentsClient
providerInventory InventoryClient
installQueue []repository.Components
configClient config.Client
repositoryClientFactory RepositoryClientFactory
proxy Proxy
providerComponents ComponentsClient
providerInventory InventoryClient
installQueue []repository.Components
currentContractVersion string
getCompatibleContractVersions func(string) sets.Set[string]
}

var _ ProviderInstaller = &providerInstaller{}
Expand Down Expand Up @@ -188,32 +189,33 @@ func (i *providerInstaller) Validate(ctx context.Context) error {
}
}

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

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

managementClusterContract, err := i.getProviderContract(ctx, providerInstanceContracts, coreProvider)
if err != nil {
return err
}
compatibleContracts := i.getCompatibleContractVersions(managementClusterContract)

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

// Gets the API Version of Cluster API (contract) the provider support and compare it with the management cluster contract.
// Gets the contract version supported by the provider and compare it with the contract versions the management cluster is compatible with.
providerContract, err := i.getProviderContract(ctx, providerInstanceContracts, provider)
if err != nil {
return err
}
if providerContract != managementClusterContract {
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)
if !compatibleContracts.Has(providerContract) {
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(), ","))
}
}

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

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

if releaseSeries.Contract != clusterv1.GroupVersion.Version {
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())
compatibleContracts := i.getCompatibleContractVersions(i.currentContractVersion)
if !compatibleContracts.Has(releaseSeries.Contract) {
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())
}

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

func newProviderInstaller(configClient config.Client, repositoryClientFactory RepositoryClientFactory, proxy Proxy, providerMetadata InventoryClient, providerComponents ComponentsClient) *providerInstaller {
func newProviderInstaller(configClient config.Client, repositoryClientFactory RepositoryClientFactory, proxy Proxy, providerMetadata InventoryClient, providerComponents ComponentsClient, currentContractVersion string, getCompatibleContractVersions func(string) sets.Set[string]) *providerInstaller {
return &providerInstaller{
configClient: configClient,
repositoryClientFactory: repositoryClientFactory,
proxy: proxy,
providerComponents: providerComponents,
providerInventory: providerMetadata,
configClient: configClient,
repositoryClientFactory: repositoryClientFactory,
proxy: proxy,
providerComponents: providerComponents,
providerInventory: providerMetadata,
currentContractVersion: currentContractVersion,
getCompatibleContractVersions: getCompatibleContractVersions,
}
}
Loading