Repository: containous/maesh
Pull request #712 (2020-08-24 14:07:08)

Speed up and improve integration tests
## What does this PR do?

This PR:
- Split the `integration_test.go` into 2 packages: `tool` and `k3d`
- Use `dig`, `curl` and `netcat` to make sure configurations work and not just dynamic config diff.
- Import all images at once with k3d
- Speed up parallel testing by pulling only the images needed in the test.
- Switch from `imagePullPolicy: Always` to `IfNotPresent` to avoid pull 2 times images. 
- Use SMI CRDs defined in the Helm chart instead of the copy made in testdata.
- Improved logging, added a `--debug` option.
- CoreDNSSuite & KubeDNSSuite:
  - Don't start a controller, just create a fake shadow service
  - For every supported versions, try to resolve a .mesh address
  - Remove unsupported versions (CoreDNS 1.2.6)
- ACLDisabledSuite & ACLEnabledSuite:
  - Deploy a controller and a proxy once and for all
  - Use curl to make sure if we can reach or not the different servers.
  - Added more test to make sure we correctly support header and path filtering on TrafficTargets

Fixes: #615 

### How to test it

* `make test-integration TESTFLAGS="--debug"`

## Additional Notes

This PR is currently blocked by #710 


Patch:
diff --git a/Makefile b/Makefile
index 12e948b..8c346a8 100644
--- a/Makefile
+++ b/Makefile
@@ -108,7 +108,7 @@ helm-lint: helm
 	helm lint helm/chart/maesh
 
 k3d:
-	@command -v k3d >/dev/null 2>&1 || curl -s https://raw.githubusercontent.com/rancher/k3d/v1.5.1/install.sh | TAG=v1.5.1 bash
+	@command -v k3d >/dev/null 2>&1 || curl -s https://raw.githubusercontent.com/rancher/k3d/v3.0.1/install.sh | TAG=v3.0.1 bash
 
 pages:
 	mkdir -p $(CURDIR)/pages
