Repository: metal3-io/baremetal-operator
Pull request #448 (2020-03-03 09:23:57)

Implement metadata interface for baremetal operator
Metadata is required for cloud-init config drive which is used by
Ironic. User should be able to create custom metadata in a secret and
reference it in a BareMetalHost object.

metal3-io-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/metal3-io/baremetal-operator/pull/448#" title="Author self-approved">dukov</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **russellb**
You can assign the PR to them by writing `/assign @russellb` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=metal3-io%2Fbaremetal-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/metal3-io/baremetal-operator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["russellb"]} -->
metal3-io-bot: Hi @dukov. Thanks for your PR.

I'm waiting for a [metal3-io](https://github.com/orgs/metal3-io/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://github.com/orgs/metal3-io/people) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=metal3-io%2Fbaremetal-operator).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

dukov: #447
dukov: #447 
dukov: /assign @russellb 
zaneb: /ok-to-test
metal3-io-bot: @dukov: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
govet | afd9062f866454d8c32689efb36c93fcdfae3260 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/448/govet/1234841860392882187) | `/test govet`



<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
maelk: /approve. I am putting together a design doc on how to handle metadata with CAPM3. I am including this there, so there probably is no need to create a dedicated design doc for it. I'll link the PR.
maelk: /approve
metal3-io-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/metal3-io/baremetal-operator/pull/448#" title="Author self-approved">dukov</a>*, *<a href="https://github.com/metal3-io/baremetal-operator/pull/448#issuecomment-597576871" title="Approved">maelk</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=metal3-io%2Fbaremetal-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/metal3-io/baremetal-operator/blob/master/OWNERS)~~ [maelk]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
maelk: /test-integration
dukov: > I am putting together a design doc on how to handle metadata with CAPM3. I am including this there, so there probably is no need to create a dedicated design doc for it. I'll link the PR.

Ok. Thank you.
maelk: /test-integration
maelk: @dhellmann Can we merge this ? The design doc is waiting for review. This change is straightforward and we need it as soon as possible.
maelk: This is the design doc btw https://github.com/metal3-io/metal3-docs/pull/79

maelk: /test-integration
maelk: The design proposal was accepted and merged. We should now proceed with merging this PR. @dukov could you please fix the conflict and we'll merge this. Thank you

Patch:
diff --git a/deploy/crds/metal3.io_baremetalhosts_crd.yaml b/deploy/crds/metal3.io_baremetalhosts_crd.yaml
index b4d599af..ed1b9aa7 100644
--- a/deploy/crds/metal3.io_baremetalhosts_crd.yaml
+++ b/deploy/crds/metal3.io_baremetalhosts_crd.yaml
@@ -156,9 +156,23 @@ spec:
               - checksum
               - url
               type: object
+            metaData:
+              description: MetaData holds the reference to the Secret containing host
+                metadata (e.g. meta_data.json which is passed to Config Drive).
+              properties:
+                name:
+                  description: Name is unique within a namespace to reference a secret
+                    resource.
+                  type: string
+                namespace:
+                  description: Namespace defines the space within which the secret
+                    name must be unique.
+                  type: string
+              type: object
             networkData:
               description: NetworkData holds the reference to the Secret containing
-                content of network_data.json which is passed to Config Drive
+                network configuration (e.g content of network_data.json which is passed
+                to Config Drive).
               properties:
                 name:
                   description: Name is unique within a namespace to reference a secret
diff --git a/docs/api.md b/docs/api.md
index 9f541d5d..cd38a21d 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -224,6 +224,12 @@ spec:
   userData:
     name: bmo-master-user-data
     namespace: bmo-project
+  networkData:
+    name: bmo-master-network-data
+    namespace: bmo-project
+  metaData:
+    name: bmo-master-meta-data
+    namespace: bmo-project
 status:
   errorMessage: ""
   goodCredentials:
