Repository: gruntwork-io/kubergrunt
Pull request #98 (2020-10-20 23:03:59)

Add support for syncing components of k8s 1.18
This adds support for Kubernetes version 1.18 to the `sync-core-components` command. Note that there was a config deprecation in `coredns` with `1.7.0`, which is the version that is deployed with k8s 1.18. This adds the logic to handle that.

See https://github.com/aws/containers-roadmap/issues/1115 for more details.


Patch:
diff --git a/eks/sync.go b/eks/sync.go
index 7a92323..1308e88 100644
--- a/eks/sync.go
+++ b/eks/sync.go
@@ -9,12 +9,14 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/blang/semver/v4"
 	"github.com/gruntwork-io/gruntwork-cli/collections"
 	"github.com/gruntwork-io/gruntwork-cli/errors"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	k8stypes "k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/kubernetes"
@@ -27,9 +29,10 @@ import (
 
 var (
 	// NOTE: Ensure that there is an entry for each supported version in the following tables.
-	supportedVersions = []string{"1.17", "1.16", "1.15", "1.14"}
+	supportedVersions = []string{"1.18", "1.17", "1.16", "1.15", "1.14"}
 
 	coreDNSVersionLookupTable = map[string]string{
+		"1.18": "1.7.0-eksbuild.1",
 		"1.17": "1.6.6-eksbuild.1",
 		"1.16": "1.6.6-eksbuild.1",
 		"1.15": "1.6.6-eksbuild.1",
@@ -37,6 +40,7 @@ var (
 	}
 
 	kubeProxyVersionLookupTable = map[string]string{
+		"1.18": "1.18.8-eksbuild.1",
 		"1.17": "1.17.9-eksbuild.1",
 		"1.16": "1.16.13-eksbuild.1",
 		"1.15": "1.15.11-eksbuild.1",
@@ -44,6 +48,7 @@ var (
 	}
 
 	amazonVPCCNIVersionLookupTable = map[string]string{
+		"1.18": "1.7.5",
 		"1.17": "1.7.5",
 		"1.16": "1.7.5",
 		"1.15": "1.7.5",
@@ -52,9 +57,11 @@ var (
 )
 
 const (
-	componentNamespace     = "kube-system"
-	kubeProxyDaemonSetName = "kube-proxy"
-	corednsDeploymentName  = "coredns"
+	componentNamespace        = "kube-system"
+	kubeProxyDaemonSetName    = "kube-proxy"
+	corednsDeploymentName     = "coredns"
+	corednsConfigMapName      = "coredns"
+	corednsConfigMapConfigKey = "Corefile"
 )
 
 // SyncClusterComponents will perform the steps described in
@@ -223,10 +230,32 @@ func upgradeCoreDNS(
 		return nil
 	}
 
+	logger.Info("Confirming compatibility of coredns configuration with latest version.")
+	corednsConfigMap, err := getCorednsConfigMap(clientset)
+	if err != nil {
+		return err
+	}
+	// Need to check config for backwards incompatibility if updating to version >= 1.7.0. The keyword upstream was
+	// removed in 1.7 series of coredns, but is used in earlier versions.
+	compareVal, err := semverStringCompare(coreDNSVersion, "1.7.0-eksbuild.1")
+	if err != nil {
+		return err
+	}
+	// Looking for 1 or 0 here, since we want to know when coreDNSVersion is >= 1.7.0
+	if compareVal >= 0 && strings.Contains(corednsConfigMap.Data[corednsConfigMapConfigKey], "upstream") {
+		logger.Info("Detected old configuration for coredns. Reformatting configuration to latest.")
+		if err := removeUpstreamKeywordFromCorednsConfigMap(clientset, corednsConfigMap); err != nil {
+			return err
+		}
+	} else {
+		logger.Info("Configuration for coredns is up to date. Skipping configuration reformat.")
+	}
+
 	logger.Infof("Upgrading current deployed version of coredns (%s) to match expected version (%s).", currentImage, targetImage)
 	if err := updateCoreDNSDeploymentImage(clientset, targetImage); err != nil {
 		return err
 	}
+
 	if shouldWait {
 		logger.Info("Waiting until new image for coredns is rolled out.")
 		// Ideally we will implement the following routine using the raw client-go library, but implementing this
@@ -283,6 +312,16 @@ func updateCoreDNSDeploymentImage(clientset *kubernetes.Clientset, targetImage s
 	return nil
 }
 
+// getCorednsConfigMap returns the configmap object containing the coredns configuration for the EKS cluster.
+func getCorednsConfigMap(clientset *kubernetes.Clientset) (*corev1.ConfigMap, error) {
+	configMapAPI := clientset.CoreV1().ConfigMaps(componentNamespace)
+	configMap, err := configMapAPI.Get(context.Background(), corednsConfigMapName, metav1.GetOptions{})
+	if err != nil {
+		return nil, errors.WithStackTrace(err)
+	}
+	return configMap, nil
+}
+
 // getBaseURLForVPCCNIManifest returns the base github URL where the manifest for the VPC CNI is located given the
 // requested version.
 func getBaseURLForVPCCNIManifest(vpcCNIVersion string) (string, error) {
@@ -368,3 +407,41 @@ func downloadVPCCNIManifestAndUpdateRegion(url string, fpath string, region stri
 	}
 	return nil
 }
+
+// semverStringCompare compares two semantic version strings. Returns:
+// -1 if v1 < v2
+// 0 if v1 == v2
+// 1 if v1 > v2
+// Note that in semantic versioning, annotations are considered an older version.
+// E.g., 1.7.0-eksbuild.1 is considered less than 1.7.0.
+func semverStringCompare(v1 string, v2 string) (int, error) {
+	parsedV1, err := semver.Make(v1)
+	if err != nil {
+		return 0, errors.WithStackTrace(err)
+	}
+	parsedV2, err := semver.Make(v2)
+	if err != nil {
+		return 0, errors.WithStackTrace(err)
+	}
+	return parsedV1.Compare(parsedV2), nil
+}
+
+// removeUpstreamKeywordFromCorednsConfigMap removes the upstream keyword from the CoreDNS ConfigMap config data and
+// saves it on the cluster.
+func removeUpstreamKeywordFromCorednsConfigMap(clientset *kubernetes.Clientset, corednsConfigMap *corev1.ConfigMap) error {
+	configData := corednsConfigMap.Data[corednsConfigMapConfigKey]
+
+	// Remove the line containing "upstream". Since this can appear in any nested block, we use regex to handle the
+	// whitespace during the removal.
+	lookForUpstreamRE, err := regexp.Compile(`[\s]+upstream[\t\r\n]+`)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+	newConfigData := lookForUpstreamRE.ReplaceAllString(configData, "\n")
+	corednsConfigMap.Data[corednsConfigMapConfigKey] = newConfigData
+
+	// Now save the new configmap
+	configMapAPI := clientset.CoreV1().ConfigMaps(corednsConfigMap.ObjectMeta.Namespace)
+	_, err = configMapAPI.Update(context.Background(), corednsConfigMap, metav1.UpdateOptions{})
+	return errors.WithStackTrace(err)
+}


Test patch:
diff --git a/eks/sync_test.go b/eks/sync_test.go
index d504fb5..95c44ec 100644
--- a/eks/sync_test.go
+++ b/eks/sync_test.go
@@ -1,13 +1,19 @@
 package eks
 
 import (
+	"context"
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
+	"github.com/gruntwork-io/terratest/modules/k8s"
+	"github.com/gruntwork-io/terratest/modules/random"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func TestGetBaseURLForVPCCNIManifest(t *testing.T) {
@@ -42,3 +48,166 @@ func TestDownloadVPCCNIManifestAndUpdateRegion(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, expectedF, actualF)
 }
+
+func TestSemverStringCompare(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name           string
+		v1             string
+		v2             string
+		expectedResult int
+		expectErr      bool
+	}{
+		{
+			"GreaterPatch",
+			"1.7.10",
+			"1.7.9",
+			1,
+			false,
+		},
+		{
+			"GreaterMinor",
+			"1.7.10",
+			"1.6.99999",
+			1,
+			false,
+		},
+		{
+			"LesserPatch",
+			"1.7.9",
+			"1.7.10",
+			-1,
+			false,
+		},
+		{
+			"LesserMinor",
+			"1.6.99999",
+			"1.7.10",
+			-1,
+			false,
+		},
+		{
+			"Equal",
+			"1.7.0",
+			"1.7.0",
+			0,
+			false,
+		},
+		{
+			"CompareAnnotations",
+			"1.7.0-eksbuild.1",
+			"1.7.0-eksbuild.2",
+			-1,
+			false,
+		},
+		{
+			"SameVersionWithAnnotation",
+			"1.7.0",
+			"1.7.0-eksbuild.1",
+			1,
+			false,
+		},
+		{
+			"OlderVersionWithAnnotation",
+			"1.7.0",
+			"1.6.0-eksbuild.1",
+			1,
+			false,
+		},
+		{
+			"BadV1",
+			"foo",
+			"1.7.0",
+			0,
+			true,
+		},
+		{
+			"BadV2",
+			"1.7.0",
+			"foo",
+			0,
+			true,
+		},
+	}
+
+	for _, tc := range testCases {
+		// Capture range variable
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			compareVal, err := semverStringCompare(tc.v1, tc.v2)
+			if tc.expectErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, compareVal, tc.expectedResult)
+			}
+		})
+	}
+}
+
+func TestRemoveUpstreamKeywordFromCorednsConfigMap(t *testing.T) {
+	t.Parallel()
+
+	namespace := strings.ToLower(random.UniqueId())
+
+	// Create a new namespace, and defer clean up steps.
+	kubectlOptions := k8s.NewKubectlOptions("", "", namespace)
+	defer k8s.DeleteNamespace(t, kubectlOptions, namespace)
+	k8s.CreateNamespace(t, kubectlOptions, namespace)
+
+	// Create the test configmap with coredns config data that needs to be updated.
+	clientset, err := k8s.GetKubernetesClientFromOptionsE(t, kubectlOptions)
+	require.NoError(t, err)
+	configmapAPI := clientset.CoreV1().ConfigMaps(namespace)
+	configMap := &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{Name: corednsConfigMapName},
+		Data:       map[string]string{corednsConfigMapConfigKey: sampleConfigData},
+	}
+	_, err = configmapAPI.Create(context.Background(), configMap, metav1.CreateOptions{})
+	require.NoError(t, err)
+
+	// Apply updates
+	testConfigMap, err := configmapAPI.Get(context.Background(), corednsConfigMapName, metav1.GetOptions{})
+	require.NoError(t, err)
+	require.NoError(t, removeUpstreamKeywordFromCorednsConfigMap(clientset, testConfigMap))
+
+	// Get the latest state of the configmap and verify the data is updated correctly
+	testConfigMapUpdated, err := configmapAPI.Get(context.Background(), corednsConfigMapName, metav1.GetOptions{})
+	require.NoError(t, err)
+	assert.Equal(t, expectedSampleConfigData, testConfigMapUpdated.Data[corednsConfigMapConfigKey])
+}
+
+const sampleConfigData = `.:53 {
+    errors
+    health
+    kubernetes cluster.local in-addr.arpa ip6.arpa {
+      pods insecure
+      upstream
+      fallthrough in-addr.arpa ip6.arpa
+    }
+    prometheus :9153
+    forward . /etc/resolv.conf
+    cache 30
+    loop
+    reload
+    loadbalance
+}
+`
+
+const expectedSampleConfigData = `.:53 {
+    errors
+    health
+    kubernetes cluster.local in-addr.arpa ip6.arpa {
+      pods insecure
+      fallthrough in-addr.arpa ip6.arpa
+    }
+    prometheus :9153
+    forward . /etc/resolv.conf
+    cache 30
+    loop
+    reload
+    loadbalance
+}
+`