Repository: kumahq/kuma
Pull request #1082 (2020-10-16 08:37:12)

feat(kumactl) apply multiple resources (#1057)
Backport of https://github.com/kumahq/kuma/pull/1057

(do not squash when merging to preserve original PR number)


Patch:
diff --git a/app/kumactl/cmd/apply/apply.go b/app/kumactl/cmd/apply/apply.go
index e4df240cd..d7fba3c4f 100644
--- a/app/kumactl/cmd/apply/apply.go
+++ b/app/kumactl/cmd/apply/apply.go
@@ -85,40 +85,49 @@ 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)
-			if err != nil {
-				return errors.Wrap(err, "YAML contains invalid resource")
-			}
-
-			if ctx.args.dryRun {
-				p, err := printers.NewGenericPrinter(output.YAMLFormat)
+			var resources []model.Resource
+			rawResources := strings.Split(string(b), "---")
+			for _, rawResource := range rawResources {
+				if len(rawResource) == 0 {
+					continue
+				}
+				bytes, err := processConfigTemplate(rawResource, ctx.args.vars)
 				if err != nil {
-					return err
+					return errors.Wrap(err, "error compiling config from template")
 				}
-				if err := p.Print(rest_types.From.Resource(res), cmd.OutOrStdout()); err != nil {
-					return err
+				res, err := parseResource(bytes)
+				if err != nil {
+					return errors.Wrap(err, "YAML contains invalid resource")
 				}
-				return nil
-			}
-
-			var rs store.ResourceStore
-			if res.GetType() == system.SecretType { // Secret is exposed via Admin Server. It will be merged into API Server eventually.
-				rs, err = pctx.CurrentAdminResourceStore()
-			} else {
-				rs, err = pctx.CurrentResourceStore()
-			}
-			if err != nil {
-				return err
+				if err := mesh.ValidateMeta(res.GetMeta().GetName(), res.GetMeta().GetMesh()); err.HasViolations() {
+					return err.OrNil()
+				}
+				resources = append(resources, res)
 			}
+			for _, resource := range resources {
+				if ctx.args.dryRun {
+					p, err := printers.NewGenericPrinter(output.YAMLFormat)
+					if err != nil {
+						return err
+					}
+					if err := p.Print(rest_types.From.Resource(resource), cmd.OutOrStdout()); err != nil {
+						return err
+					}
+				} else {
+					var rs store.ResourceStore
+					if resource.GetType() == system.SecretType { // Secret is exposed via Admin Server. It will be merged into API Server eventually.
+						rs, err = pctx.CurrentAdminResourceStore()
+					} else {
+						rs, err = pctx.CurrentResourceStore()
+					}
+					if err != nil {
+						return err
+					}
 
-			if err := upsert(rs, res); err != nil {
-				return err
+					if err := upsert(rs, resource); err != nil {
+						return err
+					}
+				}
 			}
 			return nil
 		},


Test patch:
diff --git a/app/kumactl/cmd/apply/apply_test.go b/app/kumactl/cmd/apply/apply_test.go
index 59345032c..2e89aa8e5 100644
--- a/app/kumactl/cmd/apply/apply_test.go
+++ b/app/kumactl/cmd/apply/apply_test.go
@@ -294,6 +294,56 @@ var _ = Describe("kumactl apply", func() {
 		Expect(resource.Meta.GetMesh()).To(Equal("meshinit"))
 	})
 
+	It("should apply multiple resources of same type", func() {
+		// given
+		rootCmd.SetArgs([]string{
+			"--config-file", filepath.Join("..", "testdata", "sample-kumactl.config.yaml"),
+			"apply", "-f", filepath.Join("testdata", "apply-multiple-resource-same-type.yaml")},
+		)
+
+		// when
+		err := rootCmd.Execute()
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// and
+		resource := mesh.DataplaneResourceList{}
+		err = store.List(context.Background(), &resource, core_store.ListByMesh("default"))
+		Expect(err).ToNot(HaveOccurred())
+
+		Expect(resource.Items[0].Meta.GetName()).To(Equal("sample1"))
+		Expect(resource.Items[1].Meta.GetName()).To(Equal("sample2"))
+	})
+
+	It("should apply multiple resources of different type", func() {
+		// given
+		rootCmd.SetArgs([]string{
+			"--config-file", filepath.Join("..", "testdata", "sample-kumactl.config.yaml"),
+			"apply", "-f", filepath.Join("testdata", "apply-multiple-resource-different-type.yaml")},
+		)
+
+		// when
+		err := rootCmd.Execute()
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// and
+		dataplaneResource := mesh.DataplaneResource{}
+		err = store.Get(context.Background(), &dataplaneResource, core_store.GetByKey("sample1", "default"))
+		Expect(err).ToNot(HaveOccurred())
+
+		Expect(dataplaneResource.Meta.GetName()).To(Equal("sample1"))
+		Expect(dataplaneResource.Meta.GetMesh()).To(Equal("default"))
+
+		secret := system.SecretResource{}
+		err = store.Get(context.Background(), &secret, core_store.GetByKey("sample", "default"))
+		Expect(err).ToNot(HaveOccurred())
+
+		Expect(secret.Meta.GetName()).To(Equal("sample"))
+		Expect(secret.Meta.GetMesh()).To(Equal("default"))
+
+	})
+
 	It("should return kuma api server error", func() {
 		// setup
 		rootCtx.Runtime.NewResourceStore = func(*config_proto.ControlPlaneCoordinates_ApiServer) (core_store.ResourceStore, error) {
diff --git a/app/kumactl/cmd/apply/testdata/apply-multiple-resource-different-type.yaml b/app/kumactl/cmd/apply/testdata/apply-multiple-resource-different-type.yaml
new file mode 100644
index 000000000..86205d510
--- /dev/null
+++ b/app/kumactl/cmd/apply/testdata/apply-multiple-resource-different-type.yaml
@@ -0,0 +1,22 @@
+---
+name: sample1
+mesh: default
+type: Dataplane
+networking:
+  address: 2.2.2.2
+  inbound:
+  - address: 1.1.1.1
+    port: 80
+    servicePort: 8080
+    tags:
+      service: web
+      version: "1.0"
+      env: production
+  outbound:
+  - port: 3000
+    service: postgres
+---
+type: Secret
+name: sample
+mesh: default
+data: dGVzdAo=
\ No newline at end of file
diff --git a/app/kumactl/cmd/apply/testdata/apply-multiple-resource-same-type.yaml b/app/kumactl/cmd/apply/testdata/apply-multiple-resource-same-type.yaml
new file mode 100644
index 000000000..dc0fc5b5a
--- /dev/null
+++ b/app/kumactl/cmd/apply/testdata/apply-multiple-resource-same-type.yaml
@@ -0,0 +1,33 @@
+name: sample1
+mesh: default
+type: Dataplane
+networking:
+  address: 2.2.2.2
+  inbound:
+  - address: 1.1.1.1
+    port: 80
+    servicePort: 8080
+    tags:
+      service: web
+      version: "1.0"
+      env: production
+  outbound:
+  - port: 3000
+    service: postgres
+---
+name: sample2
+mesh: default
+type: Dataplane
+networking:
+  address: 3.3.3.3
+  inbound:
+  - address: 1.1.1.1
+    port: 80
+    servicePort: 8080
+    tags:
+      service: web
+      version: "2.0"
+      env: staging
+  outbound:
+  - port: 3000
+    service: postgres
\ No newline at end of file