Repository: kumahq/kuma
Pull request #999 (2020-08-30 10:21:59)

feat(kuma-dp) new dataplane lifecycle
### Summary

This PR changes the way how `kuma-dp` starts on Universal. Instead of creating Dataplane manually before `kuma-dp run ...` now it's possible to pass `dp-resource.yaml` right into run command:
```
$ kuma-dp run --dataplane-file=/path/to/dp-resource.yaml
```

Dataplane resource will be passed to `/bootstrap` and encoded to `envoy.metadata`.  So on every `kuma-dp` connect resource will be automatically created by `kuma-cp`. On every `kuma-dp` disconnect resource will be automatically deleted by `kuma-cp`.

You can parameterize dp in a consistent way with `kumactl`
```
$ kuma-dp run --dataplane-file=/path/to/dp-resource.yaml \
    --dataplane-var name=backend-01
    --dataplane-var port=1234
```
(you can use `-v` as a shortcut)

### Notes

* `Resource` interface was enriched with a new method `Scope()`. It was needed to avoid dependencies from some high-level modules to specific constants. There were `switch` by `ResourceType` to distinguish resources that belong to `mesh` and which don't (`Zone`, `ZoneInsight`, `Mesh` ... )
* Unified unmarshalling logic from `rest.Resource` for `kumactl`, `kuma-dp` and `kuma-cp`
* We override DP when it is passed through XDS
* We only create DP if it passed through args, meaning that the old flow is still supported

### Issues resolved

Fix #XXX

### Documentation

- [ ] TBD!



Patch:
diff --git a/app/kuma-dp/cmd/dataplane.go b/app/kuma-dp/cmd/dataplane.go
new file mode 100644
index 000000000..d9df3a93d
--- /dev/null
+++ b/app/kuma-dp/cmd/dataplane.go
@@ -0,0 +1,53 @@
+package cmd
+
+import (
+	"io/ioutil"
+
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+
+	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	"github.com/kumahq/kuma/pkg/core/resources/model"
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+	"github.com/kumahq/kuma/pkg/util/template"
+)
+
+func readDataplaneResource(cmd *cobra.Command, cfg *kuma_dp.Config) (*rest.Resource, error) {
+	var b []byte
+	var err error
+	// load from file first
+	if cfg.DataplaneRuntime.ResourcePath == "-" {
+		if b, err = ioutil.ReadAll(cmd.InOrStdin()); err != nil {
+			return nil, err
+		}
+	} else if cfg.DataplaneRuntime.ResourcePath != "" {
+		if b, err = ioutil.ReadFile(cfg.DataplaneRuntime.ResourcePath); err != nil {
+			return nil, errors.Wrap(err, "error while reading provided file")
+		}
+	}
+	// override with inline resource
+	if cfg.DataplaneRuntime.Resource != "" {
+		b = []byte(cfg.DataplaneRuntime.Resource)
+	}
+
+	if len(b) == 0 {
+		return nil, nil
+	}
+
+	b = template.Render(string(b), cfg.DataplaneRuntime.ResourceVars)
+	runLog.Info("rendered dataplane", "dataplane", string(b))
+
+	res, err := rest.Unmarshall(b)
+	if err != nil {
+		return nil, err
+	}
+	if res.Meta.Type != string(core_mesh.DataplaneType) {
+		return nil, errors.Errorf("invalid resource of type: %s. Expected: Dataplane", res.Meta.Type)
+	}
+	if err := core_mesh.ValidateMeta(res.Meta.GetName(), res.Meta.GetMesh(), model.ScopeMesh); err.HasViolations() {
+		return nil, &err
+	}
+
+	return res, nil
+}
diff --git a/app/kuma-dp/cmd/run.go b/app/kuma-dp/cmd/run.go
index a0686093b..4d479f3c7 100644
--- a/app/kuma-dp/cmd/run.go
+++ b/app/kuma-dp/cmd/run.go
@@ -76,6 +76,16 @@ func newRunCmd() *cobra.Command {
 				}
 			}
 
+			dp, err := readDataplaneResource(cmd, &cfg)
+			if err != nil {
+				runLog.Error(err, "unable to read provided dataplane")
+				return err
+			}
+			if dp != nil {
+				cfg.Dataplane.Mesh = dp.Meta.GetMesh()
+				cfg.Dataplane.Name = dp.Meta.GetName()
+			}
+
 			if !cfg.Dataplane.AdminPort.Empty() {
 				// unless a user has explicitly opted out of Envoy Admin API, pick a free port from the range
 				adminPort, err := util_net.PickTCPPort("127.0.0.1", cfg.Dataplane.AdminPort.Lowest(), cfg.Dataplane.AdminPort.Highest())
@@ -114,6 +124,7 @@ func newRunCmd() *cobra.Command {
 				Catalog:   *catalog,
 				Config:    cfg,
 				Generator: bootstrapGenerator,
+				Dataplane: dp,
 				Stdout:    cmd.OutOrStdout(),
 				Stderr:    cmd.OutOrStderr(),
 			})
@@ -145,6 +156,9 @@ func newRunCmd() *cobra.Command {
 	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")
+	cmd.PersistentFlags().StringVar(&cfg.DataplaneRuntime.Resource, "dataplane", "", "Dataplane template to apply (YAML or JSON)")
+	cmd.PersistentFlags().StringVarP(&cfg.DataplaneRuntime.ResourcePath, "dataplane-file", "d", "", "Path to Dataplane template to apply (YAML or JSON)")
+	cmd.PersistentFlags().StringToStringVarP(&cfg.DataplaneRuntime.ResourceVars, "dataplane-var", "v", map[string]string{}, "Variables to replace Dataplane template")
 	return cmd
 }
 
diff --git a/app/kuma-dp/pkg/dataplane/envoy/envoy.go b/app/kuma-dp/pkg/dataplane/envoy/envoy.go
index d3b419f07..7e0fe1c75 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/envoy.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/envoy.go
@@ -6,9 +6,10 @@ import (
 	"io"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"time"
 
-	"path/filepath"
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
 
 	"github.com/golang/protobuf/proto"
 	"github.com/pkg/errors"
@@ -28,12 +29,13 @@ var (
 	newConfigFile = GenerateBootstrapFile
 )
 
-type BootstrapConfigFactoryFunc func(url string, cfg kuma_dp.Config) (proto.Message, error)
+type BootstrapConfigFactoryFunc func(url string, cfg kuma_dp.Config, dp *rest.Resource) (proto.Message, error)
 
 type Opts struct {
 	Catalog   catalog.Catalog
 	Config    kuma_dp.Config
 	Generator BootstrapConfigFactoryFunc
+	Dataplane *rest.Resource
 	Stdout    io.Writer
 	Stderr    io.Writer
 }
@@ -101,7 +103,7 @@ func lookupEnvoyPath(configuredPath string) (string, error) {
 
 func (e *Envoy) Start(stop <-chan struct{}) error {
 	runLog.Info("generating bootstrap configuration")
-	bootstrapConfig, err := e.opts.Generator(e.opts.Catalog.Apis.Bootstrap.Url, e.opts.Config)
+	bootstrapConfig, err := e.opts.Generator(e.opts.Catalog.Apis.Bootstrap.Url, e.opts.Config, e.opts.Dataplane)
 	if err != nil {
 		return errors.Errorf("Failed to generate Envoy bootstrap config. %v", err)
 	}
diff --git a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
index 9bf01aff0..433d993ba 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
@@ -7,6 +7,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	net_url "net/url"
+	"strings"
 
 	envoy_bootstrap "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v2"
 	"github.com/golang/protobuf/proto"
@@ -15,6 +16,7 @@ import (
 
 	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
 	"github.com/kumahq/kuma/pkg/core"
+	rest_types "github.com/kumahq/kuma/pkg/core/resources/model/rest"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 )
@@ -33,7 +35,15 @@ var (
 	DpNotFoundErr = errors.New("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.")
 )
 
-func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config) (proto.Message, error) {
+func InvalidRequestErr(msg string) error {
+	return errors.Errorf("Invalid request: %s", msg)
+}
+
+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) (proto.Message, error) {
 	bootstrapUrl, err := net_url.Parse(url)
 	if err != nil {
 		return nil, err
@@ -47,10 +57,13 @@ func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config) (proto.Messag
 	var respBytes []byte
 	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)
+		respBytes, err = b.requestForBootstrap(bootstrapUrl, cfg, dp)
 		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.ApiServer.Retry.Backoff)
@@ -71,8 +84,16 @@ func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config) (proto.Messag
 	return &bootstrap, nil
 }
 
-func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Config) ([]byte, error) {
+func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Config, dp *rest_types.Resource) ([]byte, error) {
 	url.Path = "/bootstrap"
+	var dataplaneResource string
+	if dp != nil {
+		dpJSON, err := json.Marshal(dp)
+		if err != nil {
+			return nil, err
+		}
+		dataplaneResource = string(dpJSON)
+	}
 	request := types.BootstrapRequest{
 		Mesh: cfg.Dataplane.Mesh,
 		Name: cfg.Dataplane.Name,
@@ -80,6 +101,7 @@ func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Conf
 		// that is set in the control plane bootstrap params
 		AdminPort:          cfg.Dataplane.AdminPort.Lowest(),
 		DataplaneTokenPath: cfg.DataplaneRuntime.TokenPath,
+		DataplaneResource:  dataplaneResource,
 	}
 	jsonBytes, err := json.Marshal(request)
 	if err != nil {
@@ -99,7 +121,7 @@ func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Conf
 			if err != nil {
 				return nil, errors.Errorf("Unable to read the response with status code: %d", resp.StatusCode)
 			}
-			return nil, errors.Errorf("Error: %s", string(bodyBytes))
+			return nil, InvalidRequestErr(string(bodyBytes))
 		}
 		return nil, errors.Errorf("unexpected status code: %d", resp.StatusCode)
 	}
diff --git a/app/kumactl/cmd/apply/apply.go b/app/kumactl/cmd/apply/apply.go
index e4df240cd..13a3b14c7 100644
--- a/app/kumactl/cmd/apply/apply.go
+++ b/app/kumactl/cmd/apply/apply.go
@@ -8,23 +8,21 @@ import (
 	"strings"
 	"time"
 
+	"github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	"github.com/kumahq/kuma/pkg/core/resources/apis/system"
+	"github.com/kumahq/kuma/pkg/util/template"
 
-	"github.com/ghodss/yaml"
-	"github.com/hoisie/mustache"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
 	kumactl_cmd "github.com/kumahq/kuma/app/kumactl/pkg/cmd"
 	"github.com/kumahq/kuma/app/kumactl/pkg/output"
 	"github.com/kumahq/kuma/app/kumactl/pkg/output/printers"
-	"github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	"github.com/kumahq/kuma/pkg/core/resources/model"
 	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
 	rest_types "github.com/kumahq/kuma/pkg/core/resources/model/rest"
 	"github.com/kumahq/kuma/pkg/core/resources/registry"
 	"github.com/kumahq/kuma/pkg/core/resources/store"
-	"github.com/kumahq/kuma/pkg/util/proto"
 )
 
 const (
@@ -86,15 +84,14 @@ func NewApplyCmd(pctx *kumactl_cmd.RootContext) *cobra.Command {
 				}
 			}
 
-			configBytes, err := processConfigTemplate(string(b), ctx.args.vars)
-			if err != nil {
-				return errors.Wrap(err, "error compiling config from template")
-			}
-
-			res, err := parseResource(configBytes)
+			bytes := template.Render(string(b), ctx.args.vars)
+			res, err := rest.UnmarshallToCore(bytes)
 			if err != nil {
 				return errors.Wrap(err, "YAML contains invalid resource")
 			}
