Repository: kumahq/kuma
Pull request #1811 (2021-04-14 11:12:42)

feat(kuma-dp) add coredns server to kuma-dp
### Summary

- Functionality to run DNS Server (CoreDNS) alongside kuma-dp
- Added to our current build/release workflow building our version of coredns which was stripped from any unnecessary plugins



Patch:
diff --git a/app/kuma-dp/cmd/run.go b/app/kuma-dp/cmd/run.go
index 5e9e5f39f..030c34dbd 100644
--- a/app/kuma-dp/cmd/run.go
+++ b/app/kuma-dp/cmd/run.go
@@ -6,7 +6,9 @@ import (
 	"path/filepath"
 
 	kumadp_config "github.com/kumahq/kuma/app/kuma-dp/pkg/config"
+	"github.com/kumahq/kuma/app/kuma-dp/pkg/dataplane/dnsserver"
 	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+	"github.com/kumahq/kuma/pkg/core/runtime/component"
 
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
@@ -74,14 +76,22 @@ func newRunCmd(rootCtx *RootContext) *cobra.Command {
 				runLog.Info("picked a free port for Envoy Admin API to listen on", "port", cfg.Dataplane.AdminPort)
 			}
 
-			if cfg.DataplaneRuntime.ConfigDir == "" {
+			if cfg.DataplaneRuntime.ConfigDir == "" || cfg.DNS.ConfigDir == "" {
 				tmpDir, err = ioutil.TempDir("", "kuma-dp-")
 				if err != nil {
-					runLog.Error(err, "unable to create a temporary directory to store generated Envoy config at")
+					runLog.Error(err, "unable to create a temporary directory to store generated config sat")
 					return err
 				}
-				cfg.DataplaneRuntime.ConfigDir = tmpDir
-				runLog.Info("generated Envoy configuration will be stored in a temporary directory", "dir", tmpDir)
+
+				if cfg.DataplaneRuntime.ConfigDir == "" {
+					cfg.DataplaneRuntime.ConfigDir = tmpDir
+				}
+
+				if cfg.DNS.ConfigDir == "" {
+					cfg.DNS.ConfigDir = tmpDir
+				}
+
+				runLog.Info("generated configurations will be stored in a temporary directory", "dir", tmpDir)
 			}
 
 			if cfg.DataplaneRuntime.Token != "" {
@@ -116,7 +126,11 @@ func newRunCmd(rootCtx *RootContext) *cobra.Command {
 					}
 				}()
 			}
+
 			shouldQuit := setupQuitChannel()
+			components := []component.Component{
+				accesslogs.NewAccessLogServer(cfg.Dataplane),
+			}
 
 			opts := envoy.Opts{
 				Config:          *cfg,
@@ -128,17 +142,34 @@ func newRunCmd(rootCtx *RootContext) *cobra.Command {
 				Quit:            shouldQuit,
 				LogLevel:        rootCtx.LogLevel,
 			}
+
 			if cfg.DNS.Enabled {
 				opts.DNSPort = cfg.DNS.EnvoyDNSPort
 				opts.EmptyDNSPort = cfg.DNS.CoreDNSEmptyPort
+
+				dnsOpts := &dnsserver.Opts{
+					Config: *cfg,
+					Stdout: cmd.OutOrStdout(),
+					Stderr: cmd.OutOrStderr(),
+					Quit:   shouldQuit,
+				}
+
+				dnsServer, err := dnsserver.New(dnsOpts)
+				if err != nil {
+					return err
+				}
+
+				components = append(components, dnsServer)
 			}
+
 			dataplane, err := envoy.New(opts)
 			if err != nil {
 				return err
 			}
-			server := accesslogs.NewAccessLogServer(cfg.Dataplane)
 
-			if err := rootCtx.ComponentManager.Add(server, dataplane); err != nil {
+			components = append(components, dataplane)
+
+			if err := rootCtx.ComponentManager.Add(components...); err != nil {
 				return err
 			}
 
@@ -171,6 +202,8 @@ func newRunCmd(rootCtx *RootContext) *cobra.Command {
 	cmd.PersistentFlags().Uint32Var(&cfg.DNS.CoreDNSEmptyPort, "dns-coredns-empty-port", cfg.DNS.CoreDNSEmptyPort, "A port that always responds with empty NXDOMAIN respond. It is required to implement a fallback to a real DNS.")
 	cmd.PersistentFlags().StringVar(&cfg.DNS.CoreDNSBinaryPath, "dns-coredns-path", cfg.DNS.CoreDNSBinaryPath, "A path to CoreDNS binary.")
 	cmd.PersistentFlags().StringVar(&cfg.DNS.CoreDNSConfigTemplatePath, "dns-coredns-config-template-path", cfg.DNS.CoreDNSConfigTemplatePath, "A path to a CoreDNS config template.")
+	cmd.PersistentFlags().StringVar(&cfg.DNS.ConfigDir, "dns-server-config-dir", cfg.DNS.ConfigDir, "Directory in which DNS Server config will be generated")
+	cmd.PersistentFlags().Uint32Var(&cfg.DNS.PrometheusPort, "dns-prometheus-port", cfg.DNS.PrometheusPort, "A port for exposing Prometheus stats")
 	return cmd
 }
 
diff --git a/app/kuma-dp/pkg/dataplane/dnsserver/config_file.go b/app/kuma-dp/pkg/dataplane/dnsserver/config_file.go
new file mode 100644
index 000000000..83b5269bf
--- /dev/null
+++ b/app/kuma-dp/pkg/dataplane/dnsserver/config_file.go
@@ -0,0 +1,26 @@
+package dnsserver
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/pkg/errors"
+
+	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
+)
+
+func GenerateConfigFile(cfg kuma_dp.DNS, config []byte) (string, error) {
+	configFile := filepath.Join(cfg.ConfigDir, "Corefile")
+	if err := writeFile(configFile, config, 0600); err != nil {
+		return "", errors.Wrap(err, "failed to persist Envoy bootstrap config on disk")
+	}
+	return configFile, nil
+}
+
+func writeFile(filename string, data []byte, perm os.FileMode) error {
+	if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
+		return err
+	}
+	return ioutil.WriteFile(filename, data, perm)
+}
diff --git a/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver.go b/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver.go
new file mode 100644
index 000000000..e8ad7392d
--- /dev/null
+++ b/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver.go
@@ -0,0 +1,184 @@
+package dnsserver
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"text/template"
+
+	"github.com/pkg/errors"
+
+	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
+	"github.com/kumahq/kuma/pkg/core"
+)
+
+var (
+	runLog = core.Log.WithName("kuma-dp").WithName("run").WithName("dns-server")
+)
+
+type DNSServer struct {
+	opts *Opts
+}
+
+type Opts struct {
+	Config kuma_dp.Config
+	Stdout io.Writer
+	Stderr io.Writer
+	Quit   chan struct{}
+}
+
+const DefaultCoreFileTemplate = `.:{{ .CoreDNSPort }} {
+    forward . 127.0.0.1:{{ .EnvoyDNSPort }}
+    alternate NXDOMAIN,SERVFAIL,REFUSED . /etc/resolv.conf
+    prometheus localhost:{{ .PrometheusPort }}
+    errors
+}
+
+.:{{ .CoreDNSEmptyPort }} {
+    template ANY ANY . {
+      rcode NXDOMAIN
+    }
+}`
+
+func getSelfPath() (string, error) {
+	ex, err := os.Executable()
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Dir(ex), nil
+}
+
+func lookupBinaryPath(candidatePaths []string) (string, error) {
+	for _, candidatePath := range candidatePaths {
+		path, err := exec.LookPath(candidatePath)
+		if err == nil {
+			return path, nil
+		}
+	}
+
+	return "", errors.Errorf("could not find binary in any of the following paths: %v", candidatePaths)
+}
+
+func lookupDNSServerPath(configuredPath string) (string, error) {
+	selfPath, err := getSelfPath()
+	if err != nil {
+		return "", err
+	}
+
+	cwd, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+
+	path, err := lookupBinaryPath([]string{
+		configuredPath,
+		selfPath + "/coredns",
+		cwd + "/coredns",
+	})
+	if err != nil {
+		return "", err
+	}
+
+	return path, nil
+}
+
+func New(opts *Opts) (*DNSServer, error) {
+	if _, err := lookupDNSServerPath(opts.Config.DNS.CoreDNSBinaryPath); err != nil {
+		runLog.Error(err, "could not find the DNS Server executable in your path")
+		return nil, err
+	}
+
+	return &DNSServer{opts: opts}, nil
+}
+
+func (s *DNSServer) NeedLeaderElection() bool {
+	return false
+}
+
+func (s *DNSServer) Start(stop <-chan struct{}) error {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	dnsConfig := s.opts.Config.DNS
+
+	var tmpl *template.Template
+
+	if dnsConfig.CoreDNSConfigTemplatePath != "" {
+		t, err := template.ParseFiles(dnsConfig.CoreDNSConfigTemplatePath)
+		if err != nil {
+			return err
+		}
+
+		tmpl = t
+	} else {
+		t, err := template.New("Corefile").Parse(DefaultCoreFileTemplate)
+		if err != nil {
+			return err
+		}
+
+		tmpl = t
+	}
+
+	bs := bytes.NewBuffer([]byte{})
+
+	if err := tmpl.Execute(bs, dnsConfig); err != nil {
+		return err
+	}
+
+	configFile, err := GenerateConfigFile(dnsConfig, bs.Bytes())
+	if err != nil {
+		return err
+	}
+	runLog.Info("configuration saved to a file", "file", configFile)
+
+	binaryPathConfig := dnsConfig.CoreDNSBinaryPath
+	resolvedPath, err := lookupDNSServerPath(binaryPathConfig)
+	if err != nil {
+		return err
+	}
+
+	args := []string{
+		"-conf", configFile,
+		"-q",
+	}
+
+	command := exec.CommandContext(ctx, resolvedPath, args...)
+	command.Stdout = s.opts.Stdout
+	command.Stderr = s.opts.Stderr
+
+	runLog.Info("starting DNS Server (coredns)", "args", args)
+
+	if err := command.Start(); err != nil {
+		runLog.Error(err, "the DNS Server executable was found at "+resolvedPath+" but an error occurred when executing it")
+		return err
+	}
+
+	done := make(chan error, 1)
+
+	go func() {
+		done <- command.Wait()
+	}()
+
+	select {
+	case <-stop:
+		runLog.Info("stopping DNS Server")
+		cancel()
+		return nil
+	case err := <-done:
+		if err != nil {
+			runLog.Error(err, "DNS Server terminated with an error")
+		} else {
+			runLog.Info("DNS Server terminated successfully")
+		}
+
+		if s.opts.Quit != nil {
+			close(s.opts.Quit)
+		}
+
+		return err
+	}
+}
diff --git a/mk/build.mk b/mk/build.mk
index 4c661e1c3..880c6556f 100644
--- a/mk/build.mk
+++ b/mk/build.mk
@@ -22,9 +22,15 @@ BUILD_KUMACTL_DIR := ${BUILD_ARTIFACTS_DIR}/kumactl
 export PATH := $(BUILD_KUMACTL_DIR):$(PATH)
 
 GO_BUILD := GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 go build -v $(GOFLAGS) $(LD_FLAGS)
