sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/proxy.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 "os" 23 "strconv" 24 "strings" 25 "time" 26 27 "github.com/pkg/errors" 28 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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/client-go/discovery" 34 "k8s.io/client-go/kubernetes" 35 "k8s.io/client-go/rest" 36 "k8s.io/client-go/tools/clientcmd" 37 "sigs.k8s.io/controller-runtime/pkg/client" 38 39 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 40 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" 41 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme" 42 "sigs.k8s.io/cluster-api/version" 43 ) 44 45 var ( 46 localScheme = scheme.Scheme 47 ) 48 49 // Proxy defines a client proxy interface. 50 type Proxy interface { 51 // GetConfig returns the rest.Config 52 GetConfig() (*rest.Config, error) 53 54 // CurrentNamespace returns the namespace from the current context in the kubeconfig file. 55 CurrentNamespace() (string, error) 56 57 // ValidateKubernetesVersion returns an error if management cluster version less than MinimumKubernetesVersion. 58 ValidateKubernetesVersion() error 59 60 // NewClient returns a new controller runtime Client object for working on the management cluster. 61 NewClient(ctx context.Context) (client.Client, error) 62 63 // CheckClusterAvailable checks if a cluster is available and reachable. 64 CheckClusterAvailable(ctx context.Context) error 65 66 // ListResources lists namespaced and cluster-wide resources for a component matching the labels. Namespaced resources are only listed 67 // in the given namespaces. 68 // Please note that we are not returning resources for the component's CRD (e.g. we are not returning 69 // Certificates for cert-manager, Clusters for CAPI, AWSCluster for CAPA and so on). 70 // This is done to avoid errors when listing resources of providers which have already been deleted/scaled down to 0 replicas/with 71 // malfunctioning webhooks. 72 ListResources(ctx context.Context, labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error) 73 74 // GetContexts returns the list of contexts in kubeconfig which begin with prefix. 75 GetContexts(prefix string) ([]string, error) 76 77 // GetResourceNames returns the list of resource names which begin with prefix. 78 GetResourceNames(ctx context.Context, groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error) 79 } 80 81 type proxy struct { 82 kubeconfig Kubeconfig 83 timeout time.Duration 84 configLoadingRules *clientcmd.ClientConfigLoadingRules 85 } 86 87 var _ Proxy = &proxy{} 88 89 // CurrentNamespace returns the namespace for the specified context or the 90 // first valid context as determined by the default config loading rules. 91 func (k *proxy) CurrentNamespace() (string, error) { 92 config, err := k.configLoadingRules.Load() 93 if err != nil { 94 return "", errors.Wrap(err, "failed to load Kubeconfig") 95 } 96 97 context := config.CurrentContext 98 // If a context is explicitly provided use that instead 99 if k.kubeconfig.Context != "" { 100 context = k.kubeconfig.Context 101 } 102 103 v, ok := config.Contexts[context] 104 if !ok { 105 if k.kubeconfig.Path != "" { 106 return "", errors.Errorf("failed to get context %q from %q", context, k.configLoadingRules.GetExplicitFile()) 107 } 108 return "", errors.Errorf("failed to get context %q from %q", context, k.configLoadingRules.GetLoadingPrecedence()) 109 } 110 111 if v.Namespace != "" { 112 return v.Namespace, nil 113 } 114 115 return metav1.NamespaceDefault, nil 116 } 117 118 func (k *proxy) ValidateKubernetesVersion() error { 119 config, err := k.GetConfig() 120 if err != nil { 121 return err 122 } 123 124 minVer := version.MinimumKubernetesVersion 125 if clusterTopologyFeatureGate, _ := strconv.ParseBool(os.Getenv("CLUSTER_TOPOLOGY")); clusterTopologyFeatureGate { 126 minVer = version.MinimumKubernetesVersionClusterTopology 127 } 128 129 return version.CheckKubernetesVersion(config, minVer) 130 } 131 132 // GetConfig returns the config for a kubernetes client. 133 func (k *proxy) GetConfig() (*rest.Config, error) { 134 config, err := k.configLoadingRules.Load() 135 if err != nil { 136 return nil, errors.Wrap(err, "failed to load Kubeconfig") 137 } 138 139 configOverrides := &clientcmd.ConfigOverrides{ 140 CurrentContext: k.kubeconfig.Context, 141 Timeout: k.timeout.String(), 142 } 143 restConfig, err := clientcmd.NewDefaultClientConfig(*config, configOverrides).ClientConfig() 144 if err != nil { 145 if strings.HasPrefix(err.Error(), "invalid configuration:") { 146 return nil, errors.New(strings.Replace(err.Error(), "invalid configuration:", "invalid kubeconfig file; clusterctl requires a valid kubeconfig file to connect to the management cluster:", 1)) 147 } 148 return nil, err 149 } 150 restConfig.UserAgent = fmt.Sprintf("clusterctl/%s (%s)", version.Get().GitVersion, version.Get().Platform) 151 152 // Set QPS and Burst to a threshold that ensures the controller runtime client/client go doesn't generate throttling log messages 153 restConfig.QPS = 20 154 restConfig.Burst = 100 155 156 return restConfig, nil 157 } 158 159 func (k *proxy) NewClient(ctx context.Context) (client.Client, error) { 160 config, err := k.GetConfig() 161 if err != nil { 162 return nil, err 163 } 164 165 var c client.Client 166 // Nb. The operation is wrapped in a retry loop to make newClientSet more resilient to temporary connection problems. 167 connectBackoff := newConnectBackoff() 168 if err := retryWithExponentialBackoff(ctx, connectBackoff, func(_ context.Context) error { 169 var err error 170 c, err = client.New(config, client.Options{Scheme: localScheme}) 171 if err != nil { 172 return err 173 } 174 return nil 175 }); err != nil { 176 return nil, errors.Wrap(err, "failed to connect to the management cluster") 177 } 178 179 return c, nil 180 } 181 182 func (k *proxy) CheckClusterAvailable(ctx context.Context) error { 183 // Check if the cluster is available by creating a client to the cluster. 184 // If creating the client times out and never established we assume that 185 // the cluster does not exist or is not reachable. 186 // For the purposes of clusterctl operations non-existent clusters and 187 // non-reachable clusters can be treated as the same. 188 config, err := k.GetConfig() 189 if err != nil { 190 return err 191 } 192 193 connectBackoff := newShortConnectBackoff() 194 return retryWithExponentialBackoff(ctx, connectBackoff, func(_ context.Context) error { 195 _, err := client.New(config, client.Options{Scheme: localScheme}) 196 return err 197 }) 198 } 199 200 // ListResources lists namespaced and cluster-wide resources for a component matching the labels. Namespaced resources are only listed 201 // in the given namespaces. 202 // Please note that we are not returning resources for the component's CRD (e.g. we are not returning 203 // Certificates for cert-manager, Clusters for CAPI, AWSCluster for CAPA and so on). 204 // This is done to avoid errors when listing resources of providers which have already been deleted/scaled down to 0 replicas/with 205 // malfunctioning webhooks. 206 // For example: 207 // - The AWS provider has already been deleted, but there are still cluster-wide resources of AWSClusterControllerIdentity. 208 // - The AWSClusterControllerIdentity resources are still stored in an older version (e.g. v1alpha4, when the preferred 209 // version is v1beta1) 210 // - If we now want to delete e.g. the kubeadm bootstrap provider, we cannot list AWSClusterControllerIdentity resources 211 // as the conversion would fail, because the AWS controller hosting the conversion webhook has already been deleted. 212 // - Thus we exclude resources of other providers if we detect that ListResources is called to list resources of a provider. 213 func (k *proxy) ListResources(ctx context.Context, labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error) { 214 cs, err := k.newClientSet(ctx) 215 if err != nil { 216 return nil, err 217 } 218 219 c, err := k.NewClient(ctx) 220 if err != nil { 221 return nil, err 222 } 223 224 // Get all the API resources in the cluster. 225 resourceListBackoff := newReadBackoff() 226 var resourceList []*metav1.APIResourceList 227 if err := retryWithExponentialBackoff(ctx, resourceListBackoff, func(context.Context) error { 228 resourceList, err = cs.Discovery().ServerPreferredResources() 229 return err 230 }); err != nil { 231 return nil, errors.Wrap(err, "failed to list api resources") 232 } 233 234 // Exclude from discovery the objects from the cert-manager/provider's CRDs. 235 // Those objects are not part of the components, and they will eventually be removed when removing the CRD definition. 236 crdsToExclude := sets.Set[string]{} 237 238 crdList := &apiextensionsv1.CustomResourceDefinitionList{} 239 if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error { 240 return c.List(ctx, crdList) 241 }); err != nil { 242 return nil, errors.Wrap(err, "failed to list CRDs") 243 } 244 for _, crd := range crdList.Items { 245 component, isCoreComponent := labels[clusterctlv1.ClusterctlCoreLabel] 246 _, isProviderResource := crd.Labels[clusterv1.ProviderNameLabel] 247 if (isCoreComponent && component == clusterctlv1.ClusterctlCoreLabelCertManagerValue) || isProviderResource { 248 for _, version := range crd.Spec.Versions { 249 crdsToExclude.Insert(metav1.GroupVersionKind{ 250 Group: crd.Spec.Group, 251 Version: version.Name, 252 Kind: crd.Spec.Names.Kind, 253 }.String()) 254 } 255 } 256 } 257 258 // Select resources with list and delete methods (list is required by this method, delete by the callers of this method) 259 resourceList = discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{"list", "delete"}}, resourceList) 260 261 var ret []unstructured.Unstructured 262 for _, resourceGroup := range resourceList { 263 for _, resourceKind := range resourceGroup.APIResources { 264 // Discard the resourceKind that exists in two api groups (we are excluding one of the two groups arbitrarily). 265 if resourceGroup.GroupVersion == "extensions/v1beta1" && 266 (resourceKind.Name == "daemonsets" || resourceKind.Name == "deployments" || resourceKind.Name == "replicasets" || resourceKind.Name == "networkpolicies" || resourceKind.Name == "ingresses") { 267 continue 268 } 269 270 // Continue if the resource is an excluded CRD. 271 gv, err := schema.ParseGroupVersion(resourceGroup.GroupVersion) 272 if err != nil { 273 return nil, errors.Wrapf(err, "failed to parse GroupVersion") 274 } 275 if crdsToExclude.Has(metav1.GroupVersionKind{ 276 Group: gv.Group, 277 Version: gv.Version, 278 Kind: resourceKind.Kind, 279 }.String()) { 280 continue 281 } 282 283 // List all the object instances of this resourceKind with the given labels 284 if resourceKind.Namespaced { 285 for _, namespace := range namespaces { 286 objList, err := listObjByGVK(ctx, c, resourceGroup.GroupVersion, resourceKind.Kind, []client.ListOption{client.MatchingLabels(labels), client.InNamespace(namespace)}) 287 if err != nil { 288 return nil, err 289 } 290 ret = append(ret, objList.Items...) 291 } 292 } else { 293 objList, err := listObjByGVK(ctx, c, resourceGroup.GroupVersion, resourceKind.Kind, []client.ListOption{client.MatchingLabels(labels)}) 294 if err != nil { 295 return nil, err 296 } 297 ret = append(ret, objList.Items...) 298 } 299 } 300 } 301 return ret, nil 302 } 303 304 // GetContexts returns the list of contexts in kubeconfig which begin with prefix. 305 func (k *proxy) GetContexts(prefix string) ([]string, error) { 306 config, err := k.configLoadingRules.Load() 307 if err != nil { 308 return nil, err 309 } 310 311 var comps []string 312 for name := range config.Contexts { 313 if strings.HasPrefix(name, prefix) { 314 comps = append(comps, name) 315 } 316 } 317 318 return comps, nil 319 } 320 321 // GetResourceNames returns the list of resource names which begin with prefix. 322 func (k *proxy) GetResourceNames(ctx context.Context, groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error) { 323 client, err := k.NewClient(ctx) 324 if err != nil { 325 return nil, err 326 } 327 328 objList, err := listObjByGVK(ctx, client, groupVersion, kind, options) 329 if err != nil { 330 return nil, err 331 } 332 333 var comps []string 334 for _, item := range objList.Items { 335 name := item.GetName() 336 337 if strings.HasPrefix(name, prefix) { 338 comps = append(comps, name) 339 } 340 } 341 342 return comps, nil 343 } 344 345 func listObjByGVK(ctx context.Context, c client.Client, groupVersion, kind string, options []client.ListOption) (*unstructured.UnstructuredList, error) { 346 objList := new(unstructured.UnstructuredList) 347 objList.SetAPIVersion(groupVersion) 348 objList.SetKind(kind) 349 350 resourceListBackoff := newReadBackoff() 351 if err := retryWithExponentialBackoff(ctx, resourceListBackoff, func(ctx context.Context) error { 352 return c.List(ctx, objList, options...) 353 }); err != nil { 354 return nil, errors.Wrapf(err, "failed to list objects for the %q GroupVersionKind", objList.GroupVersionKind()) 355 } 356 357 return objList, nil 358 } 359 360 // ProxyOption defines a function that can change proxy options. 361 type ProxyOption func(p *proxy) 362 363 // InjectProxyTimeout sets the proxy timeout. 364 func InjectProxyTimeout(t time.Duration) ProxyOption { 365 return func(p *proxy) { 366 p.timeout = t 367 } 368 } 369 370 // InjectKubeconfigPaths sets the kubeconfig paths loading rules. 371 func InjectKubeconfigPaths(paths []string) ProxyOption { 372 return func(p *proxy) { 373 p.configLoadingRules.Precedence = paths 374 } 375 } 376 377 func newProxy(kubeconfig Kubeconfig, opts ...ProxyOption) Proxy { 378 // If a kubeconfig file isn't provided, find one in the standard locations. 379 rules := clientcmd.NewDefaultClientConfigLoadingRules() 380 if kubeconfig.Path != "" { 381 rules.ExplicitPath = kubeconfig.Path 382 } 383 p := &proxy{ 384 kubeconfig: kubeconfig, 385 timeout: 30 * time.Second, 386 configLoadingRules: rules, 387 } 388 389 for _, o := range opts { 390 o(p) 391 } 392 393 return p 394 } 395 396 func (k *proxy) newClientSet(ctx context.Context) (*kubernetes.Clientset, error) { 397 config, err := k.GetConfig() 398 if err != nil { 399 return nil, err 400 } 401 402 var cs *kubernetes.Clientset 403 // Nb. The operation is wrapped in a retry loop to make newClientSet more resilient to temporary connection problems. 404 connectBackoff := newConnectBackoff() 405 if err := retryWithExponentialBackoff(ctx, connectBackoff, func(_ context.Context) error { 406 var err error 407 cs, err = kubernetes.NewForConfig(config) 408 if err != nil { 409 return err 410 } 411 return nil 412 }); err != nil { 413 return nil, errors.Wrap(err, "failed to create the client-go client") 414 } 415 416 return cs, nil 417 }