diff --git a/go.mod b/go.mod
index 26afa83..67f8512 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,12 @@ module github.com/containous/maesh
 go 1.14
 
 require (
-	github.com/abronan/valkeyrie v0.0.0-20200127174252-ef4277a138cd
 	github.com/cenkalti/backoff/v4 v4.0.2
 	github.com/containous/traefik/v2 v2.3.0-rc3
 	github.com/go-check/check v0.0.0-20180628173108-788fd7840127
 	github.com/google/uuid v1.1.1
 	github.com/gorilla/mux v1.7.3
 	github.com/hashicorp/go-version v1.2.1
-	github.com/pmezard/go-difflib v1.0.0
 	github.com/servicemeshinterface/smi-sdk-go v0.4.1
 	github.com/sirupsen/logrus v1.6.0
 	github.com/stretchr/testify v1.6.1
diff --git a/go.sum b/go.sum
index e5d7975..6cfafb2 100644
--- a/go.sum
+++ b/go.sum
@@ -1059,6 +1059,7 @@ k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
+k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8=
 k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
 k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU=
 k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
diff --git a/integration/k3d/k3d.go b/integration/k3d/k3d.go
new file mode 100644
index 0000000..aaa90a7
--- /dev/null
+++ b/integration/k3d/k3d.go
@@ -0,0 +1,414 @@
+package k3d
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path"
+	"strings"
+	"time"
+
+	"github.com/containous/maesh/integration/try"
+	"github.com/containous/maesh/pkg/k8s"
+	"github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	kubeerror "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var (
+	k3sImage   = "rancher/k3s"
+	k3sVersion = "v1.18.6-k3s1"
+)
+
+// DockerImage holds the configuration of a Docker image.
+type DockerImage struct {
+	Name  string
+	Local bool
+}
+
+// ClusterOptions holds the configuration of the cluster.
+type ClusterOptions struct {
+	Cmd    []string
+	Images []DockerImage
+}
+
+// ClusterOptionFunc mutates the given ClusterOptions.
+type ClusterOptionFunc func(opts *ClusterOptions)
+
+// WithoutTraefik tells k3d to not start a k3s cluster with Traefik already installed in.
+func WithoutTraefik() func(opts *ClusterOptions) {
+	return func(opts *ClusterOptions) {
+		opts.Cmd = append(opts.Cmd, "--k3s-server-arg", "--no-deploy=traefik")
+	}
+}
+
+// WithoutCoreDNS tells k3d to not start a k3s cluster with CoreDNS already installed in.
+func WithoutCoreDNS() func(opts *ClusterOptions) {
+	return func(opts *ClusterOptions) {
+		opts.Cmd = append(opts.Cmd, "--k3s-server-arg", "--no-deploy=coredns")
+	}
+}
+
+// WithImages tells k3d to import the given image. Images which are tagged a local won't be pull locally before being
+// imported.
+func WithImages(images ...DockerImage) func(opts *ClusterOptions) {
+	return func(opts *ClusterOptions) {
+		opts.Images = append(opts.Images, images...)
+	}
+}
+
+// Cluster is a k3s cluster.
+type Cluster struct {
+	logger     logrus.FieldLogger
+	workingDir string
+	Client     k8s.Client
+	Name       string
+}
+
+// NewCluster creates a new k3s cluster using the given configuration.
+func NewCluster(logger logrus.FieldLogger, masterURL string, name string, opts ...ClusterOptionFunc) (*Cluster, error) {
+	clusterOpts := ClusterOptions{
+		Images: []DockerImage{
+			{Name: "rancher/coredns-coredns:1.6.3"},
+		},
+	}
+
+	for _, opt := range opts {
+		opt(&clusterOpts)
+	}
+
+	workingDir, err := os.Getwd()
+	if err != nil {
+		return nil, fmt.Errorf("unable to get working directory: %w", err)
+	}
+
+	if err = pullDockerImages(logger, clusterOpts.Images); err != nil {
+		return nil, fmt.Errorf("unable to pull docker images: %w", err)
+	}
+
+	if err = createCluster(logger, name, clusterOpts.Cmd); err != nil {
+		return nil, fmt.Errorf("unable to create k3s cluster: %d", err)
+	}
+
+	if err = importDockerImages(logger, name, clusterOpts.Images); err != nil {
+		return nil, fmt.Errorf("unable to import docker images in the cluster: %w", err)
+	}
+
+	var client k8s.Client
+
+	client, err = createK8sClient(logger, name, masterURL)
+	if err != nil {
+		return nil, fmt.Errorf("unable to create kubernetes client: %w", err)
+	}
+
+	if err = waitClusterReady(client); err != nil {
+		return nil, err
+	}
+
+	return &Cluster{
+		logger:     logger,
+		workingDir: workingDir,
+		Client:     client,
+		Name:       name,
+	}, nil
+}
+
+// Stop stops the cluster.
+func (c *Cluster) Stop(logger logrus.FieldLogger) error {
+	cmd := exec.Command("k3d", "cluster", "delete", c.Name)
+	cmd.Env = os.Environ()
+
+	logger.Infof("Stopping k3s cluster %q...", c.Name)
+
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("unable to stop cluster: %w", err)
+	}
+
+	return nil
+}
+
+// CreateNamespace creates a new namespace.
+func (c *Cluster) CreateNamespace(logger logrus.FieldLogger, name string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+	defer cancel()
+
+	namespace := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{Name: name},
+	}
+
+	logger.Infof("Creating namespace %q...", name)
+
+	_, err := c.Client.KubernetesClient().CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{})
+	if err != nil {
+		return fmt.Errorf("unable to create namespace %q: %w", name, err)
+	}
+
+	return nil
+}
+
+// Apply applies the given directory/file on the cluster.
+func (c *Cluster) Apply(logger logrus.FieldLogger, resourcesPath string) error {
+	opts := []string{
+		"apply", "-f", path.Join(c.workingDir, resourcesPath),
+	}
+
+	logger.Infof("Applying %q...", resourcesPath)
+
+	cmd := exec.Command("kubectl", opts...)
+	cmd.Env = os.Environ()
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		logger.WithError(err).Errorf("unable to apply resources: %s", string(output))
+		return err
+	}
+
+	return nil
+}
+
+// Delete deletes from the cluster the resources declared in the given directory/file.
+func (c *Cluster) Delete(logger logrus.FieldLogger, resourcesPath string) {
+	opts := []string{
+		"delete", "-f", path.Join(c.workingDir, resourcesPath),
+		"--force", "--grace-period=0",
+	}
+
+	logger.Infof("Delete %q...", resourcesPath)
+
+	cmd := exec.Command("kubectl", opts...)
+	cmd.Env = os.Environ()
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		logger.WithError(err).Errorf("unable to delete resources: %s", string(output))
+	}
+}
+
+// WaitReadyDeployment waits for the given deployment to be ready.
+func (c *Cluster) WaitReadyDeployment(name, namespace string, timeout time.Duration) error {
+	err := try.Retry(func() error {
+		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+		defer cancel()
+
+		d, err := c.Client.KubernetesClient().AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			if kubeerror.IsNotFound(err) {
+				return fmt.Errorf("deployment %q has not been yet created", name)
+			}
+
+			return fmt.Errorf("unable get deployment %q in namespace %q: %v", name, namespace, err)
+		}
+
+		if d.Status.UpdatedReplicas == *(d.Spec.Replicas) &&
+			d.Status.Replicas == *(d.Spec.Replicas) &&
+			d.Status.AvailableReplicas == *(d.Spec.Replicas) &&
+			d.Status.ObservedGeneration >= d.Generation {
+			return nil
+		}
+
+		return errors.New("deployment not ready")
+	}, timeout)
+
+	if err != nil {
+		return fmt.Errorf("deployment %q in namespace %q is not ready: %w", name, namespace, err)
+	}
+
+	return nil
+}
+
+// WaitReadyDaemonSet waits for the given daemonset to be ready.
+func (c *Cluster) WaitReadyDaemonSet(name, namespace string, timeout time.Duration) error {
+	err := try.Retry(func() error {
+		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+		defer cancel()
+
+		d, err := c.Client.KubernetesClient().AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			if kubeerror.IsNotFound(err) {
+				return fmt.Errorf("daemonset %q has not been yet created", name)
+			}
+
+			return fmt.Errorf("unable get daemonset %q in namespace %q: %v", name, namespace, err)
+		}
+		if d.Status.NumberReady == d.Status.DesiredNumberScheduled {
+			return nil
+		}
+
+		return errors.New("daemonset not ready")
+	}, timeout)
+
+	if err != nil {
+		return fmt.Errorf("daemonset %q in namespace %q is not ready: %w", name, namespace, err)
+	}
+
+	return nil
+}
+
+// WaitReadyPod waits for the given pod to be ready.
+func (c *Cluster) WaitReadyPod(name, namespace string, timeout time.Duration) error {
+	err := try.Retry(func() error {
+		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+		defer cancel()
+
+		pod, err := c.Client.KubernetesClient().CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			if kubeerror.IsNotFound(err) {
+				return fmt.Errorf("pod %q has not been yet created", name)
+			}
+
+			return fmt.Errorf("unable get pod %q in namespace %q: %v", name, namespace, err)
+		}
+
+		if !isPodReady(pod) {
+			return errors.New("pod is not ready")
+		}
+
+		return nil
+	}, timeout)
+
+	if err != nil {
+		return fmt.Errorf("pod %q in namespace %q is not ready: %w", name, namespace, err)
+	}
+
+	return nil
+}
+
+func isPodReady(pod *corev1.Pod) bool {
+	var readyCondition *corev1.PodCondition
+
+	for _, condition := range pod.Status.Conditions {
+		if condition.Type == corev1.PodReady {
+			readyCondition = &condition
+			break
+		}
+	}
+
+	return readyCondition != nil && readyCondition.Status == corev1.ConditionTrue
+}
+
+func createCluster(logger logrus.FieldLogger, clusterName string, cmdOpts []string) error {
+	logger.Infof("Creating k3d cluster %s...", clusterName)
+
+	opts := []string{
+		"cluster", "create", clusterName,
+		"--no-lb",
+		"--api-port", "8443",
+		"--agents", "1",
+		"--image", fmt.Sprintf("%s:%s", k3sImage, k3sVersion),
+		"--wait",
+		"--timeout", "30s",
+	}
+
+	cmd := exec.Command("k3d", append(opts, cmdOpts...)...)
+	cmd.Env = os.Environ()
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		logger.WithError(err).Errorf("unable to create cluster: %s", string(output))
+		return err
+	}
+
+	return nil
+}
+
+func pullDockerImages(logger logrus.FieldLogger, images []DockerImage) error {
+	for _, image := range images {
+		if image.Local {
+			continue
+		}
+
+		logger.Infof("Pulling image %q...", image.Name)
+
+		cmd := exec.Command("docker", "pull", image.Name)
+		cmd.Env = os.Environ()
+
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			logger.WithError(err).Errorf("unable to pull image %q: %s", image.Name, string(output))
+			return err
+		}
+	}
+
+	return nil
+}
+
+func importDockerImages(logger logrus.FieldLogger, clusterName string, images []DockerImage) error {
+	args := []string{
+		"image", "import", "--cluster", clusterName,
+	}
+
+	for _, image := range images {
+		args = append(args, image.Name)
+	}
+
+	logger.Info("Importing images...")
+
+	cmd := exec.Command("k3d", args...)
+	cmd.Env = os.Environ()
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		logger.WithError(err).Errorf("unable to import images: %s", string(output))
+		return err
+	}
+
+	return nil
+}
+
+func createK8sClient(logger logrus.FieldLogger, clusterName, masterURL string) (k8s.Client, error) {
+	cmd := exec.Command("k3d", "kubeconfig", "write", clusterName)
+	cmd.Env = os.Environ()
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("unable to retrieve kubeconfig for cluster %q: %w", clusterName, err)
+	}
+
+	kubeConfigPath := strings.TrimSuffix(string(output), "\n")
+
+	logger.Info("Creating kubernetes client...")
+
+	var client k8s.Client
+
+	err = try.Retry(func() error {
+		client, err = k8s.NewClient(logger, masterURL, kubeConfigPath)
+		if err != nil {
+			return fmt.Errorf("unable to create clients: %v", err)
+		}
+
+		if _, err = client.KubernetesClient().Discovery().ServerVersion(); err != nil {
+			return fmt.Errorf("unable to get server version: %v", err)
+		}
+
+		return nil
+	}, 30*time.Second)
+
+	if err != nil {
+		return nil, fmt.Errorf("unable to create kubernetes client: %w", err)
+	}
+
+	logger.Infof("Setting new KUBECONFIG path: %q...", kubeConfigPath)
+
+	err = os.Setenv("KUBECONFIG", kubeConfigPath)
+	if err != nil {
+		return nil, fmt.Errorf("unable to set KUBECONFIG path: %w", err)
+	}
+
+	return client, nil
+}
+
+func waitClusterReady(client k8s.Client) error {
+	err := try.Retry(func() error {
+		_, _, err := client.KubernetesClient().Discovery().ServerGroupsAndResources()
+		return err
+	}, 60*time.Second)
+
+	if err != nil {
+		return fmt.Errorf("timed out waiting for the cluster to be ready: %w", err)
+	}
+
+	return nil
+}
diff --git a/integration/tool/tool.go b/integration/tool/tool.go
new file mode 100644
index 0000000..a26ac27
--- /dev/null
+++ b/integration/tool/tool.go
@@ -0,0 +1,133 @@
+package tool
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"net"
+	"net/http"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+
+	"github.com/containous/maesh/integration/try"
+	"github.com/sirupsen/logrus"
+)
+
+// Tool is pod capable of running operations from within the cluster.
+type Tool struct {
+	logger         logrus.FieldLogger
+	name           string
+	namespace      string
+	kubectlTimeout time.Duration
+}
+
+// New creates a new Tool.
+func New(logger logrus.FieldLogger, name, namespace string) *Tool {
+	return &Tool{
+		name:           name,
+		namespace:      namespace,
+		logger:         logger,
+		kubectlTimeout: 3 * time.Second,
+	}
+}
+
+// Dig digs the given url and make sure there is an A record.
+func (t *Tool) Dig(url string) error {
+	output, err := t.exec([]string{"dig", url, "+short", "+timeout=3"})
+	if err != nil {
+		t.logger.WithError(err).Debug("Dig command has failed")
+		return err
+	}
+
+	IP := net.ParseIP(strings.TrimSpace(string(output)))
+	if IP == nil {
+		return fmt.Errorf("could not parse an IP from dig: %s", string(output))
+	}
+
+	t.logger.Debugf("Dig %q: %s", url, IP.String())
+
+	return nil
+}
+
+// Curl curls the given url and checks if it matches the given conditions.
+func (t *Tool) Curl(url string, headers map[string]string, conditions ...try.ResponseCondition) error {
+	args := []string{"curl", "-s", "-D-", "-m", "2"}
+
+	for key, value := range headers {
+		args = append(args, "-H", key+": "+value)
+	}
+
+	output, err := t.exec(append(args, url))
+	if err != nil {
+		t.logger.WithError(err).Debug("Curl command has failed")
+		return err
+	}
+
+	reader := bufio.NewReader(bytes.NewReader(output))
+
+	resp, err := http.ReadResponse(reader, nil)
+	if err != nil {
+		t.logger.WithError(err).Debug("Curl output is not an HTTP response")
+		return err
+	}
+
+	defer resp.Body.Close()
+
+	for _, condition := range conditions {
+		if err := condition(resp); err != nil {
+			t.logger.Debugf("Curl condition did not match: %v", err)
+
+			return err
+		}
+	}
+
+	return nil
+}
+
+// Netcat netcats the given url on the given port and checks if the given conditions are fulfilled.
+func (t *Tool) Netcat(url string, port int, udp bool, conditions ...try.StringCondition) error {
+	var cmd string
+
+	if udp {
+		cmd = fmt.Sprintf("echo 'WHO' | nc -u -w 1 %s %d", url, port)
+	} else {
+		cmd = fmt.Sprintf("echo 'WHO' | nc -q 0 %s %d", url, port)
+	}
+
+	output, err := t.exec([]string{"ash", "-c", cmd})
+	if err != nil {
+		t.logger.WithError(err).Debug("Netcat command has failed")
+		return err
+	}
+
+	for _, condition := range conditions {
+		if err := condition(string(output)); err != nil {
+			t.logger.Debugf("Netcat condition did not match: %v", err)
+
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (t *Tool) exec(args []string) ([]byte, error) {
+	args = append([]string{
+		"exec", "-i", t.name,
+		"--request-timeout=3s",
+		"-n", t.namespace,
+		"--",
+	}, args...)
+
+	cmd := exec.Command("kubectl", args...)
+	cmd.Env = os.Environ()
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return []byte{}, fmt.Errorf("unable execute command 'kubectl %s' - output %s: %w", strings.Join(args, " "), output, err)
+	}
+
+	return output, nil
+}
diff --git a/integration/try/condition.go b/integration/try/condition.go
index 8a518ae..f617b59 100644
--- a/integration/try/condition.go
+++ b/integration/try/condition.go
@@ -1,34 +1,24 @@
 package try
 
 import (
-	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
-	"reflect"
 	"strings"
-
-	"github.com/abronan/valkeyrie/store"
 )
 
-// ResponseCondition is a retry condition function.
-// It receives a response, and returns an error
-// if the response failed the condition.
-type ResponseCondition func(*http.Response) error
-
-// BodyContains returns a retry condition function.
-// The condition returns an error if the request body does not contain all the given
-// strings.
-func BodyContains(values ...string) ResponseCondition {
-	return func(res *http.Response) error {
-		body, err := ioutil.ReadAll(res.Body)
-		if err != nil {
-			return fmt.Errorf("failed to read response body: %s", err)
-		}
+// StringCondition is a retry condition function.
+// It receives a string, and returns an error
+// if the string failed the condition.
+type StringCondition func(string) error
 
+// StringContains returns a retry condition function.
+// The condition returns an error if the string does not contain the given values.
+func StringContains(values ...string) StringCondition {
+	return func(res string) error {
 		for _, value := range values {
-			if !strings.Contains(string(body), value) {
-				return fmt.Errorf("could not find '%s' in body '%s'", value, string(body))
+			if !strings.Contains(res, value) {
+				return fmt.Errorf("could not find %q in %q", value, res)
 			}
 		}
 
@@ -36,30 +26,15 @@ func BodyContains(values ...string) ResponseCondition {
 	}
 }
 
-// BodyNotContains returns a retry condition function.
-// The condition returns an error if the request body  contain one of the given
-// strings.
-func BodyNotContains(values ...string) ResponseCondition {
-	return func(res *http.Response) error {
-		body, err := ioutil.ReadAll(res.Body)
-		if err != nil {
-			return fmt.Errorf("failed to read response body: %s", err)
-		}
-
-		for _, value := range values {
-			if strings.Contains(string(body), value) {
-				return fmt.Errorf("find '%s' in body '%s'", value, string(body))
-			}
-		}
-
-		return nil
-	}
-}
+// ResponseCondition is a retry condition function.
+// It receives a response, and returns an error
+// if the response failed the condition.
+type ResponseCondition func(*http.Response) error
 
-// BodyContainsOr returns a retry condition function.
-// The condition returns an error if the request body does not contain one of the given
+// BodyContains returns a retry condition function.
+// The condition returns an error if the request body does not contain all the given
 // strings.
-func BodyContainsOr(values ...string) ResponseCondition {
+func BodyContains(values ...string) ResponseCondition {
 	return func(res *http.Response) error {
 		body, err := ioutil.ReadAll(res.Body)
 		if err != nil {
@@ -67,53 +42,11 @@ func BodyContainsOr(values ...string) ResponseCondition {
 		}
 
 		for _, value := range values {
-			if strings.Contains(string(body), value) {
-				return nil
+			if !strings.Contains(string(body), value) {
+				return fmt.Errorf("could not find '%s' in body '%s'", value, string(body))
 			}
 		}
 
-		return fmt.Errorf("could not find '%v' in body '%s'", values, string(body))
-	}
-}
-
-// HasBody returns a retry condition function.
-// The condition returns an error if the request body does not have body content.
-func HasBody() ResponseCondition {
-	return func(res *http.Response) error {
-		body, err := ioutil.ReadAll(res.Body)
-		if err != nil {
-			return fmt.Errorf("failed to read response body: %s", err)
-		}
-
-		if len(body) == 0 {
-			return errors.New("response doesn't have body content")
-		}
-
-		return nil
-	}
-}
-
-// HasCn returns a retry condition function.
-// The condition returns an error if the cn is not correct.
-func HasCn(cn string) ResponseCondition {
-	return func(res *http.Response) error {
-		if res.TLS == nil {
-			return errors.New("response doesn't have TLS")
-		}
-
-		if len(res.TLS.PeerCertificates) == 0 {
-			return errors.New("response TLS doesn't have peer certificates")
-		}
-
-		if res.TLS.PeerCertificates[0] == nil {
-			return errors.New("first peer certificate is nil")
-		}
-
-		commonName := res.TLS.PeerCertificates[0].Subject.CommonName
-		if cn != commonName {
-			return fmt.Errorf("common name don't match: %s != %s", cn, commonName)
-		}
-
 		return nil
 	}
 }
@@ -130,76 +63,3 @@ func StatusCodeIs(status int) ResponseCondition {
 		return nil
 	}
 }
-
-// HasHeader returns a retry condition function.
-// The condition returns an error if the response does not have a header set.
-func HasHeader(header string) ResponseCondition {
-	return func(res *http.Response) error {
-		if _, ok := res.Header[header]; !ok {
-			return errors.New("response doesn't contain header: " + header)
-		}
-
-		return nil
-	}
-}
-
-// HasHeaderValue returns a retry condition function.
-// The condition returns an error if the response does not have a header set, and a value for that header.
-// Has an option to test for an exact header match only, not just contains.
-func HasHeaderValue(header, value string, exactMatch bool) ResponseCondition {
-	return func(res *http.Response) error {
-		if _, ok := res.Header[header]; !ok {
-			return errors.New("response doesn't contain header: " + header)
-		}
-
-		matchFound := false
-
-		for _, hdr := range res.Header[header] {
-			if value != hdr && exactMatch {
-				return fmt.Errorf("got header %s with value %s, wanted %s", header, hdr, value)
-			}
-
-			if value == hdr {
-				matchFound = true
-			}
-		}
-
-		if !matchFound {
-			return fmt.Errorf("response doesn't contain header %s with value %s", header, value)
-		}
-
-		return nil
-	}
-}
-
-// HasHeaderStruct returns a retry condition function.
-// The condition returns an error if the response does contain the headers set, and matching contents.
-func HasHeaderStruct(header http.Header) ResponseCondition {
-	return func(res *http.Response) error {
-		for key := range header {
-			if _, ok := res.Header[key]; !ok {
-				return fmt.Errorf("header %s not present in the response. Expected headers: %v Got response headers: %v", key, header, res.Header)
-			}
-
-			// Header exists in the response, test it.
-			if !reflect.DeepEqual(header[key], res.Header[key]) {
-				return fmt.Errorf("for header %s got values %v, wanted %v", key, res.Header[key], header[key])
-			}
-		}
-
-		return nil
-	}
-}
-
-// DoCondition is a retry condition function.
-// It returns an error.
-type DoCondition func() error
-
-// KVExists is a retry condition function.
-// Verify if a Key exists in the store.
-func KVExists(kv store.Store, key string) DoCondition {
-	return func() error {
-		_, err := kv.Exists(key, nil)
-		return err
-	}
-}
diff --git a/integration/try/try.go b/integration/try/try.go
index 8a1e7b6..4cfbbed 100644
--- a/integration/try/try.go
+++ b/integration/try/try.go
@@ -1,225 +1,20 @@
 package try
 
 import (
-	"context"
-	"errors"
 	"fmt"
-	"math"
-	"net/http"
 	"os"
-	"os/exec"
 	"strconv"
-	"strings"
 	"time"
 
 	"github.com/cenkalti/backoff/v4"
-	"github.com/containous/maesh/pkg/k8s"
 	"github.com/containous/maesh/pkg/safe"
-	"github.com/sirupsen/logrus"
-	appsv1 "k8s.io/api/apps/v1"
-	kubeerror "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/util/retry"
 )
 
-const (
-	// CITimeoutMultiplier is the multiplier for all timeout in the CI
-	CITimeoutMultiplier = 3
-	maxInterval         = 5 * time.Second
-)
-
-type timedAction func(timeout time.Duration, operation DoCondition) error
-
-// Try holds try configuration.
-type Try struct {
-	client k8s.Client
-	logger logrus.FieldLogger
-}
-
-// NewTry creates a new try.
-func NewTry(client k8s.Client) *Try {
-	log := logrus.New()
-
-	log.SetOutput(os.Stdout)
-	log.SetLevel(logrus.DebugLevel)
-
-	return &Try{client: client, logger: log}
-}
-
-// WaitReadyDeployment wait until the deployment is ready.
-func (t *Try) WaitReadyDeployment(name string, namespace string, timeout time.Duration) error {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
-
-	if err := backoff.Retry(safe.OperationWithRecover(func() error {
-		d, err := t.client.KubernetesClient().AppsV1().Deployments(namespace).Get(context.Background(), name, metav1.GetOptions{})
-		if err != nil {
-			if kubeerror.IsNotFound(err) {
-				return fmt.Errorf("deployment %q has not been yet created", name)
-			}
-
-			return fmt.Errorf("unable get the deployment %q in namespace %q: %v", name, namespace, err)
-		}
-
-		if d.Status.UpdatedReplicas == *(d.Spec.Replicas) &&
-			d.Status.Replicas == *(d.Spec.Replicas) &&
-			d.Status.AvailableReplicas == *(d.Spec.Replicas) &&
-			d.Status.ObservedGeneration >= d.Generation {
-			return nil
-		}
-
-		return errors.New("deployment not ready")
-	}), ebo); err != nil {
-		return fmt.Errorf("unable get the deployment %q in namespace %q: %v", name, namespace, err)
-	}
-
-	return nil
-}
-
-// WaitReadyDaemonset wait until the daemonset is ready.
-func (t *Try) WaitReadyDaemonset(name string, namespace string, timeout time.Duration) error {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
-
-	if err := backoff.Retry(safe.OperationWithRecover(func() error {
-		d, err := t.client.KubernetesClient().AppsV1().DaemonSets(namespace).Get(context.Background(), name, metav1.GetOptions{})
-		if err != nil {
-			if kubeerror.IsNotFound(err) {
-				return fmt.Errorf("daemonset %q has not been yet created", name)
-			}
-
-			return fmt.Errorf("unable get the daemonset %q in namespace %q: %v", name, namespace, err)
-		}
-		if d.Status.NumberReady == d.Status.DesiredNumberScheduled {
-			return nil
-		}
-		return errors.New("daemonset not ready")
-	}), ebo); err != nil {
-		return fmt.Errorf("unable get the daemonset %q in namespace %q: %v", name, namespace, err)
-	}
-
-	return nil
-}
-
-// WaitUpdateDeployment waits until the deployment is successfully updated and ready.
-func (t *Try) WaitUpdateDeployment(deployment *appsv1.Deployment, timeout time.Duration) error {
-	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
-		_, err := t.client.KubernetesClient().AppsV1().Deployments(deployment.Namespace).Update(context.Background(), deployment, metav1.UpdateOptions{})
-		return err
-	})
-
-	if retryErr != nil {
-		return fmt.Errorf("unable to update deployment %q: %v", deployment.Name, retryErr)
-	}
-
-	return t.WaitReadyDeployment(deployment.Name, deployment.Namespace, timeout)
-}
-
-// WaitDeleteDeployment wait until the deployment is delete.
-func (t *Try) WaitDeleteDeployment(name string, namespace string, timeout time.Duration) error {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
-
-	if err := backoff.Retry(safe.OperationWithRecover(func() error {
-		_, err := t.client.KubernetesClient().AppsV1().Deployments(namespace).Get(context.Background(), name, metav1.GetOptions{})
-		if err != nil {
-			if kubeerror.IsNotFound(err) {
-				return nil
-			}
-
-			return fmt.Errorf("unable get the deployment %q in namespace %q: %v", name, namespace, err)
-		}
-
-		return fmt.Errorf("deployment %q exist", name)
-	}), ebo); err != nil {
-		return fmt.Errorf("unable get the deployment %q in namespace %q: %v", name, namespace, err)
-	}
-
-	return nil
-}
-
-// WaitPodIPAssigned wait until the pod is assigned an IP.
-func (t *Try) WaitPodIPAssigned(name string, namespace string, timeout time.Duration) error {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
-
-	if err := backoff.Retry(safe.OperationWithRecover(func() error {
-		pod, err := t.client.KubernetesClient().CoreV1().Pods(namespace).Get(context.Background(), name, metav1.GetOptions{})
-		if err != nil {
-			return fmt.Errorf("unable get the pod %q in namespace %q: %v", name, namespace, err)
-		}
-
-		// If the pod IP is not empty, logger and return.
-		if pod.Status.PodIP != "" {
-			// IP is assigned
-			fmt.Printf("Pod %q has IP: %s\n", name, pod.Status.PodIP)
-			return nil
-		}
-
-		return errors.New("pod does not have an IP assigned")
-	}), ebo); err != nil {
-		return fmt.Errorf("unable get the pod IP for pod %s: %v", name, err)
-	}
-
-	return nil
-}
-
-// WaitCommandExecute wait until the command is executed.
-func (t *Try) WaitCommandExecute(command string, argSlice []string, expected string, timeout time.Duration) error {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
-
-	var output []byte
-
-	if err := backoff.Retry(safe.OperationWithRecover(func() error {
-		cmd := exec.Command(command, argSlice...)
-		cmd.Env = os.Environ()
-		var errOpt error
-		output, errOpt = cmd.CombinedOutput()
-		if errOpt != nil {
-			return fmt.Errorf("unable execute command %s %s - output %s: \n%v", command, strings.Join(argSlice, " "), output, errOpt)
-		}
-
-		if !strings.Contains(string(output), expected) {
-			return fmt.Errorf("output %s does not contain %s", string(output), expected)
-		}
-
-		t.logger.Debug(string(output))
-
-		return nil
-	}), ebo); err != nil {
-		return fmt.Errorf("unable execute command %s %s: \n%v", command, strings.Join(argSlice, " "), err)
-	}
-
-	return nil
-}
-
-// WaitCommandExecuteReturn wait until the command is executed.
-func (t *Try) WaitCommandExecuteReturn(command string, argSlice []string, timeout time.Duration) (string, error) {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
-
-	var output []byte
-
-	if err := backoff.Retry(safe.OperationWithRecover(func() error {
-		cmd := exec.Command(command, argSlice...)
-		cmd.Env = os.Environ()
-		var errOpt error
-		output, errOpt = cmd.CombinedOutput()
-		if errOpt != nil {
-			return fmt.Errorf("unable execute command %s %s - output %s: \n%v", command, strings.Join(argSlice, " "), output, errOpt)
-		}
-
-		return nil
-	}), ebo); err != nil {
-		return "", fmt.Errorf("unable execute command %s %s: \n%v", command, strings.Join(argSlice, " "), err)
-	}
-
-	return string(output), nil
-}
+// CITimeoutMultiplier is the multiplier for all timeout in the CI.
+const CITimeoutMultiplier = 3
 
 // Retry runs a function over and over until it doesn't return an error or the given timeout duration is reached.
-func (t *Try) Retry(f func() error, timeout time.Duration) error {
+func Retry(f func() error, timeout time.Duration) error {
 	ebo := backoff.NewExponentialBackOff()
 	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
 
@@ -230,166 +25,12 @@ func (t *Try) Retry(f func() error, timeout time.Duration) error {
 	return nil
 }
 
-// WaitDeleteNamespace wait until the namespace is delete.
-func (t *Try) WaitDeleteNamespace(name string, timeout time.Duration) error {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
-
-	if err := backoff.Retry(safe.OperationWithRecover(func() error {
-		_, err := t.client.KubernetesClient().CoreV1().Namespaces().Get(context.Background(), name, metav1.GetOptions{})
-		if err != nil {
-			if kubeerror.IsNotFound(err) {
-				return nil
-			}
-
-			return fmt.Errorf("unable get the namesapce %q: %v", name, err)
-		}
-
-		return fmt.Errorf("namesapce %q exist", name)
-	}), ebo); err != nil {
-		return fmt.Errorf("unable get the namesapce %q: %v", name, err)
-	}
-
-	return nil
-}
-
-// WaitClientCreated wait until the file is created.
-func (t *Try) WaitClientCreated(url string, kubeConfigPath string, timeout time.Duration) (k8s.Client, error) {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = applyCIMultiplier(timeout)
-
-	var (
-		clients k8s.Client
-		err     error
-	)
-
-	log := logrus.New()
-
-	log.SetOutput(os.Stdout)
-	log.SetLevel(logrus.DebugLevel)
-
-	if err = backoff.Retry(safe.OperationWithRecover(func() error {
-		clients, err = k8s.NewClient(log, url, kubeConfigPath)
-		if err != nil {
-			return fmt.Errorf("unable to create clients: %v", err)
-		}
-
-		if _, err = clients.KubernetesClient().Discovery().ServerVersion(); err != nil {
-			return fmt.Errorf("unable to get server version: %v", err)
-		}
-
-		return nil
-	}), ebo); err != nil {
-		return nil, fmt.Errorf("unable to create clients: %v", err)
-	}
-
-	return clients, nil
-}
-
-// GetRequest is like Do, but runs a request against the given URL and applies
-// the condition on the response.
-// ResponseCondition may be nil, in which case only the request against the URL must
-// succeed.
-func GetRequest(url string, timeout time.Duration, conditions ...ResponseCondition) error {
-	resp, err := doTryGet(url, timeout, nil, conditions...)
-
-	if resp != nil && resp.Body != nil {
-		defer resp.Body.Close()
-	}
-
-	return err
-}
-
-func doTryGet(url string, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
-	req, err := http.NewRequest(http.MethodGet, url, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	return doTryRequest(req, timeout, transport, conditions...)
-}
-
-func doTryRequest(request *http.Request, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
-	return doRequest(Do, timeout, request, transport, conditions...)
-}
-
-func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
-	var resp *http.Response
-
-	return resp, action(timeout, func() error {
-		var err error
-		client := http.DefaultClient
-		if transport != nil {
-			client.Transport = transport
-		}
-
-		resp, err = client.Do(request)
-		if err != nil {
-			return err
-		}
-		defer resp.Body.Close()
-
-		for _, condition := range conditions {
-			if err := condition(resp); err != nil {
-				return err
-			}
-		}
-
-		return nil
-	})
-}
-
-// Do repeatedly executes an operation until no error condition occurs or the
-// given timeout is reached, whatever comes first.
-func Do(timeout time.Duration, operation DoCondition) error {
-	if timeout <= 0 {
-		panic("timeout must be larger than zero")
-	}
-
-	interval := time.Duration(math.Ceil(float64(timeout) / 15.0))
-	if interval > maxInterval {
-		interval = maxInterval
-	}
-
-	timeout = applyCIMultiplier(timeout)
-
-	var err error
-	if err = operation(); err == nil {
-		fmt.Println("+")
-		return nil
-	}
-
-	fmt.Print("*")
-
-	stopTimer := time.NewTimer(timeout)
-	defer stopTimer.Stop()
-
-	retryTick := time.NewTicker(interval)
-	defer retryTick.Stop()
-
-	for {
-		select {
-		case <-stopTimer.C:
-			fmt.Println("-")
-			return fmt.Errorf("try operation failed: %s", err)
-		case <-retryTick.C:
-			fmt.Print("*")
-
-			if err = operation(); err == nil {
-				fmt.Println("+")
-				return err
-			}
-		}
-	}
-}
-
 func applyCIMultiplier(timeout time.Duration) time.Duration {
 	if os.Getenv("CI") == "" {
 		return timeout
 	}
 
 	ciTimeoutMultiplier := getCITimeoutMultiplier()
-	logrus.Debug("Apply CI multiplier:", ciTimeoutMultiplier)
 
 	return time.Duration(float64(timeout) * ciTimeoutMultiplier)
 }


Test patch:
diff --git a/integration/acl_disabled_test.go b/integration/acl_disabled_test.go
index 2a8bd81..110e344 100644
--- a/integration/acl_disabled_test.go
+++ b/integration/acl_disabled_test.go
@@ -1,123 +1,128 @@
 package integration
 
 import (
+	"net/http"
+	"time"
+
+	"github.com/containous/maesh/integration/k3d"
+	"github.com/containous/maesh/integration/tool"
+	"github.com/containous/maesh/integration/try"
 	"github.com/go-check/check"
+	"github.com/sirupsen/logrus"
 	checker "github.com/vdemeester/shakers"
-	corev1 "k8s.io/api/core/v1"
 )
 
 // ACLDisabledSuite.
-type ACLDisabledSuite struct{ BaseSuite }
+type ACLDisabledSuite struct {
+	logger  logrus.FieldLogger
+	cluster *k3d.Cluster
+	tool    *tool.Tool
+}
 
 func (s *ACLDisabledSuite) SetUpSuite(c *check.C) {
-	requiredImages := []string{
-		"containous/maesh:latest",
-		"containous/whoami:v1.0.1",
-		"containous/whoamitcp:v0.0.2",
-		"containous/whoamiudp:v0.0.1",
-		"coredns/coredns:1.6.3",
+	var err error
+
+	requiredImages := []k3d.DockerImage{
+		{Name: "containous/maesh:latest", Local: true},
+		{Name: "traefik:v2.3"},
+		{Name: "containous/whoami:v1.0.1"},
+		{Name: "containous/whoamitcp:v0.0.2"},
+		{Name: "containous/whoamiudp:v0.0.1"},
+		{Name: "giantswarm/tiny-tools:3.9"},
 	}
-	s.startk3s(c, requiredImages)
-	s.startAndWaitForCoreDNS(c)
-	s.createResources(c, "testdata/smi/crds/")
-}
 
-func (s *ACLDisabledSuite) TearDownSuite(c *check.C) {
-	s.stopK3s()
-}
+	s.logger = logrus.New()
+	s.cluster, err = k3d.NewCluster(s.logger, masterURL, k3dClusterName,
+		k3d.WithoutTraefik(),
+		k3d.WithImages(requiredImages...),
+	)
+	c.Assert(err, checker.IsNil)
 
-func (s *ACLDisabledSuite) TestHTTPService(c *check.C) {
-	s.createResources(c, "testdata/acl/disabled/http")
-	defer s.deleteResources(c, "testdata/acl/disabled/http")
-	defer s.deleteShadowServices(c)
+	c.Assert(s.cluster.CreateNamespace(s.logger, maeshNamespace), checker.IsNil)
+	c.Assert(s.cluster.CreateNamespace(s.logger, testNamespace), checker.IsNil)
 
-	s.waitForPods(c, []string{"server"})
+	c.Assert(s.cluster.Apply(s.logger, smiCRDs), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/tool/tool.yaml"), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/maesh/controller-acl-disabled.yaml"), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/maesh/proxy.yaml"), checker.IsNil)
 
-	cmd := s.startMaeshBinaryCmd(c, false, false)
-	err := cmd.Start()
+	c.Assert(s.cluster.WaitReadyPod("tool", testNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDeployment("maesh-controller", maeshNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDaemonSet("maesh-mesh", maeshNamespace, 60*time.Second), checker.IsNil)
 
-	c.Assert(err, checker.IsNil)
-	defer s.stopMaeshBinary(c, cmd.Process)
+	s.tool = tool.New(s.logger, "tool", testNamespace)
+}
 
-	config := s.testConfigurationWithReturn(c, "testdata/acl/disabled/http.json")
+func (s *ACLDisabledSuite) TearDownSuite(c *check.C) {
+	if s.cluster != nil {
+		c.Assert(s.cluster.Stop(s.logger), checker.IsNil)
+	}
+}
 
-	serverSvc := s.getService(c, "server")
-	serverPod := s.getPod(c, "server")
+// TestHTTPService deploys an HTTP service "server" with one Pod called "server" and asserts this service is
+// reachable and responses are served by this Pod.
+func (s *ACLDisabledSuite) TestHTTPService(c *check.C) {
+	c.Assert(s.cluster.Apply(s.logger, "testdata/acl_disabled/http"), checker.IsNil)
+	defer s.cluster.Delete(s.logger, "testdata/acl_disabled/http")
 
-	s.checkBlockAllMiddleware(c, config)
-	s.checkHTTPReadinessService(c, config)
-	s.checkHTTPServiceLoadBalancer(c, config, serverSvc, []*corev1.Pod{serverPod})
+	s.assertHTTPServiceReachable(c, "server-http.test.maesh:8080", "server-http", 60*time.Second)
 }
 
+// TestTCPService deploys a TCP service "server" with one Pod called "server" and asserts this service is
+// reachable and that a connection has been established with this Pod.
 func (s *ACLDisabledSuite) TestTCPService(c *check.C) {
-	s.createResources(c, "testdata/acl/disabled/tcp")
-	defer s.deleteResources(c, "testdata/acl/disabled/tcp")
-	defer s.deleteShadowServices(c)
-
-	s.waitForPods(c, []string{"server"})
-
-	cmd := s.startMaeshBinaryCmd(c, false, false)
-	err := cmd.Start()
-
-	c.Assert(err, checker.IsNil)
-	defer s.stopMaeshBinary(c, cmd.Process)
-
-	config := s.testConfigurationWithReturn(c, "testdata/acl/disabled/tcp.json")
+	c.Assert(s.cluster.Apply(s.logger, "testdata/acl_disabled/tcp"), checker.IsNil)
+	defer s.cluster.Delete(s.logger, "testdata/acl_disabled/tcp")
 
-	serverSvc := s.getService(c, "server")
-	serverPod := s.getPod(c, "server")
-
-	s.checkHTTPReadinessService(c, config)
-	s.checkTCPServiceLoadBalancer(c, config, serverSvc, []*corev1.Pod{serverPod})
+	s.assertTCPServiceReachable(c, "server-tcp.test.maesh", 8080, "server-tcp", 60*time.Second)
 }
 
-func (s *ACLDisabledSuite) TestUDPService(c *check.C) {
-	s.createResources(c, "testdata/acl/disabled/udp")
-	defer s.deleteResources(c, "testdata/acl/disabled/udp")
-	defer s.deleteShadowServices(c)
-
-	s.waitForPods(c, []string{"server"})
-
-	cmd := s.startMaeshBinaryCmd(c, false, false)
-	err := cmd.Start()
-
-	c.Assert(err, checker.IsNil)
-	defer s.stopMaeshBinary(c, cmd.Process)
-
-	config := s.testConfigurationWithReturn(c, "testdata/acl/disabled/udp.json")
+// TestUDPService deploys a UDP service "server" with one Pod called "server" and asserts this service is
+// reachable and that a connection has been established with this Pod.
+func (s *ACLDisabledSuite) TestUDPervice(c *check.C) {
+	c.Assert(s.cluster.Apply(s.logger, "testdata/acl_disabled/udp"), checker.IsNil)
+	defer s.cluster.Delete(s.logger, "testdata/acl_disabled/udp")
 
-	serverSvc := s.getService(c, "server")
-	serverPod := s.getPod(c, "server")
-
-	s.checkHTTPReadinessService(c, config)
-	s.checkUDPServiceLoadBalancer(c, config, serverSvc, []*corev1.Pod{serverPod})
+	s.assertUDPServiceReachable(c, "server-udp.test.maesh", 8080, "server-udp", 60*time.Second)
 }
 
+// TestSplitTraffic deploys an HTTP service "server" and a TrafficSplit attached to it configured to distribute equally
+// the load between two service "server-v1" and "server-v2", each one having a Pod with the same name. This test ensure
+// both Pods are reachable through the service "server".
 func (s *ACLDisabledSuite) TestSplitTraffic(c *check.C) {
-	s.createResources(c, "testdata/acl/disabled/traffic-split")
-	defer s.deleteResources(c, "testdata/acl/disabled/traffic-split")
-	defer s.deleteShadowServices(c)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/acl_disabled/traffic-split"), checker.IsNil)
+	defer s.cluster.Delete(s.logger, "testdata/acl_disabled/traffic-split")
 
-	s.waitForPods(c, []string{"server-v1", "server-v2"})
+	s.assertHTTPServiceReachable(c, "server-split.test.maesh:8080", "server-v1", 60*time.Second)
+	s.assertHTTPServiceReachable(c, "server-split.test.maesh:8080", "server-v2", 30*time.Second)
+}
 
-	cmd := s.startMaeshBinaryCmd(c, false, false)
-	err := cmd.Start()
+func (s *ACLDisabledSuite) assertHTTPServiceReachable(c *check.C, url, expectedHostname string, timeout time.Duration) {
+	s.logger.Infof("Asserting HTTP service is reachable on %q and Pod %q has handled the request", url, expectedHostname)
 
+	err := try.Retry(func() error {
+		return s.tool.Curl(url, nil,
+			try.StatusCodeIs(http.StatusOK),
+			try.BodyContains("Hostname: "+expectedHostname),
+		)
+	}, timeout)
 	c.Assert(err, checker.IsNil)
-	defer s.stopMaeshBinary(c, cmd.Process)
-
-	config := s.testConfigurationWithReturn(c, "testdata/acl/disabled/traffic-split.json")
-
-	s.checkBlockAllMiddleware(c, config)
-	s.checkHTTPReadinessService(c, config)
+}
 
-	serverV1Svc := s.getService(c, "server-v1")
-	serverV1Pod := s.getPod(c, "server-v1")
+func (s *ACLDisabledSuite) assertTCPServiceReachable(c *check.C, url string, port int, expectedHostname string, timeout time.Duration) {
+	s.logger.Infof("Asserting TCP service is reachable on '%s:%d' and a connection with Pod %q is established", url, port, expectedHostname)
 
-	s.checkHTTPServiceLoadBalancer(c, config, serverV1Svc, []*corev1.Pod{serverV1Pod})
+	err := try.Retry(func() error {
+		return s.tool.Netcat(url, port, false, try.StringContains("Hostname: "+expectedHostname))
+	}, timeout)
+	c.Assert(err, checker.IsNil)
+}
 
-	serverV2Svc := s.getService(c, "server-v2")
-	serverV2Pod := s.getPod(c, "server-v2")
+func (s *ACLDisabledSuite) assertUDPServiceReachable(c *check.C, url string, port int, expectedHostname string, timeout time.Duration) {
+	s.logger.Infof("Asserting UDP service is reachable on '%s:%d' and a connection with Pod %q is established", url, port, expectedHostname)
 
-	s.checkHTTPServiceLoadBalancer(c, config, serverV2Svc, []*corev1.Pod{serverV2Pod})
+	err := try.Retry(func() error {
+		return s.tool.Netcat(url, port, true, try.StringContains("Hostname: "+expectedHostname))
+	}, timeout)
+	c.Assert(err, checker.IsNil)
 }
diff --git a/integration/acl_enabled_test.go b/integration/acl_enabled_test.go
index 86bbca0..d0d4f00 100644
--- a/integration/acl_enabled_test.go
+++ b/integration/acl_enabled_test.go
@@ -1,111 +1,98 @@
 package integration
 
 import (
+	"net/http"
+	"time"
+
+	"github.com/containous/maesh/integration/k3d"
+	"github.com/containous/maesh/integration/tool"
+	"github.com/containous/maesh/integration/try"
 	"github.com/go-check/check"
+	"github.com/sirupsen/logrus"
 	checker "github.com/vdemeester/shakers"
-	corev1 "k8s.io/api/core/v1"
 )
 
 // ACLEnabledSuite.
-type ACLEnabledSuite struct{ BaseSuite }
-
-func (s *ACLEnabledSuite) SetUpSuite(c *check.C) {
-	requiredImages := []string{
-		"containous/maesh:latest",
-		"containous/whoami:v1.0.1",
-		"containous/whoamitcp:v0.0.2",
-		"coredns/coredns:1.6.3",
-		"giantswarm/tiny-tools:3.9",
-	}
-	s.startk3s(c, requiredImages)
-	s.startAndWaitForCoreDNS(c)
-	s.createResources(c, "testdata/smi/crds/")
-}
-
-func (s *ACLEnabledSuite) TearDownSuite(c *check.C) {
-	s.stopK3s()
-}
-
-func (s *ACLEnabledSuite) TestTrafficTargetWithACL(c *check.C) {
-	s.testTrafficTarget(false, true, c)
+type ACLEnabledSuite struct {
+	logger         logrus.FieldLogger
+	cluster        *k3d.Cluster
+	toolAuthorized *tool.Tool
+	toolForbidden  *tool.Tool
 }
 
-// For the sake of BC, we need be check if the SMI option is handle correctly.
-func (s *ACLEnabledSuite) TestTrafficTargetWithSMI(c *check.C) {
-	s.testTrafficTarget(true, false, c)
-}
-
-func (s *ACLEnabledSuite) testTrafficTarget(smi, acl bool, c *check.C) {
-	s.createResources(c, "testdata/acl/enabled/traffic-target")
-	defer s.deleteResources(c, "testdata/acl/enabled/traffic-target")
-	defer s.deleteShadowServices(c)
-
-	s.waitForPods(c, []string{"client-a", "client-b", "server"})
+func (s *ACLEnabledSuite) SetUpSuite(c *check.C) {
+	var err error
 
-	cmd := s.startMaeshBinaryCmd(c, smi, acl)
-	err := cmd.Start()
+	requiredImages := []k3d.DockerImage{
+		{Name: "containous/maesh:latest", Local: true},
+		{Name: "traefik:v2.3"},
+		{Name: "containous/whoami:v1.0.1"},
+		{Name: "giantswarm/tiny-tools:3.9"},
+	}
 
+	s.logger = logrus.New()
+	s.cluster, err = k3d.NewCluster(s.logger, masterURL, k3dClusterName,
+		k3d.WithoutTraefik(),
+		k3d.WithImages(requiredImages...),
+	)
 	c.Assert(err, checker.IsNil)
-	defer s.stopMaeshBinary(c, cmd.Process)
 
-	config := s.testConfigurationWithReturn(c, "testdata/acl/enabled/traffic-target.json")
+	c.Assert(s.cluster.CreateNamespace(s.logger, maeshNamespace), checker.IsNil)
+	c.Assert(s.cluster.CreateNamespace(s.logger, testNamespace), checker.IsNil)
 
-	svc := s.getService(c, "server")
-	tt := s.getTrafficTarget(c, "traffic-target")
-	serverPod := s.getPod(c, "server")
-	clientAPod := s.getPod(c, "client-a")
+	c.Assert(s.cluster.Apply(s.logger, smiCRDs), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/tool/tool-authorized.yaml"), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/tool/tool-forbidden.yaml"), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/maesh/controller-acl-enabled.yaml"), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/maesh/proxy.yaml"), checker.IsNil)
 
-	s.checkBlockAllMiddleware(c, config)
-	s.checkHTTPReadinessService(c, config)
-	s.checkTrafficTargetLoadBalancer(c, config, tt, svc, []*corev1.Pod{serverPod})
-	s.checkTrafficTargetWhitelistDirect(c, config, tt, svc, []*corev1.Pod{clientAPod})
-}
+	c.Assert(s.cluster.WaitReadyPod("tool-authorized", testNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyPod("tool-forbidden", testNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDeployment("maesh-controller", maeshNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDaemonSet("maesh-mesh", maeshNamespace, 60*time.Second), checker.IsNil)
 
-func (s *ACLEnabledSuite) TestTrafficSplitACLEnable(c *check.C) {
-	s.testTrafficSplit(false, true, c)
+	s.toolAuthorized = tool.New(s.logger, "tool-authorized", testNamespace)
+	s.toolForbidden = tool.New(s.logger, "tool-forbidden", testNamespace)
 }
 
-// For the sake of BC, we need be check if the SMI option is handle correctly.
-func (s *ACLEnabledSuite) TestTrafficSplitSMIEnable(c *check.C) {
-	s.testTrafficSplit(true, false, c)
+func (s *ACLEnabledSuite) TearDownSuite(c *check.C) {
+	if s.cluster != nil {
+		c.Assert(s.cluster.Stop(s.logger), checker.IsNil)
+	}
 }
 
-func (s *ACLEnabledSuite) testTrafficSplit(smi, acl bool, c *check.C) {
-	s.createResources(c, "testdata/acl/enabled/traffic-split")
-	defer s.deleteResources(c, "testdata/acl/enabled/traffic-split")
-	defer s.deleteShadowServices(c)
-
-	s.waitForPods(c, []string{"client-a", "client-b", "server-v1", "server-v2"})
+func (s *ACLEnabledSuite) TestHTTPServiceWithTrafficTarget(c *check.C) {
+	c.Assert(s.cluster.Apply(s.logger, "testdata/acl_enabled/http"), checker.IsNil)
+	defer s.cluster.Delete(s.logger, "testdata/acl_enabled/http")
 
-	cmd := s.startMaeshBinaryCmd(c, smi, acl)
-	err := cmd.Start()
+	s.logger.Infof("Asserting TrafficTarget with no HTTPRouteGroup are enforced")
+	s.assertHTTPServiceStatus(c, s.toolAuthorized, "server-http.test.maesh:8080", nil, http.StatusOK)
+	s.assertHTTPServiceStatus(c, s.toolForbidden, "server-http.test.maesh:8080", nil, http.StatusForbidden)
 
-	c.Assert(err, checker.IsNil)
-	defer s.stopMaeshBinary(c, cmd.Process)
-
-	config := s.testConfigurationWithReturn(c, "testdata/acl/enabled/traffic-split.json")
-
-	s.checkBlockAllMiddleware(c, config)
-	s.checkHTTPReadinessService(c, config)
+	s.logger.Infof("Asserting HTTPRouteGroup path filtering is enforced")
+	s.assertHTTPServiceStatus(c, s.toolAuthorized, "server-http-api.test.maesh:8080/api", nil, http.StatusOK)
+	s.assertHTTPServiceStatus(c, s.toolAuthorized, "server-http-api.test.maesh:8080", nil, http.StatusForbidden)
 
-	tt := s.getTrafficTarget(c, "traffic-target")
-	ts := s.getTrafficSplit(c, "traffic-split")
-	clientAPod := s.getPod(c, "client-a")
-	serverSvc := s.getService(c, "server")
+	s.logger.Infof("Asserting HTTPRouteGroup header filtering is enforced")
+	s.assertHTTPServiceStatus(c, s.toolAuthorized, "server-http-header.test.maesh:8080", map[string]string{"Authorized": "true"}, http.StatusOK)
+	s.assertHTTPServiceStatus(c, s.toolAuthorized, "server-http-header.test.maesh:8080", map[string]string{"Authorized": "false"}, http.StatusForbidden)
+}
 
-	s.checkTrafficSplitWhitelistDirect(c, config, ts, serverSvc, []*corev1.Pod{clientAPod})
+func (s *ACLEnabledSuite) TestHTTPServiceWithTrafficSplit(c *check.C) {
+	c.Assert(s.cluster.Apply(s.logger, "testdata/acl_enabled/traffic-split"), checker.IsNil)
+	defer s.cluster.Delete(s.logger, "testdata/acl_enabled/traffic-split")
 
-	serverV1Svc := s.getService(c, "server-v1")
-	serverV1Pod := s.getPod(c, "server-v1")
+	s.logger.Info("Asserting TrafficTarget is enforced")
+	s.assertHTTPServiceStatus(c, s.toolAuthorized, "server-http-split.test.maesh:8080", nil, http.StatusOK)
+	s.assertHTTPServiceStatus(c, s.toolForbidden, "server-http-split.test.maesh:8080", nil, http.StatusForbidden)
+}
 
-	s.checkTrafficTargetLoadBalancer(c, config, tt, serverV1Svc, []*corev1.Pod{serverV1Pod})
-	s.checkTrafficTargetWhitelistDirect(c, config, tt, serverV1Svc, []*corev1.Pod{clientAPod})
-	s.checkTrafficTargetWhitelistIndirect(c, config, tt, serverV1Svc, []*corev1.Pod{clientAPod})
+func (s *ACLEnabledSuite) assertHTTPServiceStatus(c *check.C, t *tool.Tool, url string, headers map[string]string, expectedStatus int) {
+	s.logger.Infof("Asserting status is %q on %q with headers: %v", http.StatusText(expectedStatus), url, headers)
 
-	serverV2Svc := s.getService(c, "server-v2")
-	serverV2Pod := s.getPod(c, "server-v2")
+	err := try.Retry(func() error {
+		return t.Curl(url, headers, try.StatusCodeIs(expectedStatus))
+	}, 60*time.Second)
 
-	s.checkTrafficTargetLoadBalancer(c, config, tt, serverV2Svc, []*corev1.Pod{serverV2Pod})
-	s.checkTrafficTargetWhitelistDirect(c, config, tt, serverV2Svc, []*corev1.Pod{clientAPod})
-	s.checkTrafficTargetWhitelistIndirect(c, config, tt, serverV2Svc, []*corev1.Pod{clientAPod})
+	c.Assert(err, checker.IsNil)
 }
diff --git a/integration/coredns_test.go b/integration/coredns_test.go
index 7266999..243d91d 100644
--- a/integration/coredns_test.go
+++ b/integration/coredns_test.go
@@ -1,153 +1,175 @@
 package integration
 
 import (
-	"os"
+	"context"
+	"fmt"
+	"time"
 
+	"github.com/containous/maesh/integration/k3d"
+	"github.com/containous/maesh/integration/tool"
+	"github.com/containous/maesh/integration/try"
 	"github.com/go-check/check"
+	"github.com/sirupsen/logrus"
 	checker "github.com/vdemeester/shakers"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/util/retry"
 )
 
 // CoreDNSSuite.
-type CoreDNSSuite struct{ BaseSuite }
+type CoreDNSSuite struct {
+	logger  logrus.FieldLogger
+	cluster *k3d.Cluster
+	tool    *tool.Tool
+}
 
 func (s *CoreDNSSuite) SetUpSuite(c *check.C) {
-	requiredImages := []string{
-		"containous/whoami:v1.0.1",
-		"coredns/coredns:1.2.6",
-		"coredns/coredns:1.3.1",
-		"coredns/coredns:1.4.0",
-		"coredns/coredns:1.5.2",
-		"coredns/coredns:1.6.3",
-		"coredns/coredns:1.7.0",
-		"giantswarm/tiny-tools:3.9",
+	var err error
+
+	requiredImages := []k3d.DockerImage{
+		{Name: "containous/whoami:v1.0.1"},
+		{Name: "coredns/coredns:1.3.1"},
+		{Name: "coredns/coredns:1.4.0"},
+		{Name: "coredns/coredns:1.5.2"},
+		{Name: "coredns/coredns:1.6.3"},
+		{Name: "coredns/coredns:1.7.0"},
+		{Name: "giantswarm/tiny-tools:3.9"},
 	}
 
-	s.startk3s(c, requiredImages)
-	s.startWhoami(c)
-	s.installTinyToolsMaesh(c)
-	s.createResources(c, "testdata/smi/crds/")
+	s.logger = logrus.New()
+	s.cluster, err = k3d.NewCluster(s.logger, masterURL, k3dClusterName,
+		k3d.WithoutTraefik(),
+		k3d.WithImages(requiredImages...),
+	)
+	c.Assert(err, checker.IsNil)
+
+	c.Assert(s.cluster.CreateNamespace(s.logger, maeshNamespace), checker.IsNil)
+	c.Assert(s.cluster.CreateNamespace(s.logger, testNamespace), checker.IsNil)
+
+	c.Assert(s.cluster.Apply(s.logger, smiCRDs), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/tool/tool.yaml"), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/coredns/whoami-shadow-service.yaml"), checker.IsNil)
+
+	c.Assert(s.cluster.WaitReadyPod("tool", testNamespace, 60*time.Second), checker.IsNil)
+
+	s.tool = tool.New(s.logger, "tool", testNamespace)
 }
 
 func (s *CoreDNSSuite) TearDownSuite(c *check.C) {
-	s.stopK3s()
+	if s.cluster != nil {
+		c.Assert(s.cluster.Stop(s.logger), checker.IsNil)
+	}
 }
 
-func (s *CoreDNSSuite) TestCoreDNSVersionSafe(c *check.C) {
-	testCases := []struct {
-		desc          string
-		version       string
-		expectedError bool
-	}{
-		{
-			desc:          "CoreDNS 1.2.6",
-			version:       "1.2.6",
-			expectedError: true,
-		},
-		{
-			desc:          "CoreDNS 1.3.1",
-			version:       "1.3.1",
-			expectedError: false,
-		},
-		{
-			desc:          "CoreDNS 1.4.0",
-			version:       "1.4.0",
-			expectedError: false,
-		},
-	}
+func (s *CoreDNSSuite) TestCoreDNS(c *check.C) {
+	versions := []string{"1.5.2", "1.6.3", "1.7.0"}
 
-	for _, test := range testCases {
-		s.createResources(c, "testdata/coredns/corednssafe.yaml")
-		s.WaitForCoreDNS(c)
+	for _, version := range versions {
+		c.Assert(s.resetCoreDNSCorefile(true), checker.IsNil)
 
-		c.Logf("Testing compatibility with %s", test.desc)
-		s.setCoreDNSVersion(c, test.version)
+		s.testCoreDNSVersion(c, version)
+	}
 
-		cmd := s.maeshPrepareWithArgs()
-		cmd.Env = os.Environ()
-		output, err := cmd.CombinedOutput()
+	// Test specific versions of CoreDNS which did not support the `ready` plugin.
+	c.Assert(s.removeCoreDNSReadinessProbe(), checker.IsNil)
 
-		c.Log(string(output))
+	versions = []string{"1.3.1", "1.4.0"}
 
-		if test.expectedError {
-			c.Assert(err, checker.NotNil)
-		} else {
-			c.Assert(err, checker.IsNil)
-		}
+	for _, version := range versions {
+		c.Assert(s.resetCoreDNSCorefile(false), checker.IsNil)
 
-		s.deleteResources(c, "testdata/coredns/corednssafe.yaml")
+		s.testCoreDNSVersion(c, version)
 	}
 }
 
-func (s *CoreDNSSuite) TestCoreDNSVersion(c *check.C) {
-	testCases := []struct {
-		desc    string
-		version string
-	}{
-		{
-			desc:    "CoreDNS 1.5.2",
-			version: "1.5.2",
-		},
-		{
-			desc:    "CoreDNS 1.6.3",
-			version: "1.6.3",
-		},
-		{
-			desc:    "CoreDNS 1.7.0",
-			version: "1.7.0",
-		},
-	}
+func (s *CoreDNSSuite) testCoreDNSVersion(c *check.C, version string) {
+	s.logger.Infof("Asserting CoreDNS %s has been patched successfully and can be dug", version)
 
-	for _, test := range testCases {
-		s.createResources(c, "testdata/coredns/coredns.yaml")
-		s.WaitForCoreDNS(c)
+	c.Assert(s.setCoreDNSVersion(version), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDeployment("coredns", metav1.NamespaceSystem, 60*time.Second), checker.IsNil)
 
-		c.Logf("Testing compatibility with %s", test.desc)
-		s.setCoreDNSVersion(c, test.version)
+	c.Assert(maeshPrepare(), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDeployment("coredns", metav1.NamespaceSystem, 60*time.Second), checker.IsNil)
 
-		cmd := s.maeshPrepareWithArgs()
-		cmd.Env = os.Environ()
-		output, err := cmd.CombinedOutput()
+	err := try.Retry(func() error {
+		return s.tool.Dig("whoami.whoami.maesh")
+	}, 30*time.Second)
+	c.Assert(err, checker.IsNil)
+}
 
-		c.Log(string(output))
-		c.Assert(err, checker.IsNil)
+func (s *CoreDNSSuite) setCoreDNSVersion(version string) error {
+	ctx := context.Background()
 
-		s.deleteResources(c, "testdata/coredns/coredns.yaml")
-	}
+	s.logger.Debugf("Updating CoreDNS version to %q...", version)
+
+	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
+		// Get current coreDNS deployment.
+		deployment, err := s.cluster.Client.KubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Get(ctx, "coredns", metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+
+		deployment.Spec.Template.Spec.Containers[0].Image = fmt.Sprintf("coredns/coredns:%s", version)
+
+		_, err = s.cluster.Client.KubernetesClient().AppsV1().Deployments(deployment.Namespace).Update(context.Background(), deployment, metav1.UpdateOptions{})
+		return err
+	})
 }
 
-func (s *CoreDNSSuite) TestCoreDNSDig(c *check.C) {
-	testCases := []struct {
-		desc    string
-		version string
-	}{
-		{
-			desc:    "CoreDNS 1.6.3",
-			version: "1.6.3",
-		},
-		{
-			desc:    "CoreDNS 1.7.0",
-			version: "1.7.0",
-		},
-	}
+func (s *CoreDNSSuite) removeCoreDNSReadinessProbe() error {
+	ctx := context.Background()
 
-	for _, test := range testCases {
-		s.createResources(c, "testdata/coredns/coredns.yaml")
-		s.WaitForCoreDNS(c)
+	s.logger.Debug("Removing CoreDNS readiness probe...")
 
-		c.Logf("Testing dig with %s", test.desc)
-		s.setCoreDNSVersion(c, test.version)
+	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
+		deployment, err := s.cluster.Client.KubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Get(ctx, "coredns", metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
 
-		cmd := s.startMaeshBinaryCmd(c, false, false)
+		deployment.Spec.Template.Spec.Containers[0].ReadinessProbe = nil
 
-		err := cmd.Start()
-		c.Assert(err, checker.IsNil)
+		_, err = s.cluster.Client.KubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Update(ctx, deployment, metav1.UpdateOptions{})
+		return err
+	})
+}
 
-		pod := s.getToolsPodMaesh(c)
-		c.Assert(pod, checker.NotNil)
+func (s *CoreDNSSuite) resetCoreDNSCorefile(ready bool) error {
+	ctx := context.Background()
 
-		s.digHost(c, pod.Name, pod.Namespace, "whoami.whoami.maesh")
-		s.stopMaeshBinary(c, cmd.Process)
+	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
+		configmap, err := s.cluster.Client.KubernetesClient().CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(ctx, "coredns", metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
 
-		s.deleteResources(c, "testdata/coredns/coredns.yaml")
+		var readyPlugin string
+
+		if ready {
+			readyPlugin = "ready"
+		}
+
+		configmap.Data["Corefile"] = fmt.Sprintf(`.:53 {
+	errors
+	health
+	%s
+	kubernetes cluster.local in-addr.arpa ip6.arpa {
+		pods insecure
+		fallthrough in-addr.arpa ip6.arpa
 	}
+	hosts /etc/coredns/NodeHosts {
+		reload 1s
+		fallthrough
+	}
+	prometheus :9153
+	forward . /etc/resolv.conf
+	cache 30
+	loop
+	reload
+	loadbalance
+}
+`, readyPlugin)
+
+		_, err = s.cluster.Client.KubernetesClient().CoreV1().ConfigMaps(metav1.NamespaceSystem).Update(ctx, configmap, metav1.UpdateOptions{})
+		return err
+	})
 }
diff --git a/integration/helm_test.go b/integration/helm_test.go
index a24c0c2..ecc0b55 100644
--- a/integration/helm_test.go
+++ b/integration/helm_test.go
@@ -1,55 +1,112 @@
 package integration
 
 import (
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+
+	"github.com/containous/maesh/integration/k3d"
 	"github.com/go-check/check"
+	"github.com/sirupsen/logrus"
 	checker "github.com/vdemeester/shakers"
 )
 
 // HelmSuite.
-type HelmSuite struct{ BaseSuite }
+type HelmSuite struct {
+	logger  logrus.FieldLogger
+	cluster *k3d.Cluster
+}
 
 func (s *HelmSuite) SetUpSuite(c *check.C) {
-	requiredImages := []string{
-		"containous/maesh:latest",
-		"coredns/coredns:1.6.3",
-		"traefik:v2.3",
+	var err error
+
+	requiredImages := []k3d.DockerImage{
+		{Name: "containous/maesh:latest", Local: true},
+		{Name: "traefik:v2.3"},
 	}
-	s.startk3s(c, requiredImages)
-	s.startAndWaitForCoreDNS(c)
-	s.startWhoami(c)
-	s.createResources(c, "testdata/smi/crds/")
+
+	s.logger = logrus.New()
+	s.cluster, err = k3d.NewCluster(s.logger, masterURL, k3dClusterName,
+		k3d.WithoutTraefik(),
+		k3d.WithImages(requiredImages...),
+	)
+	c.Assert(err, checker.IsNil)
+
+	c.Assert(s.cluster.CreateNamespace(s.logger, maeshNamespace), checker.IsNil)
 }
 
 func (s *HelmSuite) TearDownSuite(c *check.C) {
-	s.stopK3s()
+	if s.cluster != nil {
+		c.Assert(s.cluster.Stop(s.logger), checker.IsNil)
+	}
 }
 
 func (s *HelmSuite) TestACLDisabled(c *check.C) {
-	err := s.installHelmMaesh(c, false, false)
-	c.Assert(err, checker.IsNil)
-
+	s.installHelmMaesh(c, false, false)
 	defer s.uninstallHelmMaesh(c)
 
-	s.waitForMaeshControllerStarted(c)
-	s.waitForMaeshProxyStarted(c)
+	c.Assert(s.cluster.WaitReadyDeployment("maesh-controller", maeshNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDaemonSet("maesh-mesh", maeshNamespace, 60*time.Second), checker.IsNil)
 }
 
 func (s *HelmSuite) TestACLEnabled(c *check.C) {
-	err := s.installHelmMaesh(c, true, false)
-	c.Assert(err, checker.IsNil)
-
+	s.installHelmMaesh(c, true, false)
 	defer s.uninstallHelmMaesh(c)
 
-	s.waitForMaeshControllerStarted(c)
-	s.waitForMaeshProxyStarted(c)
+	c.Assert(s.cluster.WaitReadyDeployment("maesh-controller", maeshNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDaemonSet("maesh-mesh", maeshNamespace, 60*time.Second), checker.IsNil)
 }
 
 func (s *HelmSuite) TestKubeDNSEnabled(c *check.C) {
-	err := s.installHelmMaesh(c, false, true)
-	c.Assert(err, checker.IsNil)
-
+	s.installHelmMaesh(c, false, true)
 	defer s.uninstallHelmMaesh(c)
 
-	s.waitForMaeshControllerStarted(c)
-	s.waitForMaeshProxyStarted(c)
+	c.Assert(s.cluster.WaitReadyDeployment("maesh-controller", maeshNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDaemonSet("maesh-mesh", maeshNamespace, 60*time.Second), checker.IsNil)
+}
+
+func (s *HelmSuite) installHelmMaesh(c *check.C, acl bool, kubeDNS bool) {
+	s.logger.Info("Installing Maesh via helm...")
+
+	args := []string{
+		"install", "powpow", "../helm/chart/maesh",
+		"--values", "testdata/maesh/values.yaml",
+		"--namespace", maeshNamespace,
+	}
+
+	if kubeDNS {
+		args = append(args, "--set", "kubedns=true")
+	}
+
+	if acl {
+		args = append(args, "--set", "acl=true")
+	}
+
+	cmd := exec.Command("helm", args...)
+	cmd.Env = os.Environ()
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		c.Errorf("unable execute command 'helm %s' - output %s: %w", strings.Join(args, " "), output, err)
+		c.Fail()
+	}
+}
+
+func (s *HelmSuite) uninstallHelmMaesh(c *check.C) {
+	s.logger.Info("Uninstalling Maesh via helm...")
+
+	args := []string{
+		"uninstall", "powpow",
+		"--namespace", maeshNamespace,
+	}
+
+	cmd := exec.Command("helm", args...)
+	cmd.Env = os.Environ()
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		c.Errorf("unable execute command 'helm %s' - output %s: %w", strings.Join(args, " "), output, err)
+		c.Fail()
+	}
 }
diff --git a/integration/integration_test.go b/integration/integration_test.go
index 617649d..e3149d9 100644
--- a/integration/integration_test.go
+++ b/integration/integration_test.go
@@ -1,52 +1,26 @@
 package integration
 
 import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"errors"
 	"flag"
 	"fmt"
-	"io"
-	"io/ioutil"
 	"log"
-	"net"
-	"net/http"
 	"os"
 	"os/exec"
-	"path"
-	"path/filepath"
-	"regexp"
-	"strings"
 	"testing"
-	"time"
 
-	"github.com/cenkalti/backoff/v4"
-	"github.com/containous/maesh/integration/try"
-	"github.com/containous/maesh/pkg/k8s"
-	"github.com/containous/maesh/pkg/safe"
-	"github.com/containous/traefik/v2/pkg/config/dynamic"
 	"github.com/go-check/check"
-	"github.com/pmezard/go-difflib/difflib"
-	access "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/access/v1alpha2"
-	split "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha3"
-	checker "github.com/vdemeester/shakers"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"github.com/sirupsen/logrus"
 )
 
 var (
-	integration           = flag.Bool("integration", false, "run integration tests")
-	masterURL             = "https://localhost:8443"
-	images                []image
-	k3dClusterName        = "maesh-integration"
-	k3sImage              = "rancher/k3s"
-	k3sVersion            = "v0.10.1"
-	maeshNamespace        = "maesh"
-	maeshBinary           = "../dist/maesh"
-	maeshAPIPort          = 9000
-	testNamespace         = "test"
-	kubectlCreateWaitTime = 1 * time.Second
+	integration    = flag.Bool("integration", false, "run integration tests")
+	debug          = flag.Bool("debug", false, "debug log level")
+	masterURL      = "https://localhost:8443"
+	k3dClusterName = "maesh-integration"
+	maeshNamespace = "maesh"
+	maeshBinary    = "../dist/maesh"
+	smiCRDs        = "../helm/chart/maesh/crds/"
+	testNamespace  = "test"
 )
 
 func Test(t *testing.T) {
@@ -55,691 +29,36 @@ func Test(t *testing.T) {
 		return
 	}
 
+	if *debug {
+		logrus.SetLevel(logrus.DebugLevel)
+	}
+
 	check.Suite(&ACLDisabledSuite{})
 	check.Suite(&ACLEnabledSuite{})
 	check.Suite(&CoreDNSSuite{})
 	check.Suite(&KubeDNSSuite{})
 	check.Suite(&HelmSuite{})
 
-	images = append(images, image{"containous/maesh:latest", false})
-	images = append(images, image{"containous/whoami:v1.0.1", true})
-	images = append(images, image{"containous/whoamitcp:v0.0.2", true})
-	images = append(images, image{"containous/whoamiudp:v0.0.1", true})
-	images = append(images, image{"coredns/coredns:1.2.6", true})
-	images = append(images, image{"coredns/coredns:1.3.1", true})
-	images = append(images, image{"coredns/coredns:1.4.0", true})
-	images = append(images, image{"coredns/coredns:1.5.2", true})
-	images = append(images, image{"coredns/coredns:1.6.3", true})
-	images = append(images, image{"coredns/coredns:1.7.0", true})
-	images = append(images, image{"giantswarm/tiny-tools:3.9", true})
-	images = append(images, image{"gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.7", true})
-	images = append(images, image{"gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.7", true})
-	images = append(images, image{"gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.7", true})
-	images = append(images, image{"traefik:v2.3", true})
-
-	for _, image := range images {
-		if image.pull {
-			cmd := exec.Command("docker", "pull", image.name)
-			cmd.Env = os.Environ()
-
-			output, err := cmd.CombinedOutput()
-			fmt.Println(string(output))
-
-			if err != nil {
-				fmt.Printf("unable to pull docker image: %v", err)
-			}
-		}
-	}
-
 	check.TestingT(t)
 }
 
-type image struct {
-	name string
-	pull bool
-}
-
-type BaseSuite struct {
-	dir            string
-	kubeConfigPath string
-	try            *try.Try
-	client         k8s.Client
-}
-
-func (s *BaseSuite) maeshStartControllerWithArgsCmd(args ...string) *exec.Cmd {
-	controllerArgSlice := []string{
-		fmt.Sprintf("--masterurl=%s", masterURL),
-		fmt.Sprintf("--kubeconfig=%s", os.Getenv("KUBECONFIG")),
-		"--logLevel=debug",
-		fmt.Sprintf("--namespace=%s", maeshNamespace),
-		"--apihost=127.0.0.1",
-	}
-	args = append(controllerArgSlice, args...)
-
-	return exec.Command(maeshBinary, args...)
-}
-
-func (s *BaseSuite) maeshPrepareWithArgs(args ...string) *exec.Cmd {
-	prepareArgSlice := []string{"prepare", fmt.Sprintf("--masterurl=%s", masterURL), fmt.Sprintf("--kubeconfig=%s", os.Getenv("KUBECONFIG")), "--loglevel=debug", "--clusterdomain=cluster.local", fmt.Sprintf("--namespace=%s", maeshNamespace)}
-	args = append(prepareArgSlice, args...)
-
-	return exec.Command(maeshBinary, args...)
-}
-
-func (s *BaseSuite) startMaeshBinaryCmd(c *check.C, smi bool, acl bool) *exec.Cmd {
-	args := []string{}
-	if smi {
-		args = append(args, "--smi")
+func maeshPrepare() error {
+	args := []string{
+		"prepare",
+		"--masterurl=" + masterURL,
+		"--kubeconfig=" + os.Getenv("KUBECONFIG"),
+		"--loglevel=debug",
+		"--clusterdomain=cluster.local",
+		"--namespace=" + maeshNamespace,
 	}
 
-	cmd := s.maeshPrepareWithArgs(args...)
+	cmd := exec.Command(maeshBinary, args...)
 	cmd.Env = os.Environ()
-	output, err := cmd.CombinedOutput()
-	c.Log(string(output))
-	c.Assert(err, checker.IsNil)
-
-	// Ignore the kube-system namespace since we don't care about system events.
-	args = append(args, "--ignoreNamespaces=kube-system")
-	if acl {
-		args = append(args, "--acl")
-	}
-
-	return s.maeshStartControllerWithArgsCmd(args...)
-}
-
-func (s *BaseSuite) stopMaeshBinary(c *check.C, process *os.Process) {
-	err := process.Kill()
-	c.Assert(err, checker.IsNil)
-}
-
-func (s *BaseSuite) startk3s(c *check.C, requiredImages []string) {
-	c.Log("Starting k3s...")
-	// Set the base directory for the test suite
-	var err error
-	s.dir, err = os.Getwd()
-	c.Assert(err, checker.IsNil)
-
-	// Create a k3s cluster.
-	cmd := exec.Command("k3d", "create", "--name", k3dClusterName,
-		"--api-port", "8443",
-		"--workers", "1",
-		"--server-arg", "--no-deploy=traefik",
-		"--server-arg", "--no-deploy=coredns",
-		"--image", fmt.Sprintf("%s:%s", k3sImage, k3sVersion),
-		"--wait", "30",
-	)
-	cmd.Env = os.Environ()
-
-	output, err := cmd.CombinedOutput()
-
-	fmt.Println(string(output))
-	c.Assert(err, checker.IsNil)
-
-	// Load images into k3s
-	c.Log("Importing docker images in to k3s...")
-
-	err = s.loadK3sImages(c, requiredImages)
-	c.Assert(err, checker.IsNil)
-
-	s.createK8sClient(c)
-	s.createRequiredNamespaces(c)
-	c.Log("k3s start successfully.")
-}
-
-func (s *BaseSuite) createK8sClient(c *check.C) {
-	// Get kubeconfig path.
-	cmd := exec.Command("k3d", "get-kubeconfig", "--name", k3dClusterName)
-	cmd.Env = os.Environ()
-
-	output, err := cmd.CombinedOutput()
-	c.Assert(err, checker.IsNil)
-
-	s.kubeConfigPath = strings.TrimSuffix(string(output), "\n")
-
-	c.Log("Creating kube client...")
-
-	s.client, err = s.try.WaitClientCreated(masterURL, s.kubeConfigPath, 30*time.Second)
-	c.Assert(err, checker.IsNil)
-
-	s.try = try.NewTry(s.client)
-
-	c.Log("Setting new kubeconfig path...")
-	c.Assert(os.Setenv("KUBECONFIG", s.kubeConfigPath), checker.IsNil)
-}
-
-func (s *BaseSuite) loadK3sImages(c *check.C, requiredImages []string) error {
-	for _, image := range requiredImages {
-		c.Log("Importing image: " + image)
-
-		err := loadK3sImage(k3dClusterName, image, 1*time.Minute)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func loadK3sImage(clusterName, imageName string, timeout time.Duration) error {
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = timeout
-
-	return backoff.Retry(safe.OperationWithRecover(func() error {
-		cmd := exec.Command("k3d", "import-images", "--name", clusterName, imageName)
-		cmd.Env = os.Environ()
-
-		output, err := cmd.CombinedOutput()
-		if err != nil {
-			fmt.Println(string(output))
-
-			logCmd := exec.Command("docker", "events", "--since", "5m")
-			logCmd.Env = os.Environ()
-			logOutput, _ := cmd.CombinedOutput()
-			fmt.Println(string(logOutput))
-		}
-		return err
-	}), ebo)
-}
-
-func (s *BaseSuite) stopK3s() {
-	// delete the k3s cluster.
-	cmd := exec.Command("k3d", "delete", "--name", k3dClusterName)
-	cmd.Env = os.Environ()
-
-	output, _ := cmd.CombinedOutput()
 
-	fmt.Println(string(output))
-}
-
-func (s *BaseSuite) kubectlCommand(c *check.C, args ...string) {
-	args = append(args, fmt.Sprintf("--kubeconfig=%s", os.Getenv("KUBECONFIG")))
-	cmd := exec.Command("kubectl", args...)
-	cmd.Env = os.Environ()
 	output, err := cmd.CombinedOutput()
-	c.Log(string(output))
-	c.Assert(err, checker.IsNil)
-}
-
-func (s *BaseSuite) createResources(c *check.C, dirPath string) {
-	// Create the required objects from the configured directory
-	s.kubectlCommand(c, "apply", "-f", path.Join(s.dir, dirPath))
-	time.Sleep(kubectlCreateWaitTime)
-}
-
-func (s *BaseSuite) deleteResources(c *check.C, dirPath string) {
-	// Delete the required objects from the configured directory
-	args := []string{"delete", "-f", path.Join(s.dir, dirPath)}
-	args = append(args, "--force", "--grace-period=0")
-
-	s.kubectlCommand(c, args...)
-}
-
-func (s *BaseSuite) deleteShadowServices(c *check.C) {
-	ctx := context.Background()
-	opts := metav1.ListOptions{
-		LabelSelector: "app=maesh",
-	}
-	svcs, err := s.client.KubernetesClient().CoreV1().Services(maeshNamespace).List(ctx, opts)
-	c.Assert(err, checker.IsNil)
-
-	for _, svc := range svcs.Items {
-		c.Logf("Deleting shadow service %s.", svc.Name)
-		err = s.client.KubernetesClient().CoreV1().Services(maeshNamespace).Delete(ctx, svc.Name, metav1.DeleteOptions{})
-		c.Assert(err, checker.IsNil)
-	}
-}
-
-func (s *BaseSuite) waitForPods(c *check.C, pods []string) {
-	for _, pod := range pods {
-		c.Logf("Waiting for pod: %q to have IP assigned.", pod)
-		err := s.try.WaitPodIPAssigned(pod, testNamespace, 30*time.Second)
-		c.Assert(err, checker.IsNil)
-	}
-}
-
-func (s *BaseSuite) startAndWaitForCoreDNS(c *check.C) {
-	s.createResources(c, "testdata/coredns/coredns.yaml")
-	s.WaitForCoreDNS(c)
-}
-
-func (s *BaseSuite) WaitForCoreDNS(c *check.C) {
-	c.Assert(s.try.WaitReadyDeployment("coredns", metav1.NamespaceSystem, 60*time.Second), checker.IsNil)
-}
-
-func (s *BaseSuite) startAndWaitForKubeDNS(c *check.C) {
-	s.createResources(c, "testdata/kubedns")
-	c.Assert(s.try.WaitReadyDeployment("kube-dns", metav1.NamespaceSystem, 60*time.Second), checker.IsNil)
-}
-
-func (s *BaseSuite) WaitForKubeDNS(c *check.C) {
-	c.Assert(s.try.WaitReadyDeployment("kube-dns", metav1.NamespaceSystem, 60*time.Second), checker.IsNil)
-}
-
-func (s *BaseSuite) waitForMaeshControllerStarted(c *check.C) {
-	c.Assert(s.try.WaitReadyDeployment("maesh-controller", maeshNamespace, 30*time.Second), checker.IsNil)
-}
-
-func (s *BaseSuite) waitForMaeshProxyStarted(c *check.C) {
-	c.Assert(s.try.WaitReadyDaemonset("maesh-mesh", maeshNamespace, 30*time.Second), checker.IsNil)
-}
-
-func (s *BaseSuite) waitForTools(c *check.C) {
-	c.Assert(s.try.WaitReadyDeployment("tiny-tools", testNamespace, 30*time.Second), checker.IsNil)
-}
-
-func (s *BaseSuite) waitKubectlExecCommandReturn(_ *check.C, argSlice []string) (string, error) {
-	return s.try.WaitCommandExecuteReturn("kubectl", argSlice, 10*time.Second)
-}
-
-func (s *BaseSuite) startWhoami(c *check.C) {
-	s.createResources(c, "testdata/whoami")
-	c.Assert(s.try.WaitReadyDeployment("whoami", "whoami", 30*time.Second), checker.IsNil)
-}
-
-func (s *BaseSuite) createRequiredNamespaces(c *check.C) {
-	c.Log("Creating required namespaces...")
-	// Create maesh namespace, required by helm v3.
-	s.kubectlCommand(c, "create", "namespace", maeshNamespace)
-
-	// Create test namespace, for testing objects.
-	s.kubectlCommand(c, "create", "namespace", testNamespace)
-}
-
-func (s *BaseSuite) installHelmMaesh(c *check.C, acl bool, kubeDNS bool) error {
-	c.Log("Installing Maesh via helm...")
-	// Install the helm chart.
-	argSlice := []string{"install", "powpow", "../helm/chart/maesh", "--values", "testdata/values.yaml", "--namespace", maeshNamespace}
-
-	if kubeDNS {
-		argSlice = append(argSlice, "--set", "kubedns=true")
-	}
-
-	if acl {
-		argSlice = append(argSlice, "--set", "acl=true")
-	}
-
-	return s.try.WaitCommandExecute("helm", argSlice, "powpow", 10*time.Second)
-}
-
-func (s *BaseSuite) uninstallHelmMaesh(c *check.C) {
-	c.Log("Uninstalling Maesh via helm...")
-	// Uninstall the helm chart.
-	argSlice := []string{"uninstall", "powpow", "--namespace", maeshNamespace}
-	err := s.try.WaitCommandExecute("helm", argSlice, "uninstalled", 10*time.Second)
-	c.Assert(err, checker.IsNil)
-}
-
-func (s *BaseSuite) setCoreDNSVersion(c *check.C, version string) {
-	ctx := context.Background()
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = 60 * time.Second
-
-	err := backoff.Retry(safe.OperationWithRecover(func() error {
-		// Get current coreDNS deployment.
-		deployment, err := s.client.KubernetesClient().AppsV1().Deployments(metav1.NamespaceSystem).Get(ctx, "coredns", metav1.GetOptions{})
-		c.Assert(err, checker.IsNil)
-
-		newDeployment := deployment.DeepCopy()
-		c.Assert(len(newDeployment.Spec.Template.Spec.Containers), checker.Equals, 1)
-
-		newDeployment.Spec.Template.Spec.Containers[0].Image = fmt.Sprintf("coredns/coredns:%s", version)
-
-		return s.try.WaitUpdateDeployment(newDeployment, 10*time.Second)
-	}), ebo)
-
-	c.Assert(err, checker.IsNil)
-
-	s.WaitForCoreDNS(c)
-}
-
-func (s *BaseSuite) installTinyToolsMaesh(c *check.C) {
-	// Create new tiny tools deployment.
-	s.kubectlCommand(c, "apply", "-f", path.Join(s.dir, "testdata/tools/deployment.yaml"))
-
-	// Wait for tools to be initialized.
-	s.waitForTools(c)
-}
-
-func (s *BaseSuite) getToolsPodMaesh(c *check.C) *corev1.Pod {
-	ctx := context.Background()
-
-	podList, err := s.client.KubernetesClient().CoreV1().Pods(testNamespace).List(ctx, metav1.ListOptions{
-		LabelSelector: "app=tiny-tools",
-	})
-	c.Assert(err, checker.IsNil)
-	c.Assert(len(podList.Items), checker.Equals, 1)
-
-	return &podList.Items[0]
-}
-
-func (s *BaseSuite) testConfigurationWithReturn(c *check.C, path string) *dynamic.Configuration {
-	err := try.GetRequest(fmt.Sprintf("http://127.0.0.1:%d/api/configuration/current", maeshAPIPort), 20*time.Second, try.BodyContains(`"service":"readiness"`))
-	c.Assert(err, checker.IsNil)
-
-	expectedJSON := filepath.FromSlash(path)
-
-	var buf bytes.Buffer
-
-	err = try.GetRequest(fmt.Sprintf("http://127.0.0.1:%d/api/configuration/current", maeshAPIPort), 10*time.Second, try.StatusCodeIs(http.StatusOK), matchesConfig(expectedJSON, &buf))
-	if err != nil {
-		c.Error(err)
-	}
-
-	var result *dynamic.Configuration
-
-	err = json.Unmarshal(buf.Bytes(), &result)
-	c.Assert(err, checker.IsNil)
-
-	return result
-}
-
-func matchesConfig(wantConfig string, buf *bytes.Buffer) try.ResponseCondition {
-	return func(res *http.Response) error {
-		got, err := getConfigFromResponse(res, buf)
-		if err != nil {
-			return err
-		}
-
-		expected, err := ioutil.ReadFile(wantConfig)
-		if err != nil {
-			return err
-		}
-
-		// The pods IPs are dynamic, so we cannot predict them,
-		// which is why we have to ignore them in the comparison.
-		rxURL := regexp.MustCompile(`"(url|address)":\s+(".*")`)
-		sanitizedExpected := rxURL.ReplaceAll(expected, []byte(`"$1": "XXXX"`))
-		sanitizedGot := rxURL.ReplaceAll(got, []byte(`"$1": "XXXX"`))
-
-		rxHostRule := regexp.MustCompile("Host\\(\\`(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\`\\)")
-		sanitizedExpected = rxHostRule.ReplaceAll(sanitizedExpected, []byte("Host(`XXXX`)"))
-		sanitizedGot = rxHostRule.ReplaceAll(sanitizedGot, []byte("Host(`XXXX`)"))
-
-		rxServerStatus := regexp.MustCompile(`"http://.*?":\s+(".*")`)
-		sanitizedExpected = rxServerStatus.ReplaceAll(sanitizedExpected, []byte(`"http://XXXX": $1`))
-		sanitizedGot = rxServerStatus.ReplaceAll(sanitizedGot, []byte(`"http://XXXX": $1`))
-
-		// The tcp entrypoint assignments are dynamic, so we cannot predict them,
-		// which is why we have to ignore them in the comparison.
-		rxTCPEntrypoints := regexp.MustCompile(`"tcp-1000(\d)"`)
-		sanitizedExpected = rxTCPEntrypoints.ReplaceAll(sanitizedExpected, []byte(`"tcp-1000X"`))
-		sanitizedGot = rxTCPEntrypoints.ReplaceAll(sanitizedGot, []byte(`"tcp-1000X"`))
-
-		// The source range of whitelist middlewares is dynamic, so we cannot predict them,
-		// which is why we have to ignore them in the comparison.
-		rxSourceRange := regexp.MustCompile(`"sourceRange":\s*\[(\s*"((\d+)\.(\d+)\.(\d+)\.(\d+))",?)*\s*\]`)
-		sanitizedExpected = rxSourceRange.ReplaceAll(sanitizedExpected, []byte(`"sourceRange":["XXXX"]`))
-		sanitizedGot = rxSourceRange.ReplaceAll(sanitizedGot, []byte(`"sourceRange":["XXXX"]`))
-
-		if bytes.Equal(sanitizedExpected, sanitizedGot) {
-			return nil
-		}
-
-		diff := difflib.UnifiedDiff{
-			FromFile: "Expected",
-			A:        difflib.SplitLines(string(sanitizedExpected)),
-			ToFile:   "Got",
-			B:        difflib.SplitLines(string(sanitizedGot)),
-			Context:  3,
-		}
-
-		text, err := difflib.GetUnifiedDiffString(diff)
-		if err != nil {
-			return err
-		}
-
-		return errors.New(text)
-	}
-}
-
-func getConfigFromResponse(res *http.Response, buf *bytes.Buffer) ([]byte, error) {
-	body, err := ioutil.ReadAll(res.Body)
 	if err != nil {
-		return nil, fmt.Errorf("failed to read response body: %s", err)
-	}
-
-	if err = res.Body.Close(); err != nil {
-		return nil, err
-	}
-
-	var obtained dynamic.Configuration
-
-	err = json.Unmarshal(body, &obtained)
-	if err != nil {
-		return nil, err
-	}
-
-	if buf != nil {
-		buf.Reset()
-
-		if _, err = io.Copy(buf, bytes.NewReader(body)); err != nil {
-			return nil, err
-		}
-	}
-
-	return json.MarshalIndent(obtained, "", "  ")
-}
-
-func (s *BaseSuite) digHost(c *check.C, source, namespace, destination string) {
-	// Dig the host, with a short response for the A record
-	argSlice := []string{
-		"exec", "-i", source, "-n", namespace, "--", "dig", destination, "+short",
-	}
-
-	var IP net.IP
-
-	ebo := backoff.NewExponentialBackOff()
-	ebo.MaxElapsedTime = 60 * time.Second
-
-	_ = backoff.Retry(safe.OperationWithRecover(func() error {
-		output, err := s.waitKubectlExecCommandReturn(c, argSlice)
-		if err != nil {
-			return err
-		}
-		c.Log(fmt.Sprintf("Dig %s: %s", destination, strings.TrimSpace(output)))
-		IP = net.ParseIP(strings.TrimSpace(output))
-		if IP == nil {
-			return fmt.Errorf("could not parse an IP from dig")
-		}
-
-		return nil
-	}), ebo)
-
-	c.Assert(IP, checker.NotNil)
-}
-
-func (s *BaseSuite) getPod(c *check.C, name string) *corev1.Pod {
-	pod, err := s.client.KubernetesClient().CoreV1().Pods(testNamespace).Get(context.Background(), name, metav1.GetOptions{})
-	c.Assert(err, checker.IsNil)
-
-	return pod
-}
-
-func (s *BaseSuite) getService(c *check.C, name string) *corev1.Service {
-	svc, err := s.client.KubernetesClient().CoreV1().Services(testNamespace).Get(context.Background(), name, metav1.GetOptions{})
-	c.Assert(err, checker.IsNil)
-
-	return svc
-}
-
-func (s *BaseSuite) getTrafficTarget(c *check.C, name string) *access.TrafficTarget {
-	tt, err := s.client.AccessClient().AccessV1alpha2().TrafficTargets(testNamespace).Get(context.Background(), name, metav1.GetOptions{})
-	c.Assert(err, checker.IsNil)
-
-	return tt
-}
-
-func (s *BaseSuite) getTrafficSplit(c *check.C, name string) *split.TrafficSplit {
-	ts, err := s.client.SplitClient().SplitV1alpha3().TrafficSplits(testNamespace).Get(context.Background(), name, metav1.GetOptions{})
-	c.Assert(err, checker.IsNil)
-
-	return ts
-}
-
-func (s *BaseSuite) checkBlockAllMiddleware(c *check.C, config *dynamic.Configuration) {
-	c.Log("Checking if the block-all-middleware blocks everything")
-
-	middleware := config.HTTP.Middlewares["block-all-middleware"]
-	c.Assert(middleware, checker.NotNil)
-
-	c.Assert(middleware.IPWhiteList.SourceRange, checker.HasLen, 1)
-	c.Assert(middleware.IPWhiteList.SourceRange[0], checker.Equals, "255.255.255.255")
-}
-
-func (s *BaseSuite) checkHTTPReadinessService(c *check.C, config *dynamic.Configuration) {
-	c.Log("Checking if the readiness service is correctly defined")
-
-	service := config.HTTP.Services["readiness"]
-	c.Assert(service, checker.NotNil)
-
-	c.Assert(service.LoadBalancer.Servers, checker.HasLen, 1)
-	c.Assert(service.LoadBalancer.Servers[0].URL, checker.Equals, "http://127.0.0.1:8080")
-}
-
-func (s *BaseSuite) checkHTTPServiceLoadBalancer(c *check.C, config *dynamic.Configuration, svc *corev1.Service, pods []*corev1.Pod) {
-	for _, port := range svc.Spec.Ports {
-		svcKey := fmt.Sprintf("%s-%s-%d", svc.Namespace, svc.Name, port.Port)
-
-		service := config.HTTP.Services[svcKey]
-		c.Assert(service, checker.NotNil)
-
-		c.Assert(service.LoadBalancer.Servers, checker.HasLen, len(pods))
-
-		for _, pod := range pods {
-			wantURL := fmt.Sprintf("http://%s:%d", pod.Status.PodIP, port.TargetPort.IntVal)
-			c.Logf("Checking if HTTP service %q loadbalancer contains an URL for pod %q: %s", svcKey, pod.Name, wantURL)
-
-			var found bool
-
-			for _, server := range service.LoadBalancer.Servers {
-				if wantURL == server.URL {
-					found = true
-					break
-				}
-			}
-
-			c.Assert(found, checker.True)
-		}
+		return fmt.Errorf("maesh prepare has failed - %s: %w", string(output), err)
 	}
-}
-
-func (s *BaseSuite) checkTCPServiceLoadBalancer(c *check.C, config *dynamic.Configuration, svc *corev1.Service, pods []*corev1.Pod) {
-	for _, port := range svc.Spec.Ports {
-		svcKey := fmt.Sprintf("%s-%s-%d", svc.Namespace, svc.Name, port.Port)
-
-		service := config.TCP.Services[svcKey]
-		c.Assert(service, checker.NotNil)
-
-		c.Assert(service.LoadBalancer.Servers, checker.HasLen, len(pods))
-
-		for _, pod := range pods {
-			wantURL := fmt.Sprintf("%s:%d", pod.Status.PodIP, port.TargetPort.IntVal)
-			c.Logf("Checking if TCP service %q loadbalancer contains an URL for pod %q: %s", svcKey, pod.Name, wantURL)
-
-			var found bool
-
-			for _, server := range service.LoadBalancer.Servers {
-				if wantURL == server.Address {
-					found = true
-					break
-				}
-			}
-
-			c.Assert(found, checker.True)
-		}
-	}
-}
-
-func (s *BaseSuite) checkUDPServiceLoadBalancer(c *check.C, config *dynamic.Configuration, svc *corev1.Service, pods []*corev1.Pod) {
-	for _, port := range svc.Spec.Ports {
-		svcKey := fmt.Sprintf("%s-%s-%d", svc.Namespace, svc.Name, port.Port)
-
-		service := config.UDP.Services[svcKey]
-		c.Assert(service, checker.NotNil)
-
-		c.Assert(service.LoadBalancer.Servers, checker.HasLen, len(pods))
-
-		for _, pod := range pods {
-			wantURL := fmt.Sprintf("%s:%d", pod.Status.PodIP, port.TargetPort.IntVal)
-			c.Logf("Checking if UDP service %q loadbalancer contains an URL for pod %q: %s", svcKey, pod.Name, wantURL)
-
-			var found bool
 
-			for _, server := range service.LoadBalancer.Servers {
-				if wantURL == server.Address {
-					found = true
-					break
-				}
-			}
-
-			c.Assert(found, checker.True)
-		}
-	}
-}
-
-func (s *BaseSuite) checkTrafficTargetLoadBalancer(c *check.C, config *dynamic.Configuration, tt *access.TrafficTarget, svc *corev1.Service, pods []*corev1.Pod) {
-	for _, port := range svc.Spec.Ports {
-		svcKey := fmt.Sprintf("%s-%s-%s-%d-traffic-target", svc.Namespace, svc.Name, tt.Name, port.Port)
-
-		service := config.HTTP.Services[svcKey]
-		c.Assert(service, checker.NotNil)
-
-		c.Assert(service.LoadBalancer.Servers, checker.HasLen, len(pods))
-
-		for _, pod := range pods {
-			wantURL := fmt.Sprintf("http://%s:%d", pod.Status.PodIP, port.TargetPort.IntVal)
-			c.Logf("Checking if traffic-target service %q loadbalancer contains an URL for pod %q: %s", svcKey, pod.Name, wantURL)
-
-			var found bool
-
-			for _, server := range service.LoadBalancer.Servers {
-				if wantURL == server.URL {
-					found = true
-					break
-				}
-			}
-
-			c.Assert(found, checker.True)
-		}
-	}
-}
-
-func (s *BaseSuite) checkTrafficTargetWhitelistDirect(c *check.C, config *dynamic.Configuration, tt *access.TrafficTarget, svc *corev1.Service, pods []*corev1.Pod) {
-	middlewareKey := fmt.Sprintf("%s-%s-%s-whitelist-traffic-target-direct", svc.Namespace, svc.Name, tt.Name)
-
-	s.checkWhitelistSourceRange(c, config, middlewareKey, pods)
-}
-
-func (s *BaseSuite) checkTrafficTargetWhitelistIndirect(c *check.C, config *dynamic.Configuration, tt *access.TrafficTarget, svc *corev1.Service, pods []*corev1.Pod) {
-	middlewareKey := fmt.Sprintf("%s-%s-%s-whitelist-traffic-target-indirect", svc.Namespace, svc.Name, tt.Name)
-
-	s.checkWhitelistSourceRange(c, config, middlewareKey, pods)
-}
-
-func (s *BaseSuite) checkTrafficSplitWhitelistDirect(c *check.C, config *dynamic.Configuration, ts *split.TrafficSplit, svc *corev1.Service, pods []*corev1.Pod) {
-	middlewareKey := fmt.Sprintf("%s-%s-%s-whitelist-traffic-split-direct", svc.Namespace, svc.Name, ts.Name)
-
-	s.checkWhitelistSourceRange(c, config, middlewareKey, pods)
-}
-
-func (s *BaseSuite) checkWhitelistSourceRange(c *check.C, config *dynamic.Configuration, key string, pods []*corev1.Pod) {
-	middleware := config.HTTP.Middlewares[key]
-	c.Assert(middleware, checker.NotNil)
-
-	c.Assert(middleware.IPWhiteList.SourceRange, checker.HasLen, 1)
-
-	for _, pod := range pods {
-		c.Logf("Checking if traffic-target indirect whitelist middleware %q allows pod %q: %s", key, pod.Name, pod.Status.PodIP)
-
-		var found bool
-
-		for _, ip := range middleware.IPWhiteList.SourceRange {
-			if pod.Status.PodIP == ip {
-				found = true
-				break
-			}
-		}
-
-		c.Assert(found, checker.True)
-	}
+	return nil
 }
diff --git a/integration/kubedns_test.go b/integration/kubedns_test.go
index 4bd7cda..482d7ba 100644
--- a/integration/kubedns_test.go
+++ b/integration/kubedns_test.go
@@ -3,50 +3,73 @@ package integration
 import (
 	"time"
 
+	"github.com/containous/maesh/integration/k3d"
+	"github.com/containous/maesh/integration/tool"
+	"github.com/containous/maesh/integration/try"
 	"github.com/go-check/check"
+	"github.com/sirupsen/logrus"
 	checker "github.com/vdemeester/shakers"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // KubeDNSSuite.
-type KubeDNSSuite struct{ BaseSuite }
+type KubeDNSSuite struct {
+	logger  logrus.FieldLogger
+	cluster *k3d.Cluster
+	tool    *tool.Tool
+}
 
 func (s *KubeDNSSuite) SetUpSuite(c *check.C) {
-	requiredImages := []string{
-		"containous/maesh:latest",
-		"containous/whoami:v1.0.1",
-		"coredns/coredns:1.6.3",
-		"gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.7",
-		"gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.7",
-		"gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.7",
+	var err error
+
+	requiredImages := []k3d.DockerImage{
+		{Name: "containous/whoami:v1.0.1"},
+		{Name: "coredns/coredns:1.6.3"},
+		{Name: "giantswarm/tiny-tools:3.9"},
+		{Name: "gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.7"},
+		{Name: "gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.7"},
+		{Name: "gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.7"},
 	}
-	s.startk3s(c, requiredImages)
-	s.startAndWaitForKubeDNS(c)
 
-	// Wait for our created coreDNS deployment in the maesh namespace.
-	c.Assert(s.try.WaitReadyDeployment("coredns", maeshNamespace, 60*time.Second), checker.IsNil)
+	s.logger = logrus.New()
+	s.cluster, err = k3d.NewCluster(s.logger, masterURL, k3dClusterName,
+		k3d.WithoutTraefik(),
+		k3d.WithoutCoreDNS(),
+		k3d.WithImages(requiredImages...),
+	)
+	c.Assert(err, checker.IsNil)
+
+	c.Assert(s.cluster.CreateNamespace(s.logger, maeshNamespace), checker.IsNil)
+	c.Assert(s.cluster.CreateNamespace(s.logger, testNamespace), checker.IsNil)
+
+	c.Assert(s.cluster.Apply(s.logger, smiCRDs), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/tool/tool.yaml"), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/kubedns/"), checker.IsNil)
+	c.Assert(s.cluster.Apply(s.logger, "testdata/coredns/whoami-shadow-service.yaml"), checker.IsNil)
 
-	s.startWhoami(c)
-	s.installTinyToolsMaesh(c)
-	s.createResources(c, "testdata/smi/crds/")
+	c.Assert(s.cluster.WaitReadyPod("tool", testNamespace, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDeployment("kube-dns", metav1.NamespaceSystem, 60*time.Second), checker.IsNil)
+	c.Assert(s.cluster.WaitReadyDeployment("coredns", maeshNamespace, 60*time.Second), checker.IsNil)
+
+	s.tool = tool.New(s.logger, "tool", testNamespace)
 }
 
 func (s *KubeDNSSuite) TearDownSuite(c *check.C) {
-	s.stopK3s()
+	if s.cluster != nil {
+		c.Assert(s.cluster.Stop(s.logger), checker.IsNil)
+	}
 }
 
 func (s *KubeDNSSuite) TestKubeDNSDig(c *check.C) {
-	s.WaitForKubeDNS(c)
+	s.logger.Info("Asserting KubeDNS has been patched successfully and can be dug")
 
-	cmd := s.startMaeshBinaryCmd(c, false, false)
-	err := cmd.Start()
+	c.Assert(maeshPrepare(), checker.IsNil)
 
-	c.Assert(err, checker.IsNil)
-	defer s.stopMaeshBinary(c, cmd.Process)
+	// Wait for kubeDNS, as the pods will be restarted by prepare.
+	c.Assert(s.cluster.WaitReadyDeployment("kube-dns", metav1.NamespaceSystem, 60*time.Second), checker.IsNil)
 
-	pod := s.getToolsPodMaesh(c)
-	c.Assert(pod, checker.NotNil)
-
-	// We need to wait for kubeDNS again, as the pods will be restarted by prepare.
-	s.WaitForKubeDNS(c)
-	s.digHost(c, pod.Name, pod.Namespace, "whoami.whoami.maesh")
+	err := try.Retry(func() error {
+		return s.tool.Dig("whoami.whoami.maesh")
+	}, 60*time.Second)
+	c.Assert(err, checker.IsNil)
 }
diff --git a/integration/testdata/acl/disabled/http.json b/integration/testdata/acl/disabled/http.json
deleted file mode 100644
index 400c317..0000000
--- a/integration/testdata/acl/disabled/http.json
+++ /dev/null
@@ -1,57 +0,0 @@
-{
-  "http": {
-    "routers": {
-      "readiness": {
-        "entryPoints": [
-          "readiness"
-        ],
-        "service": "readiness",
-        "rule": "Path(`/ping`)"
-      },
-      "test-server-8080": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "service": "test-server-8080",
-        "rule": "Host(`server.test.maesh`) || Host(`10.43.244.152`)",
-        "priority": 1001
-      }
-    },
-    "services": {
-      "block-all-service": {
-        "loadBalancer": {
-          "passHostHeader": false
-        }
-      },
-      "readiness": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://127.0.0.1:8080"
-            }
-          ],
-          "passHostHeader": true
-        }
-      },
-      "test-server-8080": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://10.42.1.2:80"
-            }
-          ],
-          "passHostHeader": true
-        }
-      }
-    },
-    "middlewares": {
-      "block-all-middleware": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "255.255.255.255"
-          ]
-        }
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/integration/testdata/acl/disabled/http/1.server.yaml b/integration/testdata/acl/disabled/http/1.server.yaml
deleted file mode 100644
index cceab81..0000000
--- a/integration/testdata/acl/disabled/http/1.server.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-apiVersion: v1
-kind: Pod
-metadata:
-  name: server
-  namespace: test
-  labels:
-    app: server
-spec:
-  containers:
-    - name: server
-      image: containous/whoami:v1.0.1
-      imagePullPolicy: Always
-
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: server
-  namespace: test
-  labels:
-    app: server
-spec:
-  type: ClusterIP
-  ports:
-    - port: 8080
-      name: server
-      targetPort: 80
-  selector:
-    app: server
\ No newline at end of file
diff --git a/integration/testdata/acl/disabled/tcp.json b/integration/testdata/acl/disabled/tcp.json
deleted file mode 100644
index d9cd1d9..0000000
--- a/integration/testdata/acl/disabled/tcp.json
+++ /dev/null
@@ -1,61 +0,0 @@
-{
-  "http": {
-    "routers": {
-      "readiness": {
-        "entryPoints": [
-          "readiness"
-        ],
-        "service": "readiness",
-        "rule": "Path(`/ping`)"
-      }
-    },
-    "services": {
-      "block-all-service": {
-        "loadBalancer": {
-          "passHostHeader": false
-        }
-      },
-      "readiness": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://127.0.0.1:8080"
-            }
-          ],
-          "passHostHeader": true
-        }
-      }
-    },
-    "middlewares": {
-      "block-all-middleware": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "255.255.255.255"
-          ]
-        }
-      }
-    }
-  },
-  "tcp": {
-    "routers": {
-      "test-server-8080": {
-        "entryPoints": [
-          "tcp-10000"
-        ],
-        "service": "test-server-8080",
-        "rule": "HostSNI(`*`)"
-      }
-    },
-    "services": {
-      "test-server-8080": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "address": "10.42.1.5:8080"
-            }
-          ]
-        }
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/integration/testdata/acl/disabled/traffic-split.json b/integration/testdata/acl/disabled/traffic-split.json
deleted file mode 100644
index ddedf4b..0000000
--- a/integration/testdata/acl/disabled/traffic-split.json
+++ /dev/null
@@ -1,130 +0,0 @@
-{
-  "http": {
-    "routers": {
-      "readiness": {
-        "entryPoints": [
-          "readiness"
-        ],
-        "service": "readiness",
-        "rule": "Path(`/ping`)"
-      },
-      "test-server-8080": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "service": "test-server-8080",
-        "rule": "Host(`server.test.maesh`) || Host(`10.43.173.123`)",
-        "priority": 1001
-      },
-      "test-server-server-traffic-split-8080-traffic-split-direct": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "service": "test-server-server-traffic-split-8080-traffic-split",
-        "rule": "Host(`server.test.maesh`) || Host(`10.43.173.123`)",
-        "priority": 4001
-      },
-      "test-server-v1-8080": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "service": "test-server-v1-8080",
-        "rule": "Host(`server-v1.test.maesh`) || Host(`10.43.93.31`)",
-        "priority": 1001
-      },
-      "test-server-v2-8080": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "service": "test-server-v2-8080",
-        "rule": "Host(`server-v2.test.maesh`) || Host(`10.43.97.109`)",
-        "priority": 1001
-      }
-    },
-    "services": {
-      "block-all-service": {
-        "loadBalancer": {
-          "passHostHeader": false
-        }
-      },
-      "readiness": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://127.0.0.1:8080"
-            }
-          ],
-          "passHostHeader": true
-        }
-      },
-      "test-server-8080": {
-        "loadBalancer": {
-          "passHostHeader": true
-        }
-      },
-      "test-server-server-traffic-split-8080-server-v1-traffic-split-backend": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://server-v1.test.maesh:8080"
-            }
-          ],
-          "passHostHeader": false
-        }
-      },
-      "test-server-server-traffic-split-8080-server-v2-traffic-split-backend": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://server-v2.test.maesh:8080"
-            }
-          ],
-          "passHostHeader": false
-        }
-      },
-      "test-server-server-traffic-split-8080-traffic-split": {
-        "weighted": {
-          "services": [
-            {
-              "name": "test-server-server-traffic-split-8080-server-v1-traffic-split-backend",
-              "weight": 50
-            },
-            {
-              "name": "test-server-server-traffic-split-8080-server-v2-traffic-split-backend",
-              "weight": 50
-            }
-          ]
-        }
-      },
-      "test-server-v1-8080": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://10.42.1.3:80"
-            }
-          ],
-          "passHostHeader": true
-        }
-      },
-      "test-server-v2-8080": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://10.42.1.4:80"
-            }
-          ],
-          "passHostHeader": true
-        }
-      }
-    },
-    "middlewares": {
-      "block-all-middleware": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "255.255.255.255"
-          ]
-        }
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/integration/testdata/acl/disabled/traffic-split/2.server-v1.yaml b/integration/testdata/acl/disabled/traffic-split/2.server-v1.yaml
deleted file mode 100644
index ef40984..0000000
--- a/integration/testdata/acl/disabled/traffic-split/2.server-v1.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-apiVersion: v1
-kind: Pod
-metadata:
-  name: server-v1
-  namespace: test
-  labels:
-    app: server-v1
-spec:
-  containers:
-    - name: server-v1
-      image: containous/whoami:v1.0.1
-      imagePullPolicy: Always
-
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: server-v1
-  namespace: test
-  labels:
-    app: server-v1
-spec:
-  type: ClusterIP
-  ports:
-    - port: 8080
-      name: server-v1
-      targetPort: 80
-  selector:
-    app: server-v1
diff --git a/integration/testdata/acl/disabled/udp.json b/integration/testdata/acl/disabled/udp.json
deleted file mode 100644
index 38585b3..0000000
--- a/integration/testdata/acl/disabled/udp.json
+++ /dev/null
@@ -1,60 +0,0 @@
-{
-  "http": {
-    "routers": {
-      "readiness": {
-        "entryPoints": [
-          "readiness"
-        ],
-        "service": "readiness",
-        "rule": "Path(`/ping`)"
-      }
-    },
-    "services": {
-      "block-all-service": {
-        "loadBalancer": {
-          "passHostHeader": false
-        }
-      },
-      "readiness": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://127.0.0.1:8080"
-            }
-          ],
-          "passHostHeader": true
-        }
-      }
-    },
-    "middlewares": {
-      "block-all-middleware": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "255.255.255.255"
-          ]
-        }
-      }
-    }
-  },
-  "udp": {
-    "routers": {
-      "test-server-8080": {
-        "entryPoints": [
-          "udp-15000"
-        ],
-        "service": "test-server-8080"
-      }
-    },
-    "services": {
-      "test-server-8080": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "address": "10.42.1.5:8080"
-            }
-          ]
-        }
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/integration/testdata/acl/enabled/traffic-split.json b/integration/testdata/acl/enabled/traffic-split.json
deleted file mode 100644
index 4a240dd..0000000
--- a/integration/testdata/acl/enabled/traffic-split.json
+++ /dev/null
@@ -1,222 +0,0 @@
-{
-  "http": {
-    "routers": {
-      "readiness": {
-        "entryPoints": [
-          "readiness"
-        ],
-        "service": "readiness",
-        "rule": "Path(`/ping`)"
-      },
-      "test-server-8080": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "block-all-middleware"
-        ],
-        "service": "block-all-service",
-        "rule": "Host(`server.test.maesh`) || Host(`10.43.58.187`)",
-        "priority": 1
-      },
-      "test-server-traffic-split-8080-traffic-split-direct": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "test-server-traffic-split-whitelist-traffic-split-direct"
-        ],
-        "service": "test-server-traffic-split-8080-traffic-split",
-        "rule": "Host(`server.test.maesh`) || Host(`10.43.58.187`)",
-        "priority": 4001
-      },
-      "test-server-v1-8080": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "block-all-middleware"
-        ],
-        "service": "block-all-service",
-        "rule": "Host(`server-v1.test.maesh`) || Host(`10.43.28.85`)",
-        "priority": 1
-      },
-      "test-server-v1-traffic-target-8080-traffic-target-direct": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "test-server-v1-traffic-target-whitelist-traffic-target-direct"
-        ],
-        "service": "test-server-v1-traffic-target-8080-traffic-target",
-        "rule": "Host(`server-v1.test.maesh`) || Host(`10.43.28.85`)",
-        "priority": 2001
-      },
-      "test-server-v1-traffic-target-8080-traffic-target-indirect": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "test-server-v1-traffic-target-whitelist-traffic-target-indirect"
-        ],
-        "service": "test-server-v1-traffic-target-8080-traffic-target",
-        "rule": "(Host(`server-v1.test.maesh`) || Host(`10.43.28.85`)) \u0026\u0026 HeadersRegexp(`X-Forwarded-For`, `.+`)",
-        "priority": 3002
-      },
-      "test-server-v2-8080": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "block-all-middleware"
-        ],
-        "service": "block-all-service",
-        "rule": "Host(`server-v2.test.maesh`) || Host(`10.43.39.151`)",
-        "priority": 1
-      },
-      "test-server-v2-traffic-target-8080-traffic-target-direct": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "test-server-v2-traffic-target-whitelist-traffic-target-direct"
-        ],
-        "service": "test-server-v2-traffic-target-8080-traffic-target",
-        "rule": "Host(`server-v2.test.maesh`) || Host(`10.43.39.151`)",
-        "priority": 2001
-      },
-      "test-server-v2-traffic-target-8080-traffic-target-indirect": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "test-server-v2-traffic-target-whitelist-traffic-target-indirect"
-        ],
-        "service": "test-server-v2-traffic-target-8080-traffic-target",
-        "rule": "(Host(`server-v2.test.maesh`) || Host(`10.43.39.151`)) \u0026\u0026 HeadersRegexp(`X-Forwarded-For`, `.+`)",
-        "priority": 3002
-      }
-    },
-    "services": {
-      "block-all-service": {
-        "loadBalancer": {
-          "passHostHeader": false
-        }
-      },
-      "readiness": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://127.0.0.1:8080"
-            }
-          ],
-          "passHostHeader": true
-        }
-      },
-      "test-server-traffic-split-8080-server-v1-traffic-split-backend": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://server-v1.test.maesh:8080"
-            }
-          ],
-          "passHostHeader": false
-        }
-      },
-      "test-server-traffic-split-8080-server-v2-traffic-split-backend": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://server-v2.test.maesh:8080"
-            }
-          ],
-          "passHostHeader": false
-        }
-      },
-      "test-server-traffic-split-8080-traffic-split": {
-        "weighted": {
-          "services": [
-            {
-              "name": "test-server-traffic-split-8080-server-v1-traffic-split-backend",
-              "weight": 50
-            },
-            {
-              "name": "test-server-traffic-split-8080-server-v2-traffic-split-backend",
-              "weight": 50
-            }
-          ]
-        }
-      },
-      "test-server-v1-traffic-target-8080-traffic-target": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://10.42.1.7:80"
-            }
-          ],
-          "passHostHeader": true
-        }
-      },
-      "test-server-v2-traffic-target-8080-traffic-target": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://10.42.0.5:80"
-            }
-          ],
-          "passHostHeader": true
-        }
-      }
-    },
-    "middlewares": {
-      "block-all-middleware": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "255.255.255.255"
-          ]
-        }
-      },
-      "test-server-traffic-split-whitelist-traffic-split-direct": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "10.42.1.5"
-          ]
-        }
-      },
-      "test-server-v1-traffic-target-whitelist-traffic-target-direct": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "10.42.1.5"
-          ]
-        }
-      },
-      "test-server-v1-traffic-target-whitelist-traffic-target-indirect": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "10.42.1.5"
-          ],
-          "ipStrategy": {
-            "depth": 1
-          }
-        }
-      },
-      "test-server-v2-traffic-target-whitelist-traffic-target-direct": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "10.42.1.5"
-          ]
-        }
-      },
-      "test-server-v2-traffic-target-whitelist-traffic-target-indirect": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "10.42.1.5"
-          ],
-          "ipStrategy": {
-            "depth": 1
-          }
-        }
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/integration/testdata/acl/enabled/traffic-split/1.client.yaml b/integration/testdata/acl/enabled/traffic-split/1.client.yaml
deleted file mode 100644
index 2ca89d6..0000000
--- a/integration/testdata/acl/enabled/traffic-split/1.client.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: client-a
-  namespace: test
-
----
-apiVersion: v1
-kind: Pod
-metadata:
-  name: client-a
-  namespace: test
-spec:
-  serviceAccountName: client-a
-  containers:
-    - name: client-a
-      image: giantswarm/tiny-tools:3.9
-      command:
-        - "sleep"
-        - "36000"
-
----
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: client-b
-  namespace: test
-
----
-apiVersion: v1
-kind: Pod
-metadata:
-  name: client-b
-  namespace: test
-spec:
-  serviceAccountName: client-b
-  containers:
-    - name: client-b
-      image: giantswarm/tiny-tools:3.9
-      command:
-        - "sleep"
-        - "36000"
\ No newline at end of file
diff --git a/integration/testdata/acl/enabled/traffic-split/2.server.yaml b/integration/testdata/acl/enabled/traffic-split/2.server.yaml
deleted file mode 100644
index 11af244..0000000
--- a/integration/testdata/acl/enabled/traffic-split/2.server.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: server
-  namespace: test
-  labels:
-    app: server
-spec:
-  type: ClusterIP
-  ports:
-    - port: 8080
-      name: server
-  selector:
-    app: server
-
----
-apiVersion: split.smi-spec.io/v1alpha3
-kind: TrafficSplit
-metadata:
-  name: traffic-split
-  namespace: test
-spec:
-  service: server
-  backends:
-    - service: server-v1
-      weight: 50
-    - service: server-v2
-      weight: 50
-
diff --git a/integration/testdata/acl/enabled/traffic-split/4.server-v2.yaml b/integration/testdata/acl/enabled/traffic-split/4.server-v2.yaml
deleted file mode 100644
index 0ed3b1d..0000000
--- a/integration/testdata/acl/enabled/traffic-split/4.server-v2.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-apiVersion: v1
-kind: Pod
-metadata:
-  name: server-v2
-  namespace: test
-  labels:
-    app: server-v2
-spec:
-  serviceAccountName: server
-  containers:
-    - name: server-v2
-      image: containous/whoami:v1.0.1
-      imagePullPolicy: Always
-
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: server-v2
-  namespace: test
-  labels:
-    app: server-v2
-spec:
-  type: ClusterIP
-  ports:
-    - port: 8080
-      name: server-v2
-      targetPort: 80
-  selector:
-    app: server-v2
diff --git a/integration/testdata/acl/enabled/traffic-target.json b/integration/testdata/acl/enabled/traffic-target.json
deleted file mode 100644
index 218a92c..0000000
--- a/integration/testdata/acl/enabled/traffic-target.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
-  "http": {
-    "routers": {
-      "readiness": {
-        "entryPoints": [
-          "readiness"
-        ],
-        "service": "readiness",
-        "rule": "Path(`/ping`)"
-      },
-      "test-server-8080": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "block-all-middleware"
-        ],
-        "service": "block-all-service",
-        "rule": "Host(`server.test.maesh`) || Host(`10.43.174.133`)",
-        "priority": 1
-      },
-      "test-server-traffic-target-8080-traffic-target-direct": {
-        "entryPoints": [
-          "http-5000"
-        ],
-        "middlewares": [
-          "test-server-traffic-target-whitelist-traffic-target-direct"
-        ],
-        "service": "test-server-traffic-target-8080-traffic-target",
-        "rule": "(Host(`server.test.maesh`) || Host(`10.43.174.133`)) \u0026\u0026 ((PathPrefix(`/{path:api}`) \u0026\u0026 Method(`GET`)))",
-        "priority": 2003
-      }
-    },
-    "services": {
-      "block-all-service": {
-        "loadBalancer": {
-          "passHostHeader": false
-        }
-      },
-      "readiness": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://127.0.0.1:8080"
-            }
-          ],
-          "passHostHeader": true
-        }
-      },
-      "test-server-traffic-target-8080-traffic-target": {
-        "loadBalancer": {
-          "servers": [
-            {
-              "url": "http://10.42.0.6:80"
-            }
-          ],
-          "passHostHeader": true
-        }
-      }
-    },
-    "middlewares": {
-      "block-all-middleware": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "255.255.255.255"
-          ]
-        }
-      },
-      "test-server-traffic-target-whitelist-traffic-target-direct": {
-        "ipWhiteList": {
-          "sourceRange": [
-            "10.42.1.4"
-          ]
-        }
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/integration/testdata/acl/enabled/traffic-target/1.client.yaml b/integration/testdata/acl/enabled/traffic-target/1.client.yaml
deleted file mode 100644
index 0452db8..0000000
--- a/integration/testdata/acl/enabled/traffic-target/1.client.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: client-a
-  namespace: test
-
----
-apiVersion: v1
-kind: Pod
-metadata:
-  name: client-a
-  namespace: test
-spec:
-  serviceAccountName: client-a
-  containers:
-    - name: client-a
-      image: giantswarm/tiny-tools:3.9
-      command:
-        - "sleep"
-        - "36000"
----
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: client-b
-  namespace: test
-
----
-apiVersion: v1
-kind: Pod
-metadata:
-  name: client-b
-  namespace: test
-spec:
-  serviceAccountName: client-b
-  containers:
-    - name: client-b
-      image: giantswarm/tiny-tools:3.9
-      command:
-        - "sleep"
-        - "36000"
\ No newline at end of file
diff --git a/integration/testdata/acl_disabled/http/1.server.yaml b/integration/testdata/acl_disabled/http/1.server.yaml
new file mode 100644
index 0000000..4f1a983
--- /dev/null
+++ b/integration/testdata/acl_disabled/http/1.server.yaml
@@ -0,0 +1,35 @@
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: server-http
+  namespace: test
+  labels:
+    app: server-http
+spec:
+  containers:
+    - name: server-http
+      image: containous/whoami:v1.0.1
+      imagePullPolicy: IfNotPresent
+      readinessProbe:
+        httpGet:
+          path: /
+          port: 80
+        periodSeconds: 1
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: server-http
+  namespace: test
+  labels:
+    app: server-http
+spec:
+  type: ClusterIP
+  ports:
+    - port: 8080
+      name: server-http
+      targetPort: 80
+  selector:
+    app: server-http
\ No newline at end of file
diff --git a/integration/testdata/acl/disabled/tcp/1.server.yaml b/integration/testdata/acl_disabled/tcp/1.server.yaml
similarity index 55%
rename from integration/testdata/acl/disabled/tcp/1.server.yaml
rename to integration/testdata/acl_disabled/tcp/1.server.yaml
index 9135277..d167494 100644
--- a/integration/testdata/acl/disabled/tcp/1.server.yaml
+++ b/integration/testdata/acl_disabled/tcp/1.server.yaml
@@ -1,24 +1,29 @@
+---
 apiVersion: v1
 kind: Pod
 metadata:
-  name: server
+  name: server-tcp
   namespace: test
   labels:
-    app: server
+    app: server-tcp
 spec:
   containers:
-  - name: server
+  - name: server-tcp
     image: containous/whoamitcp:v0.0.2
-    imagePullPolicy: Always
+    imagePullPolicy: IfNotPresent
+    readinessProbe:
+      tcpSocket:
+        port: 8080
+      periodSeconds: 1
 
 ---
 apiVersion: v1
 kind: Service
 metadata:
-  name: server
+  name: server-tcp
   namespace: test
   labels:
-    app: server
+    app: server-tcp
     plop: cool
   annotations:
     maesh.containo.us/traffic-type: tcp
@@ -26,6 +31,6 @@ spec:
   type: ClusterIP
   ports:
     - port: 8080
-      name: server
+      name: server-tcp
   selector:
-    app: server
+    app: server-tcp
diff --git a/integration/testdata/acl/disabled/traffic-split/1.server.yaml b/integration/testdata/acl_disabled/traffic-split/1.server.yaml
similarity index 74%
rename from integration/testdata/acl/disabled/traffic-split/1.server.yaml
rename to integration/testdata/acl_disabled/traffic-split/1.server.yaml
index c825b2c..613bec7 100644
--- a/integration/testdata/acl/disabled/traffic-split/1.server.yaml
+++ b/integration/testdata/acl_disabled/traffic-split/1.server.yaml
@@ -1,17 +1,18 @@
+---
 apiVersion: v1
 kind: Service
 metadata:
-  name: server
+  name: server-split
   namespace: test
   labels:
-    app: server
+    app: server-split
 spec:
   type: ClusterIP
   ports:
     - port: 8080
-      name: server
+      name: server-split
   selector:
-    app: server
+    app: server-split
 
 ---
 apiVersion: split.smi-spec.io/v1alpha3
@@ -20,7 +21,7 @@ metadata:
   name: server-traffic-split
   namespace: test
 spec:
-  service: server
+  service: server-split
   backends:
     - service: server-v1
       weight: 50
diff --git a/integration/testdata/acl/enabled/traffic-split/3.server-v1.yaml b/integration/testdata/acl_disabled/traffic-split/2.server-v1.yaml
similarity index 75%
rename from integration/testdata/acl/enabled/traffic-split/3.server-v1.yaml
rename to integration/testdata/acl_disabled/traffic-split/2.server-v1.yaml
index 4db9563..c097646 100644
--- a/integration/testdata/acl/enabled/traffic-split/3.server-v1.yaml
+++ b/integration/testdata/acl_disabled/traffic-split/2.server-v1.yaml
@@ -1,9 +1,3 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: server
-  namespace: test
-
 ---
 apiVersion: v1
 kind: Pod
@@ -13,11 +7,15 @@ metadata:
   labels:
     app: server-v1
 spec:
-  serviceAccountName: server
   containers:
     - name: server-v1
       image: containous/whoami:v1.0.1
-      imagePullPolicy: Always
+      imagePullPolicy: IfNotPresent
+      readinessProbe:
+        httpGet:
+          path: /
+          port: 80
+        periodSeconds: 1
 
 ---
 apiVersion: v1
diff --git a/integration/testdata/acl/disabled/traffic-split/3.server-v2.yaml b/integration/testdata/acl_disabled/traffic-split/3.server-v2.yaml
similarity index 74%
rename from integration/testdata/acl/disabled/traffic-split/3.server-v2.yaml
rename to integration/testdata/acl_disabled/traffic-split/3.server-v2.yaml
index f5322a2..f4c1b5f 100644
--- a/integration/testdata/acl/disabled/traffic-split/3.server-v2.yaml
+++ b/integration/testdata/acl_disabled/traffic-split/3.server-v2.yaml
@@ -1,3 +1,4 @@
+---
 apiVersion: v1
 kind: Pod
 metadata:
@@ -9,7 +10,12 @@ spec:
   containers:
     - name: server-v2
       image: containous/whoami:v1.0.1
-      imagePullPolicy: Always
+      imagePullPolicy: IfNotPresent
+      readinessProbe:
+        httpGet:
+          path: /
+          port: 80
+        periodSeconds: 1
 
 ---
 apiVersion: v1
diff --git a/integration/testdata/acl/disabled/udp/1.server.yaml b/integration/testdata/acl_disabled/udp/1.server.yaml
similarity index 70%
rename from integration/testdata/acl/disabled/udp/1.server.yaml
rename to integration/testdata/acl_disabled/udp/1.server.yaml
index 3c139d6..fbdce4e 100644
--- a/integration/testdata/acl/disabled/udp/1.server.yaml
+++ b/integration/testdata/acl_disabled/udp/1.server.yaml
@@ -1,15 +1,16 @@
+---
 apiVersion: v1
 kind: Pod
 metadata:
-  name: server
+  name: server-udp
   namespace: test
   labels:
-    app: server
+    app: server-udp
 spec:
   containers:
-    - name: server
+    - name: server-udp
       image: containous/whoamiudp:v0.0.1
-      imagePullPolicy: Always
+      imagePullPolicy: IfNotPresent
       ports:
         - name: udp
           protocol: UDP
@@ -18,18 +19,18 @@ spec:
 apiVersion: v1
 kind: Service
 metadata:
-  name: server
+  name: server-udp
   namespace: test
   labels:
-    app: server
+    app: server-udp
     plop: cool
   annotations:
     maesh.containo.us/traffic-type: udp
 spec:
   type: ClusterIP
   ports:
-    - name: server
+    - name: server-udp
       protocol: UDP
       port: 8080
   selector:
-    app: server
+    app: server-udp
diff --git a/integration/testdata/acl_enabled/http/0.service-account.yaml b/integration/testdata/acl_enabled/http/0.service-account.yaml
new file mode 100644
index 0000000..0cab3f1
--- /dev/null
+++ b/integration/testdata/acl_enabled/http/0.service-account.yaml
@@ -0,0 +1,27 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: server
+  namespace: test
+
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: server-api
+  namespace: test
+
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: server-header
+  namespace: test
+
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: server-split
+  namespace: test
diff --git a/integration/testdata/acl/enabled/traffic-target/2.server.yaml b/integration/testdata/acl_enabled/http/1.server.yaml
similarity index 51%
rename from integration/testdata/acl/enabled/traffic-target/2.server.yaml
rename to integration/testdata/acl_enabled/http/1.server.yaml
index c4be3fe..c459062 100644
--- a/integration/testdata/acl/enabled/traffic-target/2.server.yaml
+++ b/integration/testdata/acl_enabled/http/1.server.yaml
@@ -1,37 +1,36 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: server
-  namespace: test
-
 ---
 apiVersion: v1
 kind: Pod
 metadata:
-  name: server
+  name: server-http
   namespace: test
   labels:
-    app: server
+    app: server-http
 spec:
   serviceAccountName: server
   containers:
-    - name: server
+    - name: server-http
       image: containous/whoami:v1.0.1
-      imagePullPolicy: Always
+      imagePullPolicy: IfNotPresent
+      readinessProbe:
+        httpGet:
+          path: /
+          port: 80
+        periodSeconds: 1
 
 ---
 apiVersion: v1
 kind: Service
 metadata:
-  name: server
+  name: server-http
   namespace: test
   labels:
-    app: server
+    app: server-http
 spec:
   type: ClusterIP
   ports:
     - port: 8080
-      name: server
+      name: server-http
       targetPort: 80
   selector:
-    app: server
+    app: server-http
\ No newline at end of file
diff --git a/integration/testdata/acl_enabled/http/3.server-api.yaml b/integration/testdata/acl_enabled/http/3.server-api.yaml
new file mode 100644
index 0000000..b5a9a54
--- /dev/null
+++ b/integration/testdata/acl_enabled/http/3.server-api.yaml
@@ -0,0 +1,36 @@
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: server-http-api
+  namespace: test
+  labels:
+    app: server-http-api
+spec:
+  serviceAccountName: server-api
+  containers:
+    - name: server-http-api
+      image: containous/whoami:v1.0.1
+      imagePullPolicy: IfNotPresent
+      readinessProbe:
+        httpGet:
+          path: /
+          port: 80
+        periodSeconds: 1
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: server-http-api
+  namespace: test
+  labels:
+    app: server-http-api
+spec:
+  type: ClusterIP
+  ports:
+    - port: 8080
+      name: server-http-api
+      targetPort: 80
+  selector:
+    app: server-http-api
\ No newline at end of file
diff --git a/integration/testdata/acl_enabled/http/4.server-header.yaml b/integration/testdata/acl_enabled/http/4.server-header.yaml
new file mode 100644
index 0000000..53272e8
--- /dev/null
+++ b/integration/testdata/acl_enabled/http/4.server-header.yaml
@@ -0,0 +1,36 @@
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: server-http-header
+  namespace: test
+  labels:
+    app: server-http-header
+spec:
+  serviceAccountName: server-header
+  containers:
+    - name: server-http-header
+      image: containous/whoami:v1.0.1
+      imagePullPolicy: IfNotPresent
+      readinessProbe:
+        httpGet:
+          path: /
+          port: 80
+        periodSeconds: 1
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: server-http-header
+  namespace: test
+  labels:
+    app: server-http-header
+spec:
+  type: ClusterIP
+  ports:
+    - port: 8080
+      name: server-http-header
+      targetPort: 80
+  selector:
+    app: server-http-header
\ No newline at end of file
diff --git a/integration/testdata/acl/enabled/traffic-split/5.traffic-target.yaml b/integration/testdata/acl_enabled/http/6.traffic-target.yaml
similarity index 69%
rename from integration/testdata/acl/enabled/traffic-split/5.traffic-target.yaml
rename to integration/testdata/acl_enabled/http/6.traffic-target.yaml
index e35094d..86d311e 100644
--- a/integration/testdata/acl/enabled/traffic-split/5.traffic-target.yaml
+++ b/integration/testdata/acl_enabled/http/6.traffic-target.yaml
@@ -1,7 +1,8 @@
+---
 apiVersion: access.smi-spec.io/v1alpha2
 kind: TrafficTarget
 metadata:
-  name: traffic-target
+  name: tool-authorized-to-server-http
   namespace: test
 spec:
   destination:
@@ -10,5 +11,5 @@ spec:
     namespace: test
   sources:
     - kind: ServiceAccount
-      name: client-a
+      name: tool-authorized
       namespace: test
\ No newline at end of file
diff --git a/integration/testdata/acl/enabled/traffic-target/3.traffic-target.yaml b/integration/testdata/acl_enabled/http/7.traffic-target-path-filter.yaml
similarity index 80%
rename from integration/testdata/acl/enabled/traffic-target/3.traffic-target.yaml
rename to integration/testdata/acl_enabled/http/7.traffic-target-path-filter.yaml
index 9cb6474..336f2a2 100644
--- a/integration/testdata/acl/enabled/traffic-target/3.traffic-target.yaml
+++ b/integration/testdata/acl_enabled/http/7.traffic-target-path-filter.yaml
@@ -1,3 +1,4 @@
+---
 apiVersion: specs.smi-spec.io/v1alpha3
 kind: HTTPRouteGroup
 metadata:
@@ -13,19 +14,17 @@ spec:
 apiVersion: access.smi-spec.io/v1alpha2
 kind: TrafficTarget
 metadata:
-  name: traffic-target
+  name: traffic-target-path-filter
   namespace: test
 spec:
   destination:
     kind: ServiceAccount
-    name: server
+    name: server-api
     namespace: test
   rules:
     - kind: HTTPRouteGroup
       name: http-route-group-api
-      matches:
-        - api
   sources:
     - kind: ServiceAccount
-      name: client-a
+      name: tool-authorized
       namespace: test
\ No newline at end of file
diff --git a/integration/testdata/acl_enabled/http/8.traffic-target-header-filter.yaml b/integration/testdata/acl_enabled/http/8.traffic-target-header-filter.yaml
new file mode 100644
index 0000000..10db30f
--- /dev/null
+++ b/integration/testdata/acl_enabled/http/8.traffic-target-header-filter.yaml
@@ -0,0 +1,30 @@
+---
+apiVersion: specs.smi-spec.io/v1alpha3
+kind: HTTPRouteGroup
+metadata:
+  name: http-route-group-header
+  namespace: test
+spec:
+  matches:
+    - name: header
+      headers:
+        - Authorized: "t|True"
+
+---
+apiVersion: access.smi-spec.io/v1alpha2
+kind: TrafficTarget
+metadata:
+  name: traffic-target-header-filter
+  namespace: test
+spec:
+  destination:
+    kind: ServiceAccount
+    name: server-header
+    namespace: test
+  rules:
+    - kind: HTTPRouteGroup
+      name: http-route-group-header
+  sources:
+    - kind: ServiceAccount
+      name: tool-authorized
+      namespace: test
\ No newline at end of file
diff --git a/integration/testdata/acl_enabled/traffic-split/0.service-account.yaml b/integration/testdata/acl_enabled/traffic-split/0.service-account.yaml
new file mode 100644
index 0000000..951b499
--- /dev/null
+++ b/integration/testdata/acl_enabled/traffic-split/0.service-account.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: server-split
+  namespace: test
\ No newline at end of file
diff --git a/integration/testdata/acl_enabled/traffic-split/1.server-split.yaml b/integration/testdata/acl_enabled/traffic-split/1.server-split.yaml
new file mode 100644
index 0000000..03189dc
--- /dev/null
+++ b/integration/testdata/acl_enabled/traffic-split/1.server-split.yaml
@@ -0,0 +1,119 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: server-http-split
+  namespace: test
+  labels:
+    app: server-http-split
+spec:
+  type: ClusterIP
+  ports:
+    - port: 8080
+      name: server-http-split
+  selector:
+    app: server-http-split
+
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: server-http-split-v1
+  namespace: test
+  labels:
+    app: server-http-split-v1
+spec:
+  serviceAccountName: server-split
+  containers:
+    - name: server-http-split-v1
+      image: containous/whoami:v1.0.1
+      imagePullPolicy: IfNotPresent
+      readinessProbe:
+        httpGet:
+          path: /
+          port: 80
+        periodSeconds: 1
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: server-http-split-v1
+  namespace: test
+  labels:
+    app: server-http-split-v1
+spec:
+  type: ClusterIP
+  ports:
+    - port: 8080
+      name: server-http-split-v1
+      targetPort: 80
+  selector:
+    app: server-http-split-v1
+
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: server-http-split-v2
+  namespace: test
+  labels:
+    app: server-http-split-v2
+spec:
+  serviceAccountName: server-split
+  containers:
+    - name: server-http-split-v2
+      image: containous/whoami:v1.0.1
+      imagePullPolicy: IfNotPresent
+      readinessProbe:
+        httpGet:
+          path: /
+          port: 80
+        periodSeconds: 1
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: server-http-split-v2
+  namespace: test
+  labels:
+    app: server-http-split-v2
+spec:
+  type: ClusterIP
+  ports:
+    - port: 8080
+      name: server-http-split-v2
+      targetPort: 80
+  selector:
+    app: server-http-split-v2
+
+---
+apiVersion: split.smi-spec.io/v1alpha3
+kind: TrafficSplit
+metadata:
+  name: server-http-split
+  namespace: test
+spec:
+  service: server-http-split
+  backends:
+    - service: server-http-split-v1
+      weight: 50
+    - service: server-http-split-v2
+      weight: 50
+
+---
+apiVersion: access.smi-spec.io/v1alpha2
+kind: TrafficTarget
+metadata:
+  name: tool-authorized-to-server-http-split
+  namespace: test
+spec:
+  destination:
+    kind: ServiceAccount
+    name: server-split
+    namespace: test
+  sources:
+    - kind: ServiceAccount
+      name: tool-authorized
+      namespace: test
\ No newline at end of file
diff --git a/integration/testdata/coredns/coredns.yaml b/integration/testdata/coredns/coredns.yaml
deleted file mode 100644
index 71604e5..0000000
--- a/integration/testdata/coredns/coredns.yaml
+++ /dev/null
@@ -1,190 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: coredns
-  namespace: kube-system
----
-apiVersion: rbac.authorization.k8s.io/v1beta1
-kind: ClusterRole
-metadata:
-  labels:
-    kubernetes.io/bootstrapping: rbac-defaults
-  name: system:coredns
-rules:
-  - apiGroups:
-      - ""
-    resources:
-      - endpoints
-      - services
-      - pods
-      - namespaces
-    verbs:
-      - list
-      - watch
-  - apiGroups:
-      - ""
-    resources:
-      - nodes
-    verbs:
-      - get
----
-apiVersion: rbac.authorization.k8s.io/v1beta1
-kind: ClusterRoleBinding
-metadata:
-  annotations:
-    rbac.authorization.kubernetes.io/autoupdate: "true"
-  labels:
-    kubernetes.io/bootstrapping: rbac-defaults
-  name: system:coredns
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: system:coredns
-subjects:
-  - kind: ServiceAccount
-    name: coredns
-    namespace: kube-system
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: coredns
-  namespace: kube-system
-data:
-  Corefile: |
-    .:53 {
-        errors
-        health
-        ready
-        kubernetes cluster.local in-addr.arpa ip6.arpa {
-          pods insecure
-          fallthrough in-addr.arpa ip6.arpa
-        }
-        hosts /etc/coredns/NodeHosts {
-          reload 1s
-          fallthrough
-        }
-        prometheus :9153
-        forward . /etc/resolv.conf
-        cache 30
-        loop
-        reload
-        loadbalance
-    }
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: coredns
-  namespace: kube-system
-  labels:
-    k8s-app: kube-dns
-    kubernetes.io/name: "CoreDNS"
-spec:
-  #replicas: 1
-  strategy:
-    type: RollingUpdate
-    rollingUpdate:
-      maxUnavailable: 1
-  selector:
-    matchLabels:
-      k8s-app: kube-dns
-  template:
-    metadata:
-      labels:
-        k8s-app: kube-dns
-    spec:
-      serviceAccountName: coredns
-      tolerations:
-        - key: "CriticalAddonsOnly"
-          operator: "Exists"
-      nodeSelector:
-        beta.kubernetes.io/os: linux
-      containers:
-        - name: coredns
-          image: coredns/coredns:1.6.3
-          imagePullPolicy: IfNotPresent
-          resources:
-            limits:
-              memory: 170Mi
-            requests:
-              cpu: 100m
-              memory: 70Mi
-          args: [ "-conf", "/etc/coredns/Corefile" ]
-          volumeMounts:
-            - name: config-volume
-              mountPath: /etc/coredns
-              readOnly: true
-          ports:
-            - containerPort: 53
-              name: dns
-              protocol: UDP
-            - containerPort: 53
-              name: dns-tcp
-              protocol: TCP
-            - containerPort: 9153
-              name: metrics
-              protocol: TCP
-          securityContext:
-            allowPrivilegeEscalation: false
-            capabilities:
-              add:
-                - NET_BIND_SERVICE
-              drop:
-                - all
-            readOnlyRootFilesystem: true
-          livenessProbe:
-            httpGet:
-              path: /health
-              port: 8080
-              scheme: HTTP
-            initialDelaySeconds: 60
-            timeoutSeconds: 5
-            successThreshold: 1
-            failureThreshold: 5
-          readinessProbe:
-            httpGet:
-              path: /ready
-              port: 8181
-              scheme: HTTP
-            initialDelaySeconds: 10
-            timeoutSeconds: 5
-            successThreshold: 1
-            failureThreshold: 5
-      dnsPolicy: Default
-      volumes:
-        - name: config-volume
-          configMap:
-            name: coredns
-            items:
-              - key: Corefile
-                path: Corefile
-              - key: NodeHosts
-                path: NodeHosts
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: kube-dns
-  namespace: kube-system
-  annotations:
-    prometheus.io/port: "9153"
-    prometheus.io/scrape: "true"
-  labels:
-    k8s-app: kube-dns
-    kubernetes.io/cluster-service: "true"
-    kubernetes.io/name: "CoreDNS"
-spec:
-  selector:
-    k8s-app: kube-dns
-  clusterIP: 10.43.0.10
-  ports:
-    - name: dns
-      port: 53
-      protocol: UDP
-    - name: dns-tcp
-      port: 53
-      protocol: TCP
-    - name: metrics
-      port: 9153
-      protocol: TCP
diff --git a/integration/testdata/coredns/corednssafe.yaml b/integration/testdata/coredns/corednssafe.yaml
deleted file mode 100644
index 046c283..0000000
--- a/integration/testdata/coredns/corednssafe.yaml
+++ /dev/null
@@ -1,180 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: coredns
-  namespace: kube-system
----
-apiVersion: rbac.authorization.k8s.io/v1beta1
-kind: ClusterRole
-metadata:
-  labels:
-    kubernetes.io/bootstrapping: rbac-defaults
-  name: system:coredns
-rules:
-  - apiGroups:
-      - ""
-    resources:
-      - endpoints
-      - services
-      - pods
-      - namespaces
-    verbs:
-      - list
-      - watch
-  - apiGroups:
-      - ""
-    resources:
-      - nodes
-    verbs:
-      - get
----
-apiVersion: rbac.authorization.k8s.io/v1beta1
-kind: ClusterRoleBinding
-metadata:
-  annotations:
-    rbac.authorization.kubernetes.io/autoupdate: "true"
-  labels:
-    kubernetes.io/bootstrapping: rbac-defaults
-  name: system:coredns
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: system:coredns
-subjects:
-  - kind: ServiceAccount
-    name: coredns
-    namespace: kube-system
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: coredns
-  namespace: kube-system
-data:
-  Corefile: |
-    .:53 {
-        errors
-        health
-        kubernetes cluster.local in-addr.arpa ip6.arpa {
-          pods insecure
-          fallthrough in-addr.arpa ip6.arpa
-        }
-        hosts /etc/coredns/NodeHosts {
-          reload 1s
-          fallthrough
-        }
-        prometheus :9153
-        forward . /etc/resolv.conf
-        cache 30
-        loop
-        reload
-        loadbalance
-    }
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: coredns
-  namespace: kube-system
-  labels:
-    k8s-app: kube-dns
-    kubernetes.io/name: "CoreDNS"
-spec:
-  #replicas: 1
-  strategy:
-    type: RollingUpdate
-    rollingUpdate:
-      maxUnavailable: 1
-  selector:
-    matchLabels:
-      k8s-app: kube-dns
-  template:
-    metadata:
-      labels:
-        k8s-app: kube-dns
-    spec:
-      serviceAccountName: coredns
-      tolerations:
-        - key: "CriticalAddonsOnly"
-          operator: "Exists"
-      nodeSelector:
-        beta.kubernetes.io/os: linux
-      containers:
-        - name: coredns
-          image: coredns/coredns:1.6.3
-          imagePullPolicy: IfNotPresent
-          resources:
-            limits:
-              memory: 170Mi
-            requests:
-              cpu: 100m
-              memory: 70Mi
-          args: [ "-conf", "/etc/coredns/Corefile" ]
-          volumeMounts:
-            - name: config-volume
-              mountPath: /etc/coredns
-              readOnly: true
-          ports:
-            - containerPort: 53
-              name: dns
-              protocol: UDP
-            - containerPort: 53
-              name: dns-tcp
-              protocol: TCP
-            - containerPort: 9153
-              name: metrics
-              protocol: TCP
-          securityContext:
-            allowPrivilegeEscalation: false
-            capabilities:
-              add:
-                - NET_BIND_SERVICE
-              drop:
-                - all
-            readOnlyRootFilesystem: true
-          livenessProbe:
-            httpGet:
-              path: /health
-              port: 8080
-              scheme: HTTP
-            initialDelaySeconds: 60
-            timeoutSeconds: 5
-            successThreshold: 1
-            failureThreshold: 5
-      dnsPolicy: Default
-      volumes:
-        - name: config-volume
-          configMap:
-            name: coredns
-            items:
-              - key: Corefile
-                path: Corefile
-              - key: NodeHosts
-                path: NodeHosts
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: kube-dns
-  namespace: kube-system
-  annotations:
-    prometheus.io/port: "9153"
-    prometheus.io/scrape: "true"
-  labels:
-    k8s-app: kube-dns
-    kubernetes.io/cluster-service: "true"
-    kubernetes.io/name: "CoreDNS"
-spec:
-  selector:
-    k8s-app: kube-dns
-  clusterIP: 10.43.0.10
-  ports:
-    - name: dns
-      port: 53
-      protocol: UDP
-    - name: dns-tcp
-      port: 53
-      protocol: TCP
-    - name: metrics
-      port: 9153
-      protocol: TCP
diff --git a/integration/testdata/coredns/whoami-shadow-service.yaml b/integration/testdata/coredns/whoami-shadow-service.yaml
new file mode 100644
index 0000000..3451259
--- /dev/null
+++ b/integration/testdata/coredns/whoami-shadow-service.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: maesh-whoami-6d61657368-whoami
+  namespace: maesh
+  labels:
+    app: maesh
+    type: shadow
+spec:
+  type: ClusterIP
+  ports:
+    - port: 8080
+      name: whoami
+      targetPort: 80
+  selector:
+    app: maesh-mesh
\ No newline at end of file
diff --git a/integration/testdata/kubedns/coredns.yaml b/integration/testdata/kubedns/coredns.yaml
index fd4c447..ecc1bbd 100644
--- a/integration/testdata/kubedns/coredns.yaml
+++ b/integration/testdata/kubedns/coredns.yaml
@@ -1,8 +1,10 @@
+---
 apiVersion: v1
 kind: ServiceAccount
 metadata:
   name: coredns
   namespace: maesh
+
 ---
 apiVersion: rbac.authorization.k8s.io/v1beta1
 kind: ClusterRole
@@ -27,6 +29,7 @@ rules:
       - nodes
     verbs:
       - get
+
 ---
 apiVersion: rbac.authorization.k8s.io/v1beta1
 kind: ClusterRoleBinding
@@ -78,7 +81,6 @@ metadata:
     k8s-app: coredns
     kubernetes.io/name: "CoreDNS"
 spec:
-  # replicas: 1
   strategy:
     type: RollingUpdate
     rollingUpdate:
@@ -159,6 +161,7 @@ spec:
             items:
               - key: Corefile
                 path: Corefile
+
 ---
 apiVersion: v1
 kind: Service
diff --git a/integration/testdata/kubedns/kubedns.yaml b/integration/testdata/kubedns/kubedns.yaml
index 45c39bb..ff16a7e 100644
--- a/integration/testdata/kubedns/kubedns.yaml
+++ b/integration/testdata/kubedns/kubedns.yaml
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+---
 apiVersion: v1
 kind: Service
 metadata:
@@ -33,6 +34,7 @@ spec:
     - name: dns-tcp
       port: 53
       protocol: TCP
+
 ---
 apiVersion: v1
 kind: ServiceAccount
@@ -42,6 +44,7 @@ metadata:
   labels:
     kubernetes.io/cluster-service: "true"
     addonmanager.kubernetes.io/mode: Reconcile
+
 ---
 apiVersion: v1
 kind: ConfigMap
@@ -50,6 +53,7 @@ metadata:
   namespace: kube-system
   labels:
     addonmanager.kubernetes.io/mode: EnsureExists
+
 ---
 apiVersion: apps/v1
 kind: Deployment
diff --git a/integration/testdata/maesh/controller-acl-disabled.yaml b/integration/testdata/maesh/controller-acl-disabled.yaml
new file mode 100644
index 0000000..8fb5fd9
--- /dev/null
+++ b/integration/testdata/maesh/controller-acl-disabled.yaml
@@ -0,0 +1,166 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: maesh-controller
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+  name: maesh-controller-role
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+rules:
+  - apiGroups:
+      - ""
+    resources:
+      - pods
+      - endpoints
+    verbs:
+      - list
+      - watch
+  - apiGroups:
+      - ""
+    resources:
+      - namespaces
+    verbs:
+      - get
+      - create
+  - apiGroups:
+      - ""
+    resources:
+      - configmaps
+    verbs:
+      - get
+      - create
+      - update
+      - list
+      - watch
+  - apiGroups:
+      - ""
+    resources:
+      - services
+    verbs:
+      - get
+      - list
+      - watch
+      - delete
+      - create
+      - update
+  - apiGroups:
+      - apps
+    resources:
+      - deployments
+    verbs:
+      - get
+      - update
+      - create
+  - apiGroups:
+      - access.smi-spec.io
+      - specs.smi-spec.io
+      - split.smi-spec.io
+    resources:
+      - traffictargets
+      - tcproutes
+      - httproutegroups
+      - trafficsplits
+      - events
+    verbs:
+      - get
+      - list
+      - watch
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+  name: maesh-controller
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: maesh-controller-role
+subjects:
+  - kind: ServiceAccount
+    name: maesh-controller
+    namespace: maesh
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: maesh-controller
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: maesh
+      component: controller
+  template:
+    metadata:
+      labels:
+        app: maesh
+        component: controller
+    spec:
+      serviceAccountName: maesh-controller
+      automountServiceAccountToken: true
+      containers:
+        - name: maesh-controller
+          image: "containous/maesh:latest"
+          imagePullPolicy: IfNotPresent
+          args:
+            - "--logLevel=DEBUG"
+            - "--defaultMode=http"
+            - "--namespace=maesh"
+            - "--limitHTTPPort=5"
+            - "--limitTCPPort=5"
+            - "--limitUDPPort=5"
+          ports:
+            - name: api
+              containerPort: 9000
+          readinessProbe:
+            httpGet:
+              path: /api/status/readiness
+              port: api
+            initialDelaySeconds: 3
+            periodSeconds: 1
+      initContainers:
+        - name: maesh-prepare
+          image: "containous/maesh:latest"
+          imagePullPolicy: IfNotPresent
+          args:
+            - "prepare"
+            - "--logLevel=DEBUG"
+            - "--clusterdomain=cluster.local"
+            - "--namespace=maesh"
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: maesh-controller
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+spec:
+  type: ClusterIP
+  ports:
+    - port: 9000
+      name: mesh-controller-api
+      targetPort: api
+  selector:
+    component: controller
diff --git a/integration/testdata/maesh/controller-acl-enabled.yaml b/integration/testdata/maesh/controller-acl-enabled.yaml
new file mode 100644
index 0000000..e3271b4
--- /dev/null
+++ b/integration/testdata/maesh/controller-acl-enabled.yaml
@@ -0,0 +1,168 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: maesh-controller
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+  name: maesh-controller-role
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+rules:
+  - apiGroups:
+      - ""
+    resources:
+      - pods
+      - endpoints
+    verbs:
+      - list
+      - watch
+  - apiGroups:
+      - ""
+    resources:
+      - namespaces
+    verbs:
+      - get
+      - create
+  - apiGroups:
+      - ""
+    resources:
+      - configmaps
+    verbs:
+      - get
+      - create
+      - update
+      - list
+      - watch
+  - apiGroups:
+      - ""
+    resources:
+      - services
+    verbs:
+      - get
+      - list
+      - watch
+      - delete
+      - create
+      - update
+  - apiGroups:
+      - apps
+    resources:
+      - deployments
+    verbs:
+      - get
+      - update
+      - create
+  - apiGroups:
+      - access.smi-spec.io
+      - specs.smi-spec.io
+      - split.smi-spec.io
+    resources:
+      - traffictargets
+      - tcproutes
+      - httproutegroups
+      - trafficsplits
+      - events
+    verbs:
+      - get
+      - list
+      - watch
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+  name: maesh-controller
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: maesh-controller-role
+subjects:
+  - kind: ServiceAccount
+    name: maesh-controller
+    namespace: maesh
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: maesh-controller
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: maesh
+      component: controller
+  template:
+    metadata:
+      labels:
+        app: maesh
+        component: controller
+    spec:
+      serviceAccountName: maesh-controller
+      automountServiceAccountToken: true
+      containers:
+        - name: maesh-controller
+          image: "containous/maesh:latest"
+          imagePullPolicy: IfNotPresent
+          args:
+            - "--logLevel=DEBUG"
+            - "--defaultMode=http"
+            - "--namespace=maesh"
+            - "--acl"
+            - "--limitHTTPPort=5"
+            - "--limitTCPPort=5"
+            - "--limitUDPPort=5"
+          ports:
+            - name: api
+              containerPort: 9000
+          readinessProbe:
+            httpGet:
+              path: /api/status/readiness
+              port: api
+            initialDelaySeconds: 3
+            periodSeconds: 1
+      initContainers:
+        - name: maesh-prepare
+          image: "containous/maesh:latest"
+          imagePullPolicy: IfNotPresent
+          args:
+            - "prepare"
+            - "--logLevel=DEBUG"
+            - "--clusterdomain=cluster.local"
+            - "--namespace=maesh"
+            - "--acl"
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: maesh-controller
+  namespace: maesh
+  labels:
+    app: maesh
+    component: controller
+spec:
+  type: ClusterIP
+  ports:
+    - port: 9000
+      name: mesh-controller-api
+      targetPort: api
+  selector:
+    component: controller
diff --git a/integration/testdata/maesh/proxy.yaml b/integration/testdata/maesh/proxy.yaml
new file mode 100644
index 0000000..27983e2
--- /dev/null
+++ b/integration/testdata/maesh/proxy.yaml
@@ -0,0 +1,124 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: maesh-mesh
+  namespace: maesh
+  labels:
+    app: maesh
+    component: maesh-mesh
+
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+  name: maesh-mesh-role
+  namespace: maesh
+  labels:
+    app: maesh
+    component: maesh-mesh
+rules:
+  - apiGroups:
+      - ""
+    resources:
+      - pods
+      - services
+    verbs:
+      - list
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+  name: maesh-mesh
+  namespace: maesh
+  labels:
+    app: maesh
+    component: maesh-mesh
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: maesh-mesh-role
+subjects:
+  - kind: ServiceAccount
+    name: maesh-mesh
+    namespace: maesh
+
+---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: maesh-mesh
+  namespace: maesh
+  labels:
+    app: maesh
+    component: maesh-mesh
+spec:
+  selector:
+    matchLabels:
+      app: maesh
+      component: maesh-mesh
+  template:
+    metadata:
+      labels:
+        app: maesh
+        component: maesh-mesh
+    spec:
+      serviceAccountName: maesh-mesh
+      automountServiceAccountToken: true
+      terminationGracePeriodSeconds: 30
+      containers:
+        - name: maesh-mesh
+          image: "traefik:v2.3"
+          imagePullPolicy: IfNotPresent
+          args:
+            - "--entryPoints.readiness.address=:1081"
+            - "--entryPoints.liveness.address=:1082"
+            - "--entryPoints.http-5000.address=:5000"
+            - "--entryPoints.http-5000.forwardedHeaders.insecure=true"
+            - "--entryPoints.http-5001.address=:5001"
+            - "--entryPoints.http-5001.forwardedHeaders.insecure=true"
+            - "--entryPoints.http-5002.address=:5002"
+            - "--entryPoints.http-5002.forwardedHeaders.insecure=true"
+            - "--entryPoints.http-5003.address=:5003"
+            - "--entryPoints.http-5003.forwardedHeaders.insecure=true"
+            - "--entryPoints.http-5004.address=:5004"
+            - "--entryPoints.http-5004.forwardedHeaders.insecure=true"
+            - "--entryPoints.tcp-10000.address=:10000"
+            - "--entryPoints.tcp-10000.forwardedHeaders.insecure=true"
+            - "--entryPoints.tcp-10001.address=:10001"
+            - "--entryPoints.tcp-10001.forwardedHeaders.insecure=true"
+            - "--entryPoints.tcp-10002.address=:10002"
+            - "--entryPoints.tcp-10002.forwardedHeaders.insecure=true"
+            - "--entryPoints.tcp-10003.address=:10003"
+            - "--entryPoints.tcp-10003.forwardedHeaders.insecure=true"
+            - "--entryPoints.tcp-10004.address=:10004"
+            - "--entryPoints.tcp-10004.forwardedHeaders.insecure=true"
+            - "--entryPoints.udp-15000.address=:15000/udp"
+            - "--entryPoints.udp-15001.address=:15001/udp"
+            - "--entryPoints.udp-15002.address=:15002/udp"
+            - "--entryPoints.udp-15003.address=:15003/udp"
+            - "--entryPoints.udp-15004.address=:15004/udp"
+            - "--providers.http.endpoint=http://maesh-controller.maesh.svc.cluster.local:9000/api/configuration/current"
+            - "--providers.http.pollInterval=100ms"
+            - "--providers.http.pollTimeout=100ms"
+            - "--api.dashboard=false"
+            - "--api.insecure"
+            - "--ping"
+          ports:
+            - name: readiness
+              containerPort: 1081
+            - name: liveness
+              containerPort: 1082
+            - name: api
+              containerPort: 8080
+          readinessProbe:
+            httpGet:
+              path: /ping
+              port: readiness
+            initialDelaySeconds: 3
+            periodSeconds: 1
+          livenessProbe:
+            tcpSocket:
+              port: liveness
+            initialDelaySeconds: 3
+            periodSeconds: 1
diff --git a/integration/testdata/values.yaml b/integration/testdata/maesh/values.yaml
similarity index 100%
rename from integration/testdata/values.yaml
rename to integration/testdata/maesh/values.yaml
diff --git a/integration/testdata/smi/crds/smi-access.yaml b/integration/testdata/smi/crds/smi-access.yaml
deleted file mode 100644
index ab11de1..0000000
--- a/integration/testdata/smi/crds/smi-access.yaml
+++ /dev/null
@@ -1,89 +0,0 @@
----
-apiVersion: apiextensions.k8s.io/v1beta1
-kind: CustomResourceDefinition
-metadata:
-  name: traffictargets.access.smi-spec.io
-spec:
-  group: access.smi-spec.io
-  scope: Namespaced
-  names:
-    kind: TrafficTarget
-    shortNames:
-      - tt
-    plural: traffictargets
-    singular: traffictarget
-  version: v1alpha2
-  versions:
-    - name: v1alpha2
-      served: true
-      storage: true
-    - name: v1alpha1
-      served: false
-      storage: false
-  validation:
-    openAPIV3Schema:
-      properties:
-        spec:
-          required:
-            - destination
-          properties:
-            destination:
-              description: The destination of this traffic target.
-              type: object
-              required:
-                - name
-                - kind
-              properties:
-                kind:
-                  description: Kind of the destination.
-                  type: string
-                name:
-                  description: Name of the destination.
-                  type: string
-                namespace:
-                  description: Namespace of the destination.
-                  type: string
-                port:
-                  description: Port number of the destination.
-                  type: number
-            rules:
-              description: Specifications of this traffic target.
-              type: array
-              items:
-                type: object
-                required:
-                  - name
-                  - kind
-                properties:
-                  kind:
-                    description: Kind of this spec.
-                    type: string
-                    enum:
-                      - HTTPRouteGroup
-                      - TCPRoute
-                  name:
-                    description: Name of this spec.
-                    type: string
-                  matches:
-                    description: Match conditions of this spec.
-                    type: array
-                    items:
-                      type: string
-            sources:
-              description: Sources of this traffic target.
-              type: array
-              items:
-                type: object
-                required:
-                  - name
-                  - kind
-                properties:
-                  kind:
-                    description: Kind of this source.
-                    type: string
-                  name:
-                    description: Name of this source.
-                    type: string
-                  namespace:
-                    description: Namespace of this source.
-                    type: string
\ No newline at end of file
diff --git a/integration/testdata/smi/crds/smi-specs.yaml b/integration/testdata/smi/crds/smi-specs.yaml
deleted file mode 100644
index f29f323..0000000
--- a/integration/testdata/smi/crds/smi-specs.yaml
+++ /dev/null
@@ -1,96 +0,0 @@
----
-apiVersion: apiextensions.k8s.io/v1beta1
-kind: CustomResourceDefinition
-metadata:
-  name: httproutegroups.specs.smi-spec.io
-spec:
-  group: specs.smi-spec.io
-  scope: Namespaced
-  names:
-    kind: HTTPRouteGroup
-    shortNames:
-      - htr
-    plural: httproutegroups
-    singular: httproutegroup
-  version: v1alpha3
-  versions:
-    - name: v1alpha3
-      served: true
-      storage: true
-    - name: v1alpha2
-      served: false
-      storage: false
-    - name: v1alpha1
-      served: false
-      storage: false
-  validation:
-    openAPIV3Schema:
-      properties:
-        spec:
-          required:
-            - matches
-          properties:
-            matches:
-              description: Match conditions of this route group.
-              type: array
-              items:
-                type: object
-                required:
-                  - name
-                properties:
-                  name:
-                    description: Name of the HTTP route.
-                    type: string
-                  pathRegex:
-                    description: URI path regex of the HTTP route.
-                    type: string
-                  methods:
-                    description: The HTTP methods of this HTTP route.
-                    type: array
-                    items:
-                      type: string
-                      description: The HTTP method of this HTTP route.
-                      enum:
-                        - '*'
-                        - GET
-                        - HEAD
-                        - PUT
-                        - POST
-                        - DELETE
-                        - CONNECT
-                        - OPTIONS
-                        - TRACE
-                        - PATCH
-                  headers:
-                    description: Header match conditions of this route.
-                    type: array
-                    items:
-                      description: Header match condition of this route.
-                      type: object
-                      additionalProperties:
-                        type: string
----
-apiVersion: apiextensions.k8s.io/v1beta1
-kind: CustomResourceDefinition
-metadata:
-  name: tcproutes.specs.smi-spec.io
-spec:
-  group: specs.smi-spec.io
-  scope: Namespaced
-  names:
-    kind: TCPRoute
-    shortNames:
-      - tr
-    plural: tcproutes
-    singular: tcproute
-  version: v1alpha3
-  versions:
-    - name: v1alpha3
-      served: true
-      storage: true
-    - name: v1alpha2
-      served: false
-      storage: false
-    - name: v1alpha1
-      served: false
-      storage: false
\ No newline at end of file
diff --git a/integration/testdata/smi/crds/smi-split.yaml b/integration/testdata/smi/crds/smi-split.yaml
deleted file mode 100644
index 224eb6a..0000000
--- a/integration/testdata/smi/crds/smi-split.yaml
+++ /dev/null
@@ -1,71 +0,0 @@
----
-apiVersion: apiextensions.k8s.io/v1beta1
-kind: CustomResourceDefinition
-metadata:
-  name: trafficsplits.split.smi-spec.io
-spec:
-  group: split.smi-spec.io
-  scope: Namespaced
-  names:
-    kind: TrafficSplit
-    listKind: TrafficSplitList
-    shortNames:
-      - ts
-    plural: trafficsplits
-    singular: trafficsplit
-  version: v1alpha3
-  versions:
-    - name: v1alpha3
-      served: true
-      storage: true
-    - name: v1alpha2
-      served: false
-      storage: false
-    - name: v1alpha1
-      served: false
-      storage: false
-  additionalPrinterColumns:
-    - name: Service
-      type: string
-      description: The apex service of this split.
-      JSONPath: .spec.service
-  validation:
-    openAPIV3Schema:
-      properties:
-        spec:
-          type: object
-          required:
-            - service
-            - backends
-          properties:
-            service:
-              description: The apex service of this split.
-              type: string
-            matches:
-              description: The HTTP route groups that this traffic split should match.
-              type: array
-              items:
-                type: object
-                required: ['kind', 'name']
-                properties:
-                  kind:
-                    description: Kind of the matching group.
-                    type: string
-                    enum:
-                      - HTTPRouteGroup
-                  name:
-                    description: Name of the matching group.
-                    type: string
-            backends:
-              description: The backend services of this split.
-              type: array
-              items:
-                type: object
-                required: ['service', 'weight']
-                properties:
-                  service:
-                    description: Name of the Kubernetes service.
-                    type: string
-                  weight:
-                    description: Traffic weight value of this backend.
-                    type: number
\ No newline at end of file
diff --git a/integration/testdata/tool/tool-authorized.yaml b/integration/testdata/tool/tool-authorized.yaml
new file mode 100644
index 0000000..4d637cb
--- /dev/null
+++ b/integration/testdata/tool/tool-authorized.yaml
@@ -0,0 +1,21 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: tool-authorized
+  namespace: test
+
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: tool-authorized
+  namespace: test
+spec:
+  serviceAccountName: tool-authorized
+  containers:
+    - name: tool-authorized
+      image: giantswarm/tiny-tools:3.9
+      command:
+        - "sleep"
+        - "36000"
\ No newline at end of file
diff --git a/integration/testdata/tool/tool-forbidden.yaml b/integration/testdata/tool/tool-forbidden.yaml
new file mode 100644
index 0000000..a0934bc
--- /dev/null
+++ b/integration/testdata/tool/tool-forbidden.yaml
@@ -0,0 +1,21 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: tool-forbidden
+  namespace: test
+
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: tool-forbidden
+  namespace: test
+spec:
+  serviceAccountName: tool-forbidden
+  containers:
+    - name: tool-forbidden
+      image: giantswarm/tiny-tools:3.9
+      command:
+        - "sleep"
+        - "36000"
\ No newline at end of file
diff --git a/integration/testdata/tool/tool.yaml b/integration/testdata/tool/tool.yaml
new file mode 100644
index 0000000..e8c26a2
--- /dev/null
+++ b/integration/testdata/tool/tool.yaml
@@ -0,0 +1,21 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: tool
+  namespace: test
+
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: tool
+  namespace: test
+spec:
+  serviceAccountName: tool
+  containers:
+    - name: tool
+      image: giantswarm/tiny-tools:3.9
+      command:
+        - "sleep"
+        - "36000"
\ No newline at end of file
diff --git a/integration/testdata/tools/deployment.yaml b/integration/testdata/tools/deployment.yaml
deleted file mode 100644
index 7a50564..0000000
--- a/integration/testdata/tools/deployment.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: tiny-tools
-  namespace: test
-  labels:
-    app: tiny-tools
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: tiny-tools
-  template:
-    metadata:
-      name: tiny-tools
-      namespace: test
-      labels:
-        app: tiny-tools
-    spec:
-      containers:
-      - name: tiny-tools
-        image: giantswarm/tiny-tools:3.9
-        command:
-        - "sleep"
-        - "3600"
diff --git a/integration/testdata/whoami/0-namespaces.yaml b/integration/testdata/whoami/0-namespaces.yaml
deleted file mode 100644
index fda5d03..0000000
--- a/integration/testdata/whoami/0-namespaces.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
----
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: whoami
-
----
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: whoami-test
diff --git a/integration/testdata/whoami/whoami-test.yaml b/integration/testdata/whoami/whoami-test.yaml
deleted file mode 100644
index 34a7be3..0000000
--- a/integration/testdata/whoami/whoami-test.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-kind: Deployment
-apiVersion: apps/v1
-metadata:
-  name: whoami
-  namespace: whoami-test
-spec:
-  replicas: 2
-  selector:
-    matchLabels:
-      app: whoami
-  template:
-    metadata:
-      labels:
-        app: whoami
-    spec:
-      containers:
-      - name: whoami
-        image: containous/whoami:v1.0.1
-        imagePullPolicy: IfNotPresent
-
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: whoami
-  namespace: whoami-test
-  labels:
-    app: whoami
-  annotations:
-    maesh.containo.us/traffic-type: "http"
-spec:
-  type: ClusterIP
-  ports:
-  - port: 80
-    name: whoami
-  selector:
-    app: whoami
-
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: whoami-tcp
-  namespace: whoami-test
-  labels:
-    app: whoami
-  annotations:
-    maesh.containo.us/traffic-type: "tcp"
-spec:
-  type: ClusterIP
-  ports:
-    - port: 80
-      name: whoami
-  selector:
-    app: whoami
diff --git a/integration/testdata/whoami/whoami.yaml b/integration/testdata/whoami/whoami.yaml
deleted file mode 100644
index c681785..0000000
--- a/integration/testdata/whoami/whoami.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-kind: Deployment
-apiVersion: apps/v1
-metadata:
-  name: whoami
-  namespace: whoami
-spec:
-  replicas: 2
-  selector:
-    matchLabels:
-      app: whoami
-  template:
-    metadata:
-      labels:
-        app: whoami
-    spec:
-      containers:
-      - name: whoami
-        image: containous/whoami:v1.0.1
-        imagePullPolicy: IfNotPresent
-
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: whoami
-  namespace: whoami
-  labels:
-    app: whoami
-  annotations:
-    maesh.containo.us/traffic-type: "http"
-spec:
-  type: ClusterIP
-  ports:
-  - port: 80
-    name: whoami
-  selector:
-    app: whoami
-
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: whoami-tcp
-  namespace: whoami
-  labels:
-    app: whoami
-  annotations:
-    maesh.containo.us/traffic-type: "tcp"
-spec:
-  type: ClusterIP
-  ports:
-    - port: 80
-      name: whoami
-  selector:
-    app: whoami