+GO_BUILD_COREDNS := GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 go build -v
+
+COREDNS_GIT_REPOSITORY ?= git@github.com:coredns/coredns.git
+COREDNS_VERSION ?= v1.8.3
+COREDNS_TMP_DIRECTORY ?= $(BUILD_DIR)/coredns
+COREDNS_PLUGIN_CFG_PATH ?= $(TOP)/tools/builds/coredns/templates/plugin.cfg
 
 .PHONY: build
-build: build/kuma-cp build/kuma-dp build/kumactl build/kuma-prometheus-sd ## Dev: Build all binaries
+build: build/kuma-cp build/kuma-dp build/kumactl build/kuma-prometheus-sd build/coredns ## Dev: Build all binaries
 
 .PHONY: build/kuma-cp
 build/kuma-cp: ## Dev: Build `Control Plane` binary
@@ -38,6 +44,15 @@ build/kuma-dp: ## Dev: Build `kuma-dp` binary
 build/kumactl: ## Dev: Build `kumactl` binary
 	$(GO_BUILD) -o $(BUILD_ARTIFACTS_DIR)/kumactl/kumactl ./app/kumactl
 
+.PHONY: build/coredns
+build/coredns:
+	rm -rf "$(COREDNS_TMP_DIRECTORY)"
+	git clone --branch $(COREDNS_VERSION) --depth 1 $(COREDNS_GIT_REPOSITORY) $(COREDNS_TMP_DIRECTORY)
+	cp $(COREDNS_PLUGIN_CFG_PATH) $(COREDNS_TMP_DIRECTORY)
+	cd $(COREDNS_TMP_DIRECTORY) && \
+		$(GO_BUILD_COREDNS) -ldflags="-s -w -X github.com/coredns/coredns/coremain.GitCommit=$(shell git describe --dirty --always)" -o $(BUILD_ARTIFACTS_DIR)/coredns/coredns
+	rm -rf "$(COREDNS_TMP_DIRECTORY)"
+
 .PHONY: build/kuma-prometheus-sd
 build/kuma-prometheus-sd: ## Dev: Build `kuma-prometheus-sd` binary
 	$(GO_BUILD) -o ${BUILD_ARTIFACTS_DIR}/kuma-prometheus-sd/kuma-prometheus-sd ./app/kuma-prometheus-sd
