From 487f83ec18f34eaf4dc7be2dbea32b9fd49fe96e Mon Sep 17 00:00:00 2001 From: Charly Molter Date: Thu, 25 Jul 2024 10:14:09 +0200 Subject: [PATCH] feat(kumactl): export add no-dataplanes profile and skip secrets (#10964) --- .../cmd/completion/testdata/bash.golden | 4 + app/kumactl/cmd/export/export.go | 108 ++++++++------- app/kumactl/cmd/export/export_test.go | 126 +++++++++--------- .../export/testdata/export-no-dp.golden.yaml | 55 ++++++++ 4 files changed, 187 insertions(+), 106 deletions(-) create mode 100644 app/kumactl/cmd/export/testdata/export-no-dp.golden.yaml diff --git a/app/kumactl/cmd/completion/testdata/bash.golden b/app/kumactl/cmd/completion/testdata/bash.golden index a55c9dfd3445..9db0f334b170 100644 --- a/app/kumactl/cmd/completion/testdata/bash.golden +++ b/app/kumactl/cmd/completion/testdata/bash.golden @@ -813,6 +813,10 @@ _kumactl_export() local_nonpersistent_flags+=("--format") local_nonpersistent_flags+=("--format=") local_nonpersistent_flags+=("-f") + flags+=("--include-admin") + flags+=("-a") + local_nonpersistent_flags+=("--include-admin") + local_nonpersistent_flags+=("-a") flags+=("--profile=") two_word_flags+=("--profile") two_word_flags+=("-p") diff --git a/app/kumactl/cmd/export/export.go b/app/kumactl/cmd/export/export.go index 4ee7fe69a6eb..5f602c5eeb89 100644 --- a/app/kumactl/cmd/export/export.go +++ b/app/kumactl/cmd/export/export.go @@ -1,7 +1,6 @@ package export import ( - "context" "fmt" "slices" "strings" @@ -9,7 +8,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - api_common "github.com/kumahq/kuma/api/openapi/types/common" 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" @@ -27,8 +25,9 @@ type exportContext struct { *kumactl_cmd.RootContext args struct { - profile string - format string + profile string + format string + includeAdmin bool } } @@ -36,11 +35,20 @@ const ( profileFederation = "federation" profileFederationWithPolicies = "federation-with-policies" profileAll = "all" + profileNoDataplanes = "no-dataplanes" formatUniversal = "universal" formatKubernetes = "kubernetes" ) +var allProfiles = []string{ + profileAll, profileFederation, profileFederationWithPolicies, profileNoDataplanes, +} + +func IsMigrationProfile(profile string) bool { + return slices.Contains([]string{profileFederation, profileFederationWithPolicies}, profile) +} + func NewExportCmd(pctx *kumactl_cmd.RootContext) *cobra.Command { ctx := &exportContext{RootContext: pctx} cmd := &cobra.Command{ @@ -54,20 +62,22 @@ $ kumactl export --profile federation --format universal > policies.yaml Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { version := kumactl_cmd.CheckCompatibility(pctx.FetchServerVersion, cmd.ErrOrStderr()) - cmd.Printf("# Product: %s, Version: %s, Hostname: %s, ClusterId: %s, InstanceId: %s\n", - version.Product, version.Version, version.Hostname, version.ClusterId, version.InstanceId) - - if !slices.Contains([]string{profileFederation, profileFederationWithPolicies, profileAll}, ctx.args.profile) { - return errors.New("invalid profile") + if version != nil { + cmd.Printf("# Product: %s, Version: %s, Hostname: %s, ClusterId: %s, InstanceId: %s\n", + version.Product, version.Version, version.Hostname, version.ClusterId, version.InstanceId) } - resTypes, err := resourcesTypesToDump(cmd.Context(), ctx) - if err != nil { - return err + if !slices.Contains(allProfiles, ctx.args.profile) { + return fmt.Errorf("invalid profile: %q", ctx.args.profile) } if !slices.Contains([]string{formatKubernetes, formatUniversal}, ctx.args.format) { - return errors.New("invalid format") + return fmt.Errorf("invalid format: %q", ctx.args.format) + } + + resTypes, err := resourcesTypesToDump(cmd, ctx) + if err != nil { + return err } rs, err := pctx.CurrentResourceStore() @@ -81,24 +91,18 @@ $ kumactl export --profile federation --format universal > policies.yaml } var allResources []model.Resource - var incompatibleTypes []string - for _, resType := range resTypes { - resDesc, err := pctx.Runtime.Registry.DescriptorFor(resType) - if err != nil { - incompatibleTypes = append(incompatibleTypes, string(resType)) - continue - } + for _, resDesc := range resTypes { if resDesc.Scope == model.ScopeGlobal { list := resDesc.NewList() if err := rs.List(cmd.Context(), list); err != nil { - return errors.Wrapf(err, "could not list %q", resType) + return errors.Wrapf(err, "could not list %q", resDesc.Name) } allResources = append(allResources, list.GetItems()...) } else { for _, mesh := range meshes.Items { list := resDesc.NewList() if err := rs.List(cmd.Context(), list, store.ListByMesh(mesh.GetMeta().GetName())); err != nil { - return errors.Wrapf(err, "could not list %q", resType) + return errors.Wrapf(err, "could not list %q", resDesc.Name) } allResources = append(allResources, list.GetItems()...) } @@ -122,11 +126,6 @@ $ kumactl export --profile federation --format universal > policies.yaml // put user token signing keys as last, because once we apply this, we cannot apply anything else without reconfiguring kumactl with a new auth data resources = append(resources, userTokenSigningKeys...) - if len(incompatibleTypes) > 0 { - msg := fmt.Sprintf("The following types won't be exported because they are unknown to kumactl: %s", strings.Join(incompatibleTypes, ",")) - cmd.Printf("# %s\n", msg) - cmd.PrintErrf("WARNING: %s. Are you using a compatible version of kumactl?\n", msg) - } switch ctx.args.format { case formatUniversal: for _, res := range resources { @@ -158,8 +157,9 @@ $ kumactl export --profile federation --format universal > policies.yaml return nil }, } - cmd.Flags().StringVarP(&ctx.args.profile, "profile", "p", profileFederation, fmt.Sprintf(`Profile. Available values: %q, %q, %q`, profileFederation, profileAll, profileFederationWithPolicies)) + cmd.Flags().StringVarP(&ctx.args.profile, "profile", "p", profileFederation, fmt.Sprintf(`Profile. Available values: %s`, strings.Join(allProfiles, ","))) cmd.Flags().StringVarP(&ctx.args.format, "format", "f", formatUniversal, fmt.Sprintf(`Policy format output. Available values: %q, %q`, formatUniversal, formatKubernetes)) + cmd.Flags().BoolVarP(&ctx.args.includeAdmin, "include-admin", "a", false, "Include admin resource types (like secrets), this flag is ignored on migration profiles like federation as these entities are required") return cmd } @@ -177,37 +177,53 @@ func cleanKubeObject(obj map[string]interface{}) { delete(meta, "managedFields") } -func resourcesTypesToDump(ctx context.Context, ectx *exportContext) ([]model.ResourceType, error) { +func resourcesTypesToDump(cmd *cobra.Command, ectx *exportContext) ([]model.ResourceTypeDescriptor, error) { client, err := ectx.CurrentResourcesListClient() if err != nil { return nil, err } - list, err := client.List(ctx) + list, err := client.List(cmd.Context()) if err != nil { return nil, err } - var resTypes []model.ResourceType + var resDescList []model.ResourceTypeDescriptor + var incompatibleTypes []string for _, res := range list.Resources { + resDesc, err := ectx.Runtime.Registry.DescriptorFor(model.ResourceType(res.Name)) + if err != nil { + incompatibleTypes = append(incompatibleTypes, res.Name) + continue + } + if resDesc.AdminOnly && !IsMigrationProfile(ectx.args.profile) && !ectx.args.includeAdmin { + continue + } + // For each profile remove types we don't want switch ectx.args.profile { - case profileAll: - resTypes = append(resTypes, model.ResourceType(res.Name)) case profileFederation: - if includeInFederationProfile(res) { - resTypes = append(resTypes, model.ResourceType(res.Name)) + if !res.IncludeInFederation { // base decision on `IncludeInFederation` field + continue + } + if res.Policy != nil && res.Policy.IsTargetRef { // do not include new policies + continue + } + if res.Name == string(core_mesh.MeshGatewayType) { // do not include MeshGateways + continue } case profileFederationWithPolicies: - if res.IncludeInFederation { - resTypes = append(resTypes, model.ResourceType(res.Name)) + if !res.IncludeInFederation { + continue + } + case profileNoDataplanes: + if resDesc.Name == core_mesh.DataplaneType || resDesc.Name == core_mesh.DataplaneInsightType { + continue } - default: - return nil, errors.New("invalid profile") } + resDescList = append(resDescList, resDesc) } - return resTypes, nil -} - -func includeInFederationProfile(res api_common.ResourceTypeDescription) bool { - return res.IncludeInFederation && // base decision on `IncludeInFederation` field - (res.Policy == nil || (res.Policy != nil && !res.Policy.IsTargetRef)) && // do not include new policies - res.Name != string(core_mesh.MeshGatewayType) // do not include MeshGateways + if len(incompatibleTypes) > 0 { + msg := fmt.Sprintf("The following types won't be exported because they are unknown to kumactl: %s", strings.Join(incompatibleTypes, ",")) + cmd.Printf("# %s\n", msg) + cmd.PrintErrf("WARNING: %s. Are you using a compatible version of kumactl?\n", msg) + } + return resDescList, nil } diff --git a/app/kumactl/cmd/export/export_test.go b/app/kumactl/cmd/export/export_test.go index 0236804cf44b..f83123ac2a43 100644 --- a/app/kumactl/cmd/export/export_test.go +++ b/app/kumactl/cmd/export/export_test.go @@ -57,73 +57,79 @@ var _ = Describe("kumactl export", func() { rootCmd.SetOut(buf) }) - It("should export resources in universal format", func() { - // given - resources := []model.Resource{ - samples.MeshDefault(), - samples.SampleSigningKeyGlobalSecret(), - samples.SampleSigningKeySecret(), - samples.MeshDefaultBuilder().WithName("another-mesh").Build(), - samples.SampleSigningKeySecretBuilder().WithMesh("another-mesh").Build(), - samples.ServiceInsight().WithMesh("another-mesh").Build(), - samples.SampleGlobalSecretAdminCa(), - } - for _, res := range resources { - err := store.Create(context.Background(), res, core_store.CreateByKey(res.GetMeta().GetName(), res.GetMeta().GetMesh())) - Expect(err).ToNot(HaveOccurred()) - } - - args := []string{ - "--config-file", - filepath.Join("..", "testdata", "sample-kumactl.config.yaml"), - "export", - } - rootCmd.SetArgs(args) + type testCase struct { + args []string + goldenFile string + resources []model.Resource + } + DescribeTable("should succeed", + func(given testCase) { + for _, res := range given.resources { + err := store.Create(context.Background(), res, core_store.CreateByKey(res.GetMeta().GetName(), res.GetMeta().GetMesh())) + Expect(err).ToNot(HaveOccurred()) + } - // when - err := rootCmd.Execute() + args := append([]string{ + "--config-file", + filepath.Join("..", "testdata", "sample-kumactl.config.yaml"), + "export", + }, given.args...) + rootCmd.SetArgs(args) - // then - Expect(err).ToNot(HaveOccurred()) - Expect(buf.String()).To(matchers.MatchGoldenEqual("testdata", "export.golden.yaml")) - }) + // when + err := rootCmd.Execute() - It("should export resources in kubernetes format", func() { - // given - resources := []model.Resource{ - samples.MeshDefault(), - samples.SampleSigningKeyGlobalSecret(), - samples.MeshAccessLogWithFileBackend(), - samples.Retry(), - } - for _, res := range resources { - err := store.Create(context.Background(), res, core_store.CreateByKey(res.GetMeta().GetName(), res.GetMeta().GetMesh())) + // then Expect(err).ToNot(HaveOccurred()) - } - - args := []string{ - "--config-file", - filepath.Join("..", "testdata", "sample-kumactl.config.yaml"), - "export", - "--format=kubernetes", - "--profile", "all", - } - rootCmd.SetArgs(args) - - // when - err := rootCmd.Execute() - - // then - Expect(err).ToNot(HaveOccurred()) - Expect(buf.String()).To(matchers.MatchGoldenEqual("testdata", "export-kube.golden.yaml")) - }) + Expect(buf.String()).To(matchers.MatchGoldenEqual("testdata", given.goldenFile)) + }, + Entry("no args", testCase{ + resources: []model.Resource{ + samples.MeshDefault(), + samples.SampleSigningKeyGlobalSecret(), + samples.SampleSigningKeySecret(), + samples.MeshDefaultBuilder().WithName("another-mesh").Build(), + samples.SampleSigningKeySecretBuilder().WithMesh("another-mesh").Build(), + samples.ServiceInsight().WithMesh("another-mesh").Build(), + samples.SampleGlobalSecretAdminCa(), + }, + goldenFile: "export.golden.yaml", + }), + Entry("kubernetes profile=all", testCase{ + resources: []model.Resource{ + samples.MeshDefault(), + samples.SampleSigningKeyGlobalSecret(), + samples.MeshAccessLogWithFileBackend(), + samples.Retry(), + }, + args: []string{ + "--format=kubernetes", + "--profile", "all", + "-a", + }, + goldenFile: "export-kube.golden.yaml", + }), + Entry("kubernetes profile=no-dataplanes without secrets", testCase{ + resources: []model.Resource{ + samples.MeshDefault(), + samples.SampleSigningKeyGlobalSecret(), + samples.MeshAccessLogWithFileBackend(), + samples.Retry(), + samples.DataplaneBackend(), + }, + args: []string{ + "--profile", "no-dataplanes", + }, + goldenFile: "export-no-dp.golden.yaml", + }), + ) - type testCase struct { + type invalidTestCase struct { args []string err string } DescribeTable("should fail on invalid resource", - func(given testCase) { + func(given invalidTestCase) { // given args := []string{ "--config-file", filepath.Join("..", "testdata", "sample-kumactl.config.yaml"), @@ -139,11 +145,11 @@ var _ = Describe("kumactl export", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring(given.err)) }, - Entry("invalid profile", testCase{ + Entry("invalid profile", invalidTestCase{ args: []string{"--profile", "something"}, err: "invalid profile", }), - Entry("invalid format", testCase{ + Entry("invalid format", invalidTestCase{ args: []string{"--format", "something"}, err: "invalid format", }), diff --git a/app/kumactl/cmd/export/testdata/export-no-dp.golden.yaml b/app/kumactl/cmd/export/testdata/export-no-dp.golden.yaml new file mode 100644 index 000000000000..4865a987eafe --- /dev/null +++ b/app/kumactl/cmd/export/testdata/export-no-dp.golden.yaml @@ -0,0 +1,55 @@ +# Product: Kuma, Version: 0.0.0-testversion, Hostname: localhost, ClusterId: test-cluster, InstanceId: test-instance +--- +creationTime: "0001-01-01T00:00:00Z" +modificationTime: "0001-01-01T00:00:00Z" +name: default +type: Mesh +--- +creationTime: "0001-01-01T00:00:00Z" +mesh: default +modificationTime: "0001-01-01T00:00:00Z" +name: mal-1 +spec: + targetRef: + kind: MeshService + name: web + to: + - default: + backends: + - file: + path: /tmp/access.logs + type: File + targetRef: + kind: Mesh + - default: + backends: + - file: + path: /tmp/access.logs + type: File + targetRef: + kind: Mesh +type: MeshAccessLog +--- +creationTime: "0001-01-01T00:00:00Z" +mesh: default +modificationTime: "0001-01-01T00:00:00Z" +name: retry-all-default +type: Retry +conf: + http: + backOff: + baseInterval: 0.025s + maxInterval: 0.250s + numRetries: 5 + perTryTimeout: 16s + retriableStatusCodes: + - 500 + - 504 + tcp: + maxConnectAttempts: 5 +destinations: +- match: + kuma.io/service: '*' +sources: +- match: + kuma.io/service: '*'