Repository: containous/maesh
Pull request #462 (2020-03-10 11:04:27)

Refactor `prepare` package for external use
## Goal of this PR

This pull request introduces multiple changes to the prepare package, in order to make it externally usable.

It is a follow-up to the externalization of the prepare code into a package, in PR #454.

The changes are:

* The `CheckCluster` method is renamed to `CheckDNSProvider` to be more explicit on its actual purpose/logic.
* The `CheckCluster` method now returns an enum representing the DNS provider if it is valid, instead of returning a boolean. This is necessary to use the newly split DNS patching code.
* Bringing back the bugfix previously introduced in #349 that was never merged.
* Functions are split into smaller logic blocks, and some more helpers are added (`isPatched`, for example, that checks whether a deployment has the `maesh-patched` label in its metadata).
* The code that gets the CoreDNS ConfigMap has been revamped to be more reliable, it now looks specifically for a ConfigMap containing a `Corefile`, instead of just picking the first one it finds.
* In the KubeDNS ConfigMap getter code, a similar change has been made to no longer access `Volumes[0]` despite `Volumes` potentially being `nil`.

The new methods are unit-tested using the client mock and YAML fixtures.

### Logging is no longer a part of this package, since it would make the package unusable from the outside. This PR does not include adding logs in the `maesh` and `prepare` commands to match the previous behavior.


Patch:
diff --git a/cmd/maesh/maesh.go b/cmd/maesh/maesh.go
index ab3f014..29bcfa8 100644
--- a/cmd/maesh/maesh.go
+++ b/cmd/maesh/maesh.go
@@ -68,9 +68,11 @@ func maeshCommand(iConfig *cmd.MaeshConfiguration) error {
 		return fmt.Errorf("error building clients: %v", err)
 	}
 
