Repository: kumahq/kuma
Pull request #1420 (2021-01-14 11:09:53)

feat(kuma-dp) override bootstrap version (bp #1412)
This is an automatic backport of pull request #1412 done by [Mergify](https://mergify.io).

Cherry-pick of 118e86f43b62d4e339e0fceadc0588c5ad748a11 has failed:
```
On branch mergify/bp/release-1.0/pr-1412
Your branch is up to date with 'origin/release-1.0'.

You are currently cherry-picking commit 118e86f43.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	modified:   app/kuma-dp/cmd/run.go
	modified:   app/kuma-dp/cmd/run_test.go
	modified:   app/kuma-dp/pkg/dataplane/envoy/envoy.go
	modified:   app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
	modified:   app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
	modified:   app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
	modified:   pkg/config/app/kuma-dp/config.go
	modified:   pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
	modified:   pkg/sds/server/v2/server_test.go
	modified:   pkg/xds/bootstrap/generator_test.go
	modified:   pkg/xds/bootstrap/handler.go
	modified:   pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
	modified:   pkg/xds/bootstrap/types/bootstrap_request.go

Unmerged paths:
  (use "git add/rm <file>..." as appropriate to mark resolution)
	deleted by us:   pkg/sds/server/v3/server_test.go
	both modified:   pkg/xds/bootstrap/generator.go

```

To fixup this pull request, you can check out it locally. See documentation: https://help.github.com/articles/checking-out-pull-requests-locally/

---


<details>
<summary>Mergify commands and options</summary>

<br />

More conditions and actions can be found in the [documentation](https://docs.mergify.io/).

You can also trigger Mergify actions by commenting on this pull request:

- `@Mergifyio refresh` will re-evaluate the rules
- `@Mergifyio rebase` will rebase this PR on its base branch
- `@Mergifyio update` will merge the base branch into this PR
- `@Mergifyio backport <destination>` will backport this PR on `<destination>` branch

Additionally, on Mergify [dashboard](https://dashboard.mergify.io/) you can:

- look at your merge queues
- generate the Mergify configuration with the config editor.

Finally, you can contact us on https://mergify.io/
</details>



Patch:
diff --git a/app/kuma-dp/cmd/run.go b/app/kuma-dp/cmd/run.go
index be4223cda..3a0ab8bae 100644
--- a/app/kuma-dp/cmd/run.go
+++ b/app/kuma-dp/cmd/run.go
@@ -145,6 +145,7 @@ func newRunCmd() *cobra.Command {
 	cmd.PersistentFlags().StringVar(&cfg.ControlPlane.URL, "cp-address", cfg.ControlPlane.URL, "URL of the Control Plane Dataplane Server. Example: https://localhost:5678")
 	cmd.PersistentFlags().StringVar(&cfg.ControlPlane.CaCertFile, "ca-cert-file", cfg.ControlPlane.CaCert, "Path to CA cert by which connection to the Control Plane will be verified if HTTPS is used")
 	cmd.PersistentFlags().StringVar(&cfg.DataplaneRuntime.BinaryPath, "binary-path", cfg.DataplaneRuntime.BinaryPath, "Binary path of Envoy executable")
+	cmd.PersistentFlags().StringVar(&cfg.Dataplane.BootstrapVersion, "bootstrap-version", cfg.Dataplane.BootstrapVersion, "Bootstrap version (and API version) of xDS config. If empty, default version defined in Kuma CP will be used. (ex. '2', '3')")
 	cmd.PersistentFlags().StringVar(&cfg.DataplaneRuntime.ConfigDir, "config-dir", cfg.DataplaneRuntime.ConfigDir, "Directory in which Envoy config will be generated")
 	cmd.PersistentFlags().StringVar(&cfg.DataplaneRuntime.TokenPath, "dataplane-token-file", cfg.DataplaneRuntime.TokenPath, "Path to a file with dataplane token (use 'kumactl generate dataplane-token' to get one)")
 	cmd.PersistentFlags().StringVar(&cfg.DataplaneRuntime.Token, "dataplane-token", cfg.DataplaneRuntime.Token, "Dataplane Token")
diff --git a/app/kuma-dp/pkg/dataplane/envoy/envoy.go b/app/kuma-dp/pkg/dataplane/envoy/envoy.go
index 1bdc83a0e..5b3c731c9 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/envoy.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/envoy.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 
 	"github.com/pkg/errors"
 
@@ -24,7 +25,7 @@ var (
 	runLog = core.Log.WithName("kuma-dp").WithName("run").WithName("envoy")
 )
 
-type BootstrapConfigFactoryFunc func(url string, cfg kuma_dp.Config, dp *rest.Resource, ev EnvoyVersion) ([]byte, error)
+type BootstrapConfigFactoryFunc func(url string, cfg kuma_dp.Config, dp *rest.Resource, bootstrapVersion types.BootstrapVersion, ev EnvoyVersion) ([]byte, types.BootstrapVersion, error)
 
 type Opts struct {
 	Config    kuma_dp.Config
@@ -107,7 +108,7 @@ func (e *Envoy) Start(stop <-chan struct{}) error {
 	}
 	runLog.Info("fetched Envoy version", "version", envoyVersion)
 	runLog.Info("generating bootstrap configuration")
-	bootstrapConfig, err := e.opts.Generator(e.opts.Config.ControlPlane.URL, e.opts.Config, e.opts.Dataplane, *envoyVersion)
+	bootstrapConfig, version, err := e.opts.Generator(e.opts.Config.ControlPlane.URL, e.opts.Config, e.opts.Dataplane, types.BootstrapVersion(e.opts.Config.Dataplane.BootstrapVersion), *envoyVersion)
 	if err != nil {
 		return errors.Errorf("Failed to generate Envoy bootstrap config. %v", err)
 	}
@@ -140,6 +141,9 @@ func (e *Envoy) Start(stop <-chan struct{}) error {
 		// so, let's turn it off to simplify getting started experience.
 		"--disable-hot-restart",
 	}
+	if version != "" { // version is always send by Kuma CP, but we check empty for backwards compatibility reasons (new Kuma DP connects to old Kuma CP)
+		args = append(args, "--bootstrap-version", string(version))
+	}
 	command := exec.CommandContext(ctx, resolvedPath, args...)
 	command.Stdout = e.opts.Stdout
 	command.Stderr = e.opts.Stderr
diff --git a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
index 45a7aba44..460ee5c30 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
@@ -43,17 +43,17 @@ func IsInvalidRequestErr(err error) bool {
 	return strings.HasPrefix(err.Error(), "Invalid request: ")
 }
 
-func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config, dp *rest_types.Resource, ev EnvoyVersion) ([]byte, error) {
+func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config, dp *rest_types.Resource, bootstrapVersion types.BootstrapVersion, ev EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
 	bootstrapUrl, err := net_url.Parse(url)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
 	if bootstrapUrl.Scheme == "https" {
 		if cfg.ControlPlane.CaCert != "" {
 			certPool := x509.NewCertPool()
 			if ok := certPool.AppendCertsFromPEM([]byte(cfg.ControlPlane.CaCert)); !ok {
-				return nil, errors.New("could not add certificate")
+				return nil, "", errors.New("could not add certificate")
 			}
 			b.client.Transport = &http.Transport{
 				TLSClientConfig: &tls.Config{
@@ -67,19 +67,21 @@ func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config, dp *rest_type
 
 	backoff, err := retry.NewConstant(cfg.ControlPlane.Retry.Backoff)
 	if err != nil {
-		return nil, errors.Wrap(err, "could not create retry backoff")
+		return nil, "", errors.Wrap(err, "could not create retry backoff")
 	}
 	backoff = retry.WithMaxDuration(cfg.ControlPlane.Retry.MaxDuration, backoff)
 	var respBytes []byte
+	var version types.BootstrapVersion
 	err = retry.Do(context.Background(), backoff, func(ctx context.Context) error {
 		log.Info("trying to fetch bootstrap configuration from the Control Plane")
-		respBytes, err = b.requestForBootstrap(bootstrapUrl, cfg, dp, ev)
+		respBytes, version, err = b.requestForBootstrap(bootstrapUrl, cfg, dp, bootstrapVersion, ev)
 		if err == nil {
 			return nil
 		}
 		if IsInvalidRequestErr(err) { // there is no point in retrying invalid request
 			return err
 		}
+
 		switch err {
 		case DpNotFoundErr:
 			log.Info("Dataplane entity is not yet found in the Control Plane. If you are running on Kubernetes, CP is most likely still in the process of converting Pod to Dataplane. Retrying.", "backoff", cfg.ControlPlane.Retry.Backoff)
@@ -89,18 +91,18 @@ func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config, dp *rest_type
 		return retry.RetryableError(err)
 	})
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
-	return respBytes, nil
+	return respBytes, version, nil
 }
 
-func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Config, dp *rest_types.Resource, ev EnvoyVersion) ([]byte, error) {
+func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Config, dp *rest_types.Resource, bootstrapVersion types.BootstrapVersion, ev EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
 	url.Path = "/bootstrap"
 	var dataplaneResource string
 	if dp != nil {
 		dpJSON, err := json.Marshal(dp)
 		if err != nil {
-			return nil, err
+			return nil, "", err
 		}
 		dataplaneResource = string(dpJSON)
 	}
@@ -112,6 +114,7 @@ func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Conf
 		AdminPort:          cfg.Dataplane.AdminPort.Lowest(),
 		DataplaneTokenPath: cfg.DataplaneRuntime.TokenPath,
 		DataplaneResource:  dataplaneResource,
+		BootstrapVersion:   bootstrapVersion,
 		Version: types.Version{
 			KumaDp: types.KumaDpVersion{
 				Version:   kuma_version.Build.Version,
@@ -127,32 +130,32 @@ func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Conf
 	}
 	jsonBytes, err := json.Marshal(request)
 	if err != nil {
-		return nil, errors.Wrap(err, "could not marshal request to json")
+		return nil, "", errors.Wrap(err, "could not marshal request to json")
 	}
 	resp, err := b.client.Post(url.String(), "application/json", bytes.NewReader(jsonBytes))
 	if err != nil {
-		return nil, errors.Wrap(err, "request to bootstrap server failed")
+		return nil, "", errors.Wrap(err, "request to bootstrap server failed")
 	}
 	defer resp.Body.Close()
 	if resp.StatusCode != http.StatusOK {
 		bodyBytes, err := ioutil.ReadAll(resp.Body)
 		if err != nil {
-			return nil, errors.Wrapf(err, "Unable to read the response with status code: %d. Make sure you are using https URL", resp.StatusCode)
+			return nil, "", errors.Wrapf(err, "Unable to read the response with status code: %d. Make sure you are using https URL", resp.StatusCode)
 		}
 		if resp.StatusCode == http.StatusNotFound && len(bodyBytes) == 0 {
-			return nil, DpNotFoundErr
+			return nil, "", DpNotFoundErr
 		}
 		if resp.StatusCode == http.StatusNotFound && string(bodyBytes) == "404: Page Not Found" { // response body of Go HTTP Server when hit for invalid endpoint
-			return nil, errors.New("There is no /bootstrap endpoint for provided CP address. Double check if the address passed to the CP has a DP Server port (5678 by default), not HTTP API (5681 by default)")
+			return nil, "", errors.New("There is no /bootstrap endpoint for provided CP address. Double check if the address passed to the CP has a DP Server port (5678 by default), not HTTP API (5681 by default)")
 		}
 		if resp.StatusCode/100 == 4 {
-			return nil, InvalidRequestErr(string(bodyBytes))
+			return nil, "", InvalidRequestErr(string(bodyBytes))
 		}
-		return nil, errors.Errorf("unexpected status code: %d", resp.StatusCode)
+		return nil, "", errors.Errorf("unexpected status code: %d", resp.StatusCode)
 	}
 	respBytes, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
-		return nil, errors.Wrap(err, "could not read the body of the response")
+		return nil, "", errors.Wrap(err, "could not read the body of the response")
 	}
-	return respBytes, nil
+	return respBytes, types.BootstrapVersion(resp.Header.Get(types.BootstrapVersionHeader)), nil
 }
diff --git a/pkg/config/app/kuma-dp/config.go b/pkg/config/app/kuma-dp/config.go
index c4fa720bf..3737b23a9 100644
--- a/pkg/config/app/kuma-dp/config.go
+++ b/pkg/config/app/kuma-dp/config.go
@@ -102,6 +102,9 @@ type Dataplane struct {
 	AdminPort config_types.PortRange `yaml:"adminPort,omitempty" envconfig:"kuma_dataplane_admin_port"`
 	// Drain time for listeners.
 	DrainTime time.Duration `yaml:"drainTime,omitempty" envconfig:"kuma_dataplane_drain_time"`
+	// BootstrapVersion defines bootstrap version (and API version) of xDS config.
+	// If empty, default version defined in Kuma CP will be used.
+	BootstrapVersion string `yaml:"bootstrapVersion" envconfig:"kuma_dataplane_bootstrap_version"`
 }
 
 // DataplaneRuntime defines the context in which dataplane (Envoy) runs.
diff --git a/pkg/xds/bootstrap/generator.go b/pkg/xds/bootstrap/generator.go
index 8db9cfdc0..8ad0ed8f7 100644
--- a/pkg/xds/bootstrap/generator.go
+++ b/pkg/xds/bootstrap/generator.go
@@ -34,7 +34,7 @@ import (
 )
 
 type BootstrapGenerator interface {
-	Generate(ctx context.Context, request types.BootstrapRequest) (proto.Message, error)
+	Generate(ctx context.Context, request types.BootstrapRequest) (proto.Message, types.BootstrapVersion, error)
 }
 
 func NewDefaultBootstrapGenerator(
@@ -67,19 +67,19 @@ type bootstrapGenerator struct {
 	hostsAndIps   SANSet
 }
 
-func (b *bootstrapGenerator) Generate(ctx context.Context, request types.BootstrapRequest) (proto.Message, error) {
+func (b *bootstrapGenerator) Generate(ctx context.Context, request types.BootstrapRequest) (proto.Message, types.BootstrapVersion, error) {
 	if err := b.validateRequest(request); err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
 	proxyId, err := core_xds.BuildProxyId(request.Mesh, request.Name)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
 	dataplane, err := b.dataplaneFor(ctx, request, proxyId)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
 	return b.generateFor(*proxyId, dataplane, request)
@@ -87,6 +87,8 @@ func (b *bootstrapGenerator) Generate(ctx context.Context, request types.Bootstr
 
 var DpTokenRequired = errors.New("Dataplane Token is required. Generate token using 'kumactl generate dataplane-token > /path/file' and provide it via --dataplane-token-file=/path/file argument to Kuma DP")
 
+var InvalidBootstrapVersion = errors.New(`Invalid BootstrapVersion. Available values are: "2"`)
+
 func SANMismatchErr(host string, sans []string) error {
 	return errors.Errorf("A data plane proxy is trying to connect to the control plane using %q address, but the certificate in the control plane has the following SANs %q. "+
 		"Either change the --cp-address in kuma-dp to one of those or execute the following steps:\n"+
@@ -161,7 +163,7 @@ func (b *bootstrapGenerator) validateMeshExist(ctx context.Context, mesh string)
 	return nil
 }
 
-func (b *bootstrapGenerator) generateFor(proxyId core_xds.ProxyId, dataplane *core_mesh.DataplaneResource, request types.BootstrapRequest) (proto.Message, error) {
+func (b *bootstrapGenerator) generateFor(proxyId core_xds.ProxyId, dataplane *core_mesh.DataplaneResource, request types.BootstrapRequest) (proto.Message, types.BootstrapVersion, error) {
 	// if dataplane has no service - fill this with placeholder. Otherwise take the first service
 	service := dataplane.Spec.GetIdentifyingService()
 
@@ -171,14 +173,14 @@ func (b *bootstrapGenerator) generateFor(proxyId core_xds.ProxyId, dataplane *co
 	}
 
 	if err := b.verifyAdminPort(adminPort, dataplane); err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
 	var certBytes = ""
 	if b.xdsCertFile != "" {
 		cert, err := ioutil.ReadFile(b.xdsCertFile)
 		if err != nil {
-			return nil, err
+			return nil, "", err
 		}
 		certBytes = base64.StdEncoding.EncodeToString(cert)
 	}
@@ -204,7 +206,7 @@ func (b *bootstrapGenerator) generateFor(proxyId core_xds.ProxyId, dataplane *co
 		EnvoyBuild:         request.Version.Envoy.Build,
 	}
 	log.WithValues("params", params).Info("Generating bootstrap config")
-	return b.configForParameters(params)
+	return b.configForParameters(params, request.BootstrapVersion)
 }
 
 func (b *bootstrapGenerator) xdsHost(request types.BootstrapRequest) string {
@@ -227,12 +229,19 @@ func (b *bootstrapGenerator) verifyAdminPort(adminPort uint32, dataplane *core_m
 	return nil
 }
 
-func (b *bootstrapGenerator) configForParameters(params configParameters) (proto.Message, error) {
-	switch b.config.APIVersion {
-	case envoy_common.APIV2:
-		return b.configForParametersV2(params)
+func (b *bootstrapGenerator) configForParameters(params configParameters, version types.BootstrapVersion) (proto.Message, types.BootstrapVersion, error) {
+	switch {
+	// V2
+	case version == types.BootstrapV2:
+		fallthrough
+	case version == "" && b.config.APIVersion == envoy_common.APIV2: // if client did not overridden bootstrap version, provide bootstrap based on Kuma CP config
+		cfg, err := b.configForParametersV2(params)
+		if err != nil {
+			return nil, "", err
+		}
+		return cfg, types.BootstrapV2, nil
 	default:
-		return nil, errors.Errorf("invalid API Version %s", b.config.APIVersion)
+		return nil, "", InvalidBootstrapVersion
 	}
 }
 
diff --git a/pkg/xds/bootstrap/handler.go b/pkg/xds/bootstrap/handler.go
index 02300de70..b7c4a1887 100644
--- a/pkg/xds/bootstrap/handler.go
+++ b/pkg/xds/bootstrap/handler.go
@@ -49,7 +49,7 @@ func (b *BootstrapHandler) Handle(resp http.ResponseWriter, req *http.Request) {
 	reqParams.Host = hostname
 	logger := log.WithValues("params", reqParams)
 
-	config, err := b.Generator.Generate(req.Context(), reqParams)
+	config, version, err := b.Generator.Generate(req.Context(), reqParams)
 	if err != nil {
 		handleError(resp, err, logger)
 		return
@@ -64,6 +64,7 @@ func (b *BootstrapHandler) Handle(resp http.ResponseWriter, req *http.Request) {
 
 	resp.WriteHeader(http.StatusOK)
 	resp.Header().Set("content-type", "text/x-yaml")
+	resp.Header().Set(types.BootstrapVersionHeader, string(version))
 	_, err = resp.Write(bytes)
 	if err != nil {
 		logger.Error(err, "Error while writing the response")
@@ -80,6 +81,13 @@ func handleError(resp http.ResponseWriter, err error, logger logr.Logger) {
 		}
 		return
 	}
+	if err == InvalidBootstrapVersion {
+		resp.WriteHeader(http.StatusBadRequest)
+		if _, err := resp.Write([]byte(err.Error())); err != nil {
+			logger.Error(err, "Error while writing the response")
+		}
+		return
+	}
 	if ISSANMismatchErr(err) {
 		resp.WriteHeader(http.StatusBadRequest)
 		_, err = resp.Write([]byte(err.Error()))
diff --git a/pkg/xds/bootstrap/types/bootstrap_request.go b/pkg/xds/bootstrap/types/bootstrap_request.go
index c3d326a18..66fa2239d 100644
--- a/pkg/xds/bootstrap/types/bootstrap_request.go
+++ b/pkg/xds/bootstrap/types/bootstrap_request.go
@@ -1,5 +1,17 @@
 package types
 
+type BootstrapVersion string
+
+const (
+	BootstrapV2 BootstrapVersion = "2"
+	BootstrapV3 BootstrapVersion = "3"
+)
+
+// Bootstrap is sent to a client (Kuma DP) by putting YAML into a response body.
+// This YAML has no information about Bootstrap version therefore we put extra header with a version
+// Value of this header is then used in CLI arg --bootstrap-version when Envoy is run
+const BootstrapVersionHeader = "kuma-bootstrap-version"
+
 type BootstrapRequest struct {
 	Mesh               string  `json:"mesh"`
 	Name               string  `json:"name"`
@@ -8,6 +20,8 @@ type BootstrapRequest struct {
 	DataplaneResource  string  `json:"dataplaneResource,omitempty"`
 	Host               string  `json:"-"`
 	Version            Version `json:"version"`
+	// BootstrapVersion is an optional version to override the control plane's default setting
+	BootstrapVersion BootstrapVersion `json:"bootstrapVersion"`
 }
 
 type Version struct {


Test patch:
diff --git a/app/kuma-dp/cmd/run_test.go b/app/kuma-dp/cmd/run_test.go
index b868df163..c5dcbd687 100644
--- a/app/kuma-dp/cmd/run_test.go
+++ b/app/kuma-dp/cmd/run_test.go
@@ -14,6 +14,7 @@ import (
 	"syscall"
 
 	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
@@ -33,10 +34,10 @@ var _ = Describe("run", func() {
 	BeforeEach(func() {
 		backupSetupSignalHandler = core.SetupSignalHandler
 		backupBootstrapGenerator = bootstrapGenerator
-		bootstrapGenerator = func(_ string, cfg kumadp.Config, _ *rest.Resource, version envoy.EnvoyVersion) ([]byte, error) {
+		bootstrapGenerator = func(_ string, cfg kumadp.Config, _ *rest.Resource, _ types.BootstrapVersion, version envoy.EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
 			respBytes, err := ioutil.ReadFile(filepath.Join("testdata", "bootstrap-config.golden.yaml"))
 			Expect(err).ToNot(HaveOccurred())
-			return respBytes, nil
+			return respBytes, "", nil
 		}
 	})
 	AfterEach(func() {
diff --git a/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go b/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
index 76cd3f8df..93d792e57 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
@@ -17,6 +17,7 @@ import (
 
 	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
 	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 )
 
 var _ = Describe("Envoy", func() {
@@ -68,9 +69,9 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, EnvoyVersion) ([]byte, error) {
+			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, types.BootstrapVersion, EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
 				return []byte(`node:
-  id: example`), nil
+  id: example`), types.BootstrapV2, nil
 			}
 			expectedConfigFile := filepath.Join(configDir, "bootstrap.yaml")
 
@@ -112,7 +113,7 @@ var _ = Describe("Envoy", func() {
 			// then
 			Expect(err).ToNot(HaveOccurred())
 			// and
-			Expect(strings.TrimSpace(buf.String())).To(Equal(fmt.Sprintf("-c %s --drain-time-s 15 --disable-hot-restart", expectedConfigFile)))
+			Expect(strings.TrimSpace(buf.String())).To(Equal(fmt.Sprintf("-c %s --drain-time-s 15 --disable-hot-restart --bootstrap-version 2", expectedConfigFile)))
 
 			By("verifying the contents Envoy config file")
 			// when
@@ -136,8 +137,8 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, EnvoyVersion) ([]byte, error) {
-				return nil, nil
+			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, types.BootstrapVersion, EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
+				return nil, "", nil
 			}
 
 			By("starting a mock dataplane")
@@ -178,8 +179,8 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, EnvoyVersion) ([]byte, error) {
-				return nil, nil
+			sampleConfig := func(string, kuma_dp.Config, *rest.Resource, types.BootstrapVersion, EnvoyVersion) ([]byte, types.BootstrapVersion, error) {
+				return nil, "", nil
 			}
 
 			By("starting a mock dataplane")
diff --git a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
index 065150c98..cf8f3cd35 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	kuma_version "github.com/kumahq/kuma/pkg/version"
+	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
@@ -26,6 +27,7 @@ var _ = Describe("Remote Bootstrap", func() {
 
 	type testCase struct {
 		config                   kuma_dp.Config
+		bootstrapVersion         types.BootstrapVersion
 		dataplane                *rest.Resource
 		expectedBootstrapRequest string
 	}
@@ -50,6 +52,7 @@ var _ = Describe("Remote Bootstrap", func() {
 			Expect(err).ToNot(HaveOccurred())
 			Expect(body).To(MatchJSON(given.expectedBootstrapRequest))
 
+			writer.Header().Set(types.BootstrapVersionHeader, string(given.bootstrapVersion))
 			response, err := ioutil.ReadFile(filepath.Join("testdata", "remote-bootstrap-config.golden.yaml"))
 			Expect(err).ToNot(HaveOccurred())
 			_, err = writer.Write(response)
@@ -62,12 +65,13 @@ var _ = Describe("Remote Bootstrap", func() {
 		generator := NewRemoteBootstrapGenerator(http.DefaultClient)
 
 		// when
-		config, err := generator(fmt.Sprintf("http://localhost:%d", port), given.config, given.dataplane, EnvoyVersion{
+		config, version, err := generator(fmt.Sprintf("http://localhost:%d", port), given.config, given.dataplane, given.bootstrapVersion, EnvoyVersion{
 			Build:   "hash/1.15.0/RELEASE",
 			Version: "1.15.0",
 		})
 
 		// then
+		Expect(version).To(Equal(given.bootstrapVersion))
 		Expect(err).ToNot(HaveOccurred())
 		Expect(config).ToNot(BeNil())
 	},
@@ -80,7 +84,8 @@ var _ = Describe("Remote Bootstrap", func() {
 				cfg.DataplaneRuntime.TokenPath = "/tmp/token"
 
 				return testCase{
-					config: cfg,
+					config:           cfg,
+					bootstrapVersion: "2",
 					dataplane: &rest.Resource{
 						Meta: rest.ResourceMeta{
 							Type: "Dataplane",
@@ -106,7 +111,8 @@ var _ = Describe("Remote Bootstrap", func() {
 						  "version": "1.15.0",
 						  "build": "hash/1.15.0/RELEASE"
 						}
-					  }
+					  },
+					  "bootstrapVersion": "2"
 					}`,
 				}
 			}()),
@@ -128,6 +134,7 @@ var _ = Describe("Remote Bootstrap", func() {
 							Name: "sample",
 						},
 					},
+					bootstrapVersion: "3",
 					expectedBootstrapRequest: `
                     {
                       "mesh": "demo",
@@ -146,7 +153,8 @@ var _ = Describe("Remote Bootstrap", func() {
                           "version": "1.15.0",
                           "build": "hash/1.15.0/RELEASE"
                         }
-                      }
+                      },
+                      "bootstrapVersion": "3"
                     }`,
 				}
 			}()),
@@ -184,7 +192,8 @@ var _ = Describe("Remote Bootstrap", func() {
                           "version": "1.15.0",
                           "build": "hash/1.15.0/RELEASE"
                         }
-                      }
+                      },
+                      "bootstrapVersion": ""
                     }`,
 				}
 			}()),
@@ -217,13 +226,13 @@ var _ = Describe("Remote Bootstrap", func() {
 		// when
 		cfg := kuma_dp.DefaultConfig()
 		cfg.ControlPlane.Retry.Backoff = 10 * time.Millisecond
-		_, err = generator(fmt.Sprintf("http://localhost:%d", port), cfg, &rest.Resource{
+		_, _, err = generator(fmt.Sprintf("http://localhost:%d", port), cfg, &rest.Resource{
 			Meta: rest.ResourceMeta{
 				Type: "Dataplane",
 				Mesh: "default",
 				Name: "dp-1",
 			},
-		}, EnvoyVersion{})
+		}, "", EnvoyVersion{})
 
 		// then
 		Expect(err).ToNot(HaveOccurred())
@@ -249,9 +258,9 @@ var _ = Describe("Remote Bootstrap", func() {
 		config := kuma_dp.DefaultConfig()
 		config.ControlPlane.Retry.Backoff = 10 * time.Millisecond
 		config.ControlPlane.Retry.MaxDuration = 100 * time.Millisecond
-		_, err = generator(fmt.Sprintf("http://localhost:%d", port), config, &rest.Resource{
+		_, _, err = generator(fmt.Sprintf("http://localhost:%d", port), config, &rest.Resource{
 			Meta: rest.ResourceMeta{Mesh: "default", Name: "dp-1"},
-		}, EnvoyVersion{})
+		}, "", EnvoyVersion{})
 
 		// then
 		Expect(err).To(MatchError("retryable: Dataplane entity not found. If you are running on Universal please create a Dataplane entity on kuma-cp before starting kuma-dp. If you are running on Kubernetes, please check the kuma-cp logs to determine why the Dataplane entity could not be created by the automatic sidecar injection."))
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 e3538a6b0..02933b400 100644
--- a/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
+++ b/pkg/config/app/kuma-dp/testdata/default-config.golden.yaml
@@ -8,5 +8,6 @@ controlPlane:
 dataplane:
   mesh: default
   drainTime: 30s
+  bootstrapVersion: ""
 dataplaneRuntime:
   binaryPath: envoy
diff --git a/pkg/sds/server/v2/server_test.go b/pkg/sds/server/v2/server_test.go
index c6a0a2b56..95072a5b7 100644
--- a/pkg/sds/server/v2/server_test.go
+++ b/pkg/sds/server/v2/server_test.go
@@ -7,6 +7,8 @@ import (
 	"sync/atomic"
 	"time"
 
+	prometheus_client "github.com/prometheus/client_model/go"
+
 	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
 
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
@@ -203,9 +205,13 @@ var _ = Describe("SDS Server", func() {
 		expirationSeconds := now.Load().(time.Time).Add(60 * time.Second).Unix()
 		Expect(dpInsight.Spec.MTLS.CertificateExpirationTime.Seconds).To(Equal(expirationSeconds))
 
-		// and metrics are published
-		Expect(test_metrics.FindMetric(metrics, "sds_cert_generation").GetCounter().GetValue()).To(Equal(1.0))
-		Expect(test_metrics.FindMetric(metrics, "sds_generation")).ToNot(BeNil())
+		// and metrics are published (metrics are published async, it does not have to be done before response is sent)
+		Eventually(func() float64 {
+			return test_metrics.FindMetric(metrics, "sds_cert_generation").GetCounter().GetValue()
+		}, "5s").Should(Equal(1.0))
+		Eventually(func() *prometheus_client.Metric {
+			return test_metrics.FindMetric(metrics, "sds_generation")
+		}, "5s").ShouldNot(BeNil())
 
 		close(done)
 	}, 10)
diff --git a/pkg/xds/bootstrap/generator_test.go b/pkg/xds/bootstrap/generator_test.go
index 7a9a1eaff..890084b99 100644
--- a/pkg/xds/bootstrap/generator_test.go
+++ b/pkg/xds/bootstrap/generator_test.go
@@ -82,10 +82,11 @@ var _ = Describe("bootstrapGenerator", func() {
 	})
 
 	type testCase struct {
-		config             func() *bootstrap_config.BootstrapServerConfig
-		dpAuthEnabled      bool
-		request            types.BootstrapRequest
-		expectedConfigFile string
+		config                   func() *bootstrap_config.BootstrapServerConfig
+		dpAuthEnabled            bool
+		request                  types.BootstrapRequest
+		expectedConfigFile       string
+		expectedBootstrapVersion types.BootstrapVersion
 	}
 	DescribeTable("should generate bootstrap configuration",
 		func(given testCase) {
@@ -94,22 +95,20 @@ var _ = Describe("bootstrapGenerator", func() {
 			Expect(err).ToNot(HaveOccurred())
 
 			// when
-			bootstrapConfig, err := generator.Generate(context.Background(), given.request)
+			bootstrapConfig, version, err := generator.Generate(context.Background(), given.request)
+
 			// then
 			Expect(err).ToNot(HaveOccurred())
 
-			// when
+			// and config is as expected
 			actual, err := util_proto.ToYAML(bootstrapConfig)
-			// then
 			Expect(err).ToNot(HaveOccurred())
-
-			// when
 			expected, err := ioutil.ReadFile(filepath.Join("testdata", given.expectedConfigFile))
-			// then
 			Expect(err).ToNot(HaveOccurred())
-
-			// expect
 			Expect(actual).To(MatchYAML(expected))
+
+			// and
+			Expect(version).To(Equal(given.expectedBootstrapVersion))
 		},
 		Entry("default config with minimal request", testCase{
 			dpAuthEnabled: false,
@@ -125,7 +124,8 @@ var _ = Describe("bootstrapGenerator", func() {
 				Name:    "name.namespace",
 				Version: defaultVersion,
 			},
-			expectedConfigFile: "generator.default-config-minimal-request.golden.yaml",
+			expectedConfigFile:       "generator.default-config-minimal-request.golden.yaml",
+			expectedBootstrapVersion: types.BootstrapV2,
 		}),
 		Entry("default config", testCase{
 			dpAuthEnabled: true,
@@ -143,7 +143,8 @@ var _ = Describe("bootstrapGenerator", func() {
 				DataplaneTokenPath: "/tmp/token",
 				Version:            defaultVersion,
 			},
-			expectedConfigFile: "generator.default-config.golden.yaml",
+			expectedConfigFile:       "generator.default-config.golden.yaml",
+			expectedBootstrapVersion: types.BootstrapV2,
 		}),
 		Entry("custom config with minimal request", testCase{
 			dpAuthEnabled: false,
@@ -157,15 +158,17 @@ var _ = Describe("bootstrapGenerator", func() {
 						XdsPort:            15678,
 						XdsConnectTimeout:  2 * time.Second,
 					},
-					APIVersion: envoy_common.APIV2,
+					APIVersion: envoy_common.APIV3,
 				}
 			},
 			request: types.BootstrapRequest{
-				Mesh:    "mesh",
-				Name:    "name.namespace",
-				Version: defaultVersion,
+				Mesh:             "mesh",
+				Name:             "name.namespace",
+				Version:          defaultVersion,
+				BootstrapVersion: types.BootstrapV2,
 			},
-			expectedConfigFile: "generator.custom-config-minimal-request.golden.yaml",
+			expectedConfigFile:       "generator.custom-config-minimal-request.golden.yaml",
+			expectedBootstrapVersion: types.BootstrapV2,
 		}),
 		Entry("custom config", testCase{
 			dpAuthEnabled: true,
@@ -210,7 +213,8 @@ var _ = Describe("bootstrapGenerator", func() {
 }`,
 				Version: defaultVersion,
 			},
-			expectedConfigFile: "generator.custom-config.golden.yaml",
+			expectedConfigFile:       "generator.custom-config.golden.yaml",
+			expectedBootstrapVersion: types.BootstrapV2,
 		}),
 	)
 
@@ -266,7 +270,7 @@ var _ = Describe("bootstrapGenerator", func() {
 		}
 
 		// when
-		_, err = generator.Generate(context.Background(), request)
+		_, _, err = generator.Generate(context.Background(), request)
 		// then
 		Expect(err).To(HaveOccurred())
 		// and
@@ -279,7 +283,7 @@ var _ = Describe("bootstrapGenerator", func() {
 		}
 
 		// when
-		_, err = generator.Generate(context.Background(), request)
+		_, _, err = generator.Generate(context.Background(), request)
 		// then
 		Expect(err).To(HaveOccurred())
 		// and
@@ -331,7 +335,7 @@ var _ = Describe("bootstrapGenerator", func() {
 		}
 
 		// when
-		_, err = generator.Generate(context.Background(), request)
+		_, _, err = generator.Generate(context.Background(), request)
 		// then
 		Expect(err).To(HaveOccurred())
 		// and
@@ -352,7 +356,7 @@ var _ = Describe("bootstrapGenerator", func() {
 		}
 
 		// when
-		_, err = generator.Generate(context.Background(), request)
+		_, _, err = generator.Generate(context.Background(), request)
 		// then
 		Expect(err).To(HaveOccurred())
 		// and
@@ -361,4 +365,27 @@ var _ = Describe("bootstrapGenerator", func() {
 2) Set KUMA_GENERAL_TLS_CERT_FILE and KUMA_GENERAL_TLS_KEY_FILE or the equivalent in Kuma CP config file to the new certificate.
 3) Restart the control plane to read the new certificate and start kuma-dp.`))
 	})
+
+	It("should fail bootstrap due to invalid bootstrap version", func() {
+		// given
+		cfg := bootstrap_config.DefaultBootstrapServerConfig()
+		cfg.Params.XdsHost = "localhost"
+		cfg.Params.XdsPort = 5678
+
+		generator, err := NewDefaultBootstrapGenerator(resManager, cfg, filepath.Join("..", "..", "..", "test", "certs", "server-cert.pem"), false)
+		Expect(err).ToNot(HaveOccurred())
+		request := types.BootstrapRequest{
+			Mesh:             "mesh",
+			Name:             "name.namespace",
+			AdminPort:        9901,
+			BootstrapVersion: "5",
+		}
+
+		// when
+		_, _, err = generator.Generate(context.Background(), request)
+		// then
+		Expect(err).To(HaveOccurred())
+		// and
+		Expect(err.Error()).To(Equal(`Invalid BootstrapVersion. Available values are: "2"`))
+	})
 })
diff --git a/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml b/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
index c476372a3..0d9e8cd7c 100644
--- a/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
@@ -35,14 +35,14 @@ node:
     dataplane.admin.port: "1234"
     dataplaneTokenPath: /tmp/token
     version:
-      kumaDp:
-        version: 0.0.1
-        gitTag: v0.0.1
-        gitCommit: 91ce236824a9d875601679aa80c63783fb0e8725
-        buildDate: 2019-08-07T11:26:06Z
       envoy:
-        version: 1.15.0
         build: hash/1.15.0/RELEASE
+        version: 1.15.0
+      kumaDp:
+        buildDate: "2019-08-07T11:26:06Z"
+        gitCommit: 91ce236824a9d875601679aa80c63783fb0e8725
+        gitTag: v0.0.1
+        version: 0.0.1
 staticResources:
   clusters:
     - connectTimeout: 1s