diff --git a/go.mod b/go.mod
index a3f28354..f44d0185 100644
--- a/go.mod
+++ b/go.mod
@@ -12,13 +12,13 @@ require (
 	github.com/operator-framework/operator-sdk v0.15.1
 	github.com/pkg/errors v0.8.1
 	github.com/prometheus/client_golang v1.2.1
-	github.com/securego/gosec v0.0.0-20200203094520-d13bb6d2420c // indirect
+	github.com/securego/gosec v0.0.0-20200302134848-c998389da2ac // indirect
 	go.uber.org/multierr v1.4.0 // indirect
 	go.uber.org/zap v1.12.0 // indirect
 	golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a // indirect
-	golang.org/x/lint v0.0.0-20200130185559-910be7a94367 // indirect
+	golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
 	golang.org/x/net v0.0.0-20191109021931-daa7c04131f5 // indirect
-	golang.org/x/tools v0.0.0-20200226224502-204d844ad48d // indirect
+	golang.org/x/tools v0.0.0-20200302225559-9b52d559c609 // indirect
 	k8s.io/api v0.0.0
 	k8s.io/apimachinery v0.0.0
 	k8s.io/client-go v12.0.0+incompatible
diff --git a/go.sum b/go.sum
index 6b3bf946..353a6fbb 100644
--- a/go.sum
+++ b/go.sum
@@ -498,6 +498,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
 github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
+github.com/mozilla/tls-observatory v0.0.0-20200220173314-aae45faa4006/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
 github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/munnerz/goautoneg v0.0.0-20190414153302-2ae31c8b6b30/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
@@ -620,6 +621,8 @@ github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24
 github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
 github.com/securego/gosec v0.0.0-20200203094520-d13bb6d2420c h1:pThusIwnQVcKbuZSds3HgB/ODEqxMqZf/SgVp89JXY0=
 github.com/securego/gosec v0.0.0-20200203094520-d13bb6d2420c/go.mod h1:gp0gaHj0WlmPh9BdsTmo1aq6C27yIPWdxCKGFGdVKBE=
+github.com/securego/gosec v0.0.0-20200302134848-c998389da2ac h1:hLEDRUGWq/gqX5PrRK0WRJagL3IK4LAN9GSZeASlac4=
+github.com/securego/gosec v0.0.0-20200302134848-c998389da2ac/go.mod h1:NurAFZsWJAEZjogSwdVPlHkOZB3DOAU7gsPP8VFZCHc=
 github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@@ -749,10 +752,13 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNT
 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
 golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw=
 golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -883,6 +889,9 @@ golang.org/x/tools v0.0.0-20200203023011-6f24f261dadb h1:Mjk7HEiAvEl5eS8doSYHgS8
 golang.org/x/tools v0.0.0-20200203023011-6f24f261dadb/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200226224502-204d844ad48d h1:loGv/4fxITSrCD4t2P8ZF4oUC4RlRFDAsczcoUS2g6c=
 golang.org/x/tools v0.0.0-20200226224502-204d844ad48d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200228224639-71482053b885/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200302225559-9b52d559c609 h1:3/QY44rOqJoMLCsQz9bAgInYa08qsu+dH52Uk4DWH3w=
+golang.org/x/tools v0.0.0-20200302225559-9b52d559c609/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
diff --git a/pkg/apis/metal3/v1alpha1/baremetalhost_types.go b/pkg/apis/metal3/v1alpha1/baremetalhost_types.go
index e1c457e2..8e34d92c 100644
--- a/pkg/apis/metal3/v1alpha1/baremetalhost_types.go
+++ b/pkg/apis/metal3/v1alpha1/baremetalhost_types.go
@@ -179,10 +179,15 @@ type BareMetalHostSpec struct {
 	// data to be passed to the host before it boots.
 	UserData *corev1.SecretReference `json:"userData,omitempty"`
 
-	// NetworkData holds the reference to the Secret containing content
-	// of network_data.json which is passed to Config Drive
+	// NetworkData holds the reference to the Secret containing network
+	// configuration (e.g content of network_data.json which is passed
+	// to Config Drive).
 	NetworkData *corev1.SecretReference `json:"networkData,omitempty"`
 
+	// MetaData holds the reference to the Secret containing host metadata
+	// (e.g. meta_data.json which is passed to Config Drive).
+	MetaData *corev1.SecretReference `json:"metaData,omitempty"`
+
 	// Description is a human-entered text used to help identify the host
 	Description string `json:"description,omitempty"`
 
diff --git a/pkg/apis/metal3/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/metal3/v1alpha1/zz_generated.deepcopy.go
index c84bfe5b..f3bc79c6 100644
--- a/pkg/apis/metal3/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/metal3/v1alpha1/zz_generated.deepcopy.go
@@ -133,6 +133,11 @@ func (in *BareMetalHostSpec) DeepCopyInto(out *BareMetalHostSpec) {
 		*out = new(v1.SecretReference)
 		**out = **in
 	}
+	if in.MetaData != nil {
+		in, out := &in.MetaData, &out.MetaData
+		*out = new(v1.SecretReference)
+		**out = **in
+	}
 	return
 }
 
diff --git a/pkg/controller/baremetalhost/host_config_data.go b/pkg/controller/baremetalhost/host_config_data.go
index 6c8f6946..c6e5b89e 100644
--- a/pkg/controller/baremetalhost/host_config_data.go
+++ b/pkg/controller/baremetalhost/host_config_data.go
@@ -87,3 +87,20 @@ func (hcd *hostConfigData) NetworkData() (string, error) {
 		"networkData",
 	)
 }