-	prepare := preparepkg.NewPrepare(log, clients)
-	if err = prepare.CheckCluster(); err != nil {
-		return fmt.Errorf("error during cluster check: %v", err)
+	prep := preparepkg.NewPrepare(log, clients)
+
+	_, err = prep.CheckDNSProvider()
+	if err != nil {
+		return fmt.Errorf("no valid DNS provider found: %v", err)
 	}
 
 	minHTTPPort := int32(5000)
diff --git a/cmd/prepare/prepare.go b/cmd/prepare/prepare.go
index df7d220..74aa185 100644
--- a/cmd/prepare/prepare.go
+++ b/cmd/prepare/prepare.go
@@ -45,7 +45,8 @@ func prepareCommand(pConfig *cmd.PrepareConfig) error {
 
 	p := prepare.NewPrepare(log, clients)
 
-	if err = p.CheckCluster(); err != nil {
+	provider, err := p.CheckDNSProvider()
+	if err != nil {
 		return fmt.Errorf("error during cluster check: %v", err)
 	}
 
@@ -53,8 +54,15 @@ func prepareCommand(pConfig *cmd.PrepareConfig) error {
 		return fmt.Errorf("error during informer check: %v, this can be caused by pre-existing objects in your cluster that do not conform to the spec", err)
 	}
 
-	if err = p.PatchDNS(pConfig.Namespace, pConfig.ClusterDomain); err != nil {
-		return fmt.Errorf("error initializing cluster: %v", err)
+	switch provider {
+	case prepare.CoreDNS:
+		if err := p.ConfigureCoreDNS(pConfig.ClusterDomain, pConfig.Namespace); err != nil {
+			return fmt.Errorf("unable to configure CoreDNS: %v", err)
+		}
+	case prepare.KubeDNS:
+		if err := p.ConfigureKubeDNS(); err != nil {
+			return fmt.Errorf("unable to configure KubeDNS: %v", err)
+		}
 	}
 
 	return nil
diff --git a/pkg/prepare/fixtures/checkdnsprovider_kubedns.yaml b/pkg/prepare/fixtures/checkdnsprovider_kubedns.yaml
new file mode 100644
index 0000000..2b6f1b5
--- /dev/null
+++ b/pkg/prepare/fixtures/checkdnsprovider_kubedns.yaml
@@ -0,0 +1,11 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: kube-dns
+  namespace: kube-system
+spec:
+  template:
+    spec:
+      containers:
+        - name: titi
+          image: titi/toto:latest
diff --git a/pkg/prepare/fixtures/checkdnsprovider_no_provider.yaml b/pkg/prepare/fixtures/checkdnsprovider_no_provider.yaml
new file mode 100644
index 0000000..d36992b
--- /dev/null
+++ b/pkg/prepare/fixtures/checkdnsprovider_no_provider.yaml
@@ -0,0 +1,11 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: notaprovider
+  namespace: kube-system
+spec:
+  template:
+    spec:
+      containers:
+        - name: titi
+          image: titi/toto:latest
diff --git a/pkg/prepare/fixtures/checkdnsprovider_supported_version.yaml b/pkg/prepare/fixtures/checkdnsprovider_supported_version.yaml
new file mode 100644
index 0000000..1a5dfd7
--- /dev/null
+++ b/pkg/prepare/fixtures/checkdnsprovider_supported_version.yaml
@@ -0,0 +1,13 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coredns
+  namespace: kube-system
+spec:
+  template:
+    spec:
+      containers:
+        - name: coredns
+          image: image-registry.canonical.com:5000/cdk/coredns/coredns-amd64:1.6.9
+        - name: titi
+          image: titi/toto:latest
diff --git a/pkg/prepare/fixtures/checkdnsprovider_unsupported_version.yaml b/pkg/prepare/fixtures/checkdnsprovider_unsupported_version.yaml
new file mode 100644
index 0000000..f111ebe
--- /dev/null
+++ b/pkg/prepare/fixtures/checkdnsprovider_unsupported_version.yaml
@@ -0,0 +1,13 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coredns
+  namespace: kube-system
+spec:
+  template:
+    spec:
+      containers:
+        - name: coredns
+          image: coredns-amd64:0.4.2
+        - name: titi
+          image: titi/toto:latest
diff --git a/pkg/prepare/fixtures/configurecoredns_already_patched.yaml b/pkg/prepare/fixtures/configurecoredns_already_patched.yaml
new file mode 100644
index 0000000..33738e0
--- /dev/null
+++ b/pkg/prepare/fixtures/configurecoredns_already_patched.yaml
@@ -0,0 +1,64 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coredns
+  namespace: kube-system
+spec:
+  template:
+    spec:
+      volumes:
+        - configMap:
+            name: "other-cfgmap"
+        - configMap:
+            name: "coredns-cfgmap"
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: other-cfgmap
+  namespace: kube-system
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: coredns-cfgmap
+  namespace: kube-system
+  labels:
+    maesh-patched: "true"
+data:
+  Corefile: |
+    .:53 {
+            errors
+            health {
+                lameduck 5s
+            }
+            ready
+            kubernetes {{ pillar['dns_domain'] }} in-addr.arpa ip6.arpa {
+                pods insecure
+                fallthrough in-addr.arpa ip6.arpa
+                ttl 30
+            }
+            prometheus :9153
+            forward . /etc/resolv.conf
+            cache 30
+            loop
+            reload
+            loadbalance
+        }
+    maesh:53 {
+        errors
+        rewrite continue {
+            name regex ([a-zA-Z0-9-_]*)\.([a-zv0-9-_]*)\.maesh toto-{1}-6d61657368-{2}.toto.svc.titi
+            answer name toto-([a-zA-Z0-9-_]*)-6d61657368-([a-zA-Z0-9-_]*)\.toto\.svc\.titi {1}.{2}.maesh
+        }
+        kubernetes titi in-addr.arpa ip6.arpa {
+            pods insecure
+            upstream
+        	fallthrough in-addr.arpa ip6.arpa
+        }
+        forward . /etc/resolv.conf
+        cache 30
+        loop
+        reload
+        loadbalance
+    }
diff --git a/pkg/prepare/fixtures/configurecoredns_missing_configmap.yaml b/pkg/prepare/fixtures/configurecoredns_missing_configmap.yaml
new file mode 100644
index 0000000..678dbd7
--- /dev/null
+++ b/pkg/prepare/fixtures/configurecoredns_missing_configmap.yaml
@@ -0,0 +1,5 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coredns
+  namespace: kube-system
diff --git a/pkg/prepare/fixtures/configurecoredns_missing_deployment.yaml b/pkg/prepare/fixtures/configurecoredns_missing_deployment.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/pkg/prepare/fixtures/configurecoredns_not_patched.yaml b/pkg/prepare/fixtures/configurecoredns_not_patched.yaml
new file mode 100644
index 0000000..0ea6c1b
--- /dev/null
+++ b/pkg/prepare/fixtures/configurecoredns_not_patched.yaml
@@ -0,0 +1,45 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coredns
+  namespace: kube-system
+spec:
+  template:
+    spec:
+      volumes:
+        - configMap:
+            name: "other-cfgmap"
+        - configMap:
+            name: "coredns-cfgmap"
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: other-cfgmap
+  namespace: kube-system
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: coredns-cfgmap
+  namespace: kube-system
+data:
+  Corefile: |
+    .:53 {
+        errors
+        health {
+            lameduck 5s
+        }
+        ready
+        kubernetes {{ pillar['dns_domain'] }} in-addr.arpa ip6.arpa {
+            pods insecure
+            fallthrough in-addr.arpa ip6.arpa
+            ttl 30
+        }
+        prometheus :9153
+        forward . /etc/resolv.conf
+        cache 30
+        loop
+        reload
+        loadbalance
+    }
diff --git a/pkg/prepare/fixtures/configurekubedns_already_patched.yaml b/pkg/prepare/fixtures/configurekubedns_already_patched.yaml
new file mode 100644
index 0000000..f58fd7d
--- /dev/null
+++ b/pkg/prepare/fixtures/configurekubedns_already_patched.yaml
@@ -0,0 +1,27 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: kube-dns
+  namespace: kube-system
+spec:
+  template:
+    spec:
+      volumes:
+        - configMap:
+            name: "kubedns-cfgmap"
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: kubedns-cfgmap
+  namespace: kube-system
+  labels:
+    maesh-patched: "true"
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: coredns
+  namespace: maesh
+spec:
+  clusterIP: "1.2.3.4"
diff --git a/pkg/prepare/fixtures/configurekubedns_missing_deployment.yaml b/pkg/prepare/fixtures/configurekubedns_missing_deployment.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/pkg/prepare/fixtures/configurekubedns_not_patched.yaml b/pkg/prepare/fixtures/configurekubedns_not_patched.yaml
new file mode 100644
index 0000000..da97728
--- /dev/null
+++ b/pkg/prepare/fixtures/configurekubedns_not_patched.yaml
@@ -0,0 +1,25 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: kube-dns
+  namespace: kube-system
+spec:
+  template:
+    spec:
+      volumes:
+        - configMap:
+            name: "kubedns-cfgmap"
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: kubedns-cfgmap
+  namespace: kube-system
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: coredns
+  namespace: maesh
+spec:
+  clusterIP: "1.2.3.4"
diff --git a/pkg/prepare/prepare.go b/pkg/prepare/prepare.go
index 3f0f935..2b759bd 100644
--- a/pkg/prepare/prepare.go
+++ b/pkg/prepare/prepare.go
@@ -17,25 +17,36 @@ import (
 	"github.com/google/uuid"
 	"github.com/sirupsen/logrus"
 	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	kubeerror "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/informers"
 	"k8s.io/client-go/tools/cache"
 )
 
-var (
-	supportedCoreDNSVersions = []string{
-		"1.3",
-		"1.4",
-		"1.5",
-		"1.6",
-	}
+// DNSProvider represents a DNS provider.
+type DNSProvider int
+
+// Supported DNS providers.
+const (
+	UnknownDNS DNSProvider = iota
+	CoreDNS
+	KubeDNS
 )
 
+var supportedCoreDNSVersions = []string{
+	"1.3",
+	"1.4",
+	"1.5",
+	"1.6",
+}
+
 // Preparer is an interface for the prepare methods.
 type Preparer interface {
-	CheckCluster() error
+	CheckDNSProvider() (DNSProvider, error)
 	StartInformers(smi bool) error
-	PatchDNS(namespace string, clusterDomain string) error
+	ConfigureCoreDNS(namespace, clusterDomain string) error
+	ConfigureKubeDNS() error
 }
 
 // Ensure the Prepare fits the Preparer interface
@@ -55,46 +66,45 @@ func NewPrepare(log logrus.FieldLogger, client k8s.Client) Preparer {
 	}
 }
 
-// CheckCluster is used to check the cluster.
-func (p *Prepare) CheckCluster() error {
-	p.log.Infoln("Checking Cluster...")
+// CheckDNSProvider checks that the DNS provider that is deployed in the cluster
+// is supported and returns it.
+func (p *Prepare) CheckDNSProvider() (DNSProvider, error) {
+	p.log.Infoln("Checking DNS provider")
 
 	match, err := p.coreDNSMatch()
 	if err != nil {
-		return err
+		return UnknownDNS, err
 	}
 
-	if !match {
-		match, err = p.kubeDNSMatch()
-		if err != nil {
-			return err
-		}
+	if match {
+		return CoreDNS, nil
 	}
 
-	if !match {
-		return fmt.Errorf("no core dns service available for installing maesh: %v", err)
+	match, err = p.kubeDNSMatch()
+	if err != nil {
+		return UnknownDNS, err
 	}
 
-	return nil
+	if match {
+		return KubeDNS, nil
+	}
+
+	return UnknownDNS, fmt.Errorf("no core DNS service available for installing maesh: %w", err)
 }
 
-// coreDNSMatch checks if CoreDNS service can match.
 func (p *Prepare) coreDNSMatch() (bool, error) {
-	p.log.Infoln("Checking CoreDNS...")
-	p.log.Debugln("Get CoreDNS version...")
+	p.log.Infoln("Checking CoreDNS")
 
 	deployment, err := p.client.GetKubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Get("coredns", metav1.GetOptions{})
-	exists, err := k8s.TranslateNotFoundError(err)
-
-	if err != nil {
-		return false, fmt.Errorf("unable to get deployment %q in namespace %q: %v", "coredns", metav1.NamespaceSystem, err)
-	}
-
-	if !exists {
+	if kubeerror.IsNotFound(err) {
 		p.log.Debugf("%s does not exist in namespace %s", "coredns", metav1.NamespaceSystem)
 		return false, nil
 	}
 
+	if err != nil {
+		return false, fmt.Errorf("unable to get deployment %q in namesapce %q: %w", "coredns", metav1.NamespaceSystem, err)
+	}
+
 	var version string
 
 	for _, c := range deployment.Spec.Template.Spec.Containers {
@@ -102,10 +112,8 @@ func (p *Prepare) coreDNSMatch() (bool, error) {
 			continue
 		}
 
-		split := strings.Split(c.Image, ":")
-		if len(split) == 2 {
-			version = split[1]
-		}
+		sp := strings.Split(c.Image, ":")
+		version = sp[len(sp)-1]
 	}
 
 	if !isCoreDNSVersionSupported(version) {
@@ -117,161 +125,156 @@ func (p *Prepare) coreDNSMatch() (bool, error) {
 	return true, nil
 }
 
-// kubeDNSMatch checks if KubeDNS service can match.
-func (p *Prepare) kubeDNSMatch() (bool, error) {
-	p.log.Infoln("Checking KubeDNS...")
-	p.log.Debugln("Get KubeDNS version...")
+func isCoreDNSVersionSupported(versionLine string) bool {
+	for _, v := range supportedCoreDNSVersions {
+		if strings.Contains(versionLine, v) || strings.Contains(versionLine, "v"+v) {
+			return true
+		}
+	}
 
-	_, err := p.client.GetKubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Get("kube-dns", metav1.GetOptions{})
-	exists, err := k8s.TranslateNotFoundError(err)
+	return false
+}
 
-	if err != nil {
-		return false, fmt.Errorf("unable to get deployment %q in namesapce %q: %v", "kube-dns", metav1.NamespaceSystem, err)
-	}
+func (p *Prepare) kubeDNSMatch() (bool, error) {
+	p.log.Infoln("Checking KubeDNS")
 
-	if !exists {
+	_, err := p.client.GetKubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Get("kube-dns", metav1.GetOptions{})
+	if kubeerror.IsNotFound(err) {
 		p.log.Debugf("%s does not exist in namespace %s", "kube-dns", metav1.NamespaceSystem)
 		return false, nil
 	}
 
+	if err != nil {
+		return false, fmt.Errorf("unable to get deployment %q in namesapce %q: %w", "kube-dns", metav1.NamespaceSystem, err)
+	}
+
 	p.log.Info("KubeDNS match")
 
 	return true, nil
 }
 
-// StartInformers checks if the required informers can start and sync in a reasonable time.
-func (p *Prepare) StartInformers(smi bool) error {
-	p.log.Debug("Creating and Starting Informers")
-
-	stopCh := make(chan struct{})
-
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-	defer cancel()
-
-	// Create a new SharedInformerFactory, and register the event handler to informers.
-	kubeFactory := informers.NewSharedInformerFactoryWithOptions(p.client.GetKubernetesClient(), k8s.ResyncPeriod)
-	kubeFactory.Core().V1().Services().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
-	kubeFactory.Core().V1().Endpoints().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
-	kubeFactory.Core().V1().Pods().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
-	kubeFactory.Start(stopCh)
+// ConfigureCoreDNS patches the CoreDNS configuration for Maesh.
+func (p *Prepare) ConfigureCoreDNS(clusterDomain, maeshNamespace string) error {
+	p.log.Debugln("Patching CoreDNS")
 
-	for t, ok := range kubeFactory.WaitForCacheSync(ctx.Done()) {
-		if !ok {
-			return fmt.Errorf("timed out waiting for controller caches to sync: %s", t.String())
-		}
+	deployment, err := p.client.GetKubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Get("coredns", metav1.GetOptions{})
+	if err != nil {
+		return err
 	}
 
-	if smi {
-		// Create new SharedInformerFactories, and register the event handler to informers.
-		accessFactory := accessInformer.NewSharedInformerFactoryWithOptions(p.client.GetAccessClient(), k8s.ResyncPeriod)
-		accessFactory.Access().V1alpha1().TrafficTargets().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
-		accessFactory.Start(stopCh)
-
-		for t, ok := range accessFactory.WaitForCacheSync(ctx.Done()) {
-			if !ok {
-				return fmt.Errorf("timed out waiting for controller caches to sync: %s", t.String())
-			}
-		}
+	coreConfigMap, err := p.getCorefileConfigMap(deployment)
+	if err != nil {
+		return err
+	}
 
-		specsFactory := specsInformer.NewSharedInformerFactoryWithOptions(p.client.GetSpecsClient(), k8s.ResyncPeriod)
-		specsFactory.Specs().V1alpha1().HTTPRouteGroups().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
-		specsFactory.Specs().V1alpha1().TCPRoutes().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
-		specsFactory.Start(stopCh)
+	if isPatched(coreConfigMap) {
+		// CoreDNS has already been patched.
+		p.log.Debugln("Configmap already patched")
+		return nil
+	}
 
-		for t, ok := range specsFactory.WaitForCacheSync(ctx.Done()) {
-			if !ok {
-				return fmt.Errorf("timed out waiting for controller caches to sync: %s", t.String())
-			}
-		}
+	p.log.Debugln("Patching CoreDNS configmap")
 
-		splitFactory := splitInformer.NewSharedInformerFactoryWithOptions(p.client.GetSplitClient(), k8s.ResyncPeriod)
-		splitFactory.Split().V1alpha2().TrafficSplits().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
-		splitFactory.Start(stopCh)
+	if err := p.patchCoreDNSConfigMap(coreConfigMap, clusterDomain, maeshNamespace, deployment.Namespace); err != nil {
+		return err
+	}
 
-		for t, ok := range splitFactory.WaitForCacheSync(ctx.Done()) {
-			if !ok {
-				return fmt.Errorf("timed out waiting for controller caches to sync: %s", t.String())
-			}
-		}
+	if err := p.restartPods(deployment); err != nil {
+		return err
 	}
 
 	return nil
 }
 
-// PatchDNS is used to initialize a kubernetes cluster with a variety of configuration options.
-func (p *Prepare) PatchDNS(namespace string, clusterDomain string) error {
-	p.log.Infoln("Preparing Cluster...")
-	p.log.Debugln("Patching DNS...")
-
-	if err := p.patchDNS(metav1.NamespaceSystem, clusterDomain, namespace); err != nil {
-		return err
-	}
+func (p *Prepare) patchCoreDNSConfigMap(coreConfigMap *corev1.ConfigMap, clusterDomain, maeshNamespace, coreNamespace string) error {
+	serverBlock := fmt.Sprintf(
+		`
+maesh:53 {
+    errors
+    rewrite continue {
+        name regex ([a-zA-Z0-9-_]*)\.([a-zv0-9-_]*)\.maesh %[3]s-{1}-6d61657368-{2}.%[3]s.svc.%[1]s
+        answer name %[3]s-([a-zA-Z0-9-_]*)-6d61657368-([a-zA-Z0-9-_]*)\.%[3]s\.svc\.%[2]s {1}.{2}.maesh
+    }
+    kubernetes %[1]s in-addr.arpa ip6.arpa {
+        pods insecure
+        upstream
+    	fallthrough in-addr.arpa ip6.arpa
+    }
+    forward . /etc/resolv.conf
+    cache 30
+    loop
+    reload
+    loadbalance
+}
+`,
+		clusterDomain,
+		strings.Replace(clusterDomain, ".", "\\.", -1),
+		maeshNamespace,
+	)
 
-	p.log.Infoln("Cluster Preparation Complete...")
+	originalBlock := coreConfigMap.Data["Corefile"]
+	newBlock := originalBlock + serverBlock
+	coreConfigMap.Data["Corefile"] = newBlock
 
-	return nil
-}
+	if coreConfigMap.ObjectMeta.Labels == nil {
+		coreConfigMap.ObjectMeta.Labels = make(map[string]string)
+	}
 
-// patchDNS is used to patch the CoreDNS configmap if needed.
-func (p *Prepare) patchDNS(coreNamespace, clusterDomain, maeshNamespace string) error {
-	deployment, err := p.client.GetKubernetesClient().AppsV1().Deployments(coreNamespace).Get("coredns", metav1.GetOptions{})
-	exists, err := k8s.TranslateNotFoundError(err)
+	coreConfigMap.ObjectMeta.Labels["maesh-patched"] = "true"
 
-	if err != nil {
+	if _, err := p.client.GetKubernetesClient().CoreV1().ConfigMaps(coreNamespace).Update(coreConfigMap); err != nil {
 		return err
 	}
 
-	// If CoreDNS exist we will patch it.
-	if exists {
-		p.log.Debugln("Patching CoreDNS configmap...")
+	return nil
+}
 
-		var patched bool
+// getCorefileConfigMap returns the name of a coreDNS config map.
+func (p *Prepare) getCorefileConfigMap(coreDeployment *appsv1.Deployment) (*corev1.ConfigMap, error) {
+	for _, volume := range coreDeployment.Spec.Template.Spec.Volumes {
+		if volume.ConfigMap == nil {
+			continue
+		}
 
-		patched, err = p.patchCoreDNSConfigMap(deployment, clusterDomain, maeshNamespace)
+		cfgMap, err := p.client.GetKubernetesClient().CoreV1().ConfigMaps(coreDeployment.Namespace).Get(volume.ConfigMap.Name, metav1.GetOptions{})
 		if err != nil {
-			return err
+			return nil, err
 		}
 
-		if !patched {
-			p.log.Debugln("Restarting CoreDNS pods...")
-
-			if err = p.restartPods(deployment); err != nil {
-				return err
-			}
+		if cfgMap.Data == nil {
+			continue
+		}
 
-			return nil
+		if _, exists := cfgMap.Data["Corefile"]; !exists {
+			continue
 		}
 
-		return nil
+		return cfgMap, nil
 	}
 
-	p.log.Debugln("coredns not available fallback to kube-dns")
-	// If coreDNS does not exist we try to get the kube-dns
-	deployment, err = p.client.GetKubernetesClient().AppsV1().Deployments(coreNamespace).Get("kube-dns", metav1.GetOptions{})
-	exists, err = k8s.TranslateNotFoundError(err)
+	return nil, errors.New("corefile configmap not found")
+}
+
+// ConfigureKubeDNS patches the KubeDNS configuration for Maesh.
+func (p *Prepare) ConfigureKubeDNS() error {
+	p.log.Debugln("Patching KubeDNS")
 
+	deployment, err := p.client.GetKubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Get("kube-dns", metav1.GetOptions{})
 	if err != nil {
 		return err
 	}
 
-	if !exists {
-		return fmt.Errorf("neither CoreDNS or KubeDNS are available in namespace %q", coreNamespace)
-	}
-
-	ebo := backoff.NewConstantBackOff(10 * time.Second)
-
-	var serviceIP string
+	var (
+		serviceIP string
+		ebo       = backoff.NewConstantBackOff(10 * time.Second)
+	)
 
-	p.log.Debugln("Get CoreDNS service IP")
+	p.log.Debugln("Getting CoreDNS service IP")
 
 	if err = backoff.Retry(safe.OperationWithRecover(func() error {
 		svc, errSvc := p.client.GetKubernetesClient().CoreV1().Services("maesh").Get("coredns", metav1.GetOptions{})
-		exists, errSvc := k8s.TranslateNotFoundError(errSvc)
 		if errSvc != nil {
-			return fmt.Errorf("unable get the service %q in namespace %q: %v", "coredns", "maesh", errSvc)
-		}
-		if !exists {
-			return fmt.Errorf("service %q has not been yet created", "coredns")
+			return fmt.Errorf("unable get the service %q in namespace %q: %w", "coredns", "maesh", errSvc)
 		}
 		if svc.Spec.ClusterIP == "" {
 			return fmt.Errorf("service %q has no clusterIP", "coredns")
@@ -280,153 +283,157 @@ func (p *Prepare) patchDNS(coreNamespace, clusterDomain, maeshNamespace string)
 		serviceIP = svc.Spec.ClusterIP
 		return nil
 	}), ebo); err != nil {
-		return fmt.Errorf("unable get the service %q in namespace %q: %v", "coredns", "maesh", err)
+		return fmt.Errorf("unable get the service %q in namespace %q: %w", "coredns", "maesh", err)
 	}
 
-	// Patch KubeDNS
-	p.log.Debugln("Patching KubeDNS configmap... with IP: ", serviceIP)
-
-	patched, err := p.patchKubeDNSConfigMap(deployment, serviceIP)
+	configMap, err := p.getKubeDNSConfigMap(deployment)
 	if err != nil {
 		return err
 	}
 
-	if !patched {
-		p.log.Debugln("Restarting KubeDNS pods...")
+	if isPatched(configMap) {
+		p.log.Debugln("Configmap already patched")
 
-		if err := p.restartPods(deployment); err != nil {
-			return err
-		}
+		return nil
 	}
 
-	return nil
-}
+	p.log.Debugln("Patching KubeDNS configmap with IP", serviceIP)
 
-func (p *Prepare) patchCoreDNSConfigMap(coreDeployment *appsv1.Deployment, clusterDomain, maeshNamespace string) (bool, error) {
-	var coreConfigMapName string
+	if err := p.patchKubeDNSConfigMap(configMap, deployment.Namespace, serviceIP); err != nil {
+		return err
+	}
 
-	if len(coreDeployment.Spec.Template.Spec.Volumes) == 0 {
-		return false, errors.New("coreDNS configmap not defined")
+	if err := p.restartPods(deployment); err != nil {
+		return err
 	}
 
-	coreConfigMapName = getCoreDNSConfigMapName(coreDeployment)
+	return nil
+}
 
-	coreConfigMap, err := p.client.GetKubernetesClient().CoreV1().ConfigMaps(coreDeployment.Namespace).Get(coreConfigMapName, metav1.GetOptions{})
-	if err != nil {
-		return false, err
-	}
+func (p *Prepare) getKubeDNSConfigMap(kubeDeployment *appsv1.Deployment) (*corev1.ConfigMap, error) {
+	for _, volume := range kubeDeployment.Spec.Template.Spec.Volumes {
+		if volume.ConfigMap == nil {
+			continue
+		}
 
-	if len(coreConfigMap.ObjectMeta.Labels) > 0 {
-		if _, ok := coreConfigMap.ObjectMeta.Labels["maesh-patched"]; ok {
-			p.log.Debugln("Configmap already patched...")
-			return true, nil
+		cfgMap, err := p.client.GetKubernetesClient().CoreV1().ConfigMaps(kubeDeployment.Namespace).Get(volume.ConfigMap.Name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
 		}
+
+		return cfgMap, nil
 	}
 
-	serverBlock := fmt.Sprintf(
-		`
-maesh:53 {
-    errors
-    rewrite continue {
-        name regex ([a-zA-Z0-9-_]*)\.([a-zv0-9-_]*)\.maesh %[3]s-{1}-6d61657368-{2}.%[3]s.svc.%[1]s
-        answer name %[3]s-([a-zA-Z0-9-_]*)-6d61657368-([a-zA-Z0-9-_]*)\.%[3]s\.svc\.%[2]s {1}.{2}.maesh
-    }
-    kubernetes %[1]s in-addr.arpa ip6.arpa {
-        pods insecure
-        upstream
-    	fallthrough in-addr.arpa ip6.arpa
-    }
-    forward . /etc/resolv.conf
-    cache 30
-    loop
-    reload
-    loadbalance
+	return nil, errors.New("corefile configmap not found")
 }
-`,
-		clusterDomain,
-		strings.Replace(clusterDomain, ".", "\\.", -1),
-		maeshNamespace,
-	)
 
-	originalBlock := coreConfigMap.Data["Corefile"]
-	newBlock := originalBlock + serverBlock
-	coreConfigMap.Data["Corefile"] = newBlock
+func (p *Prepare) patchKubeDNSConfigMap(kubeConfigMap *corev1.ConfigMap, namespace, coreDNSIp string) error {
+	originalBlock, exist := kubeConfigMap.Data["stubDomains"]
+	if !exist {
+		originalBlock = "{}"
+	}
 
-	if len(coreConfigMap.ObjectMeta.Labels) == 0 {
-		coreConfigMap.ObjectMeta.Labels = make(map[string]string)
+	stubDomains := make(map[string][]string)
+	if err := json.Unmarshal([]byte(originalBlock), &stubDomains); err != nil {
+		return err
 	}
 
-	coreConfigMap.ObjectMeta.Labels["maesh-patched"] = "true"
+	stubDomains["maesh"] = []string{coreDNSIp}
 
-	if _, err = p.client.GetKubernetesClient().CoreV1().ConfigMaps(coreDeployment.Namespace).Update(coreConfigMap); err != nil {
-		return false, err
+	var newData []byte
+
+	newData, err := json.Marshal(stubDomains)
+	if err != nil {
+		return err
 	}
 
-	return false, nil
-}
+	if kubeConfigMap.Data == nil {
+		kubeConfigMap.Data = make(map[string]string)
+	}
 
-func (p *Prepare) patchKubeDNSConfigMap(deployment *appsv1.Deployment, coreDNSIp string) (bool, error) {
-	var configMapName string
+	kubeConfigMap.Data["stubDomains"] = string(newData)
 
-	if len(deployment.Spec.Template.Spec.Volumes) == 0 {
-		return false, errors.New("kube-dns configmap not defined")
+	if len(kubeConfigMap.ObjectMeta.Labels) == 0 {
+		kubeConfigMap.ObjectMeta.Labels = make(map[string]string)
 	}
 
-	configMapName = deployment.Spec.Template.Spec.Volumes[0].ConfigMap.Name
+	kubeConfigMap.ObjectMeta.Labels["maesh-patched"] = "true"
 
-	configMap, err := p.client.GetKubernetesClient().CoreV1().ConfigMaps(deployment.Namespace).Get(configMapName, metav1.GetOptions{})
-	if err != nil {
-		return false, err
+	if _, err := p.client.GetKubernetesClient().CoreV1().ConfigMaps(namespace).Update(kubeConfigMap); err != nil {
+		return err
 	}
 
-	if len(configMap.ObjectMeta.Labels) > 0 {
-		if _, ok := configMap.ObjectMeta.Labels["maesh-patched"]; ok {
-			p.log.Debugln("Configmap already patched...")
-			return true, nil
-		}
-	}
+	return nil
+}
 
-	stubDomains := make(map[string][]string)
-	originalBlock, exist := configMap.Data["stubDomains"]
+// StartInformers checks if the required informers can start and sync in a reasonable time.
+func (p *Prepare) StartInformers(smi bool) error {
+	stopCh := make(chan struct{})
 
-	if !exist {
-		originalBlock = "{}"
-	}
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	// Create a new SharedInformerFactory, and register the event handler to informers.
+	kubeFactory := informers.NewSharedInformerFactoryWithOptions(p.client.GetKubernetesClient(), k8s.ResyncPeriod)
+	kubeFactory.Core().V1().Services().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
+	kubeFactory.Core().V1().Endpoints().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
+	kubeFactory.Core().V1().Pods().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
+	kubeFactory.Start(stopCh)
 
-	if err = json.Unmarshal([]byte(originalBlock), &stubDomains); err != nil {
-		return false, err
+	for t, ok := range kubeFactory.WaitForCacheSync(ctx.Done()) {
+		if !ok {
+			return fmt.Errorf("timed out waiting for controller caches to sync: %s", t.String())
+		}
 	}
 
-	stubDomains["maesh"] = []string{coreDNSIp}
+	if smi {
+		// Create new SharedInformerFactories, and register the event handler to informers.
+		accessFactory := accessInformer.NewSharedInformerFactoryWithOptions(p.client.GetAccessClient(), k8s.ResyncPeriod)
+		accessFactory.Access().V1alpha1().TrafficTargets().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
+		accessFactory.Start(stopCh)
 
-	var newData []byte
+		for t, ok := range accessFactory.WaitForCacheSync(ctx.Done()) {
+			if !ok {
+				return fmt.Errorf("timed out waiting for controller caches to sync: %s", t.String())
+			}
+		}
 
-	newData, err = json.Marshal(stubDomains)
-	if err != nil {
-		return false, err
-	}
+		specsFactory := specsInformer.NewSharedInformerFactoryWithOptions(p.client.GetSpecsClient(), k8s.ResyncPeriod)
+		specsFactory.Specs().V1alpha1().HTTPRouteGroups().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
+		specsFactory.Specs().V1alpha1().TCPRoutes().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
+		specsFactory.Start(stopCh)
 
-	if configMap.Data == nil {
-		configMap.Data = make(map[string]string)
-	}
+		for t, ok := range specsFactory.WaitForCacheSync(ctx.Done()) {
+			if !ok {
+				return fmt.Errorf("timed out waiting for controller caches to sync: %s", t.String())
+			}
+		}
 
-	configMap.Data["stubDomains"] = string(newData)
+		splitFactory := splitInformer.NewSharedInformerFactoryWithOptions(p.client.GetSplitClient(), k8s.ResyncPeriod)
+		splitFactory.Split().V1alpha2().TrafficSplits().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
+		splitFactory.Start(stopCh)
 
-	if len(configMap.ObjectMeta.Labels) == 0 {
-		configMap.ObjectMeta.Labels = make(map[string]string)
+		for t, ok := range splitFactory.WaitForCacheSync(ctx.Done()) {
+			if !ok {
+				return fmt.Errorf("timed out waiting for controller caches to sync: %s", t.String())
+			}
+		}
 	}
 
-	configMap.ObjectMeta.Labels["maesh-patched"] = "true"
+	return nil
+}
 
-	if _, err = p.client.GetKubernetesClient().CoreV1().ConfigMaps(deployment.Namespace).Update(configMap); err != nil {
-		return false, err
+func isPatched(cfgMap *corev1.ConfigMap) bool {
+	var patched bool
+	if len(cfgMap.ObjectMeta.Labels) > 0 {
+		_, patched = cfgMap.ObjectMeta.Labels["maesh-patched"]
 	}
 
-	return false, nil
+	return patched
 }
 
 func (p *Prepare) restartPods(deployment *appsv1.Deployment) error {
-	p.log.Infof("Restarting %s pods...\n", deployment.Name)
+	p.log.Infof("Restarting %s pods", deployment.Name)
 
 	// Never edit original object, always work with a clone for updates.
 	newDeployment := deployment.DeepCopy()
@@ -442,25 +449,3 @@ func (p *Prepare) restartPods(deployment *appsv1.Deployment) error {
 
 	return err
 }
-
-// getCoreDNSConfigMapName returns the dected coredns configmap name
-func getCoreDNSConfigMapName(coreDeployment *appsv1.Deployment) string {
-	for _, volume := range coreDeployment.Spec.Template.Spec.Volumes {
-		if volume.ConfigMap != nil {
-			return volume.ConfigMap.Name
-		}
-	}
-
-	return ""
-}
-
-// isCoreDNSVersionSupported returns true if the provided string contains a supported CoreDNS version.
-func isCoreDNSVersionSupported(versionLine string) bool {
-	for _, v := range supportedCoreDNSVersions {
-		if strings.Contains(versionLine, v) || strings.Contains(versionLine, "v"+v) {
-			return true
-		}
-	}
-
-	return false
-}


Test patch:
diff --git a/pkg/prepare/prepare_test.go b/pkg/prepare/prepare_test.go
new file mode 100644
index 0000000..05400a8
--- /dev/null
+++ b/pkg/prepare/prepare_test.go
@@ -0,0 +1,199 @@
+package prepare_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/containous/maesh/pkg/k8s"
+	"github.com/containous/maesh/pkg/prepare"
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestCheckDNSProvider(t *testing.T) {
+	tests := []struct {
+		desc string
+
+		mockFile string
+
+		expectedProvider prepare.DNSProvider
+		expectedErr      bool
+	}{
+		{
+			desc: "CoreDNS supported version",
+
+			mockFile: "checkdnsprovider_supported_version.yaml",
+
+			expectedProvider: prepare.CoreDNS,
+			expectedErr:      false,
+		},
+		{
+			desc: "KubeDNS",
+
+			mockFile: "checkdnsprovider_kubedns.yaml",
+
+			expectedProvider: prepare.KubeDNS,
+			expectedErr:      false,
+		},
+		{
+			desc: "CoreDNS unsupported version",
+
+			mockFile: "checkdnsprovider_unsupported_version.yaml",
+
+			expectedProvider: prepare.UnknownDNS,
+			expectedErr:      true,
+		},
+		{
+			desc: "No known DNS provider",
+
+			mockFile: "checkdnsprovider_no_provider.yaml",
+
+			expectedProvider: prepare.UnknownDNS,
+			expectedErr:      true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			clt := k8s.NewClientMock(ctx.Done(), test.mockFile, false)
+
+			prep := prepare.NewPrepare(logrus.New(), clt)
+			provider, err := prep.CheckDNSProvider()
+
+			if test.expectedErr {
+				assert.Error(t, err)
+				return
+			}
+
+			assert.NoError(t, err)
+			assert.Equal(t, test.expectedProvider, provider)
+		})
+	}
+}
+
+func TestConfigureCoreDNS(t *testing.T) {
+	tests := []struct {
+		desc string
+
+		mockFile string
+
+		expectedCorefile string
+		expectedErr      bool
+	}{
+		{
+			desc: "First time config of CoreDNS",
+
+			mockFile: "configurecoredns_not_patched.yaml",
+
+			expectedErr:      false,
+			expectedCorefile: ".:53 {\n    errors\n    health {\n        lameduck 5s\n    }\n    ready\n    kubernetes {{ pillar['dns_domain'] }} in-addr.arpa ip6.arpa {\n        pods insecure\n        fallthrough in-addr.arpa ip6.arpa\n        ttl 30\n    }\n    prometheus :9153\n    forward . /etc/resolv.conf\n    cache 30\n    loop\n    reload\n    loadbalance\n}\n\nmaesh:53 {\n    errors\n    rewrite continue {\n        name regex ([a-zA-Z0-9-_]*)\\.([a-zv0-9-_]*)\\.maesh toto-{1}-6d61657368-{2}.toto.svc.titi\n        answer name toto-([a-zA-Z0-9-_]*)-6d61657368-([a-zA-Z0-9-_]*)\\.toto\\.svc\\.titi {1}.{2}.maesh\n    }\n    kubernetes titi in-addr.arpa ip6.arpa {\n        pods insecure\n        upstream\n    \tfallthrough in-addr.arpa ip6.arpa\n    }\n    forward . /etc/resolv.conf\n    cache 30\n    loop\n    reload\n    loadbalance\n}\n",
+		},
+		{
+			desc: "Missing Corefile configmap",
+
+			mockFile: "configurecoredns_missing_configmap.yaml",
+
+			expectedErr: true,
+		},
+		{
+			desc: "Missing CoreDNS deployment",
+
+			mockFile: "configurecoredns_missing_deployment.yaml",
+
+			expectedErr: true,
+		},
+		{
+			desc: "Already patched",
+
+			mockFile: "configurecoredns_already_patched.yaml",
+
+			expectedErr:      false,
+			expectedCorefile: ".:53 {\n        errors\n        health {\n            lameduck 5s\n        }\n        ready\n        kubernetes {{ pillar['dns_domain'] }} in-addr.arpa ip6.arpa {\n            pods insecure\n            fallthrough in-addr.arpa ip6.arpa\n            ttl 30\n        }\n        prometheus :9153\n        forward . /etc/resolv.conf\n        cache 30\n        loop\n        reload\n        loadbalance\n    }\nmaesh:53 {\n    errors\n    rewrite continue {\n        name regex ([a-zA-Z0-9-_]*)\\.([a-zv0-9-_]*)\\.maesh toto-{1}-6d61657368-{2}.toto.svc.titi\n        answer name toto-([a-zA-Z0-9-_]*)-6d61657368-([a-zA-Z0-9-_]*)\\.toto\\.svc\\.titi {1}.{2}.maesh\n    }\n    kubernetes titi in-addr.arpa ip6.arpa {\n        pods insecure\n        upstream\n    \tfallthrough in-addr.arpa ip6.arpa\n    }\n    forward . /etc/resolv.conf\n    cache 30\n    loop\n    reload\n    loadbalance\n}\n",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			clt := k8s.NewClientMock(ctx.Done(), test.mockFile, false)
+
+			prep := prepare.NewPrepare(logrus.New(), clt)
+			err := prep.ConfigureCoreDNS("titi", "toto")
+			if test.expectedErr {
+				assert.Error(t, err)
+				return
+			}
+
+			assert.NoError(t, err)
+
+			cfgMap, err := clt.GetKubernetesClient().CoreV1().ConfigMaps("kube-system").Get("coredns-cfgmap", metav1.GetOptions{})
+			require.NoError(t, err)
+
+			assert.Equal(t, test.expectedCorefile, cfgMap.Data["Corefile"])
+		})
+	}
+}
+
+func TestConfigureKubeDNS(t *testing.T) {
+	tests := []struct {
+		desc string
+
+		mockFile string
+
+		expectedStubDomains string
+		expectedErr         bool
+	}{
+		{
+			desc: "First time config of KubeDNS",
+
+			mockFile: "configurekubedns_not_patched.yaml",
+
+			expectedErr:         false,
+			expectedStubDomains: `{"maesh":["1.2.3.4"]}`,
+		},
+		{
+			desc: "Already patched",
+
+			mockFile: "configurekubedns_already_patched.yaml",
+
+			expectedErr: false,
+		},
+		{
+			desc: "Missing KubeDNS deployment",
+
+			mockFile: "configurekubedns_missing_deployment.yaml",
+
+			expectedErr: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			clt := k8s.NewClientMock(ctx.Done(), test.mockFile, false)
+
+			prep := prepare.NewPrepare(logrus.New(), clt)
+			err := prep.ConfigureKubeDNS()
+			if test.expectedErr {
+				assert.Error(t, err)
+				return
+			}
+
+			assert.NoError(t, err)
+
+			cfgMap, err := clt.GetKubernetesClient().CoreV1().ConfigMaps("kube-system").Get("kubedns-cfgmap", metav1.GetOptions{})
+			require.NoError(t, err)
+
+			assert.Equal(t, test.expectedStubDomains, cfgMap.Data["stubDomains"])
+		})
+	}
+}