Skip to content

Commit

Permalink
feat(kumactl): export add no-dataplanes profile and skip secrets (#10964
Browse files Browse the repository at this point in the history
)
  • Loading branch information
lahabana authored Jul 25, 2024
1 parent 251e964 commit 487f83e
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 106 deletions.
4 changes: 4 additions & 0 deletions app/kumactl/cmd/completion/testdata/bash.golden
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
108 changes: 62 additions & 46 deletions app/kumactl/cmd/export/export.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package export

import (
"context"
"fmt"
"slices"
"strings"

"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"
Expand All @@ -27,20 +25,30 @@ type exportContext struct {
*kumactl_cmd.RootContext

args struct {
profile string
format string
profile string
format string
includeAdmin bool
}
}

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{
Expand All @@ -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()
Expand All @@ -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()...)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}
126 changes: 66 additions & 60 deletions app/kumactl/cmd/export/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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",
}),
Expand Down
Loading

0 comments on commit 487f83e

Please sign in to comment.