+
+// MetaData get host metatdata
+func (hcd *hostConfigData) MetaData() (string, error) {
+	if hcd.host.Spec.MetaData == nil {
+		hcd.log.Info("MetaData is not set returning empty(nil) data")
+		return "", nil
+	}
+	namespace := hcd.host.Spec.MetaData.Namespace
+	if namespace == "" {
+		namespace = hcd.host.Namespace
+	}
+	return hcd.getSecretData(
+		hcd.host.Spec.MetaData.Name,
+		namespace,
+		"metaData",
+	)
+}
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 68f843a8..761fda5b 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -981,32 +981,45 @@ func (p *ironicProvisioner) Provision(hostConf provisioner.HostConfigData) (resu
 		// setting the state to "active".
 		p.log.Info("making host active")
 
+		// Retrieve cloud-init user data
 		userData, err := hostConf.UserData()
 		if err != nil {
 			return result, errors.Wrap(err, "could not retrieve user data")
 		}
 
+		// Retrieve cloud-init network_data.json. Default value is empty
 		networkDataRaw, err := hostConf.NetworkData()
-
+		if err != nil {
+			return result, errors.Wrap(err, "could not retrieve network data")
+		}
 		var networkData map[string]interface{}
 		if err = yaml.Unmarshal([]byte(networkDataRaw), &networkData); err != nil {
 			return result, errors.Wrap(err, "failed to unmarshal network_data.json from secret")
 		}
 
+		// Retrieve cloud-init meta_data.json with falback to default
+		metaData := map[string]interface{}{
+			"uuid":             string(p.host.ObjectMeta.UID),
+			"metal3-namespace": p.host.ObjectMeta.Namespace,
+			"metal3-name":      p.host.ObjectMeta.Name,
+			"local-hostname":   p.host.ObjectMeta.Name,
+			"local_hostname":   p.host.ObjectMeta.Name,
+		}
+		metaDataRaw, err := hostConf.MetaData()
+		if err != nil {
+			return result, errors.Wrap(err, "could not retrieve metadata")
+		}
+		if metaDataRaw != "" {
+			if err = yaml.Unmarshal([]byte(metaDataRaw), &metaData); err != nil {
+				return result, errors.Wrap(err, "failed to unmarshal metadata from secret")
+			}
+		}
+
 		var configDrive nodes.ConfigDrive
 		if userData != "" {
 			configDrive = nodes.ConfigDrive{
-				UserData: userData,
-				// cloud-init requires that meta_data.json exists and
-				// that the "uuid" field is present to process
-				// any of the config drive contents.
-				MetaData: map[string]interface{}{
-					"uuid":             string(p.host.ObjectMeta.UID),
-					"metal3-namespace": p.host.ObjectMeta.Namespace,
-					"metal3-name":      p.host.ObjectMeta.Name,
-					"local-hostname":   p.host.ObjectMeta.Name,
-					"local_hostname":   p.host.ObjectMeta.Name,
-				},
+				UserData:    userData,
+				MetaData:    metaData,
 				NetworkData: networkData,
 			}
 			if err != nil {
diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go
index 1665c72b..55beaf42 100644
--- a/pkg/provisioner/provisioner.go
+++ b/pkg/provisioner/provisioner.go
@@ -28,7 +28,9 @@ type HostConfigData interface {
 	// configuration for a host.
 	NetworkData() (string, error)
 
-	// TODO add MetaDataSource method
+	// MetaData is the interface for a function to retrieve metadata
+	// configuration for a host.
+	MetaData() (string, error)
 }
 
 // Provisioner holds the state information for talking to the


Test patch:
diff --git a/pkg/controller/baremetalhost/host_config_data_test.go b/pkg/controller/baremetalhost/host_config_data_test.go
index 07121a76..b41c2e91 100644
--- a/pkg/controller/baremetalhost/host_config_data_test.go
+++ b/pkg/controller/baremetalhost/host_config_data_test.go
@@ -25,6 +25,8 @@ func TestProvisionWithHostConfig(t *testing.T) {
 		ErrUserData         bool
 		ExpectedNetworkData string
 		ErrNetworkData      bool
+		ExpectedMetaData    string
+		ErrMetaData         bool
 	}{
 		{
 			Scenario: "host with user data only",
@@ -100,6 +102,39 @@ func TestProvisionWithHostConfig(t *testing.T) {
 			ExpectedNetworkData: base64.StdEncoding.EncodeToString([]byte("key: value")),
 			ErrNetworkData:      false,
 		},
+		{
+			Scenario: "host with metadata only",
+			Host: newHost("host-meta-data",
+				&metal3v1alpha1.BareMetalHostSpec{
+					BMC: metal3v1alpha1.BMCDetails{
+						Address:         "ipmi://192.168.122.1:6233",
+						CredentialsName: defaultSecretName,
+					},
+					MetaData: &corev1.SecretReference{
+						Name:      "meta-data",
+						Namespace: namespace,
+					},
+				}),
+			NetworkDataSecret: newSecret("meta-data", map[string]string{"metaData": "key: value"}),
+			ExpectedMetaData:  base64.StdEncoding.EncodeToString([]byte("key: value")),
+			ErrMetaData:       false,
+		},
+		{
+			Scenario: "host with metadata only, no namespace",
+			Host: newHost("host-meta-data",
+				&metal3v1alpha1.BareMetalHostSpec{
+					BMC: metal3v1alpha1.BMCDetails{
+						Address:         "ipmi://192.168.122.1:6233",
+						CredentialsName: defaultSecretName,
+					},
+					MetaData: &corev1.SecretReference{
+						Name: "meta-data",
+					},
+				}),
+			NetworkDataSecret: newSecret("meta-data", map[string]string{"metaData": "key: value"}),
+			ExpectedMetaData:  base64.StdEncoding.EncodeToString([]byte("key: value")),
+			ErrMetaData:       false,
+		},
 		{
 			Scenario: "fall back to value",
 			Host: newHost("host-user-data",
@@ -212,6 +247,15 @@ func TestProvisionWithHostConfig(t *testing.T) {
 			if actualNetworkData != tc.ExpectedNetworkData {
 				t.Fatal(fmt.Errorf("Failed to assert NetworkData. Expected '%s' got '%s'", actualNetworkData, tc.ExpectedNetworkData))
 			}
+
+			actualMetaData, err := hcd.MetaData()
+			if err != nil && !tc.ErrMetaData {
+				t.Fatal(err)
+			}
+
+			if actualMetaData != tc.ExpectedMetaData {
+				t.Fatal(fmt.Errorf("Failed to assert MetaData. Expected '%s' got '%s'", actualMetaData, tc.ExpectedMetaData))
+			}
 		})
 	}
 }