sigs.k8s.io/cluster-api@v1.7.1/controlplane/kubeadm/internal/workload_cluster_coredns.go (about) 1 /* 2 Copyright 2020 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 internal 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "strings" 24 25 "github.com/blang/semver/v4" 26 "github.com/coredns/corefile-migration/migration" 27 "github.com/pkg/errors" 28 appsv1 "k8s.io/api/apps/v1" 29 corev1 "k8s.io/api/core/v1" 30 rbacv1 "k8s.io/api/rbac/v1" 31 apierrors "k8s.io/apimachinery/pkg/api/errors" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/client-go/util/retry" 34 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 35 36 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" 37 controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" 38 "sigs.k8s.io/cluster-api/internal/util/kubeadm" 39 containerutil "sigs.k8s.io/cluster-api/util/container" 40 "sigs.k8s.io/cluster-api/util/patch" 41 "sigs.k8s.io/cluster-api/util/version" 42 ) 43 44 const ( 45 corefileKey = "Corefile" 46 corefileBackupKey = "Corefile-backup" 47 coreDNSKey = "coredns" 48 coreDNSVolumeKey = "config-volume" 49 coreDNSClusterRoleName = "system:coredns" 50 51 oldCoreDNSImageName = "coredns" 52 coreDNSImageName = "coredns/coredns" 53 54 oldControlPlaneTaint = "node-role.kubernetes.io/master" // Deprecated: https://github.com/kubernetes/kubeadm/issues/2200 55 controlPlaneTaint = "node-role.kubernetes.io/control-plane" 56 ) 57 58 var ( 59 // Source: https://github.com/kubernetes/kubernetes/blob/v1.22.0-beta.1/cmd/kubeadm/app/phases/addons/dns/manifests.go#L178-L207 60 coreDNS181PolicyRules = []rbacv1.PolicyRule{ 61 { 62 Verbs: []string{"list", "watch"}, 63 APIGroups: []string{""}, 64 Resources: []string{"endpoints", "services", "pods", "namespaces"}, 65 }, 66 { 67 Verbs: []string{"get"}, 68 APIGroups: []string{""}, 69 Resources: []string{"nodes"}, 70 }, 71 { 72 Verbs: []string{"list", "watch"}, 73 APIGroups: []string{"discovery.k8s.io"}, 74 Resources: []string{"endpointslices"}, 75 }, 76 } 77 ) 78 79 type coreDNSMigrator interface { 80 Migrate(currentVersion string, toVersion string, corefile string, deprecations bool) (string, error) 81 } 82 83 // CoreDNSMigrator is a shim that can be used to migrate CoreDNS files from one version to another. 84 type CoreDNSMigrator struct{} 85 86 // Migrate calls the CoreDNS migration library to migrate a corefile. 87 func (c *CoreDNSMigrator) Migrate(fromCoreDNSVersion, toCoreDNSVersion, corefile string, deprecations bool) (string, error) { 88 return migration.Migrate(fromCoreDNSVersion, toCoreDNSVersion, corefile, deprecations) 89 } 90 91 type coreDNSInfo struct { 92 Corefile string 93 Deployment *appsv1.Deployment 94 95 FromImageTag string 96 ToImageTag string 97 98 CurrentMajorMinorPatch string 99 TargetMajorMinorPatch string 100 101 FromImage string 102 ToImage string 103 } 104 105 // UpdateCoreDNS updates the kubeadm configmap, coredns corefile and coredns 106 // deployment. 107 func (w *Workload) UpdateCoreDNS(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane, version semver.Version) error { 108 // Return early if we've been asked to skip CoreDNS upgrades entirely. 109 if _, ok := kcp.Annotations[controlplanev1.SkipCoreDNSAnnotation]; ok { 110 return nil 111 } 112 113 // Return early if the configuration is nil. 114 if kcp.Spec.KubeadmConfigSpec.ClusterConfiguration == nil { 115 return nil 116 } 117 118 clusterConfig := kcp.Spec.KubeadmConfigSpec.ClusterConfiguration 119 120 // Get the CoreDNS info needed for the upgrade. 121 info, err := w.getCoreDNSInfo(ctx, clusterConfig, version) 122 if err != nil { 123 // Return early if we get a not found error, this can happen if any of the CoreDNS components 124 // cannot be found, e.g. configmap, deployment. 125 if apierrors.IsNotFound(errors.Cause(err)) { 126 return nil 127 } 128 return err 129 } 130 131 // Update the cluster role independent of image change. Kubernetes may get updated 132 // to v1.22 which requires updating the cluster role without image changes. 133 if err := w.updateCoreDNSClusterRole(ctx, version, info); err != nil { 134 return err 135 } 136 137 // Return early if the from/to image is the same. 138 if info.FromImage == info.ToImage { 139 return nil 140 } 141 142 // Validate the image tag. 143 if err := validateCoreDNSImageTag(info.FromImageTag, info.ToImageTag); err != nil { 144 return errors.Wrapf(err, "failed to validate CoreDNS") 145 } 146 147 // Perform the upgrade. 148 if err := w.UpdateClusterConfiguration(ctx, version, w.updateCoreDNSImageInfoInKubeadmConfigMap(&clusterConfig.DNS)); err != nil { 149 return err 150 } 151 if err := w.updateCoreDNSCorefile(ctx, info); err != nil { 152 return err 153 } 154 155 if err := w.updateCoreDNSDeployment(ctx, info, version); err != nil { 156 return errors.Wrap(err, "unable to update coredns deployment") 157 } 158 return nil 159 } 160 161 // getCoreDNSInfo returns all necessary coredns based information. 162 func (w *Workload) getCoreDNSInfo(ctx context.Context, clusterConfig *bootstrapv1.ClusterConfiguration, version semver.Version) (*coreDNSInfo, error) { 163 // Get the coredns configmap and corefile. 164 key := ctrlclient.ObjectKey{Name: coreDNSKey, Namespace: metav1.NamespaceSystem} 165 cm, err := w.getConfigMap(ctx, key) 166 if err != nil { 167 return nil, errors.Wrapf(err, "error getting %v config map from target cluster", key) 168 } 169 corefile, ok := cm.Data[corefileKey] 170 if !ok { 171 return nil, errors.New("unable to find the CoreDNS Corefile data") 172 } 173 174 // Get the current CoreDNS deployment. 175 deployment := &appsv1.Deployment{} 176 if err := w.Client.Get(ctx, key, deployment); err != nil { 177 return nil, errors.Wrapf(err, "unable to get %v deployment from target cluster", key) 178 } 179 180 var container *corev1.Container 181 for _, c := range deployment.Spec.Template.Spec.Containers { 182 if c.Name == coreDNSKey { 183 container = c.DeepCopy() 184 break 185 } 186 } 187 if container == nil { 188 return nil, errors.Errorf("failed to update coredns deployment: deployment spec has no %q container", coreDNSKey) 189 } 190 191 // Parse container image. 192 parsedImage, err := containerutil.ImageFromString(container.Image) 193 if err != nil { 194 return nil, errors.Wrapf(err, "unable to parse %q deployment image", container.Image) 195 } 196 197 // Handle imageRepository. 198 toImageRepository := parsedImage.Repository 199 // Overwrite the image repository if a value was explicitly set or an upgrade is required. 200 if imageRegistryRepository := ImageRepositoryFromClusterConfig(clusterConfig, version); imageRegistryRepository != "" { 201 if imageRegistryRepository == kubeadm.DefaultImageRepository { 202 // Only patch to DefaultImageRepository if OldDefaultImageRepository is set as prefix. 203 if strings.HasPrefix(toImageRepository, kubeadm.OldDefaultImageRepository) { 204 // Ensure to keep the repository subpaths when patching from OldDefaultImageRepository to new DefaultImageRepository. 205 toImageRepository = strings.TrimSuffix(imageRegistryRepository+strings.TrimPrefix(toImageRepository, kubeadm.OldDefaultImageRepository), "/") 206 } 207 } else { 208 toImageRepository = strings.TrimSuffix(imageRegistryRepository, "/") 209 } 210 } 211 if clusterConfig.DNS.ImageRepository != "" { 212 toImageRepository = strings.TrimSuffix(clusterConfig.DNS.ImageRepository, "/") 213 } 214 215 // Handle imageTag. 216 if parsedImage.Tag == "" { 217 return nil, errors.Errorf("failed to update coredns deployment: does not have a valid image tag: %q", container.Image) 218 } 219 currentMajorMinorPatch, err := extractImageVersion(parsedImage.Tag) 220 if err != nil { 221 return nil, err 222 } 223 toImageTag := parsedImage.Tag 224 if clusterConfig.DNS.ImageTag != "" { 225 toImageTag = clusterConfig.DNS.ImageTag 226 } 227 targetMajorMinorPatch, err := extractImageVersion(toImageTag) 228 if err != nil { 229 return nil, err 230 } 231 232 // Handle the renaming of the upstream image from: 233 // * "registry.k8s.io/coredns" to "registry.k8s.io/coredns/coredns" or 234 // * "k8s.gcr.io/coredns" to "k8s.gcr.io/coredns/coredns" 235 toImageName := parsedImage.Name 236 if (toImageRepository == kubeadm.OldDefaultImageRepository || toImageRepository == kubeadm.DefaultImageRepository) && 237 toImageName == oldCoreDNSImageName && targetMajorMinorPatch.GTE(semver.MustParse("1.8.0")) { 238 toImageName = coreDNSImageName 239 } 240 241 return &coreDNSInfo{ 242 Corefile: corefile, 243 Deployment: deployment, 244 CurrentMajorMinorPatch: currentMajorMinorPatch.String(), 245 TargetMajorMinorPatch: targetMajorMinorPatch.String(), 246 FromImageTag: parsedImage.Tag, 247 ToImageTag: toImageTag, 248 FromImage: container.Image, 249 ToImage: fmt.Sprintf("%s/%s:%s", toImageRepository, toImageName, toImageTag), 250 }, nil 251 } 252 253 // updateCoreDNSDeployment will patch the deployment image to the 254 // imageRepo:imageTag in the KCP dns. It will also ensure the volume of the 255 // deployment uses the Corefile key of the coredns configmap. 256 func (w *Workload) updateCoreDNSDeployment(ctx context.Context, info *coreDNSInfo, kubernetesVersion semver.Version) error { 257 helper, err := patch.NewHelper(info.Deployment, w.Client) 258 if err != nil { 259 return err 260 } 261 // Form the final image before issuing the patch. 262 patchCoreDNSDeploymentImage(info.Deployment, info.ToImage) 263 264 // Flip the deployment volume back to Corefile (from the backup key). 265 patchCoreDNSDeploymentVolume(info.Deployment, corefileBackupKey, corefileKey) 266 267 // Patch the tolerations according to the Kubernetes Version. 268 patchCoreDNSDeploymentTolerations(info.Deployment, kubernetesVersion) 269 return helper.Patch(ctx, info.Deployment) 270 } 271 272 // updateCoreDNSImageInfoInKubeadmConfigMap updates the kubernetes version in the kubeadm config map. 273 func (w *Workload) updateCoreDNSImageInfoInKubeadmConfigMap(dns *bootstrapv1.DNS) func(*bootstrapv1.ClusterConfiguration) { 274 return func(c *bootstrapv1.ClusterConfiguration) { 275 c.DNS.ImageRepository = dns.ImageRepository 276 c.DNS.ImageTag = dns.ImageTag 277 } 278 } 279 280 // updateCoreDNSClusterRole updates the CoreDNS ClusterRole when necessary. 281 // CoreDNS >= 1.8.1 uses EndpointSlices. kubeadm < 1.22 doesn't include the EndpointSlice rule in the CoreDNS ClusterRole. 282 // To support Kubernetes clusters >= 1.22 (which have been initialized with kubeadm < 1.22) with CoreDNS versions >= 1.8.1 283 // we have to update the ClusterRole accordingly. 284 func (w *Workload) updateCoreDNSClusterRole(ctx context.Context, kubernetesVersion semver.Version, info *coreDNSInfo) error { 285 // Do nothing for Kubernetes < 1.22. 286 if version.Compare(kubernetesVersion, semver.Version{Major: 1, Minor: 22, Patch: 0}, version.WithoutPreReleases()) < 0 { 287 return nil 288 } 289 290 // Do nothing for CoreDNS < 1.8.1. 291 targetCoreDNSVersion, err := extractImageVersion(info.ToImageTag) 292 if err != nil { 293 return err 294 } 295 if targetCoreDNSVersion.LT(semver.Version{Major: 1, Minor: 8, Patch: 1}) { 296 return nil 297 } 298 299 sourceCoreDNSVersion, err := extractImageVersion(info.FromImageTag) 300 if err != nil { 301 return err 302 } 303 // Do nothing for Kubernetes > 1.22 and sourceCoreDNSVersion >= 1.8.1. 304 // With those versions we know that the ClusterRole has already been updated, 305 // as there must have been a previous upgrade to Kubernetes 1.22 306 // (Kubernetes minor versions cannot be skipped) and to CoreDNS >= v1.8.1. 307 if kubernetesVersion.GTE(semver.Version{Major: 1, Minor: 23, Patch: 0}) && 308 sourceCoreDNSVersion.GTE(semver.Version{Major: 1, Minor: 8, Patch: 1}) { 309 return nil 310 } 311 312 key := ctrlclient.ObjectKey{Name: coreDNSClusterRoleName, Namespace: metav1.NamespaceSystem} 313 return retry.RetryOnConflict(retry.DefaultBackoff, func() error { 314 currentClusterRole := &rbacv1.ClusterRole{} 315 if err := w.Client.Get(ctx, key, currentClusterRole); err != nil { 316 return fmt.Errorf("failed to get ClusterRole %q", coreDNSClusterRoleName) 317 } 318 319 if !semanticDeepEqualPolicyRules(currentClusterRole.Rules, coreDNS181PolicyRules) { 320 currentClusterRole.Rules = coreDNS181PolicyRules 321 if err := w.Client.Update(ctx, currentClusterRole); err != nil { 322 return errors.Wrapf(err, "failed to update ClusterRole %q", coreDNSClusterRoleName) 323 } 324 } 325 return nil 326 }) 327 } 328 329 func semanticDeepEqualPolicyRules(r1, r2 []rbacv1.PolicyRule) bool { 330 return reflect.DeepEqual(generateClusterRolePolicies(r1), generateClusterRolePolicies(r2)) 331 } 332 333 // generateClusterRolePolicies generates a nested map with the full data of an array of PolicyRules so it can 334 // be compared with reflect.DeepEqual. If we would use reflect.DeepEqual directly on the PolicyRule array, 335 // differences in the order of the array elements would lead to the arrays not being considered equal. 336 func generateClusterRolePolicies(policyRules []rbacv1.PolicyRule) map[string]map[string]map[string]struct{} { 337 policies := map[string]map[string]map[string]struct{}{} 338 for _, policyRule := range policyRules { 339 for _, apiGroup := range policyRule.APIGroups { 340 if _, ok := policies[apiGroup]; !ok { 341 policies[apiGroup] = map[string]map[string]struct{}{} 342 } 343 344 for _, resource := range policyRule.Resources { 345 if _, ok := policies[apiGroup][resource]; !ok { 346 policies[apiGroup][resource] = map[string]struct{}{} 347 } 348 349 for _, verb := range policyRule.Verbs { 350 policies[apiGroup][resource][verb] = struct{}{} 351 } 352 } 353 } 354 } 355 return policies 356 } 357 358 // updateCoreDNSCorefile migrates the coredns corefile if there is an increase 359 // in version number. It also creates a corefile backup and patches the 360 // deployment to point to the backup corefile before migrating. 361 func (w *Workload) updateCoreDNSCorefile(ctx context.Context, info *coreDNSInfo) error { 362 // Run the CoreDNS migration tool first because if it cannot migrate the 363 // corefile, then there's no point in continuing further. 364 updatedCorefile, err := w.CoreDNSMigrator.Migrate(info.CurrentMajorMinorPatch, info.TargetMajorMinorPatch, info.Corefile, false) 365 if err != nil { 366 return errors.Wrap(err, "unable to migrate CoreDNS corefile") 367 } 368 369 // First we backup the Corefile by backing it up. 370 if err := w.Client.Update(ctx, &corev1.ConfigMap{ 371 ObjectMeta: metav1.ObjectMeta{ 372 Name: coreDNSKey, 373 Namespace: metav1.NamespaceSystem, 374 }, 375 Data: map[string]string{ 376 corefileKey: info.Corefile, 377 corefileBackupKey: info.Corefile, 378 }, 379 }); err != nil { 380 return errors.Wrap(err, "unable to update CoreDNS config map with backup Corefile") 381 } 382 383 // Patching the coredns deployment to point to the Corefile-backup 384 // contents before performing the migration. 385 helper, err := patch.NewHelper(info.Deployment, w.Client) 386 if err != nil { 387 return err 388 } 389 patchCoreDNSDeploymentVolume(info.Deployment, corefileKey, corefileBackupKey) 390 if err := helper.Patch(ctx, info.Deployment); err != nil { 391 return err 392 } 393 394 if err := w.Client.Update(ctx, &corev1.ConfigMap{ 395 ObjectMeta: metav1.ObjectMeta{ 396 Name: coreDNSKey, 397 Namespace: metav1.NamespaceSystem, 398 }, 399 Data: map[string]string{ 400 corefileKey: updatedCorefile, 401 corefileBackupKey: info.Corefile, 402 }, 403 }); err != nil { 404 return errors.Wrap(err, "unable to update CoreDNS config map") 405 } 406 407 return nil 408 } 409 410 func patchCoreDNSDeploymentVolume(deployment *appsv1.Deployment, fromKey, toKey string) { 411 for _, volume := range deployment.Spec.Template.Spec.Volumes { 412 if volume.Name == coreDNSVolumeKey && volume.ConfigMap != nil && volume.ConfigMap.Name == coreDNSKey { 413 for i, item := range volume.ConfigMap.Items { 414 if item.Key == fromKey || item.Key == toKey { 415 volume.ConfigMap.Items[i].Key = toKey 416 } 417 } 418 } 419 } 420 } 421 422 func patchCoreDNSDeploymentImage(deployment *appsv1.Deployment, image string) { 423 containers := deployment.Spec.Template.Spec.Containers 424 for idx, c := range containers { 425 if c.Name == coreDNSKey { 426 containers[idx].Image = image 427 } 428 } 429 } 430 431 // patchCoreDNSDeploymentTolerations patches the CoreDNS Deployment to make sure 432 // it has the right control plane tolerations. 433 // Kubernetes nodes created with kubeadm have the following taints depending on version: 434 // * -v1.23: only old taint is set 435 // * v1.24: both taints are set 436 // * v1.25+: only new taint is set 437 // To be absolutely safe this func will ensure that both tolerations are present 438 // for Kubernetes < v1.26.0. Starting with v1.26.0 we will only set the new toleration. 439 func patchCoreDNSDeploymentTolerations(deployment *appsv1.Deployment, kubernetesVersion semver.Version) { 440 // We always add the toleration for the new control plane taint. 441 tolerations := []corev1.Toleration{ 442 { 443 Key: controlPlaneTaint, 444 Effect: corev1.TaintEffectNoSchedule, 445 }, 446 } 447 448 // We add the toleration for the old control plane taint for Kubernetes < v1.26.0. 449 if kubernetesVersion.LT(semver.Version{Major: 1, Minor: 26, Patch: 0}) { 450 tolerations = append(tolerations, corev1.Toleration{ 451 Key: oldControlPlaneTaint, 452 Effect: corev1.TaintEffectNoSchedule, 453 }) 454 } 455 456 // Add all other already existing tolerations. 457 for _, currentToleration := range deployment.Spec.Template.Spec.Tolerations { 458 // Skip the old control plane toleration as it has been already added above, 459 // for Kubernetes < v1.26.0. 460 if currentToleration.Key == oldControlPlaneTaint && 461 currentToleration.Effect == corev1.TaintEffectNoSchedule && 462 currentToleration.Value == "" { 463 continue 464 } 465 466 // Skip the new control plane toleration as it has been already added above. 467 if currentToleration.Key == controlPlaneTaint && 468 currentToleration.Effect == corev1.TaintEffectNoSchedule && 469 currentToleration.Value == "" { 470 continue 471 } 472 473 tolerations = append(tolerations, currentToleration) 474 } 475 476 deployment.Spec.Template.Spec.Tolerations = tolerations 477 } 478 479 func extractImageVersion(tag string) (semver.Version, error) { 480 ver, err := version.ParseMajorMinorPatchTolerant(tag) 481 if err != nil { 482 return semver.Version{}, errors.Wrapf(err, "error parsing semver from %q", tag) 483 } 484 return ver, nil 485 } 486 487 // validateCoreDNSImageTag returns error if the versions don't meet requirements. 488 // Some of the checks come from 489 // https://github.com/coredns/corefile-migration/blob/v1.0.6/migration/migrate.go#L414 490 func validateCoreDNSImageTag(fromTag, toTag string) error { 491 from, err := version.ParseMajorMinorPatchTolerant(fromTag) 492 if err != nil { 493 return errors.Wrapf(err, "failed to parse CoreDNS current version %q", fromTag) 494 } 495 to, err := version.ParseMajorMinorPatchTolerant(toTag) 496 if err != nil { 497 return errors.Wrapf(err, "failed to parse CoreDNS target version %q", toTag) 498 } 499 // make sure that the version we're upgrading to is greater or equal to the current one, 500 if x := from.Compare(to); x > 0 { 501 return fmt.Errorf("toVersion %q must be greater than or equal to fromVersion %q", toTag, fromTag) 502 } 503 // check if the from version is even in the list of coredns versions 504 if _, ok := migration.Versions[fmt.Sprintf("%d.%d.%d", from.Major, from.Minor, from.Patch)]; !ok { 505 return fmt.Errorf("fromVersion %q is not a compatible coredns version", from.String()) 506 } 507 return nil 508 }