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  }