@@ -58,6 +73,10 @@ build/kumactl/linux-amd64:
 build/kuma-prometheus-sd/linux-amd64:
 	GOOS=linux GOARCH=amd64 $(MAKE) build/kuma-prometheus-sd
 
+.PHONY: build/coredns/linux-amd64
+build/coredns/linux-amd64:
+	GOOS=linux GOARCH=amd64 $(MAKE) build/coredns
+
 .PHONY: clean
 clean: clean/build ## Dev: Clean
 
diff --git a/mk/docker.mk b/mk/docker.mk
index 6a26e63aa..7e8fdc6c5 100644
--- a/mk/docker.mk
+++ b/mk/docker.mk
@@ -34,7 +34,7 @@ docker/build/kuma-cp: build/artifacts-linux-amd64/kuma-cp/kuma-cp ## Dev: Build
 	docker build -t $(KUMA_CP_DOCKER_IMAGE) -f tools/releases/dockerfiles/Dockerfile.kuma-cp .
 
 .PHONY: docker/build/kuma-dp
-docker/build/kuma-dp: build/artifacts-linux-amd64/kuma-dp/kuma-dp ## Dev: Build `kuma-dp` Docker image using existing artifact
+docker/build/kuma-dp: build/artifacts-linux-amd64/kuma-dp/kuma-dp build/artifacts-linux-amd64/coredns/coredns ## Dev: Build `kuma-dp` Docker image using existing artifact
 	DOCKER_BUILDKIT=1 \
 	docker build -t $(KUMA_DP_DOCKER_IMAGE) -f tools/releases/dockerfiles/Dockerfile.kuma-dp .
 
