sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/installer.go (about) 1 /* 2 Copyright 2019 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 "sort" 22 "strings" 23 "time" 24 25 "github.com/pkg/errors" 26 appsv1 "k8s.io/api/apps/v1" 27 corev1 "k8s.io/api/core/v1" 28 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 29 apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/apimachinery/pkg/util/sets" 33 "k8s.io/apimachinery/pkg/util/version" 34 "k8s.io/apimachinery/pkg/util/wait" 35 "sigs.k8s.io/controller-runtime/pkg/client" 36 37 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 38 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" 39 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" 40 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository" 41 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme" 42 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" 43 logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log" 44 "sigs.k8s.io/cluster-api/util/contract" 45 ) 46 47 // ProviderInstaller defines methods for enforcing consistency rules for provider installation. 48 type ProviderInstaller interface { 49 // Add adds a provider to the install queue. 50 // NB. By deferring the installation, the installer service can perform validation of the target state of the management cluster 51 // before actually starting the installation of new providers. 52 Add(repository.Components) 53 54 // Install performs the installation of the providers ready in the install queue. 55 Install(context.Context, InstallOptions) ([]repository.Components, error) 56 57 // Validate performs steps to validate a management cluster by looking at the current state and the providers in the queue. 58 // The following checks are performed in order to ensure a fully operational cluster: 59 // - 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) 61 // - All provider CRDs that are referenced in core Cluster API CRDs must comply with the CRD naming scheme, 62 // otherwise a warning is logged. 63 Validate(context.Context) error 64 65 // Images returns the list of images required for installing the providers ready in the install queue. 66 Images() []string 67 } 68 69 // InstallOptions defines the options used to configure installation. 70 type InstallOptions struct { 71 WaitProviders bool 72 WaitProviderTimeout time.Duration 73 } 74 75 // providerInstaller implements ProviderInstaller. 76 type providerInstaller struct { 77 configClient config.Client 78 repositoryClientFactory RepositoryClientFactory 79 proxy Proxy 80 providerComponents ComponentsClient 81 providerInventory InventoryClient 82 installQueue []repository.Components 83 } 84 85 var _ ProviderInstaller = &providerInstaller{} 86 87 func (i *providerInstaller) Add(components repository.Components) { 88 i.installQueue = append(i.installQueue, components) 89 90 // Ensure Providers are installed in the following order: Core, Bootstrap, ControlPlane, Infrastructure. 91 sort.Slice(i.installQueue, func(a, b int) bool { 92 return i.installQueue[a].Type().Order() < i.installQueue[b].Type().Order() 93 }) 94 } 95 96 func (i *providerInstaller) Install(ctx context.Context, opts InstallOptions) ([]repository.Components, error) { 97 ret := make([]repository.Components, 0, len(i.installQueue)) 98 for _, components := range i.installQueue { 99 if err := installComponentsAndUpdateInventory(ctx, components, i.providerComponents, i.providerInventory); err != nil { 100 return nil, err 101 } 102 103 ret = append(ret, components) 104 } 105 106 return ret, waitForProvidersReady(ctx, opts, i.installQueue, i.proxy) 107 } 108 109 func installComponentsAndUpdateInventory(ctx context.Context, components repository.Components, providerComponents ComponentsClient, providerInventory InventoryClient) error { 110 log := logf.Log 111 log.Info("Installing", "Provider", components.ManifestLabel(), "Version", components.Version(), "TargetNamespace", components.TargetNamespace()) 112 113 inventoryObject := components.InventoryObject() 114 115 log.V(1).Info("Creating objects", "Provider", components.ManifestLabel(), "Version", components.Version(), "TargetNamespace", components.TargetNamespace()) 116 if err := providerComponents.Create(ctx, components.Objs()); err != nil { 117 return err 118 } 119 120 log.V(1).Info("Creating inventory entry", "Provider", components.ManifestLabel(), "Version", components.Version(), "TargetNamespace", components.TargetNamespace()) 121 return providerInventory.Create(ctx, inventoryObject) 122 } 123 124 // waitForProvidersReady waits till the installed components are ready. 125 func waitForProvidersReady(ctx context.Context, opts InstallOptions, installQueue []repository.Components, proxy Proxy) error { 126 // If we dont have to wait for providers to be installed 127 // return early. 128 if !opts.WaitProviders { 129 return nil 130 } 131 132 log := logf.Log 133 log.Info("Waiting for providers to be available...") 134 135 return waitManagerDeploymentsReady(ctx, opts, installQueue, proxy) 136 } 137 138 // waitManagerDeploymentsReady waits till the installed manager deployments are ready. 139 func waitManagerDeploymentsReady(ctx context.Context, opts InstallOptions, installQueue []repository.Components, proxy Proxy) error { 140 for _, components := range installQueue { 141 for _, obj := range components.Objs() { 142 if util.IsDeploymentWithManager(obj) { 143 if err := waitDeploymentReady(ctx, obj, opts.WaitProviderTimeout, proxy); err != nil { 144 return errors.Wrapf(err, "deployment %q is not ready after %s", obj.GetName(), opts.WaitProviderTimeout) 145 } 146 } 147 } 148 } 149 return nil 150 } 151 152 func waitDeploymentReady(ctx context.Context, deployment unstructured.Unstructured, timeout time.Duration, proxy Proxy) error { 153 return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, timeout, false, func(ctx context.Context) (bool, error) { 154 c, err := proxy.NewClient(ctx) 155 if err != nil { 156 return false, err 157 } 158 key := client.ObjectKey{ 159 Namespace: deployment.GetNamespace(), 160 Name: deployment.GetName(), 161 } 162 dep := &appsv1.Deployment{} 163 if err := c.Get(ctx, key, dep); err != nil { 164 return false, err 165 } 166 for _, c := range dep.Status.Conditions { 167 if c.Type == appsv1.DeploymentAvailable && c.Status == corev1.ConditionTrue { 168 return true, nil 169 } 170 } 171 return false, nil 172 }) 173 } 174 175 func (i *providerInstaller) Validate(ctx context.Context) error { 176 // Get the list of providers currently in the cluster. 177 providerList, err := i.providerInventory.List(ctx) 178 if err != nil { 179 return err 180 } 181 182 // Starts simulating what will be the resulting management cluster by adding to the list the providers in the installQueue. 183 // During this operation following checks are performed: 184 // - There must be only one instance of the same provider 185 for _, components := range i.installQueue { 186 if providerList, err = simulateInstall(providerList, components); err != nil { 187 return errors.Wrapf(err, "installing provider %q can lead to a non functioning management cluster", components.ManifestLabel()) 188 } 189 } 190 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. 193 providerInstanceContracts := map[string]string{} 194 195 coreProviders := providerList.FilterCore() 196 if len(coreProviders) != 1 { 197 return errors.Errorf("invalid management cluster: there should a core provider, found %d", len(coreProviders)) 198 } 199 coreProvider := coreProviders[0] 200 201 managementClusterContract, err := i.getProviderContract(ctx, providerInstanceContracts, coreProvider) 202 if err != nil { 203 return err 204 } 205 206 // Checks if all the providers supports the same API Version of Cluster API (contract). 207 for _, components := range i.installQueue { 208 provider := components.InventoryObject() 209 210 // Gets the API Version of Cluster API (contract) the provider support and compare it with the management cluster contract. 211 providerContract, err := i.getProviderContract(ctx, providerInstanceContracts, provider) 212 if err != nil { 213 return err 214 } 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 } 218 } 219 220 // Validate if provider CRDs comply with the naming scheme. 221 for _, components := range i.installQueue { 222 componentObjects := components.Objs() 223 224 for _, obj := range componentObjects { 225 // Continue if object is not a CRD. 226 if obj.GetKind() != customResourceDefinitionKind { 227 continue 228 } 229 230 gk, err := getCRDGroupKind(obj) 231 if err != nil { 232 return errors.Wrap(err, "failed to read group and kind from CustomResourceDefinition") 233 } 234 235 if err := validateCRDName(obj, gk); err != nil { 236 return err 237 } 238 } 239 } 240 241 return nil 242 } 243 244 func getCRDGroupKind(obj unstructured.Unstructured) (*schema.GroupKind, error) { 245 var group string 246 var kind string 247 version := obj.GroupVersionKind().Version 248 switch version { 249 case apiextensionsv1beta1.SchemeGroupVersion.Version: 250 crd := &apiextensionsv1beta1.CustomResourceDefinition{} 251 if err := scheme.Scheme.Convert(&obj, crd, nil); err != nil { 252 return nil, errors.Wrapf(err, "failed to convert %s CustomResourceDefinition %q", version, obj.GetName()) 253 } 254 group = crd.Spec.Group 255 kind = crd.Spec.Names.Kind 256 case apiextensionsv1.SchemeGroupVersion.Version: 257 crd := &apiextensionsv1.CustomResourceDefinition{} 258 if err := scheme.Scheme.Convert(&obj, crd, nil); err != nil { 259 return nil, errors.Wrapf(err, "failed to convert %s CustomResourceDefinition %q", version, obj.GetName()) 260 } 261 group = crd.Spec.Group 262 kind = crd.Spec.Names.Kind 263 default: 264 return nil, errors.Errorf("failed to read %s CustomResourceDefinition %q", version, obj.GetName()) 265 } 266 return &schema.GroupKind{Group: group, Kind: kind}, nil 267 } 268 269 func validateCRDName(obj unstructured.Unstructured, gk *schema.GroupKind) error { 270 // Return if CRD has skip CRD name preflight check annotation. 271 if _, ok := obj.GetAnnotations()[clusterctlv1.SkipCRDNamePreflightCheckAnnotation]; ok { 272 return nil 273 } 274 275 correctCRDName := contract.CalculateCRDName(gk.Group, gk.Kind) 276 277 // Return if name is correct. 278 if obj.GetName() == correctCRDName { 279 return nil 280 } 281 282 return errors.Errorf("ERROR: CRD name %q is invalid for a CRD referenced in a core Cluster API CRD,"+ 283 "it should be %q. Support for CRDs that are referenced in core Cluster API resources with invalid names has been "+ 284 "dropped. Note: Please check if this CRD is actually referenced in core Cluster API "+ 285 "CRDs. If not, this warning can be hidden by setting the %q' annotation.", obj.GetName(), correctCRDName, clusterctlv1.SkipCRDNamePreflightCheckAnnotation) 286 } 287 288 // getProviderContract returns the API Version of Cluster API (contract) for a provider instance. 289 func (i *providerInstaller) getProviderContract(ctx context.Context, providerInstanceContracts map[string]string, provider clusterctlv1.Provider) (string, error) { 290 // If the contract for the provider instance is already known, return it. 291 if contract, ok := providerInstanceContracts[provider.InstanceName()]; ok { 292 return contract, nil 293 } 294 295 // Otherwise get the contract for the providers instance. 296 297 // Gets the providers metadata. 298 configRepository, err := i.configClient.Providers().Get(provider.ProviderName, provider.GetProviderType()) 299 if err != nil { 300 return "", err 301 } 302 303 providerRepository, err := i.repositoryClientFactory(ctx, configRepository, i.configClient) 304 if err != nil { 305 return "", err 306 } 307 308 latestMetadata, err := providerRepository.Metadata(provider.Version).Get(ctx) 309 if err != nil { 310 return "", err 311 } 312 313 // Gets the contract for the current release. 314 currentVersion, err := version.ParseSemantic(provider.Version) 315 if err != nil { 316 return "", errors.Wrapf(err, "failed to parse current version for the %s provider", provider.InstanceName()) 317 } 318 319 releaseSeries := latestMetadata.GetReleaseSeriesForVersion(currentVersion) 320 if releaseSeries == nil { 321 return "", errors.Errorf("invalid provider metadata: version %s for the provider %s does not match any release series", provider.Version, provider.InstanceName()) 322 } 323 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 } 327 328 providerInstanceContracts[provider.InstanceName()] = releaseSeries.Contract 329 return releaseSeries.Contract, nil 330 } 331 332 // simulateInstall adds a provider to the list of providers in a cluster (without installing it). 333 func simulateInstall(providerList *clusterctlv1.ProviderList, components repository.Components) (*clusterctlv1.ProviderList, error) { 334 provider := components.InventoryObject() 335 336 existingInstances := providerList.FilterByProviderNameAndType(provider.ProviderName, provider.GetProviderType()) 337 if len(existingInstances) > 0 { 338 namespaces := func() string { 339 var namespaces []string 340 for _, provider := range existingInstances { 341 namespaces = append(namespaces, provider.Namespace) 342 } 343 return strings.Join(namespaces, ", ") 344 }() 345 return providerList, errors.Errorf("there is already an instance of the %q provider installed in the %q namespace", provider.ManifestLabel(), namespaces) 346 } 347 348 providerList.Items = append(providerList.Items, provider) 349 return providerList, nil 350 } 351 352 func (i *providerInstaller) Images() []string { 353 ret := sets.Set[string]{} 354 for _, components := range i.installQueue { 355 ret = ret.Insert(components.Images()...) 356 } 357 return sets.List(ret) 358 } 359 360 func newProviderInstaller(configClient config.Client, repositoryClientFactory RepositoryClientFactory, proxy Proxy, providerMetadata InventoryClient, providerComponents ComponentsClient) *providerInstaller { 361 return &providerInstaller{ 362 configClient: configClient, 363 repositoryClientFactory: repositoryClientFactory, 364 proxy: proxy, 365 providerComponents: providerComponents, 366 providerInventory: providerMetadata, 367 } 368 }