sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/components.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 "fmt" 22 "strings" 23 24 "github.com/pkg/errors" 25 corev1 "k8s.io/api/core/v1" 26 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 apierrors "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 kerrors "k8s.io/apimachinery/pkg/util/errors" 32 "k8s.io/apimachinery/pkg/util/sets" 33 "sigs.k8s.io/controller-runtime/pkg/client" 34 35 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 36 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" 37 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" 38 logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log" 39 ) 40 41 const ( 42 namespaceKind = "Namespace" 43 validatingWebhookConfigurationKind = "ValidatingWebhookConfiguration" 44 mutatingWebhookConfigurationKind = "MutatingWebhookConfiguration" 45 customResourceDefinitionKind = "CustomResourceDefinition" 46 providerGroupKind = "Provider.clusterctl.cluster.x-k8s.io" 47 ) 48 49 // DeleteOptions holds options for ComponentsClient.Delete func. 50 type DeleteOptions struct { 51 Provider clusterctlv1.Provider 52 IncludeNamespace bool 53 IncludeCRDs bool 54 SkipInventory bool 55 } 56 57 // ComponentsClient has methods to work with provider components in the cluster. 58 type ComponentsClient interface { 59 // Create creates the provider components in the management cluster. 60 Create(ctx context.Context, objs []unstructured.Unstructured) error 61 62 // Delete deletes the provider components from the management cluster. 63 // The operation is designed to prevent accidental deletion of user created objects, so 64 // it is required to explicitly opt-in for the deletion of the namespace where the provider components are hosted 65 // and for the deletion of the provider's CRDs. 66 Delete(ctx context.Context, options DeleteOptions) error 67 68 // DeleteWebhookNamespace deletes the core provider webhook namespace (eg. capi-webhook-system). 69 // This is required when upgrading to v1alpha4 where webhooks are included in the controller itself. 70 DeleteWebhookNamespace(ctx context.Context) error 71 72 // ValidateNoObjectsExist checks if custom resources of the custom resource definitions exist and returns an error if so. 73 ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error 74 } 75 76 // providerComponents implements ComponentsClient. 77 type providerComponents struct { 78 proxy Proxy 79 } 80 81 func (p *providerComponents) Create(ctx context.Context, objs []unstructured.Unstructured) error { 82 createComponentObjectBackoff := newWriteBackoff() 83 for i := range objs { 84 obj := objs[i] 85 86 // Create the Kubernetes object. 87 // Nb. The operation is wrapped in a retry loop to make Create more resilient to unexpected conditions. 88 if err := retryWithExponentialBackoff(ctx, createComponentObjectBackoff, func(ctx context.Context) error { 89 return p.createObj(ctx, obj) 90 }); err != nil { 91 return err 92 } 93 } 94 95 return nil 96 } 97 98 func (p *providerComponents) createObj(ctx context.Context, obj unstructured.Unstructured) error { 99 log := logf.Log 100 c, err := p.proxy.NewClient(ctx) 101 if err != nil { 102 return err 103 } 104 105 // check if the component already exists, and eventually update it 106 currentR := &unstructured.Unstructured{} 107 currentR.SetGroupVersionKind(obj.GroupVersionKind()) 108 109 key := client.ObjectKey{ 110 Namespace: obj.GetNamespace(), 111 Name: obj.GetName(), 112 } 113 if err := c.Get(ctx, key, currentR); err != nil { 114 if !apierrors.IsNotFound(err) { 115 return errors.Wrapf(err, "failed to get current provider object") 116 } 117 118 // if it does not exists, create the component 119 log.V(5).Info("Creating", logf.UnstructuredToValues(obj)...) 120 if err := c.Create(ctx, &obj); err != nil { 121 return errors.Wrapf(err, "failed to create provider object %s, %s/%s", obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName()) 122 } 123 return nil 124 } 125 126 // otherwise update the component 127 // NB. we are using client.Merge PatchOption so the new objects gets compared with the current one server side 128 log.V(5).Info("Patching", logf.UnstructuredToValues(obj)...) 129 obj.SetResourceVersion(currentR.GetResourceVersion()) 130 if err := c.Patch(ctx, &obj, client.Merge); err != nil { 131 return errors.Wrapf(err, "failed to patch provider object") 132 } 133 return nil 134 } 135 136 func (p *providerComponents) Delete(ctx context.Context, options DeleteOptions) error { 137 log := logf.Log 138 log.Info("Deleting", "Provider", options.Provider.Name, "Version", options.Provider.Version, "Namespace", options.Provider.Namespace) 139 140 // Fetch all the components belonging to a provider. 141 // We want that the delete operation is able to clean-up everything. 142 labels := map[string]string{ 143 clusterctlv1.ClusterctlLabel: "", 144 clusterv1.ProviderNameLabel: options.Provider.ManifestLabel(), 145 } 146 147 namespaces := []string{options.Provider.Namespace} 148 resources, err := p.proxy.ListResources(ctx, labels, namespaces...) 149 if err != nil { 150 return err 151 } 152 153 // Filter the resources according to the delete options 154 resourcesToDelete := []unstructured.Unstructured{} 155 namespacesToDelete := sets.Set[string]{} 156 instanceNamespacePrefix := fmt.Sprintf("%s-", options.Provider.Namespace) 157 for _, obj := range resources { 158 // If the CRDs should NOT be deleted, skip it; 159 // NB. Skipping CRDs deletion ensures that also the objects of Kind defined in the CRDs Kind are not deleted. 160 isCRD := obj.GroupVersionKind().Kind == customResourceDefinitionKind 161 if !options.IncludeCRDs && isCRD { 162 continue 163 } 164 // If the resource is a namespace 165 isNamespace := obj.GroupVersionKind().Kind == namespaceKind 166 if isNamespace { 167 // Skip all the namespaces not related to the provider instance being processed. 168 if obj.GetName() != options.Provider.Namespace { 169 continue 170 } 171 // If the Namespace should NOT be deleted, skip it, otherwise keep track of the namespaces we are deleting; 172 // NB. Skipping Namespaces deletion ensures that also the objects hosted in the namespace but without the "clusterctl.cluster.x-k8s.io" and the "cluster.x-k8s.io/provider" label are not deleted. 173 if !options.IncludeNamespace { 174 continue 175 } 176 namespacesToDelete.Insert(obj.GetName()) 177 } 178 179 // If the resource is part of the inventory for clusterctl don't delete it at this point as losing this information makes the 180 // upgrade function non-reentrant. Instead keep the inventory objects around until the upgrade is finished and working and 181 // delete them at the end of the upgrade flow. 182 isInventory := obj.GroupVersionKind().GroupKind().String() == providerGroupKind 183 if isInventory && options.SkipInventory { 184 continue 185 } 186 187 // If the resource is a cluster resource, skip it if the resource name does not start with the instance prefix. 188 // This is required because there are cluster resources like e.g. ClusterRoles and ClusterRoleBinding, which are instance specific; 189 // During the installation, clusterctl adds the instance namespace prefix to such resources (see fixRBAC), and so we can rely 190 // on that for deleting only the global resources belonging the instance we are processing. 191 // NOTE: namespace and CRD are special case managed above; webhook instead goes hand by hand with the controller they 192 // should always be deleted. 193 isWebhook := obj.GroupVersionKind().Kind == validatingWebhookConfigurationKind || obj.GroupVersionKind().Kind == mutatingWebhookConfigurationKind 194 195 if util.IsClusterResource(obj.GetKind()) && 196 !isNamespace && !isCRD && !isWebhook && 197 // TODO(oscr) Delete the check below condition when the min version to upgrade from is CAPI v1.3 198 // This check is needed due to the (now removed) support for multiple instances of the same provider. 199 // For more context read GitHub issue #7318 and/or PR #7339 200 !strings.HasPrefix(obj.GetName(), instanceNamespacePrefix) { 201 continue 202 } 203 resourcesToDelete = append(resourcesToDelete, obj) 204 } 205 206 // Delete all the provider components. 207 cs, err := p.proxy.NewClient(ctx) 208 if err != nil { 209 return err 210 } 211 212 errList := []error{} 213 for i := range resourcesToDelete { 214 obj := resourcesToDelete[i] 215 216 // if the objects is in a namespace that is going to be deleted, skip deletion 217 // because everything that is contained in the namespace will be deleted by the Namespace controller 218 if namespacesToDelete.Has(obj.GetNamespace()) { 219 continue 220 } 221 222 // Otherwise delete the object 223 log.V(5).Info("Deleting", logf.UnstructuredToValues(obj)...) 224 deleteBackoff := newWriteBackoff() 225 if err := retryWithExponentialBackoff(ctx, deleteBackoff, func(ctx context.Context) error { 226 if err := cs.Delete(ctx, &obj); err != nil { 227 if apierrors.IsNotFound(err) { 228 // Tolerate IsNotFound error that might happen because we are not enforcing a deletion order 229 // that considers relation across objects (e.g. Deployments -> ReplicaSets -> Pods) 230 return nil 231 } 232 return err 233 } 234 return nil 235 }); err != nil { 236 errList = append(errList, errors.Wrapf(err, "Error deleting object %s, %s/%s", obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName())) 237 } 238 } 239 240 return kerrors.NewAggregate(errList) 241 } 242 243 func (p *providerComponents) DeleteWebhookNamespace(ctx context.Context) error { 244 const webhookNamespaceName = "capi-webhook-system" 245 246 log := logf.Log 247 log.V(5).Info("Deleting", "namespace", webhookNamespaceName) 248 249 c, err := p.proxy.NewClient(ctx) 250 if err != nil { 251 return err 252 } 253 254 coreProviderWebhookNs := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: webhookNamespaceName}} 255 if err := c.Delete(ctx, coreProviderWebhookNs); err != nil { 256 if apierrors.IsNotFound(err) { 257 return nil 258 } 259 return errors.Wrapf(err, "failed to delete namespace %s", webhookNamespaceName) 260 } 261 262 return nil 263 } 264 265 func (p *providerComponents) ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error { 266 log := logf.Log 267 log.Info("Checking for CRs", "Provider", provider.Name, "Version", provider.Version, "Namespace", provider.Namespace) 268 269 proxyClient, err := p.proxy.NewClient(ctx) 270 if err != nil { 271 return err 272 } 273 274 // Fetch all the components belonging to a provider. 275 // We want that the delete operation is able to clean-up everything. 276 labels := map[string]string{ 277 clusterctlv1.ClusterctlLabel: "", 278 clusterv1.ProviderNameLabel: provider.ManifestLabel(), 279 } 280 281 customResources := &apiextensionsv1.CustomResourceDefinitionList{} 282 if err := proxyClient.List(ctx, customResources, client.MatchingLabels(labels)); err != nil { 283 return err 284 } 285 286 // Filter the resources according to the delete options 287 crsHavingObjects := []string{} 288 for _, crd := range customResources.Items { 289 crd := crd 290 storageVersion, err := storageVersionForCRD(&crd) 291 if err != nil { 292 return err 293 } 294 295 list := &unstructured.UnstructuredList{} 296 list.SetGroupVersionKind(schema.GroupVersionKind{ 297 Group: crd.Spec.Group, 298 Version: storageVersion, 299 Kind: crd.Spec.Names.ListKind, 300 }) 301 302 if err := proxyClient.List(ctx, list); err != nil { 303 return err 304 } 305 306 if len(list.Items) > 0 { 307 crsHavingObjects = append(crsHavingObjects, crd.Kind) 308 } 309 } 310 311 if len(crsHavingObjects) > 0 { 312 return fmt.Errorf("found existing objects for provider CRDs %q: [%s]. Please delete these objects first before running clusterctl delete with --include-crd", provider.GetName(), strings.Join(crsHavingObjects, ", ")) 313 } 314 315 return nil 316 } 317 318 // newComponentsClient returns a providerComponents. 319 func newComponentsClient(proxy Proxy) *providerComponents { 320 return &providerComponents{ 321 proxy: proxy, 322 } 323 }