@@ -64,7 +64,7 @@ docker/build/kuma-universal: build/artifacts-linux-amd64/kuma-cp/kuma-cp build/a
 image/kuma-cp: build/kuma-cp/linux-amd64 docker/build/kuma-cp ## Dev: Rebuild `kuma-cp` Docker image
 
 .PHONY: image/kuma-dp
-image/kuma-dp: build/kuma-dp/linux-amd64 docker/build/kuma-dp ## Dev: Rebuild `kuma-dp` Docker image
+image/kuma-dp: build/kuma-dp/linux-amd64 build/coredns/linux-amd64 docker/build/kuma-dp ## Dev: Rebuild `kuma-dp` Docker image
 
 .PHONY: image/kumactl
 image/kumactl: build/kumactl/linux-amd64 docker/build/kumactl ## Dev: Rebuild `kumactl` Docker image
diff --git a/pkg/config/app/kuma-dp/config.go b/pkg/config/app/kuma-dp/config.go
index 4b74a625c..efb0ee178 100644
--- a/pkg/config/app/kuma-dp/config.go
+++ b/pkg/config/app/kuma-dp/config.go
@@ -37,6 +37,8 @@ func DefaultConfig() Config {
 			CoreDNSEmptyPort:          15055,
 			CoreDNSBinaryPath:         "coredns",
 			CoreDNSConfigTemplatePath: "",
+			ConfigDir:                 "", // if left empty, a temporary directory will be generated automatically
+			PrometheusPort:            19153,
 		},
 	}
 }