+			if err := mesh.ValidateMeta(res.GetMeta().GetName(), res.GetMeta().GetMesh(), res.Scope()); err.HasViolations() {
+				return err.OrNil()
+			}
 
 			if ctx.args.dryRun {
 				p, err := printers.NewGenericPrinter(output.YAMLFormat)
@@ -129,38 +126,6 @@ func NewApplyCmd(pctx *kumactl_cmd.RootContext) *cobra.Command {
 	return cmd
 }
 
-type contextMap map[string]interface{}
-
-func (cm contextMap) merge(other contextMap) {
-	for k, v := range other {
-		cm[k] = v
-	}
-}
-
-func newContextMap(key, value string) contextMap {
-	if !strings.Contains(key, ".") {
-		return map[string]interface{}{
-			key: value,
-		}
-	}
-
-	parts := strings.SplitAfterN(key, ".", 2)
-	return map[string]interface{}{
-		parts[0][:len(parts[0])-1]: newContextMap(parts[1], value),
-	}
-}
-
-func processConfigTemplate(config string, values map[string]string) ([]byte, error) {
-	// TODO error checking -- match number of placeholders with number of
-	// passed values
-	ctx := contextMap{}
-	for k, v := range values {
-		ctx.merge(newContextMap(k, v))
-	}
-	data := mustache.Render(config, ctx)
-	return []byte(data), nil
-}
-
 func upsert(rs store.ResourceStore, res model.Resource) error {
 	newRes, err := registry.Global().NewObject(res.GetType())
 	if err != nil {
@@ -179,61 +144,3 @@ func upsert(rs store.ResourceStore, res model.Resource) error {
 	}
 	return rs.Update(context.Background(), newRes)
 }
-
-func parseResource(bytes []byte) (model.Resource, error) {
-	resMeta := rest.ResourceMeta{}
-	if err := yaml.Unmarshal(bytes, &resMeta); err != nil {
-		return nil, err
-	}
-	if resMeta.Name == "" {
-		return nil, errors.New("Name field cannot be empty")
-	}
-	if resMeta.Mesh == "" &&
-		resMeta.Type != string(mesh.MeshType) &&
-		resMeta.Type != string(system.ZoneType) {
-		return nil, errors.New("Mesh field cannot be empty")
-	}
-	resource, err := registry.Global().NewObject(model.ResourceType(resMeta.Type))
-	if err != nil {
-		return nil, err
-	}
-	if err := proto.FromYAML(bytes, resource.GetSpec()); err != nil {
-		return nil, err
-	}
-	resource.SetMeta(meta{
-		Name: resMeta.Name,
-		Mesh: resMeta.Mesh,
-	})
-	return resource, nil
-}
-
-var _ model.ResourceMeta = &meta{}
-
-type meta struct {
-	Name string
-	Mesh string
-}
-
-func (m meta) GetName() string {
-	return m.Name
-}
-
-func (m meta) GetNameExtensions() model.ResourceNameExtensions {
-	return model.ResourceNameExtensionsUnsupported
-}
-
-func (m meta) GetVersion() string {
-	return ""
-}
-
-func (m meta) GetMesh() string {
-	return m.Mesh
-}
-
-func (m meta) GetCreationTime() time.Time {
-	return time.Unix(0, 0).UTC() // the date doesn't matter since it is set on server side anyways
-}
-
-func (m meta) GetModificationTime() time.Time {
-	return time.Unix(0, 0).UTC() // the date doesn't matter since it is set on server side anyways
-}
diff --git a/pkg/admin-server/secrets_ws.go b/pkg/admin-server/secrets_ws.go
index 6c8977ee0..f33b5aeb4 100644
--- a/pkg/admin-server/secrets_ws.go
+++ b/pkg/admin-server/secrets_ws.go
@@ -163,6 +163,6 @@ func (s *secretsEndpoints) validateResourceRequest(request *restful.Request, res
 	if string(core_system.SecretType) != resource.Meta.Type {
 		err.AddViolation("type", "type from the URL has to be the same as in body")
 	}
-	err.AddError("", core_mesh.ValidateMeta(name, meshName))
+	err.AddError("", core_mesh.ValidateMeta(name, meshName, core_model.ScopeMesh))
 	return err.OrNil()
 }
diff --git a/pkg/api-server/resource_endpoints.go b/pkg/api-server/resource_endpoints.go
index c847433e7..955da1ebf 100644
--- a/pkg/api-server/resource_endpoints.go
+++ b/pkg/api-server/resource_endpoints.go
@@ -7,8 +7,6 @@ import (
 
 	config_core "github.com/kumahq/kuma/pkg/config/core"
 
-	"github.com/kumahq/kuma/pkg/core/resources/apis/system"
-
 	"github.com/emicklei/go-restful"
 
 	"github.com/kumahq/kuma/pkg/api-server/definitions"
@@ -216,12 +214,10 @@ func (r *resourceEndpoints) validateResourceRequest(request *restful.Request, re
 	if string(r.ResourceFactory().GetType()) != resource.Meta.Type {
 		err.AddViolation("type", "type from the URL has to be the same as in body")
 	}
-	if meshName != resource.Meta.Mesh &&
-		r.ResourceFactory().GetType() != mesh.MeshType &&
-		r.ResourceFactory().GetType() != system.ZoneType {
+	if r.ResourceFactory().Scope() == model.ScopeMesh && meshName != resource.Meta.Mesh {
 		err.AddViolation("mesh", "mesh from the URL has to be the same as in body")
 	}
-	err.AddError("", mesh.ValidateMeta(name, meshName))
+	err.AddError("", mesh.ValidateMeta(name, meshName, r.ResourceFactory().Scope()))
 	return err.OrNil()
 }
 
diff --git a/pkg/config/app/kuma-dp/config.go b/pkg/config/app/kuma-dp/config.go
index 0f3120500..793852be6 100644
--- a/pkg/config/app/kuma-dp/config.go
+++ b/pkg/config/app/kuma-dp/config.go
@@ -156,6 +156,12 @@ type DataplaneRuntime struct {
 	TokenPath string `yaml:"dataplaneTokenPath,omitempty" envconfig:"kuma_dataplane_runtime_token_path"`
 	// Token is dataplane token's value provided directly, will be stored to a temporary file before applying
 	Token string `yaml:"dataplaneToken,omitempty" envconfig:"kuma_dataplane_runtime_token"`
+	// Resource is a Dataplane resource that will be applied on Kuma CP
+	Resource string `yaml:"resource,omitempty" envconfig:"kuma_dataplane_runtime_resource"`
+	// ResourcePath is a path to Dataplane resource that will be applied on Kuma CP
+	ResourcePath string `yaml:"resourcePath,omitempty" envconfig:"kuma_dataplane_runtime_resource_path"`
+	// ResourceVars are the StringToString values that can fill the Resource template
+	ResourceVars map[string]string `yaml:"resourceVars,omitempty"`
 }
 
 var _ config.Config = &Config{}
@@ -164,9 +170,16 @@ func (c *Config) Validate() (errs error) {
 	if err := c.ControlPlane.Validate(); err != nil {
 		errs = multierr.Append(errs, errors.Wrapf(err, ".ControlPlane is not valid"))
 	}
-	if err := c.Dataplane.Validate(); err != nil {
-		errs = multierr.Append(errs, errors.Wrapf(err, ".Dataplane is not valid"))
+	if c.DataplaneRuntime.Resource != "" || c.DataplaneRuntime.ResourcePath != "" {
+		if err := c.Dataplane.ValidateForTemplate(); err != nil {
+			errs = multierr.Append(errs, errors.Wrapf(err, ".Dataplane is not valid"))
+		}
+	} else {
+		if err := c.Dataplane.Validate(); err != nil {
+			errs = multierr.Append(errs, errors.Wrapf(err, ".Dataplane is not valid"))
+		}
 	}
+
 	if err := c.DataplaneRuntime.Validate(); err != nil {
 		errs = multierr.Append(errs, errors.Wrapf(err, ".DataplaneRuntime is not valid"))
 	}
@@ -208,6 +221,14 @@ func (d *Dataplane) Validate() (errs error) {
 	return
 }
 
+func (d *Dataplane) ValidateForTemplate() (errs error) {
+	// Notice that d.AdminPort is always valid by design of PortRange
+	if d.DrainTime <= 0 {
+		errs = multierr.Append(errs, errors.Errorf(".DrainTime must be positive"))
+	}
+	return
+}
+
 var _ config.Config = &DataplaneRuntime{}
 
 func (d *DataplaneRuntime) Sanitize() {
diff --git a/pkg/core/managers/apis/dataplane/dataplane_manager.go b/pkg/core/managers/apis/dataplane/dataplane_manager.go
index b84a4f907..bd0923487 100644
--- a/pkg/core/managers/apis/dataplane/dataplane_manager.go
+++ b/pkg/core/managers/apis/dataplane/dataplane_manager.go
@@ -41,6 +41,12 @@ func (m *dataplaneManager) Create(ctx context.Context, resource core_model.Resou
 	m.setInboundsClusterTag(dp)
 	m.setGatewayClusterTag(dp)
 
+	opts := core_store.NewCreateOptions(fs...)
+	owner := &core_mesh.MeshResource{}
+	if err := m.store.Get(ctx, owner, core_store.GetByKey(opts.Mesh, opts.Mesh)); err != nil {
+		return core_manager.MeshNotFound(opts.Mesh)
+	}
+
 	return m.store.Create(ctx, resource, append(fs, core_store.CreatedAt(core.Now()))...)
 }
 
diff --git a/pkg/core/resources/apis/mesh/circuit_breaker.go b/pkg/core/resources/apis/mesh/circuit_breaker.go
index 74cd3a953..d8ca2cd07 100644
--- a/pkg/core/resources/apis/mesh/circuit_breaker.go
+++ b/pkg/core/resources/apis/mesh/circuit_breaker.go
@@ -46,6 +46,10 @@ func (c *CircuitBreakerResource) SetSpec(spec model.ResourceSpec) error {
 	}
 }
 
+func (c *CircuitBreakerResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
+
 var _ model.ResourceList = &CircuitBreakerResourceList{}
 
 type CircuitBreakerResourceList struct {
diff --git a/pkg/core/resources/apis/mesh/dataplane.go b/pkg/core/resources/apis/mesh/dataplane.go
index a7efa05b8..62002c529 100644
--- a/pkg/core/resources/apis/mesh/dataplane.go
+++ b/pkg/core/resources/apis/mesh/dataplane.go
@@ -41,6 +41,10 @@ func (t *DataplaneResource) SetSpec(spec model.ResourceSpec) error {
 	}
 }
 
+func (t *DataplaneResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
+
 var _ model.ResourceList = &DataplaneResourceList{}
 
 type DataplaneResourceList struct {
diff --git a/pkg/core/resources/apis/mesh/dataplane_insight.go b/pkg/core/resources/apis/mesh/dataplane_insight.go
index 16f17790c..dcd9c1cf1 100644
--- a/pkg/core/resources/apis/mesh/dataplane_insight.go
+++ b/pkg/core/resources/apis/mesh/dataplane_insight.go
@@ -44,6 +44,9 @@ func (t *DataplaneInsightResource) SetSpec(spec model.ResourceSpec) error {
 func (t *DataplaneInsightResource) Validate() error {
 	return nil
 }
+func (t *DataplaneInsightResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &DataplaneInsightResourceList{}
 
diff --git a/pkg/core/resources/apis/mesh/dataplane_overview.go b/pkg/core/resources/apis/mesh/dataplane_overview.go
index a77275590..d73969c73 100644
--- a/pkg/core/resources/apis/mesh/dataplane_overview.go
+++ b/pkg/core/resources/apis/mesh/dataplane_overview.go
@@ -50,6 +50,10 @@ func (t *DataplaneOverviewResource) Validate() error {
 	return nil
 }
 
+func (t *DataplaneOverviewResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
+
 var _ model.ResourceList = &DataplaneOverviewResourceList{}
 
 type DataplaneOverviewResourceList struct {
diff --git a/pkg/core/resources/apis/mesh/fault_injection.go b/pkg/core/resources/apis/mesh/fault_injection.go
index b5331faa5..085cdcdd9 100644
--- a/pkg/core/resources/apis/mesh/fault_injection.go
+++ b/pkg/core/resources/apis/mesh/fault_injection.go
@@ -46,6 +46,10 @@ func (f *FaultInjectionResource) SetSpec(spec model.ResourceSpec) error {
 	}
 }
 
+func (f *FaultInjectionResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
+
 var _ model.ResourceList = &FaultInjectionResourceList{}
 
 type FaultInjectionResourceList struct {
diff --git a/pkg/core/resources/apis/mesh/health_check.go b/pkg/core/resources/apis/mesh/health_check.go
index 73979fb03..095288001 100644
--- a/pkg/core/resources/apis/mesh/health_check.go
+++ b/pkg/core/resources/apis/mesh/health_check.go
@@ -46,6 +46,9 @@ func (t *HealthCheckResource) Sources() []*mesh_proto.Selector {
 func (t *HealthCheckResource) Destinations() []*mesh_proto.Selector {
 	return t.Spec.GetDestinations()
 }
+func (t *HealthCheckResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &HealthCheckResourceList{}
 
diff --git a/pkg/core/resources/apis/mesh/mesh.go b/pkg/core/resources/apis/mesh/mesh.go
index 91f76fd15..5de3e674b 100644
--- a/pkg/core/resources/apis/mesh/mesh.go
+++ b/pkg/core/resources/apis/mesh/mesh.go
@@ -40,6 +40,9 @@ func (t *MeshResource) SetSpec(spec model.ResourceSpec) error {
 		return nil
 	}
 }
+func (t *MeshResource) Scope() model.ResourceScope {
+	return model.ScopeGlobal
+}
 
 var _ model.ResourceList = &MeshResourceList{}
 
diff --git a/pkg/core/resources/apis/mesh/meta_validator.go b/pkg/core/resources/apis/mesh/meta_validator.go
index 5a311d172..0025fe7f5 100644
--- a/pkg/core/resources/apis/mesh/meta_validator.go
+++ b/pkg/core/resources/apis/mesh/meta_validator.go
@@ -3,18 +3,27 @@ package mesh
 import (
 	"regexp"
 
+	"github.com/kumahq/kuma/pkg/core/resources/model"
 	"github.com/kumahq/kuma/pkg/core/validators"
 )
 
 var nameMeshRegexp = regexp.MustCompile("^[0-9a-z-_]*$")
 
-func ValidateMeta(name, mesh string) validators.ValidationError {
+func ValidateMeta(name, mesh string, scope model.ResourceScope) validators.ValidationError {
 	var err validators.ValidationError
+	if name == "" {
+		err.AddViolation("name", "cannot be empty")
+	}
 	if !nameMeshRegexp.MatchString(name) {
 		err.AddViolation("name", "invalid characters. Valid characters are numbers, lowercase latin letters and '-', '_' symbols.")
 	}
-	if !nameMeshRegexp.MatchString(mesh) {
-		err.AddViolation("mesh", "invalid characters. Valid characters are numbers, lowercase latin letters and '-', '_' symbols.")
+	if scope == model.ScopeMesh {
+		if mesh == "" {
+			err.AddViolation("mesh", "cannot be empty")
+		}
+		if !nameMeshRegexp.MatchString(mesh) {
+			err.AddViolation("mesh", "invalid characters. Valid characters are numbers, lowercase latin letters and '-', '_' symbols.")
+		}
 	}
 	return err
 }
diff --git a/pkg/core/resources/apis/mesh/proxytemplate.go b/pkg/core/resources/apis/mesh/proxytemplate.go
index 730e89733..243158c06 100644
--- a/pkg/core/resources/apis/mesh/proxytemplate.go
+++ b/pkg/core/resources/apis/mesh/proxytemplate.go
@@ -40,6 +40,9 @@ func (t *ProxyTemplateResource) SetSpec(spec model.ResourceSpec) error {
 		return nil
 	}
 }
+func (t *ProxyTemplateResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &ProxyTemplateResourceList{}
 
diff --git a/pkg/core/resources/apis/mesh/traffic_log.go b/pkg/core/resources/apis/mesh/traffic_log.go
index 7112f04c8..60adf8260 100644
--- a/pkg/core/resources/apis/mesh/traffic_log.go
+++ b/pkg/core/resources/apis/mesh/traffic_log.go
@@ -40,6 +40,9 @@ func (t *TrafficLogResource) SetSpec(spec model.ResourceSpec) error {
 		return nil
 	}
 }
+func (t *TrafficLogResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &TrafficLogResourceList{}
 
diff --git a/pkg/core/resources/apis/mesh/traffic_permission.go b/pkg/core/resources/apis/mesh/traffic_permission.go
index bdfcc4117..bbf9e0b90 100644
--- a/pkg/core/resources/apis/mesh/traffic_permission.go
+++ b/pkg/core/resources/apis/mesh/traffic_permission.go
@@ -40,6 +40,9 @@ func (t *TrafficPermissionResource) SetSpec(spec model.ResourceSpec) error {
 		return nil
 	}
 }
+func (t *TrafficPermissionResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &TrafficPermissionResourceList{}
 
diff --git a/pkg/core/resources/apis/mesh/traffic_route.go b/pkg/core/resources/apis/mesh/traffic_route.go
index 6a8a3417b..9766ff817 100644
--- a/pkg/core/resources/apis/mesh/traffic_route.go
+++ b/pkg/core/resources/apis/mesh/traffic_route.go
@@ -40,6 +40,9 @@ func (t *TrafficRouteResource) SetSpec(spec model.ResourceSpec) error {
 		return nil
 	}
 }
+func (t *TrafficRouteResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &TrafficRouteResourceList{}
 
diff --git a/pkg/core/resources/apis/mesh/traffic_trace.go b/pkg/core/resources/apis/mesh/traffic_trace.go
index 7d0a0e331..b9adf3abb 100644
--- a/pkg/core/resources/apis/mesh/traffic_trace.go
+++ b/pkg/core/resources/apis/mesh/traffic_trace.go
@@ -40,6 +40,9 @@ func (t *TrafficTraceResource) SetSpec(spec model.ResourceSpec) error {
 		return nil
 	}
 }
+func (t *TrafficTraceResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &TrafficTraceResourceList{}
 
diff --git a/pkg/core/resources/apis/system/config.go b/pkg/core/resources/apis/system/config.go
index a33ccaf91..2805044b8 100644
--- a/pkg/core/resources/apis/system/config.go
+++ b/pkg/core/resources/apis/system/config.go
@@ -43,6 +43,9 @@ func (t *ConfigResource) SetSpec(spec model.ResourceSpec) error {
 func (t *ConfigResource) Validate() error {
 	return nil
 }
+func (t *ConfigResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &ConfigResourceList{}
 
diff --git a/pkg/core/resources/apis/system/secret.go b/pkg/core/resources/apis/system/secret.go
index 85fe01478..f4c23a12b 100644
--- a/pkg/core/resources/apis/system/secret.go
+++ b/pkg/core/resources/apis/system/secret.go
@@ -43,6 +43,9 @@ func (t *SecretResource) SetSpec(spec model.ResourceSpec) error {
 func (t *SecretResource) Validate() error {
 	return nil
 }
+func (t *SecretResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
 
 var _ model.ResourceList = &SecretResourceList{}
 
diff --git a/pkg/core/resources/apis/system/zone.go b/pkg/core/resources/apis/system/zone.go
index 0b8727736..5d634d28a 100644
--- a/pkg/core/resources/apis/system/zone.go
+++ b/pkg/core/resources/apis/system/zone.go
@@ -40,6 +40,9 @@ func (t *ZoneResource) SetSpec(spec model.ResourceSpec) error {
 		return nil
 	}
 }
+func (t *ZoneResource) Scope() model.ResourceScope {
+	return model.ScopeGlobal
+}
 
 var _ model.ResourceList = &ZoneResourceList{}
 
diff --git a/pkg/core/resources/apis/system/zone_insight.go b/pkg/core/resources/apis/system/zone_insight.go
index 19be0ec19..20a5a7cf0 100644
--- a/pkg/core/resources/apis/system/zone_insight.go
+++ b/pkg/core/resources/apis/system/zone_insight.go
@@ -49,6 +49,10 @@ func (t *ZoneInsightResource) Validate() error {
 	return nil
 }
 
+func (t *ZoneInsightResource) Scope() model.ResourceScope {
+	return model.ScopeGlobal
+}
+
 var _ model.ResourceList = &ZoneInsightResourceList{}
 
 type ZoneInsightResourceList struct {
diff --git a/pkg/core/resources/apis/system/zone_overview.go b/pkg/core/resources/apis/system/zone_overview.go
index fc1fd2ae4..1b449ad41 100644
--- a/pkg/core/resources/apis/system/zone_overview.go
+++ b/pkg/core/resources/apis/system/zone_overview.go
@@ -50,6 +50,10 @@ func (t *ZoneOverviewResource) Validate() error {
 	return nil
 }
 
+func (t *ZoneOverviewResource) Scope() model.ResourceScope {
+	return model.ScopeGlobal
+}
+
 var _ model.ResourceList = &ZoneOverviewResourceList{}
 
 type ZoneOverviewResourceList struct {
diff --git a/pkg/core/resources/manager/manager.go b/pkg/core/resources/manager/manager.go
index 9c22dfbd8..fa0abce31 100644
--- a/pkg/core/resources/manager/manager.go
+++ b/pkg/core/resources/manager/manager.go
@@ -89,6 +89,24 @@ func (r *resourcesManager) Update(ctx context.Context, resource model.Resource,
 	return r.Store.Update(ctx, resource, append(fs, store.ModifiedAt(time.Now()))...)
 }
 
+func Upsert(manager ResourceManager, key model.ResourceKey, resource model.Resource, fn func(resource model.Resource)) error {
+	create := false
+	err := manager.Get(context.Background(), resource, store.GetBy(key))
+	if err != nil {
+		if store.IsResourceNotFound(err) {
+			create = true
+		} else {
+			return err
+		}
+	}
+	fn(resource)
+	if create {
+		return manager.Create(context.Background(), resource, store.CreateBy(key))
+	} else {
+		return manager.Update(context.Background(), resource)
+	}
+}
+
 type MeshNotFoundError struct {
 	Mesh string
 }
diff --git a/pkg/core/resources/model/resource.go b/pkg/core/resources/model/resource.go
index be71c4c49..7b6cdec39 100644
--- a/pkg/core/resources/model/resource.go
+++ b/pkg/core/resources/model/resource.go
@@ -23,6 +23,13 @@ type ResourceKey struct {
 	Name string
 }
 
+type ResourceScope string
+
+const (
+	ScopeMesh   = "Mesh"
+	ScopeGlobal = "Global"
+)
+
 type Resource interface {
 	GetType() ResourceType
 	GetMeta() ResourceMeta
@@ -30,6 +37,7 @@ type Resource interface {
 	GetSpec() ResourceSpec
 	SetSpec(ResourceSpec) error
 	Validate() error
+	Scope() ResourceScope
 }
 
 type ResourceType string
diff --git a/pkg/core/resources/model/rest/api.go b/pkg/core/resources/model/rest/api.go
index 3e055d400..4594bea28 100644
--- a/pkg/core/resources/model/rest/api.go
+++ b/pkg/core/resources/model/rest/api.go
@@ -3,11 +3,10 @@ package rest
 import (
 	"fmt"
 
-	"github.com/kumahq/kuma/pkg/core/resources/apis/system"
+	"github.com/kumahq/kuma/pkg/core/resources/registry"
 
 	"github.com/pkg/errors"
 
-	"github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	"github.com/kumahq/kuma/pkg/core/resources/model"
 )
 
@@ -21,48 +20,36 @@ type ResourceApi interface {
 }
 
 func NewResourceApi(resType model.ResourceType, path string) ResourceApi {
-	switch resType {
-	case mesh.MeshType:
-		return &meshApi{}
-	case system.ZoneType:
-		return &zoneApi{}
-	default:
-		return &resourceApi{CollectionPath: path}
+	res, _ := registry.Global().NewObject(resType)
+	if res.Scope() == model.ScopeGlobal {
+		return &nonMeshedApi{CollectionPath: path}
+	} else {
+		return &meshedApi{CollectionPath: path}
 	}
 }
 
-type resourceApi struct {
+type meshedApi struct {
 	CollectionPath string
 }
 
-func (r *resourceApi) List(mesh string) string {
+func (r *meshedApi) List(mesh string) string {
 	return fmt.Sprintf("/meshes/%s/%s", mesh, r.CollectionPath)
 }
 
-func (r resourceApi) Item(mesh string, name string) string {
+func (r meshedApi) Item(mesh string, name string) string {
 	return fmt.Sprintf("/meshes/%s/%s/%s", mesh, r.CollectionPath, name)
 }
 
-type meshApi struct {
-}
-
-func (r *meshApi) List(string) string {
-	return "/meshes"
-}
-
-func (r *meshApi) Item(string, name string) string {
-	return fmt.Sprintf("/meshes/%s", name)
-}
-
-type zoneApi struct {
+type nonMeshedApi struct {
+	CollectionPath string
 }
 
-func (r *zoneApi) List(string) string {
-	return "/zones"
+func (r *nonMeshedApi) List(string) string {
+	return fmt.Sprintf("/%s", r.CollectionPath)
 }
 
-func (r *zoneApi) Item(string, name string) string {
-	return fmt.Sprintf("/zones/%s", name)
+func (r *nonMeshedApi) Item(string, name string) string {
+	return fmt.Sprintf("/%s/%s", r.CollectionPath, name)
 }
 
 var _ Api = &ApiDescriptor{}
diff --git a/pkg/core/resources/model/rest/converter.go b/pkg/core/resources/model/rest/converter.go
index bc31456a8..8d5daabab 100644
--- a/pkg/core/resources/model/rest/converter.go
+++ b/pkg/core/resources/model/rest/converter.go
@@ -1,9 +1,8 @@
 package rest
 
 import (
-	"github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
-	"github.com/kumahq/kuma/pkg/core/resources/apis/system"
 	"github.com/kumahq/kuma/pkg/core/resources/model"
+	"github.com/kumahq/kuma/pkg/core/resources/registry"
 )
 
 var From = &from{}
@@ -12,10 +11,7 @@ type from struct{}
 
 func (c *from) Resource(r model.Resource) *Resource {
 	var meshName string
-	switch r.GetType() {
-	case mesh.MeshType:
-	case system.ZoneType:
-	default:
+	if r.Scope() == model.ScopeMesh {
 		meshName = r.GetMeta().GetMesh()
 	}
 	return &Resource{
@@ -40,3 +36,15 @@ func (c *from) ResourceList(rs model.ResourceList) *ResourceList {
 		Items: items,
 	}
 }
+
+func (r *Resource) ToCore() (model.Resource, error) {
+	resource, err := registry.Global().NewObject(model.ResourceType(r.Meta.Type))
+	if err != nil {
+		return nil, err
+	}
+	resource.SetMeta(&r.Meta)
+	if err := resource.SetSpec(r.Spec); err != nil {
+		return nil, err
+	}
+	return resource, nil
+}
diff --git a/pkg/core/resources/model/rest/resource.go b/pkg/core/resources/model/rest/resource.go
index 7d0958785..82116aea5 100644
--- a/pkg/core/resources/model/rest/resource.go
+++ b/pkg/core/resources/model/rest/resource.go
@@ -19,6 +19,32 @@ type ResourceMeta struct {
 	ModificationTime time.Time `json:"modificationTime"`
 }
 
+func (r *ResourceMeta) GetName() string {
+	return r.Name
+}
+
+func (r *ResourceMeta) GetNameExtensions() model.ResourceNameExtensions {
+	return model.ResourceNameExtensionsUnsupported
+}
+
+func (r *ResourceMeta) GetVersion() string {
+	return ""
+}
+
+func (r *ResourceMeta) GetMesh() string {
+	return r.Mesh
+}
+
+func (r *ResourceMeta) GetCreationTime() time.Time {
+	return r.CreationTime
+}
+
+func (r *ResourceMeta) GetModificationTime() time.Time {
+	return r.ModificationTime
+}
+
+var _ model.ResourceMeta = &ResourceMeta{}
+
 type Resource struct {
 	Meta ResourceMeta
 	Spec model.ResourceSpec
diff --git a/pkg/core/resources/model/rest/unmarshaller.go b/pkg/core/resources/model/rest/unmarshaller.go
new file mode 100644
index 000000000..4e15065e5
--- /dev/null
+++ b/pkg/core/resources/model/rest/unmarshaller.go
@@ -0,0 +1,36 @@
+package rest
+
+import (
+	"github.com/ghodss/yaml"
+
+	"github.com/kumahq/kuma/pkg/core/resources/model"
+	"github.com/kumahq/kuma/pkg/core/resources/registry"
+	"github.com/kumahq/kuma/pkg/util/proto"
+)
+
+func UnmarshallToCore(bytes []byte) (model.Resource, error) {
+	res, err := Unmarshall(bytes)
+	if err != nil {
+		return nil, err
+	}
+	return res.ToCore()
+}
+
+func Unmarshall(bytes []byte) (*Resource, error) {
+	resMeta := ResourceMeta{}
+	if err := yaml.Unmarshal(bytes, &resMeta); err != nil {
+		return nil, err
+	}
+	resource, err := registry.Global().NewObject(model.ResourceType(resMeta.Type))
+	if err != nil {
+		return nil, err
+	}
+	res := &Resource{
+		Meta: resMeta,
+		Spec: resource.GetSpec(),
+	}
+	if err := proto.FromYAML(bytes, res.Spec); err != nil {
+		return nil, err
+	}
+	return res, nil
+}
diff --git a/pkg/core/xds/metadata.go b/pkg/core/xds/metadata.go
index 432d0e9a5..7687af52d 100644
--- a/pkg/core/xds/metadata.go
+++ b/pkg/core/xds/metadata.go
@@ -3,6 +3,9 @@ package xds
 import (
 	"strconv"
 
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+
 	"github.com/kumahq/kuma/pkg/core"
 
 	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
@@ -13,8 +16,9 @@ var metadataLog = core.Log.WithName("xds-server").WithName("metadata-tracker")
 const (
 	// Supported Envoy node metadata fields.
 
-	fieldDataplaneTokenPath = "dataplaneTokenPath"
-	fieldDataplaneAdminPort = "dataplane.admin.port"
+	fieldDataplaneTokenPath         = "dataplaneTokenPath"
+	fieldDataplaneAdminPort         = "dataplane.admin.port"
+	fieldDataplaneDataplaneResource = "dataplane.resource"
 )
 
 // DataplaneMetadata represents environment-specific part of a dataplane configuration.
@@ -33,6 +37,7 @@ const (
 // to generate xDS resources that depend on environment-specific configuration.
 type DataplaneMetadata struct {
 	DataplaneTokenPath string
+	DataplaneResource  *core_mesh.DataplaneResource
 	AdminPort          uint32
 }
 
@@ -43,6 +48,13 @@ func (m *DataplaneMetadata) GetDataplaneTokenPath() string {
 	return m.DataplaneTokenPath
 }
 
+func (m *DataplaneMetadata) GetDataplaneResource() *core_mesh.DataplaneResource {
+	if m == nil {
+		return nil
+	}
+	return m.DataplaneResource
+}
+
 func (m *DataplaneMetadata) GetAdminPort() uint32 {
 	if m == nil {
 		return 0
@@ -65,5 +77,16 @@ func DataplaneMetadataFromNode(node *envoy_core.Node) *DataplaneMetadata {
 			metadataLog.Error(err, "invalid value in dataplane metadata", "field", fieldDataplaneAdminPort, "value", value)
 		}
 	}
+	if value := node.Metadata.Fields[fieldDataplaneDataplaneResource]; value != nil {
+		res, err := rest.UnmarshallToCore([]byte(value.GetStringValue()))
+		if err != nil {
+			metadataLog.Error(err, "invalid value in dataplane metadata", "field", fieldDataplaneDataplaneResource, "value", value)
+		}
+		dp, ok := res.(*core_mesh.DataplaneResource)
+		if !ok {
+			metadataLog.Error(err, "invalid value in dataplane metadata", "field", fieldDataplaneDataplaneResource, "value", value)
+		}
+		metadata.DataplaneResource = dp
+	}
 	return &metadata
 }
diff --git a/pkg/kds/server/status_sink.go b/pkg/kds/server/status_sink.go
index f7bfc9e0a..762a6c6b0 100644
--- a/pkg/kds/server/status_sink.go
+++ b/pkg/kds/server/status_sink.go
@@ -1,7 +1,6 @@
 package server
 
 import (
-	"context"
 	"time"
 
 	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
@@ -14,7 +13,6 @@ import (
 	"github.com/golang/protobuf/proto"
 
 	"github.com/kumahq/kuma/pkg/core/resources/manager"
-	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
 )
 
 type ZoneInsightSink interface {
@@ -84,20 +82,12 @@ type zoneInsightStore struct {
 }
 
 func (s *zoneInsightStore) Upsert(zone string, subscription *system_proto.KDSSubscription) error {
-	create := false
-	zoneInsight := &system.ZoneInsightResource{}
-	err := s.resManager.Get(context.Background(), zoneInsight, core_store.GetByKey(zone, core_model.DefaultMesh))
-	if err != nil {
-		if core_store.IsResourceNotFound(err) {
-			create = true
-		} else {
-			return err
-		}
-	}
-	zoneInsight.Spec.UpdateSubscription(subscription)
-	if create {
-		return s.resManager.Create(context.Background(), zoneInsight, core_store.CreateByKey(zone, core_model.DefaultMesh))
-	} else {
-		return s.resManager.Update(context.Background(), zoneInsight)
+	key := core_model.ResourceKey{
+		Mesh: core_model.DefaultMesh,
+		Name: zone,
 	}
+	zoneInsight := &system.ZoneInsightResource{}
+	return manager.Upsert(s.resManager, key, zoneInsight, func(resource core_model.Resource) {
+		zoneInsight.Spec.UpdateSubscription(subscription)
+	})
 }
diff --git a/pkg/util/template/render.go b/pkg/util/template/render.go
new file mode 100644
index 000000000..1fe9b9c00
--- /dev/null
+++ b/pkg/util/template/render.go
@@ -0,0 +1,37 @@
+package template
+
+import (
+	"strings"
+
+	"github.com/hoisie/mustache"
+)
+
+type contextMap map[string]interface{}
+
+func (cm contextMap) merge(other contextMap) {
+	for k, v := range other {
+		cm[k] = v
+	}
+}
+
+func newContextMap(key, value string) contextMap {
+	if !strings.Contains(key, ".") {
+		return map[string]interface{}{
+			key: value,
+		}
+	}
+
+	parts := strings.SplitAfterN(key, ".", 2)
+	return map[string]interface{}{
+		parts[0][:len(parts[0])-1]: newContextMap(parts[1], value),
+	}
+}
+
+func Render(template string, values map[string]string) []byte {
+	ctx := contextMap{}
+	for k, v := range values {
+		ctx.merge(newContextMap(k, v))
+	}
+	data := mustache.Render(template, ctx)
+	return []byte(data)
+}
diff --git a/pkg/xds/bootstrap/generator.go b/pkg/xds/bootstrap/generator.go
index ee9fea8e4..d508a841a 100644
--- a/pkg/xds/bootstrap/generator.go
+++ b/pkg/xds/bootstrap/generator.go
@@ -8,6 +8,10 @@ import (
 	"io/ioutil"
 	"text/template"
 
+	"github.com/kumahq/kuma/pkg/config/core"
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+	"github.com/kumahq/kuma/pkg/core/validators"
+
 	envoy_bootstrap "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v2"
 	"github.com/golang/protobuf/proto"
 	"github.com/pkg/errors"
@@ -28,18 +32,21 @@ type BootstrapGenerator interface {
 func NewDefaultBootstrapGenerator(
 	resManager core_manager.ResourceManager,
 	config *bootstrap_config.BootstrapParamsConfig,
-	cacertFile string) BootstrapGenerator {
+	cacertFile string,
+	environmentType core.EnvironmentType) BootstrapGenerator {
 	return &bootstrapGenerator{
-		resManager:  resManager,
-		config:      config,
-		xdsCertFile: cacertFile,
+		resManager:      resManager,
+		config:          config,
+		xdsCertFile:     cacertFile,
+		environmentType: environmentType,
 	}
 }
 
 type bootstrapGenerator struct {
-	resManager  core_manager.ResourceManager
-	config      *bootstrap_config.BootstrapParamsConfig
-	xdsCertFile string
+	resManager      core_manager.ResourceManager
+	config          *bootstrap_config.BootstrapParamsConfig
+	xdsCertFile     string
+	environmentType core.EnvironmentType
 }
 
 func (b *bootstrapGenerator) Generate(ctx context.Context, request types.BootstrapRequest) (proto.Message, error) {
@@ -47,10 +54,12 @@ func (b *bootstrapGenerator) Generate(ctx context.Context, request types.Bootstr
 	if err != nil {
 		return nil, err
 	}
-	dataplane, err := b.fetchDataplane(ctx, proxyId)
+
+	dataplane, err := b.dataplaneFor(ctx, request, proxyId)
 	if err != nil {
 		return nil, err
 	}
+
 	bootstrapCfg, err := b.generateFor(*proxyId, dataplane, request)
 	if err != nil {
 		return nil, err
@@ -58,6 +67,47 @@ func (b *bootstrapGenerator) Generate(ctx context.Context, request types.Bootstr
 	return bootstrapCfg, nil
 }
 
+// dataplaneFor returns dataplane for two flows
+// 1) Dataplane is passed to kuma-dp run, in this case we just read DP from the BootstrapRequest
+// 2) Dataplane is created before kuma-dp run, in this case we access storage to fetch it (ex. Kubernetes)
+func (b *bootstrapGenerator) dataplaneFor(ctx context.Context, request types.BootstrapRequest, proxyId *core_xds.ProxyId) (*core_mesh.DataplaneResource, error) {
+	if request.DataplaneResource != "" {
+		res, err := rest.UnmarshallToCore([]byte(request.DataplaneResource))
+		if err != nil {
+			return nil, err
+		}
+		dp, ok := res.(*core_mesh.DataplaneResource)
+		if !ok {
+			return nil, errors.Errorf("invalid resource")
+		}
+		if err := dp.Validate(); err != nil {
+			return nil, err
+		}
+		if err := b.validateMeshExist(ctx, dp.Meta.GetMesh()); err != nil {
+			return nil, err
+		}
+		return dp, nil
+	} else {
+		dataplane := &core_mesh.DataplaneResource{}
+		if err := b.resManager.Get(ctx, dataplane, core_store.GetBy(proxyId.ToResourceKey())); err != nil {
+			return nil, err
+		}
+		return dataplane, nil
+	}
+}
+
+func (b *bootstrapGenerator) validateMeshExist(ctx context.Context, mesh string) error {
+	if err := b.resManager.Get(ctx, &core_mesh.MeshResource{}, core_store.GetByKey(mesh, mesh)); err != nil {
+		if core_store.IsResourceNotFound(err) {
+			verr := validators.ValidationError{}
+			verr.AddViolation("mesh", fmt.Sprintf("mesh %q does not exist", mesh))
+			return verr.OrNil()
+		}
+		return err
+	}
+	return nil
+}
+
 func (b *bootstrapGenerator) generateFor(proxyId core_xds.ProxyId, dataplane *core_mesh.DataplaneResource, request types.BootstrapRequest) (*envoy_bootstrap.Bootstrap, error) {
 	// if dataplane has no service - fill this with placeholder. Otherwise take the first service
 	service := dataplane.Spec.GetIdentifyingService()
@@ -91,6 +141,7 @@ func (b *bootstrapGenerator) generateFor(proxyId core_xds.ProxyId, dataplane *co
 		XdsConnectTimeout:  b.config.XdsConnectTimeout,
 		AccessLogPipe:      accessLogPipe,
 		DataplaneTokenPath: request.DataplaneTokenPath,
+		DataplaneResource:  request.DataplaneResource,
 		CertBytes:          certBytes,
 	}
 	log.WithValues("params", params).Info("Generating bootstrap config")
@@ -109,14 +160,6 @@ func (b *bootstrapGenerator) verifyAdminPort(adminPort uint32, dataplane *core_m
 	return nil
 }
 
-func (b *bootstrapGenerator) fetchDataplane(ctx context.Context, proxyId *core_xds.ProxyId) (*core_mesh.DataplaneResource, error) {
-	res := core_mesh.DataplaneResource{}
-	if err := b.resManager.Get(ctx, &res, core_store.GetBy(proxyId.ToResourceKey())); err != nil {
-		return nil, err
-	}
-	return &res, nil
-}
-
 func (b *bootstrapGenerator) configForParameters(params configParameters) (*envoy_bootstrap.Bootstrap, error) {
 	tmpl, err := template.New("bootstrap").Parse(configTemplate)
 	if err != nil {
diff --git a/pkg/xds/bootstrap/server.go b/pkg/xds/bootstrap/server.go
index 42cc418d3..0b0a4d664 100644
--- a/pkg/xds/bootstrap/server.go
+++ b/pkg/xds/bootstrap/server.go
@@ -10,6 +10,7 @@ import (
 	"github.com/kumahq/kuma/pkg/core"
 	"github.com/kumahq/kuma/pkg/core/resources/store"
 	"github.com/kumahq/kuma/pkg/core/runtime/component"
+	"github.com/kumahq/kuma/pkg/core/validators"
 	"github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
 )
@@ -89,6 +90,15 @@ func (b *BootstrapServer) handleBootstrapRequest(resp http.ResponseWriter, req *
 			}
 			return
 		}
+		if validators.IsValidationError(err) {
+			resp.WriteHeader(http.StatusUnprocessableEntity)
+			_, err = resp.Write([]byte(err.Error()))
+			if err != nil {
+				log.WithValues("params", reqParams).Error(err, "Error while writing the response")
+				return
+			}
+			return
+		}
 		log.WithValues("params", reqParams).Error(err, "Could not generate a bootstrap configuration")
 		resp.WriteHeader(http.StatusInternalServerError)
 		return
diff --git a/pkg/xds/bootstrap/template.go b/pkg/xds/bootstrap/template.go
index 7b9732353..4c760709e 100644
--- a/pkg/xds/bootstrap/template.go
+++ b/pkg/xds/bootstrap/template.go
@@ -13,6 +13,7 @@ type configParameters struct {
 	XdsConnectTimeout  time.Duration
 	AccessLogPipe      string
 	DataplaneTokenPath string
+	DataplaneResource  string
 	CertBytes          string
 }
 
@@ -24,6 +25,9 @@ node:
 {{if .DataplaneTokenPath}}
     dataplaneTokenPath: {{.DataplaneTokenPath}}
 {{end}}
+{{if .DataplaneResource}}
+    dataplane.resource: '{{.DataplaneResource}}'
+{{end}}
 {{if .AdminPort }}
     dataplane.admin.port: "{{ .AdminPort }}"
 {{ end }}
diff --git a/pkg/xds/bootstrap/types/bootstrap_request.go b/pkg/xds/bootstrap/types/bootstrap_request.go
index c754ce7b9..0e3064872 100644
--- a/pkg/xds/bootstrap/types/bootstrap_request.go
+++ b/pkg/xds/bootstrap/types/bootstrap_request.go
@@ -5,4 +5,5 @@ type BootstrapRequest struct {
 	Name               string `json:"name"`
 	AdminPort          uint32 `json:"adminPort,omitempty"`
 	DataplaneTokenPath string `json:"dataplaneTokenPath,omitempty"`
+	DataplaneResource  string `json:"dataplaneResource,omitempty"`
 }
diff --git a/pkg/xds/server/components.go b/pkg/xds/server/components.go
index b26cfffb4..183de7496 100644
--- a/pkg/xds/server/components.go
+++ b/pkg/xds/server/components.go
@@ -46,6 +46,7 @@ func SetupServer(rt core_runtime.Runtime) error {
 	reconciler := DefaultReconciler(rt)
 
 	metadataTracker := NewDataplaneMetadataTracker()
+	lifecycle := NewDataplaneLifecycle(rt.ResourceManager())
 
 	ingressReconciler := DefaultIngressReconciler(rt)
 
@@ -56,6 +57,7 @@ func SetupServer(rt core_runtime.Runtime) error {
 	callbacks := util_xds.CallbacksChain{
 		tracker,
 		metadataTracker,
+		lifecycle,
 		DefaultDataplaneStatusTracker(rt),
 	}
 
@@ -65,8 +67,9 @@ func SetupServer(rt core_runtime.Runtime) error {
 		&grpcServer{srv, rt.Config().XdsServer.GrpcPort, rt.Config().XdsServer.TlsCertFile, rt.Config().XdsServer.TlsKeyFile},
 		// bootstrap server
 		&xds_bootstrap.BootstrapServer{
-			Port:      rt.Config().BootstrapServer.Port,
-			Generator: xds_bootstrap.NewDefaultBootstrapGenerator(rt.ResourceManager(), rt.Config().BootstrapServer.Params, rt.Config().XdsServer.TlsCertFile),
+			Port: rt.Config().BootstrapServer.Port,
+			Generator: xds_bootstrap.NewDefaultBootstrapGenerator(rt.ResourceManager(),
+				rt.Config().BootstrapServer.Params, rt.Config().XdsServer.TlsCertFile, rt.Config().Environment),
 		},
 	)
 }
diff --git a/pkg/xds/server/dataplane_lifecycle.go b/pkg/xds/server/dataplane_lifecycle.go
new file mode 100644
index 000000000..54e12672f
--- /dev/null
+++ b/pkg/xds/server/dataplane_lifecycle.go
@@ -0,0 +1,113 @@
+package server
+
+import (
+	"context"
+	"sync"
+
+	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	go_cp_server "github.com/envoyproxy/go-control-plane/pkg/server/v2"
+	"github.com/pkg/errors"
+
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	"github.com/kumahq/kuma/pkg/core/resources/manager"
+	"github.com/kumahq/kuma/pkg/core/resources/model"
+	"github.com/kumahq/kuma/pkg/core/resources/store"
+	"github.com/kumahq/kuma/pkg/core/xds"
+)
+
+// DataplaneLifecycle is responsible for creating a deleting dataplanes that are passed through metadata
+// There are two possible workflows
+// 1) apply Dataplane resource before kuma-dp run and run kuma-dp
+// 2) run kuma-dp and pass Dataplane resource as an argument to kuma-dp
+// This component support second use case. When user passes Dataplane to kuma-dp it is attached to bootstrap request.
+// Then, bootstrap server generates bootstrap configuration with Dataplane embedded in Envoy metadata.
+// Here, we read Dataplane resource from metadata and a create resource on first DiscoveryRequest and remove on StreamClosed.
+//
+// This flow is optional, you may still want to go with 1. an example of this is Kubernetes deployment.
+type DataplaneLifecycle struct {
+	resManager manager.ResourceManager
+	// createdDpForStream stores map from StreamID to created ResourceKey of Dataplane.
+	// we store nil values for streams without Dataplane in metadata to avoid accessing metadata with every DiscoveryRequest
+	createdDpForStream map[xds.StreamID]*model.ResourceKey
+	sync.RWMutex       // protects createdDpForStream
+}
+
+var _ go_cp_server.Callbacks = &DataplaneLifecycle{}
+
+func NewDataplaneLifecycle(resManager manager.ResourceManager) *DataplaneLifecycle {
+	return &DataplaneLifecycle{
+		resManager:         resManager,
+		createdDpForStream: map[xds.StreamID]*model.ResourceKey{},
+	}
+}
+
+func (d *DataplaneLifecycle) OnStreamOpen(_ context.Context, _ int64, _ string) error {
+	return nil
+}
+
+func (d *DataplaneLifecycle) OnStreamClosed(streamID int64) {
+	d.Lock()
+	defer d.Unlock()
+	key := d.createdDpForStream[streamID]
+	delete(d.createdDpForStream, streamID)
+	if key != nil {
+		xdsServerLog.Info("unregistering dataplane", "dataplaneKey", key, "streamID", streamID)
+		if err := d.unregisterDataplane(*key); err != nil {
+			xdsServerLog.Error(err, "could not unregister dataplane")
+		}
+	}
+}
+
+func (d *DataplaneLifecycle) OnStreamRequest(streamID int64, request *envoy_api_v2.DiscoveryRequest) error {
+	if request.Node == nil { // Only the first request on a stream is guaranteed to carry the node identifier.
+		return nil
+	}
+
+	if d.streamProcessed(streamID) {
+		return nil
+	}
+
+	d.Lock()
+	defer d.Unlock()
+	md := xds.DataplaneMetadataFromNode(request.Node)
+	if md.DataplaneResource != nil {
+		xdsServerLog.Info("registering dataplane", "dataplane", md.DataplaneResource, "streamID", streamID, "nodeID", request.Node.Id)
+		if err := d.registerDataplane(md.DataplaneResource); err != nil {
+			return errors.Wrap(err, "could not register dataplane passed in kuma-dp run")
+		}
+		key := model.MetaToResourceKey(md.DataplaneResource.GetMeta())
+		d.createdDpForStream[streamID] = &key
+	} else {
+		d.createdDpForStream[streamID] = nil // put nil so we don't have to read metadata every time
+	}
+	return nil
+}
+
+func (d *DataplaneLifecycle) streamProcessed(streamID int64) bool {
+	d.RLock()
+	defer d.RUnlock()
+	_, ok := d.createdDpForStream[streamID]
+	return ok
+}
+
+func (d *DataplaneLifecycle) registerDataplane(dp *core_mesh.DataplaneResource) error {
+	key := model.MetaToResourceKey(dp.GetMeta())
+	existing := &core_mesh.DataplaneResource{}
+	return manager.Upsert(d.resManager, key, existing, func(resource model.Resource) {
+		_ = existing.SetSpec(dp.GetSpec()) // ignore error because the spec type is the same
+	})
+}
+
+func (d *DataplaneLifecycle) unregisterDataplane(key model.ResourceKey) error {
+	return d.resManager.Delete(context.Background(), &core_mesh.DataplaneResource{}, store.DeleteBy(key))
+}
+
+func (d *DataplaneLifecycle) OnStreamResponse(_ int64, _ *envoy_api_v2.DiscoveryRequest, _ *envoy_api_v2.DiscoveryResponse) {
+}
+
+func (d *DataplaneLifecycle) OnFetchRequest(_ context.Context, _ *envoy_api_v2.DiscoveryRequest) error {
+	return nil
+}
+
+func (d *DataplaneLifecycle) OnFetchResponse(request *envoy_api_v2.DiscoveryRequest, response *envoy_api_v2.DiscoveryResponse) {
+}
diff --git a/pkg/xds/server/dataplane_status_sink.go b/pkg/xds/server/dataplane_status_sink.go
index d6290ee82..9f737b938 100644
--- a/pkg/xds/server/dataplane_status_sink.go
+++ b/pkg/xds/server/dataplane_status_sink.go
@@ -1,7 +1,6 @@
 package server
 
 import (
-	"context"
 	"time"
 
 	"github.com/golang/protobuf/proto"
@@ -10,7 +9,6 @@ import (
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	"github.com/kumahq/kuma/pkg/core/resources/manager"
 	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
-	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
 )
 
 type DataplaneInsightSink interface {
@@ -42,14 +40,25 @@ func (s *dataplaneInsightSink) Start(stop <-chan struct{}) {
 
 	var lastStoredState *mesh_proto.DiscoverySubscription
 
-	flush := func() {
+	flush := func(closing bool) {
 		dataplaneId, currentState := s.accessor.GetStatus()
 		if proto.Equal(currentState, lastStoredState) {
 			return
 		}
 		copy := proto.Clone(currentState).(*mesh_proto.DiscoverySubscription)
 		if err := s.store.Upsert(dataplaneId, copy); err != nil {
-			xdsServerLog.Error(err, "failed to flush Dataplane status", "dataplaneid", dataplaneId)
+			if closing {
+				// When XDS stream is closed, Dataplane Status Tracker executes OnStreamClose which closes stop channel
+				// The problem is that close() does not wait for this sink to do it's final work
+				// In the meantime Dataplane Lifecycle executes OnStreamClose which can remove Dataplane entity (and Insights due to ownership). Therefore both scenarios can happen:
+				// 1) upsert fail because it successfully retrieved DataplaneInsight but cannot Update because by this time, Insight is gone (ResourceConflict error)
+				// 2) upsert fail because it tries to create a new insight, but there is no Dataplane so ownership returns an error
+				// We could build a synchronous mechanism that waits for Sink to be stopped before moving on to next Callbacks, but this is potentially dangerous
+				// that we could block waiting for storage instead of executing next callbacks.
+				xdsServerLog.V(1).Info("failed to flush Dataplane status on stream close. It can happen when Dataplane is deleted at the same time", "dataplaneid", dataplaneId, "err", err)
+			} else {
+				xdsServerLog.Error(err, "failed to flush Dataplane status", "dataplaneid", dataplaneId)
+			}
 		} else {
 			xdsServerLog.V(1).Info("saved Dataplane status", "dataplaneid", dataplaneId, "subscription", currentState)
 			lastStoredState = currentState
@@ -59,9 +68,9 @@ func (s *dataplaneInsightSink) Start(stop <-chan struct{}) {
 	for {
 		select {
 		case <-ticker.C:
-			flush()
+			flush(false)
 		case <-stop:
-			flush()
+			flush(true)
 			return
 		}
 	}
@@ -78,20 +87,8 @@ type dataplaneInsightStore struct {
 }
 
 func (s *dataplaneInsightStore) Upsert(dataplaneId core_model.ResourceKey, subscription *mesh_proto.DiscoverySubscription) error {
-	create := false
-	dataplaneInsight := &mesh_core.DataplaneInsightResource{}
-	err := s.resManager.Get(context.Background(), dataplaneInsight, core_store.GetBy(dataplaneId))
-	if err != nil {
-		if core_store.IsResourceNotFound(err) {
-			create = true
-		} else {
-			return err
-		}
-	}
-	dataplaneInsight.Spec.UpdateSubscription(subscription)
-	if create {
-		return s.resManager.Create(context.Background(), dataplaneInsight, core_store.CreateBy(dataplaneId))
-	} else {
-		return s.resManager.Update(context.Background(), dataplaneInsight)
-	}
+	insight := &mesh_core.DataplaneInsightResource{}
+	return manager.Upsert(s.resManager, dataplaneId, insight, func(resource core_model.Resource) {
+		insight.Spec.UpdateSubscription(subscription)
+	})
 }


Test patch:
diff --git a/app/kuma-dp/cmd/run_test.go b/app/kuma-dp/cmd/run_test.go
index 141017e6b..62d627b4f 100644
--- a/app/kuma-dp/cmd/run_test.go
+++ b/app/kuma-dp/cmd/run_test.go
@@ -13,6 +13,8 @@ import (
 	"strings"
 	"syscall"
 
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+
 	envoy_bootstrap "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v2"
 	"github.com/golang/protobuf/proto"
 	. "github.com/onsi/ginkgo"
@@ -54,7 +56,7 @@ var _ = Describe("run", func() {
 		backupSetupSignalHandler = core.SetupSignalHandler
 		backupBootstrapGenerator = bootstrapGenerator
 		backupCatalogClientFactory = catalogClientFactory
-		bootstrapGenerator = func(_ string, cfg kumadp.Config) (proto.Message, error) {
+		bootstrapGenerator = func(_ string, cfg kumadp.Config, _ *rest.Resource) (proto.Message, error) {
 			bootstrap := envoy_bootstrap.Bootstrap{}
 			respBytes, err := ioutil.ReadFile(filepath.Join("testdata", "bootstrap-config.golden.yaml"))
 			Expect(err).ToNot(HaveOccurred())
@@ -313,6 +315,21 @@ var _ = Describe("run", func() {
 				expectedFile: "",
 			}
 		}),
+		Entry("can be launched with dataplane template", func() testCase {
+			return testCase{
+				envVars: map[string]string{},
+				args: []string{
+					"--cp-address", "http://localhost:1234",
+					"--admin-port", fmt.Sprintf("%d", port),
+					"--binary-path", filepath.Join("testdata", "envoy-mock.sleep.sh"),
+					"--dataplane-token-file", filepath.Join("testdata", "token"),
+					"--dataplane-file", filepath.Join("testdata", "dataplane_template.yaml"),
+					"--dataplane-var", "name=example",
+					"--dataplane-var", "address=127.0.0.1",
+				},
+				expectedFile: "",
+			}
+		}),
 	)
 
 	It("should fail when dataplane token server is enabled but token is not provided", func() {
diff --git a/app/kuma-dp/cmd/testdata/dataplane_template.yaml b/app/kuma-dp/cmd/testdata/dataplane_template.yaml
new file mode 100644
index 000000000..a836b7e1e
--- /dev/null
+++ b/app/kuma-dp/cmd/testdata/dataplane_template.yaml
@@ -0,0 +1,15 @@
+type: Dataplane
+mesh: default
+name: {{ name }}
+networking:
+  address: {{ address }}
+  inbound:
+    - port: 11011
+      servicePort: 11012
+      tags:
+        kuma.io/service: backend
+        kuma.io/protocol: http
+  outbound:
+    - port: 33033
+      tags:
+        kuma.io/service: redis
diff --git a/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go b/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
index 7d9e4bdfe..a5b68d047 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/envoy_test.go
@@ -21,6 +21,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
 )
 
 var _ = Describe("Envoy", func() {
@@ -72,7 +73,7 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config) (proto.Message, error) {
+			sampleConfig := func(string, kuma_dp.Config, *rest.Resource) (proto.Message, error) {
 				return &envoy_bootstrap.Bootstrap{
 					Node: &envoy_core.Node{
 						Id: "example",
@@ -143,7 +144,7 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config) (proto.Message, error) {
+			sampleConfig := func(string, kuma_dp.Config, *rest.Resource) (proto.Message, error) {
 				return &envoy_bootstrap.Bootstrap{}, nil
 			}
 
@@ -185,7 +186,7 @@ var _ = Describe("Envoy", func() {
 					ConfigDir:  configDir,
 				},
 			}
-			sampleConfig := func(string, kuma_dp.Config) (proto.Message, error) {
+			sampleConfig := func(string, kuma_dp.Config, *rest.Resource) (proto.Message, error) {
 				return &envoy_bootstrap.Bootstrap{}, nil
 			}
 
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 84d9c5fb2..37a97ff4e 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
@@ -14,6 +14,8 @@ import (
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+
 	kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
 	config_types "github.com/kumahq/kuma/pkg/config/types"
 )
@@ -22,6 +24,7 @@ var _ = Describe("Remote Bootstrap", func() {
 
 	type testCase struct {
 		config                   kuma_dp.Config
+		dataplane                *rest.Resource
 		expectedBootstrapRequest string
 	}
 
@@ -48,7 +51,7 @@ var _ = Describe("Remote Bootstrap", func() {
 		generator := NewRemoteBootstrapGenerator(http.DefaultClient)
 
 		// when
-		config, err := generator(fmt.Sprintf("http://localhost:%d", port), given.config)
+		config, err := generator(fmt.Sprintf("http://localhost:%d", port), given.config, given.dataplane)
 
 		// then
 		Expect(err).ToNot(HaveOccurred())
@@ -64,12 +67,20 @@ var _ = Describe("Remote Bootstrap", func() {
 
 				return testCase{
 					config: cfg,
+					dataplane: &rest.Resource{
+						Meta: rest.ResourceMeta{
+							Type: "Dataplane",
+							Mesh: "demo",
+							Name: "sample",
+						},
+					},
 					expectedBootstrapRequest: `
                     {
                       "mesh": "demo",
                       "name": "sample",
                       "adminPort": 4321,
-                      "dataplaneTokenPath": "/tmp/token"
+                      "dataplaneTokenPath": "/tmp/token",
+                      "dataplaneResource": "{\"type\":\"Dataplane\",\"mesh\":\"demo\",\"name\":\"sample\",\"creationTime\":\"0001-01-01T00:00:00Z\",\"modificationTime\":\"0001-01-01T00:00:00Z\"}"
                     }
 `,
 				}
@@ -85,12 +96,20 @@ var _ = Describe("Remote Bootstrap", func() {
 
 				return testCase{
 					config: cfg,
+					dataplane: &rest.Resource{
+						Meta: rest.ResourceMeta{
+							Type: "Dataplane",
+							Mesh: "demo",
+							Name: "sample",
+						},
+					},
 					expectedBootstrapRequest: `
                     {
                       "mesh": "demo",
                       "name": "sample",
                       "adminPort": 4321,
-                      "dataplaneTokenPath": "/tmp/token"
+                      "dataplaneTokenPath": "/tmp/token",
+                      "dataplaneResource": "{\"type\":\"Dataplane\",\"mesh\":\"demo\",\"name\":\"sample\",\"creationTime\":\"0001-01-01T00:00:00Z\",\"modificationTime\":\"0001-01-01T00:00:00Z\"}"
                     }
 `,
 				}
@@ -105,11 +124,19 @@ var _ = Describe("Remote Bootstrap", func() {
 
 				return testCase{
 					config: cfg,
+					dataplane: &rest.Resource{
+						Meta: rest.ResourceMeta{
+							Type: "Dataplane",
+							Mesh: "demo",
+							Name: "sample",
+						},
+					},
 					expectedBootstrapRequest: `
                     {
                       "mesh": "demo",
                       "name": "sample",
-                      "dataplaneTokenPath": "/tmp/token"
+                      "dataplaneTokenPath": "/tmp/token",
+                      "dataplaneResource": "{\"type\":\"Dataplane\",\"mesh\":\"demo\",\"name\":\"sample\",\"creationTime\":\"0001-01-01T00:00:00Z\",\"modificationTime\":\"0001-01-01T00:00:00Z\"}"
                     }
 `,
 				}
@@ -143,7 +170,13 @@ var _ = Describe("Remote Bootstrap", func() {
 		// when
 		cfg := kuma_dp.DefaultConfig()
 		cfg.ControlPlane.BootstrapServer.Retry.Backoff = 10 * time.Millisecond
-		_, err = generator(fmt.Sprintf("http://localhost:%d", port), cfg)
+		_, err = generator(fmt.Sprintf("http://localhost:%d", port), cfg, &rest.Resource{
+			Meta: rest.ResourceMeta{
+				Type: "Dataplane",
+				Mesh: "default",
+				Name: "dp-1",
+			},
+		})
 
 		// then
 		Expect(err).ToNot(HaveOccurred())
@@ -169,7 +202,9 @@ var _ = Describe("Remote Bootstrap", func() {
 		config := kuma_dp.DefaultConfig()
 		config.ControlPlane.BootstrapServer.Retry.Backoff = 10 * time.Millisecond
 		config.ControlPlane.BootstrapServer.Retry.MaxDuration = 100 * time.Millisecond
-		_, err = generator(fmt.Sprintf("http://localhost:%d", port), config)
+		_, err = generator(fmt.Sprintf("http://localhost:%d", port), config, &rest.Resource{
+			Meta: rest.ResourceMeta{Mesh: "default", Name: "dp-1"},
+		})
 
 		// 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/app/kumactl/cmd/apply/apply_test.go b/app/kumactl/cmd/apply/apply_test.go
index 59345032c..b3b9c3188 100644
--- a/app/kumactl/cmd/apply/apply_test.go
+++ b/app/kumactl/cmd/apply/apply_test.go
@@ -373,9 +373,9 @@ var _ = Describe("kumactl apply", func() {
 
 		// then
 		Expect(buf.String()).To(Equal(
-			`creationTime: "1970-01-01T00:00:00Z"
+			`creationTime: "0001-01-01T00:00:00Z"
 mesh: default
-modificationTime: "1970-01-01T00:00:00Z"
+modificationTime: "0001-01-01T00:00:00Z"
 name: sample
 networking:
   address: 2.2.2.2
@@ -431,14 +431,14 @@ type: Dataplane
 type: Dataplane
 name: dp-1
 `,
-			err: "YAML contains invalid resource: Mesh field cannot be empty",
+			err: "mesh: cannot be empty",
 		}),
 		Entry("no name", testCase{
 			resource: `
 type: Dataplane
 mesh: default
 `,
-			err: "YAML contains invalid resource: Name field cannot be empty",
+			err: "name: cannot be empty",
 		}),
 		Entry("invalid data", testCase{
 			resource: `
diff --git a/app/kumactl/cmd/inspect/testdata/inspect-zone.golden.yaml b/app/kumactl/cmd/inspect/testdata/inspect-zone.golden.yaml
index 02998cbd5..24256c387 100644
--- a/app/kumactl/cmd/inspect/testdata/inspect-zone.golden.yaml
+++ b/app/kumactl/cmd/inspect/testdata/inspect-zone.golden.yaml
@@ -1,6 +1,5 @@
 items:
   - creationTime: "2018-07-17T16:05:36.995Z"
-    mesh: default
     modificationTime: "2019-07-17T18:08:41Z"
     name: zone-1
     type: ZoneOverview
@@ -58,7 +57,6 @@ items:
               responsesRejected: "2"
               responsesSent: "20"
   - creationTime: "2018-07-17T16:05:36.995Z"
-    mesh: default
     modificationTime: "2019-07-17T18:08:41Z"
     name: zone-2
     type: ZoneOverview
diff --git a/app/kumactl/cmd/inspect/testdata/inspect-zones.golden.json b/app/kumactl/cmd/inspect/testdata/inspect-zones.golden.json
index 22b6d1836..19f723d0e 100644
--- a/app/kumactl/cmd/inspect/testdata/inspect-zones.golden.json
+++ b/app/kumactl/cmd/inspect/testdata/inspect-zones.golden.json
@@ -3,7 +3,6 @@
   "items": [
     {
       "type": "ZoneOverview",
-      "mesh": "default",
       "name": "zone-1",
       "creationTime": "2018-07-17T16:05:36.995Z",
       "modificationTime": "2019-07-17T18:08:41Z",
@@ -87,7 +86,6 @@
     },
     {
       "type": "ZoneOverview",
-      "mesh": "default",
       "name": "zone-2",
       "creationTime": "2018-07-17T16:05:36.995Z",
       "modificationTime": "2019-07-17T18:08:41Z",
diff --git a/pkg/api-server/zone_overview_endpoints_test.go b/pkg/api-server/zone_overview_endpoints_test.go
index 3c4f12722..617c3ee2e 100644
--- a/pkg/api-server/zone_overview_endpoints_test.go
+++ b/pkg/api-server/zone_overview_endpoints_test.go
@@ -92,7 +92,6 @@ var _ = Describe("Zone Overview Endpoints", func() {
 	zone1Json := `
 {
  "type": "ZoneOverview",
- "mesh": "default",
  "name": "zone-1",
  "creationTime": "2018-07-17T16:05:36.995Z",
  "modificationTime": "2018-07-17T16:05:36.995Z",
@@ -118,7 +117,6 @@ var _ = Describe("Zone Overview Endpoints", func() {
 	zone2Json := `
 {
  "type": "ZoneOverview",
- "mesh": "default",
  "name": "zone-2",
  "creationTime": "2018-07-17T16:05:36.995Z",
  "modificationTime": "2018-07-17T16:05:36.995Z",
@@ -144,7 +142,6 @@ var _ = Describe("Zone Overview Endpoints", func() {
 	zone3Json := `
 {
  "type": "ZoneOverview",
- "mesh": "default",
  "name": "zone-3",
  "creationTime": "2018-07-17T16:05:36.995Z",
  "modificationTime": "2018-07-17T16:05:36.995Z",
diff --git a/pkg/core/managers/apis/dataplane/dataplane_manager_test.go b/pkg/core/managers/apis/dataplane/dataplane_manager_test.go
index d3434fe6e..e9079148c 100644
--- a/pkg/core/managers/apis/dataplane/dataplane_manager_test.go
+++ b/pkg/core/managers/apis/dataplane/dataplane_manager_test.go
@@ -20,6 +20,8 @@ var _ = Describe("Dataplane Manager", func() {
 		// setup
 		s := memory.NewStore()
 		manager := dataplane.NewDataplaneManager(s, "zone-1")
+		err := s.Create(context.Background(), &mesh_core.MeshResource{}, store.CreateByKey("default", "default"))
+		Expect(err).ToNot(HaveOccurred())
 
 		// given
 		input := mesh_core.DataplaneResource{
@@ -40,7 +42,7 @@ var _ = Describe("Dataplane Manager", func() {
 		}
 
 		// when
-		err := manager.Create(context.Background(), &input, store.CreateByKey("dp1", "default"))
+		err = manager.Create(context.Background(), &input, store.CreateByKey("dp1", "default"))
 		Expect(err).ToNot(HaveOccurred())
 
 		actual := mesh_core.DataplaneResource{}
@@ -56,6 +58,8 @@ var _ = Describe("Dataplane Manager", func() {
 		// setup
 		s := memory.NewStore()
 		manager := dataplane.NewDataplaneManager(s, "zone-1")
+		err := s.Create(context.Background(), &mesh_core.MeshResource{}, store.CreateByKey("default", "default"))
+		Expect(err).ToNot(HaveOccurred())
 
 		// given
 		input := mesh_core.DataplaneResource{
@@ -75,7 +79,7 @@ var _ = Describe("Dataplane Manager", func() {
 			},
 		}
 
-		err := s.Create(context.Background(), &input, store.CreateByKey("dp1", "default"))
+		err = s.Create(context.Background(), &input, store.CreateByKey("dp1", "default"))
 		Expect(err).ToNot(HaveOccurred())
 
 		actual := mesh_core.DataplaneResource{}
@@ -102,6 +106,8 @@ var _ = Describe("Dataplane Manager", func() {
 		// setup
 		s := memory.NewStore()
 		manager := dataplane.NewDataplaneManager(s, "zone-1")
+		err := s.Create(context.Background(), &mesh_core.MeshResource{}, store.CreateByKey("default", "default"))
+		Expect(err).ToNot(HaveOccurred())
 
 		// given
 		input := mesh_core.DataplaneResource{
@@ -118,7 +124,7 @@ var _ = Describe("Dataplane Manager", func() {
 		}
 
 		// when
-		err := manager.Create(context.Background(), &input, store.CreateByKey("dp1", "default"))
+		err = manager.Create(context.Background(), &input, store.CreateByKey("dp1", "default"))
 		Expect(err).ToNot(HaveOccurred())
 
 		actual := mesh_core.DataplaneResource{}
@@ -134,6 +140,8 @@ var _ = Describe("Dataplane Manager", func() {
 		// setup
 		s := memory.NewStore()
 		manager := dataplane.NewDataplaneManager(s, "zone-1")
+		err := s.Create(context.Background(), &mesh_core.MeshResource{}, store.CreateByKey("default", "default"))
+		Expect(err).ToNot(HaveOccurred())
 
 		// given
 		input := mesh_core.DataplaneResource{
@@ -149,7 +157,7 @@ var _ = Describe("Dataplane Manager", func() {
 			},
 		}
 
-		err := s.Create(context.Background(), &input, store.CreateByKey("dp1", "default"))
+		err = s.Create(context.Background(), &input, store.CreateByKey("dp1", "default"))
 		Expect(err).ToNot(HaveOccurred())
 
 		actual := mesh_core.DataplaneResource{}
diff --git a/pkg/core/resources/apis/mesh/dataplane_helpers_test.go b/pkg/core/resources/apis/mesh/dataplane_helpers_test.go
index 59193dac0..021eb4d79 100644
--- a/pkg/core/resources/apis/mesh/dataplane_helpers_test.go
+++ b/pkg/core/resources/apis/mesh/dataplane_helpers_test.go
@@ -485,5 +485,4 @@ var _ = Describe("ParseProtocol()", func() {
 			expected: ProtocolUnknown,
 		}),
 	)
-
 })
diff --git a/pkg/test/resources/apis/sample/sample_types.go b/pkg/test/resources/apis/sample/sample_types.go
index ff7b1685c..03e42e3af 100644
--- a/pkg/test/resources/apis/sample/sample_types.go
+++ b/pkg/test/resources/apis/sample/sample_types.go
@@ -49,6 +49,10 @@ func (t *TrafficRouteResource) Validate() error {
 	return err.OrNil()
 }
 
+func (t *TrafficRouteResource) Scope() model.ResourceScope {
+	return model.ScopeMesh
+}
+
 var _ model.ResourceList = &TrafficRouteResourceList{}
 
 type TrafficRouteResourceList struct {
diff --git a/pkg/xds/bootstrap/generator_test.go b/pkg/xds/bootstrap/generator_test.go
index 054bb1dd6..a399c1fbc 100644
--- a/pkg/xds/bootstrap/generator_test.go
+++ b/pkg/xds/bootstrap/generator_test.go
@@ -6,6 +6,10 @@ import (
 	"path/filepath"
 	"time"
 
+	"github.com/kumahq/kuma/pkg/core"
+
+	config_core "github.com/kumahq/kuma/pkg/config/core"
+
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
@@ -28,6 +32,10 @@ var _ = Describe("bootstrapGenerator", func() {
 
 	BeforeEach(func() {
 		resManager = core_manager.NewResourceManager(memory.NewStore())
+		core.Now = func() time.Time {
+			now, _ := time.Parse(time.RFC3339, "2018-07-17T16:05:36.995+00:00")
+			return now
+		}
 	})
 
 	BeforeEach(func() {
@@ -69,7 +77,7 @@ var _ = Describe("bootstrapGenerator", func() {
 	DescribeTable("should generate bootstrap configuration",
 		func(given testCase) {
 			// setup
-			generator := NewDefaultBootstrapGenerator(resManager, given.config(), "")
+			generator := NewDefaultBootstrapGenerator(resManager, given.config(), "", config_core.KubernetesEnvironment)
 
 			// when
 			bootstrapConfig, err := generator.Generate(context.Background(), given.request)
@@ -150,6 +158,27 @@ var _ = Describe("bootstrapGenerator", func() {
 				Name:               "name.namespace",
 				AdminPort:          1234,
 				DataplaneTokenPath: "/tmp/token",
+				DataplaneResource: `
+{
+  "type": "Dataplane",
+  "mesh": "mesh",
+  "name": "name.namespace",
+  "creationTime": "1970-01-01T00:00:00Z",
+  "modificationTime": "1970-01-01T00:00:00Z",
+  "networking": {
+    "address": "127.0.0.1",
+    "inbound": [
+      {
+        "port": 22022,
+        "servicePort": 8443,
+        "tags": {
+          "kuma.io/protocol": "http2",
+          "kuma.io/service": "backend"
+        }
+      },
+    ]
+  }
+}`,
 			},
 			expectedConfigFile: "generator.custom-config.golden.yaml",
 		}),
@@ -198,7 +227,7 @@ var _ = Describe("bootstrapGenerator", func() {
 		params.XdsHost = "127.0.0.1"
 		params.XdsPort = 5678
 
-		generator := NewDefaultBootstrapGenerator(resManager, params, "")
+		generator := NewDefaultBootstrapGenerator(resManager, params, "", config_core.KubernetesEnvironment)
 		request := types.BootstrapRequest{
 			Mesh:      "mesh",
 			Name:      "name-1.namespace",
@@ -262,7 +291,7 @@ var _ = Describe("bootstrapGenerator", func() {
 		params.XdsHost = "127.0.0.1"
 		params.XdsPort = 5678
 
-		generator := NewDefaultBootstrapGenerator(resManager, params, "")
+		generator := NewDefaultBootstrapGenerator(resManager, params, "", config_core.KubernetesEnvironment)
 		request := types.BootstrapRequest{
 			Mesh:      "mesh",
 			Name:      "name-3.namespace",
diff --git a/pkg/xds/bootstrap/server_test.go b/pkg/xds/bootstrap/server_test.go
index cbc3f330b..f97ddb5be 100644
--- a/pkg/xds/bootstrap/server_test.go
+++ b/pkg/xds/bootstrap/server_test.go
@@ -7,6 +7,11 @@ import (
 	"path/filepath"
 	"strconv"
 	"strings"
+	"time"
+
+	"github.com/kumahq/kuma/pkg/core"
+
+	config_core "github.com/kumahq/kuma/pkg/config/core"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
@@ -39,7 +44,7 @@ var _ = Describe("Bootstrap Server", func() {
 		Expect(err).ToNot(HaveOccurred())
 		server := BootstrapServer{
 			Port:      uint32(port),
-			Generator: NewDefaultBootstrapGenerator(resManager, config, ""),
+			Generator: NewDefaultBootstrapGenerator(resManager, config, "", config_core.KubernetesEnvironment),
 		}
 		stop = make(chan struct{})
 		go func() {
@@ -64,6 +69,10 @@ var _ = Describe("Bootstrap Server", func() {
 	BeforeEach(func() {
 		err := resManager.Create(context.Background(), &mesh.MeshResource{}, store.CreateByKey("default", "default"))
 		Expect(err).ToNot(HaveOccurred())
+		core.Now = func() time.Time {
+			now, _ := time.Parse(time.RFC3339, "2018-07-17T16:05:36.995+00:00")
+			return now
+		}
 	})
 
 	type testCase struct {
diff --git a/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml b/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
index a2dee6f2c..03c67cea2 100644
--- a/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
@@ -51,4 +51,4 @@ staticResources:
       name: access_log_sink
       type: STATIC
       upstreamConnectionOptions:
-        tcpKeepalive: {}
\ No newline at end of file
+        tcpKeepalive: {}
diff --git a/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml b/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
index 9975486c4..a0d4f2a6d 100644
--- a/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
@@ -51,4 +51,4 @@ staticResources:
       name: access_log_sink
       type: STATIC
       upstreamConnectionOptions:
-        tcpKeepalive: {}
\ No newline at end of file
+        tcpKeepalive: {}
diff --git a/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml b/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
index 95ffae88f..5d2b17467 100644
--- a/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
@@ -19,6 +19,7 @@ node:
   id: mesh.name.namespace
   metadata:
     dataplane.admin.port: "1234"
+    dataplane.resource: ' { "type": "Dataplane", "mesh": "mesh", "name": "name.namespace", "creationTime": "1970-01-01T00:00:00Z", "modificationTime": "1970-01-01T00:00:00Z", "networking": { "address": "127.0.0.1", "inbound": [ { "port": 22022, "servicePort": 8443, "tags": { "kuma.io/protocol": "http2", "kuma.io/service": "backend" } }, ] } }'
     dataplaneTokenPath: /tmp/token
 statsConfig:
   statsTags:
diff --git a/pkg/xds/server/dataplane_lifecycle_test.go b/pkg/xds/server/dataplane_lifecycle_test.go
new file mode 100644
index 000000000..df962ceee
--- /dev/null
+++ b/pkg/xds/server/dataplane_lifecycle_test.go
@@ -0,0 +1,132 @@
+package server_test
+
+import (
+	"context"
+
+	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	pstruct "github.com/golang/protobuf/ptypes/struct"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_manager "github.com/kumahq/kuma/pkg/core/resources/manager"
+	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
+	"github.com/kumahq/kuma/pkg/plugins/resources/memory"
+	"github.com/kumahq/kuma/pkg/test/resources/model"
+	"github.com/kumahq/kuma/pkg/xds/server"
+)
+
+var _ = Describe("Dataplane Lifecycle", func() {
+
+	var resManager core_manager.ResourceManager
+	var dpLifecycle *server.DataplaneLifecycle
+
+	BeforeEach(func() {
+		store := memory.NewStore()
+		resManager = core_manager.NewResourceManager(store)
+		dpLifecycle = server.NewDataplaneLifecycle(resManager)
+
+		err := resManager.Create(context.Background(), &core_mesh.MeshResource{}, core_store.CreateByKey("default", "default"))
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	It("should create a DP on the first DiscoveryRequest when it is carried with metadata and delete on stream close", func() {
+		// given
+		req := v2.DiscoveryRequest{
+			Node: &envoy_core.Node{
+				Id: "default.backend-01",
+				Metadata: &pstruct.Struct{
+					Fields: map[string]*pstruct.Value{
+						"dataplane.resource": &pstruct.Value{
+							Kind: &pstruct.Value_StringValue{
+								StringValue: `
+                                {
+                                  "type": "Dataplane",
+                                  "mesh": "default",
+                                  "name": "backend-01",
+                                  "networking": {
+                                    "address": "127.0.0.1",
+                                    "inbound": [
+                                      {
+                                        "port": 22022,
+                                        "servicePort": 8443,
+                                        "tags": {
+                                          "kuma.io/service": "backend"
+                                        }
+                                      },
+                                    ]
+                                  }
+                                }
+                                `,
+							},
+						},
+					},
+				},
+			},
+		}
+		const streamId = 123
+
+		// when
+		err := dpLifecycle.OnStreamRequest(streamId, &req)
+
+		// then dp is created
+		Expect(err).ToNot(HaveOccurred())
+		err = resManager.Get(context.Background(), &core_mesh.DataplaneResource{}, core_store.GetByKey("backend-01", "default"))
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		dpLifecycle.OnStreamClosed(streamId)
+
+		// then dataplane should be deleted
+		err = resManager.Get(context.Background(), &core_mesh.DataplaneResource{}, core_store.GetByKey("backend-01", "default"))
+		Expect(core_store.IsResourceNotFound(err)).To(BeTrue())
+	})
+
+	It("should not delete DP when it is not carried in metadata", func() {
+		// given already created DP
+		dp := &core_mesh.DataplaneResource{
+			Meta: &model.ResourceMeta{
+				Mesh: "default",
+				Name: "backend-01",
+			},
+			Spec: mesh_proto.Dataplane{
+				Networking: &mesh_proto.Dataplane_Networking{
+					Address: "192.168.0.1",
+					Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
+						{
+							Port:        8080,
+							ServicePort: 8081,
+							Tags: map[string]string{
+								"kuma.io/service": "backend",
+							},
+						},
+					},
+				},
+			},
+		}
+		err := resManager.Create(context.Background(), dp, core_store.CreateByKey("backend-01", "default"))
+		Expect(err).ToNot(HaveOccurred())
+
+		req := v2.DiscoveryRequest{
+			Node: &envoy_core.Node{
+				Id: "default.backend-01",
+			},
+		}
+		const streamId = 123
+
+		// when
+		err = dpLifecycle.OnStreamRequest(streamId, &req)
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		dpLifecycle.OnStreamClosed(streamId)
+
+		// then DP is not deleted because it was not carried in metadata
+		err = resManager.Get(context.Background(), &core_mesh.DataplaneResource{}, core_store.GetByKey("backend-01", "default"))
+		Expect(err).ToNot(HaveOccurred())
+	})
+})
diff --git a/test/framework/universal_app.go b/test/framework/universal_app.go
index f8170010a..73e7d71eb 100644
--- a/test/framework/universal_app.go
+++ b/test/framework/universal_app.go
@@ -30,7 +30,7 @@ type: Dataplane
 mesh: default
 name: dp-ingress
 networking:
-  address: %s
+  address: {{ address }}
   ingress: {}
   inbound:
   - port: %d	
@@ -40,9 +40,9 @@ networking:
 	EchoServerDataplane = `
 type: Dataplane
 mesh: default
-name: %s
+name: {{ name }}
 networking:
-  address: %s
+  address:  {{ address }}
   inbound:
   - port: %s
     servicePort: %s
@@ -54,9 +54,9 @@ networking:
 	DemoClientDataplane = `
 type: Dataplane
 mesh: default
-name: %s
+name: {{ name }}
 networking:
-  address: %s
+  address: {{ address }}
   inbound:
   - port: %s
     servicePort: %s
@@ -64,9 +64,11 @@ networking:
       kuma.io/service: demo-client
   outbound:
   - port: 4000
-    service: echo-server_kuma-test_svc_%s
+    tags:
+      kuma.io/service: echo-server_kuma-test_svc_%s
   - port: 4001
-    service: echo-server_kuma-test_svc_%s
+    tags:
+      kuma.io/service: echo-server_kuma-test_svc_%s
 `
 )
 
@@ -202,19 +204,24 @@ func (s *UniversalApp) CreateMainApp(env []string, args []string) {
 	s.mainApp = NewSshApp(s.verbose, s.ports[sshPort], env, args)
 }
 
-func (s *UniversalApp) CreateDP(token, cpAddress, appname string) {
+func (s *UniversalApp) CreateDP(token, cpAddress, appname, ip, dpyaml string) {
 	// and echo it to the Application Node
 	err := NewSshApp(s.verbose, s.ports[sshPort], []string{}, []string{"printf ", "\"" + token + "\"", ">", "/kuma/token-" + appname}).Run()
 	if err != nil {
 		panic(err)
 	}
 
+	err = NewSshApp(s.verbose, s.ports[sshPort], []string{}, []string{"printf ", "\"" + dpyaml + "\"", ">", "/kuma/dpyaml-" + appname}).Run()
+	if err != nil {
+		panic(err)
+	}
 	// run the DP
 	s.dpApp = NewSshApp(s.verbose, s.ports[sshPort], []string{}, []string{"kuma-dp", "run",
-		"--name=dp-" + appname,
-		"--mesh=default",
 		"--cp-address=" + cpAddress,
 		"--dataplane-token-file=/kuma/token-" + appname,
+		"--dataplane-file=/kuma/dpyaml-" + appname,
+		"--dataplane-var", "name=dp-" + appname,
+		"--dataplane-var", "address=" + ip,
 		"--binary-path", "/usr/local/bin/envoy"})
 }
 
diff --git a/test/framework/universal_cluster.go b/test/framework/universal_cluster.go
index 4e6e263eb..193336aa6 100644
--- a/test/framework/universal_cluster.go
+++ b/test/framework/universal_cluster.go
@@ -92,8 +92,8 @@ func (c *UniversalCluster) DeployKuma(mode string, fs ...DeployOptionsFunc) erro
 
 	switch mode {
 	case core.Remote:
-		dpyaml := fmt.Sprintf(IngressDataplane, app.ip, kdsPort)
-		err = c.CreateDP(app, "ingress", dpyaml)
+		dpyaml := fmt.Sprintf(IngressDataplane, kdsPort)
+		err = c.CreateDP(app, "ingress", app.ip, dpyaml)
 		if err != nil {
 			return err
 		}
@@ -142,13 +142,7 @@ func (c *UniversalCluster) DeleteNamespace(namespace string) error {
 	return nil
 }
 
-func (c *UniversalCluster) CreateDP(app *UniversalApp, appname, dpyaml string) error {
-	// apply the dataplane
-	err := c.controlplane.kumactl.KumactlApplyFromString(dpyaml)
-	if err != nil {
-		return err
-	}
-
+func (c *UniversalCluster) CreateDP(app *UniversalApp, appname, ip, dpyaml string) error {
 	// generate the token on the CP node
 	sshApp := NewSshApp(c.verbose, c.apps[AppModeCP].ports["22"], []string{}, []string{"curl",
 		"-H", "\"Content-Type: application/json\"",
@@ -161,7 +155,7 @@ func (c *UniversalCluster) CreateDP(app *UniversalApp, appname, dpyaml string) e
 	token := sshApp.Out()
 
 	cpAddress := "http://" + c.apps[AppModeCP].ip + ":5681"
-	app.CreateDP(token, cpAddress, appname)
+	app.CreateDP(token, cpAddress, appname, ip, dpyaml)
 
 	return app.dpApp.Start()
 }
@@ -191,12 +185,12 @@ func (c *UniversalCluster) DeployApp(namespace, appname string) error {
 	dpyaml := ""
 	switch appname {
 	case AppModeEchoServer:
-		dpyaml = fmt.Sprintf(EchoServerDataplane, "dp-"+appname, ip, "8080", "80", "8080")
+		dpyaml = fmt.Sprintf(EchoServerDataplane, "8080", "80", "8080")
 	case AppModeDemoClient:
-		dpyaml = fmt.Sprintf(DemoClientDataplane, "dp-"+appname, ip, "13000", "3000", "80", "8080")
+		dpyaml = fmt.Sprintf(DemoClientDataplane, "13000", "3000", "80", "8080")
 	}
 
-	err = c.CreateDP(app, appname, dpyaml)
+	err = c.CreateDP(app, appname, ip, dpyaml)
 	if err != nil {
 		return err
 	}