@@ -246,6 +248,10 @@ type DNS struct {
 	CoreDNSBinaryPath string `yaml:"coreDnsBinaryPath,omitempty" envconfig:"kuma_dns_core_dns_binary_path"`
 	// CoreDNSConfigTemplatePath defines a path to a CoreDNS config template.
 	CoreDNSConfigTemplatePath string `yaml:"coreDnsConfigTemplatePath,omitempty" envconfig:"kuma_dns_core_dns_config_template_path"`
+	// Dir to store auto-generated DNS Server config in.
+	ConfigDir string `yaml:"configDir,omitempty" envconfig:"kuma_dns_config_dir"`
+	// Port where Prometheus stats will be exposed for the DNS Server
+	PrometheusPort uint32 `yaml:"prometheusPort,omitempty" envconfig:"kuma_dns_prometheus_port"`
 }
 
 func (d *DNS) Sanitize() {
@@ -264,6 +270,9 @@ func (d *DNS) Validate() error {
 	if d.EnvoyDNSPort > 65353 {
 		return errors.New(".EnvoyDNSPort has to be in [0, 65353] range")
 	}
+	if d.PrometheusPort > 65353 {
+		return errors.New(".PrometheusPort has to be in [0, 65353] range")
+	}
 	if d.CoreDNSBinaryPath == "" {
 		return errors.New(".CoreDNSBinaryPath cannot be empty")
 	}
diff --git a/tools/builds/coredns/templates/plugin.cfg b/tools/builds/coredns/templates/plugin.cfg
new file mode 100644
index 000000000..ec2bff38b
--- /dev/null
+++ b/tools/builds/coredns/templates/plugin.cfg
@@ -0,0 +1,6 @@
+prometheus:metrics
+errors:errors
+log:log
+template:template
+alternate:github.com/coredns/alternate
+forward:forward
diff --git a/tools/releases/distros.sh b/tools/releases/distros.sh
index 0ab8fffab..402e622e5 100755
--- a/tools/releases/distros.sh
+++ b/tools/releases/distros.sh
@@ -82,6 +82,7 @@ function create_tarball {
   cp -p build/artifacts-$system-$arch/kuma-cp/kuma-cp $kuma_dir/bin
   cp -p build/artifacts-$system-$arch/kuma-dp/kuma-dp $kuma_dir/bin
   cp -p build/artifacts-$system-$arch/kumactl/kumactl $kuma_dir/bin
+  cp -p build/artifacts-$system-$arch/coredns/coredns $kuma_dir/bin
   cp -p build/artifacts-$system-$arch/kuma-prometheus-sd/kuma-prometheus-sd $kuma_dir/bin
   cp -p $KUMA_CONFIG_PATH $kuma_dir/conf/kuma-cp.conf.yml
 
@@ -203,4 +204,3 @@ function main {
 
 
 main $@
-
diff --git a/tools/releases/dockerfiles/Dockerfile.kuma-dp b/tools/releases/dockerfiles/Dockerfile.kuma-dp
index 375c474cd..65edba2aa 100644
--- a/tools/releases/dockerfiles/Dockerfile.kuma-dp
+++ b/tools/releases/dockerfiles/Dockerfile.kuma-dp
@@ -2,6 +2,7 @@
 FROM envoyproxy/envoy-alpine:v1.17.1
 
 ADD $KUMA_ROOT/build/artifacts-linux-amd64/kuma-dp/kuma-dp /usr/bin
+ADD $KUMA_ROOT/build/artifacts-linux-amd64/coredns/coredns /usr/bin
 
 RUN mkdir /kuma
 COPY $KUMA_ROOT/tools/releases/templates/LICENSE /kuma
diff --git a/tools/releases/dockerfiles/Dockerfile.kuma-dp.dockerignore b/tools/releases/dockerfiles/Dockerfile.kuma-dp.dockerignore
index ac2cfdb1a..7e3458e0c 100644
--- a/tools/releases/dockerfiles/Dockerfile.kuma-dp.dockerignore
+++ b/tools/releases/dockerfiles/Dockerfile.kuma-dp.dockerignore
@@ -1,5 +1,6 @@
 *
 !build/artifacts-linux-amd64/kuma-dp/kuma-dp
+!build/artifacts-linux-amd64/coredns/coredns
 !tools/releases/templates/LICENSE
 !tools/releases/templates/NOTICE
 !tools/releases/templates/README


Test patch:
diff --git a/app/kuma-dp/pkg/dataplane/dnsserver/config_file_test.go b/app/kuma-dp/pkg/dataplane/dnsserver/config_file_test.go
new file mode 100644
index 000000000..e823497b8
--- /dev/null
+++ b/app/kuma-dp/pkg/dataplane/dnsserver/config_file_test.go
@@ -0,0 +1,60 @@
+package dnsserver
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
+)
+
+var _ = Describe("Config File", func() {
+	Describe("GenerateConfigFile(..)", func() {
+
+		var configDir string
+
+		BeforeEach(func() {
+			var err error
+			configDir, err = ioutil.TempDir("", "")
+			Expect(err).ToNot(HaveOccurred())
+		})
+		AfterEach(func() {
+			if configDir != "" {
+				// when
+				err := os.RemoveAll(configDir)
+				// then
+				Expect(err).ToNot(HaveOccurred())
+			}
+		})
+
+		It("should create DNS Server config file on disk", func() {
+			// given
+			config := `. {
+	errors
+}`
+			// and
+			dnsConfig := kuma_dp.DNS{
+				ConfigDir: configDir,
+			}
+
+			// when
+			filename, err := GenerateConfigFile(dnsConfig, []byte(config))
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(filename).To(Equal(filepath.Join(configDir, "Corefile")))
+
+			// when
+			actual, err := ioutil.ReadFile(filename)
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(actual).To(Equal([]byte(`. {
+	errors
+}`)))
+		})
+	})
+})
diff --git a/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver_suite_test.go b/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver_suite_test.go
new file mode 100644
index 000000000..0749ea444
--- /dev/null
+++ b/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver_suite_test.go
@@ -0,0 +1,13 @@
+package dnsserver
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestEnvoy(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "DNS Server Suite")
+}
diff --git a/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver_test.go b/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver_test.go
new file mode 100644
index 000000000..6c985d109
--- /dev/null
+++ b/app/kuma-dp/pkg/dataplane/dnsserver/dnsserver_test.go
@@ -0,0 +1,201 @@
+// +build !windows
+
+package dnsserver
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
+)
+
+var _ = Describe("DNS Server", func() {
+
+	var configDir string
+
+	BeforeEach(func() {
+		var err error
+		configDir, err = ioutil.TempDir("", "")
+		Expect(err).ToNot(HaveOccurred())
+	})
+	AfterEach(func() {
+		if configDir != "" {
+			// when
+			err := os.RemoveAll(configDir)
+			// then
+			Expect(err).ToNot(HaveOccurred())
+		}
+	})
+
+	var outReader *os.File
+	var outWriter, errWriter *os.File
+
+	BeforeEach(func() {
+		var err error
+		outReader, outWriter, err = os.Pipe()
+		Expect(err).ToNot(HaveOccurred())
+		_, errWriter, err = os.Pipe()
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	var stopCh chan struct{}
+	var errCh chan error
+
+	BeforeEach(func() {
+		stopCh = make(chan struct{})
+		errCh = make(chan error)
+	})
+
+	Describe("Run(..)", func() {
+		It("should generate bootstrap config file and start Envoy", func(done Done) {
+			// given
+			cfg := kuma_dp.Config{
+				DNS: kuma_dp.DNS{
+					Enabled:           true,
+					CoreDNSPort:       16001,
+					CoreDNSEmptyPort:  16002,
+					EnvoyDNSPort:      16002,
+					PrometheusPort:    16003,
+					CoreDNSBinaryPath: filepath.Join("testdata", "binary-mock.exit-0.sh"),
+					ConfigDir:         configDir,
+				},
+			}
+
+			expectedConfigFile := filepath.Join(configDir, "Corefile")
+
+			By("starting a mock DNS Server")
+			// when
+			dnsServer, err := New(&Opts{
+				Config: cfg,
+				Stdout: outWriter,
+				Stderr: errWriter,
+			})
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			go func() {
+				errCh <- dnsServer.Start(stopCh)
+			}()
+
+			By("waiting for mock DNS Server to complete")
+			// then
+			Eventually(func() bool {
+				select {
+				case err := <-errCh:
+					Expect(err).ToNot(HaveOccurred())
+					return true
+				default:
+					return false
+				}
+			}, "5s", "10ms").Should(BeTrue())
+
+			By("closing the write side of the pipe")
+			// when
+			err = outWriter.Close()
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			By("verifying the output of mock DNS Server")
+			// when
+			var buf bytes.Buffer
+			_, err = buf.ReadFrom(outReader)
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(strings.TrimSpace(buf.String())).To(Equal(fmt.Sprintf("-conf %s -q", expectedConfigFile)))
+
+			By("verifying the contents DNS Server config file")
+			// when
+			actual, err := ioutil.ReadFile(expectedConfigFile)
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(string(actual)).To(Equal(`.:16001 {
+    forward . 127.0.0.1:16002
+    alternate NXDOMAIN,SERVFAIL,REFUSED . /etc/resolv.conf
+    prometheus localhost:16003
+    errors
+}
+
+.:16002 {
+    template ANY ANY . {
+      rcode NXDOMAIN
+    }
+}`))
+			// complete
+			close(done)
+		}, 10)
+
+		It("should return an error if DNS Server crashes", func(done Done) {
+			// given
+			cfg := kuma_dp.Config{
+				DNS: kuma_dp.DNS{
+					Enabled:           true,
+					CoreDNSBinaryPath: filepath.Join("testdata", "binary-mock.exit-1.sh"),
+					ConfigDir:         configDir,
+				},
+			}
+
+			By("starting a mock DNS Server")
+			// when
+			dnsServer, err := New(&Opts{
+				Config: cfg,
+				Stdout: &bytes.Buffer{},
+				Stderr: &bytes.Buffer{},
+			})
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			go func() {
+				errCh <- dnsServer.Start(stopCh)
+			}()
+
+			By("waiting for mock DNS Server to complete")
+			// when
+			err = <-errCh
+			// then
+			Expect(err).To(BeAssignableToTypeOf(&exec.ExitError{}))
+
+			// when
+			exitError := err.(*exec.ExitError)
+			// then
+			Expect(exitError.ProcessState.ExitCode()).To(Equal(1))
+
+			// complete
+			close(done)
+		}, 10)
+
+		It("should return an error if DNS Server binary path is not found", func(done Done) {
+			// given
+			cfg := kuma_dp.Config{
+				DNS: kuma_dp.DNS{
+					Enabled:           true,
+					CoreDNSBinaryPath: filepath.Join("testdata"),
+					ConfigDir:         configDir,
+				},
+			}
+
+			By("starting a mock DNS Server")
+			// when
+			dnsServer, err := New(&Opts{
+				Config: cfg,
+				Stdout: &bytes.Buffer{},
+				Stderr: &bytes.Buffer{},
+			})
+			// then
+			Expect(dnsServer).To(BeNil())
+			// and
+			Expect(err.Error()).To(ContainSubstring("could not find binary in any of the following paths"))
+
+			// complete
+			close(done)
+		}, 10)
+	})
+})
diff --git a/app/kuma-dp/pkg/dataplane/dnsserver/testdata/binary-mock.exit-0.sh b/app/kuma-dp/pkg/dataplane/dnsserver/testdata/binary-mock.exit-0.sh
new file mode 100755
index 000000000..238f1bd8d
--- /dev/null
+++ b/app/kuma-dp/pkg/dataplane/dnsserver/testdata/binary-mock.exit-0.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+# print arguments to verify in the test
+echo $@
diff --git a/app/kuma-dp/pkg/dataplane/dnsserver/testdata/binary-mock.exit-1.sh b/app/kuma-dp/pkg/dataplane/dnsserver/testdata/binary-mock.exit-1.sh
new file mode 100755
index 000000000..12d9dfb67
--- /dev/null
+++ b/app/kuma-dp/pkg/dataplane/dnsserver/testdata/binary-mock.exit-1.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+>&2 echo Binary crashed
+
+# simulate crash of the process
+exit 1
diff --git a/pkg/config/app/kuma-dp/config_test.go b/pkg/config/app/kuma-dp/config_test.go
index 856692ece..fbe6970e2 100644
--- a/pkg/config/app/kuma-dp/config_test.go
+++ b/pkg/config/app/kuma-dp/config_test.go
@@ -69,6 +69,8 @@ var _ = Describe("Config", func() {
 				"KUMA_DNS_ENVOY_DNS_PORT":                                "5302",
 				"KUMA_DNS_CORE_DNS_BINARY_PATH":                          "/tmp/coredns",
 				"KUMA_DNS_CORE_DNS_CONFIG_TEMPLATE_PATH":                 "/tmp/Corefile",
+				"KUMA_DNS_CONFIG_DIR":                                    "/var/run/dnsserver",
+				"KUMA_DNS_PROMETHEUS_PORT":                               "6001",
 			}
 			for key, value := range env {
 				os.Setenv(key, value)
@@ -100,6 +102,8 @@ var _ = Describe("Config", func() {
 			Expect(cfg.DNS.EnvoyDNSPort).To(Equal(uint32(5302)))
 			Expect(cfg.DNS.CoreDNSBinaryPath).To(Equal("/tmp/coredns"))
 			Expect(cfg.DNS.CoreDNSConfigTemplatePath).To(Equal("/tmp/Corefile"))
+			Expect(cfg.DNS.ConfigDir).To(Equal("/var/run/dnsserver"))
+			Expect(cfg.DNS.PrometheusPort).To(Equal(uint32(6001)))
 		})
 	})
 
diff --git a/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml b/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
index 1eb97f74f..6888e5001 100644
--- a/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
+++ b/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
@@ -15,3 +15,4 @@ dns:
   coreDnsEmptyPort: 15055
   coreDnsPort: 15053
   envoyDnsPort: 15054
+  prometheusPort: 19153