Skip to content

Commit ba12b17

Browse files
authored
Handle extensions duplicates with base commands (#2414)
1 parent b95fadc commit ba12b17

File tree

7 files changed

+123
-66
lines changed

7 files changed

+123
-66
lines changed

gen-docs/main.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ import (
1212
"github.com/spf13/cobra"
1313
"github.com/spf13/pflag"
1414

15-
"github.com/kyma-project/cli.v3/internal/clierror"
1615
"github.com/kyma-project/cli.v3/internal/cmd"
1716
)
1817

1918
func main() {
20-
command, clierr := cmd.NewKymaCMD()
21-
clierror.Check(clierr)
19+
command := cmd.NewKymaCMD()
2220

2321
docsTargetDir := "./docs/user/gen-docs"
2422

internal/cmd/alpha/alpha.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package alpha
22

33
import (
4-
"github.com/kyma-project/cli.v3/internal/clierror"
54
"github.com/kyma-project/cli.v3/internal/cmd/alpha/app"
65
"github.com/kyma-project/cli.v3/internal/cmd/alpha/hana"
76
"github.com/kyma-project/cli.v3/internal/cmd/alpha/kubeconfig"
@@ -15,18 +14,15 @@ import (
1514
"github.com/spf13/cobra"
1615
)
1716

18-
func NewAlphaCMD() (*cobra.Command, clierror.Error) {
17+
func NewAlphaCMD() *cobra.Command {
1918
cmd := &cobra.Command{
2019
Use: "alpha <command> [flags]",
2120
Short: "Groups command prototypes for which the API may still change",
2221
Long: `A set of alpha prototypes that may still change. Use them in automation at your own risk.`,
2322
DisableFlagsInUseLine: true,
2423
}
2524

26-
kymaConfig, err := cmdcommon.NewKymaConfig(cmd)
27-
if err != nil {
28-
return nil, err
29-
}
25+
kymaConfig := cmdcommon.NewKymaConfig(cmd)
3026

3127
cmd.AddCommand(app.NewAppCMD(kymaConfig))
3228
cmd.AddCommand(hana.NewHanaCMD(kymaConfig))
@@ -45,8 +41,11 @@ func NewAlphaCMD() (*cobra.Command, clierror.Error) {
4541
// map of available core commands
4642
"registry_config": config.NewConfigCMD,
4743
"registry_image-import": imageimport.NewImportCMD,
48-
})
44+
}, cmd, kymaConfig)
45+
46+
kymaConfig.DisplayExtensionsErrors(cmd.ErrOrStderr())
47+
4948
cmd.AddCommand(cmds...)
5049

51-
return cmd, nil
50+
return cmd
5251
}

internal/cmd/kyma.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package cmd
22

33
import (
4-
"github.com/kyma-project/cli.v3/internal/clierror"
54
"github.com/kyma-project/cli.v3/internal/cmd/alpha"
65
"github.com/kyma-project/cli.v3/internal/cmd/version"
76
"github.com/kyma-project/cli.v3/internal/cmdcommon"
87
"github.com/spf13/cobra"
98
)
109

11-
func NewKymaCMD() (*cobra.Command, clierror.Error) {
10+
func NewKymaCMD() *cobra.Command {
1211
cmd := &cobra.Command{
1312
Use: "kyma <command> [flags]",
1413
Short: "A simple set of commands to manage a Kyma cluster",
@@ -21,13 +20,10 @@ func NewKymaCMD() (*cobra.Command, clierror.Error) {
2120
cmdcommon.AddExtensionsFlags(cmd)
2221
cmd.PersistentFlags().BoolP("help", "h", false, "Help for the command")
2322

24-
alpha, err := alpha.NewAlphaCMD()
25-
if err != nil {
26-
return nil, err
27-
}
23+
alpha := alpha.NewAlphaCMD()
2824

2925
cmd.AddCommand(alpha)
3026
cmd.AddCommand(version.NewCmd())
3127

32-
return cmd, nil
28+
return cmd
3329
}

internal/cmdcommon/extension.go

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
v1 "k8s.io/api/core/v1"
89
"os"
910
"slices"
1011
"strconv"
@@ -19,11 +20,12 @@ import (
1920
)
2021

2122
type KymaExtensionsConfig struct {
22-
kymaConfig *KymaConfig
23-
extensions ExtensionList
23+
kymaConfig *KymaConfig
24+
extensions ExtensionList
25+
parseErrors error
2426
}
2527

26-
func newExtensionsConfig(warningWriter io.Writer, config *KymaConfig) *KymaExtensionsConfig {
28+
func newExtensionsConfig(config *KymaConfig) *KymaExtensionsConfig {
2729
extensionsConfig := &KymaExtensionsConfig{
2830
kymaConfig: config,
2931
}
@@ -35,15 +37,8 @@ func newExtensionsConfig(warningWriter io.Writer, config *KymaConfig) *KymaExten
3537
}
3638
}
3739

38-
extensions, err := loadExtensionsFromCluster(config.Ctx, config.KubeClientConfig)
39-
if err != nil && getBoolFlagValue("--show-extensions-error") {
40-
// print error as warning if expected and continue
41-
fmt.Fprintf(warningWriter, "Extensions Warning:\n%s\n\n", err.Error())
42-
} else if err != nil {
43-
fmt.Fprintf(warningWriter, "Extensions Warning:\nfailed to fetch all extensions from the cluster. Use the '--show-extensions-error' flag to see more details.\n\n")
44-
}
40+
extensionsConfig.extensions, extensionsConfig.parseErrors = loadExtensionsFromCluster(config.Ctx, config.KubeClientConfig)
4541

46-
extensionsConfig.extensions = extensions
4742
return extensionsConfig
4843
}
4944

@@ -57,17 +52,47 @@ func (kec *KymaExtensionsConfig) GetRawExtensions() ExtensionList {
5752
return kec.extensions
5853
}
5954

60-
func (kec *KymaExtensionsConfig) BuildExtensions(availableTemplateCommands *TemplateCommandsList, availableCoreCommands CoreCommandsMap) []*cobra.Command {
61-
cmds := make([]*cobra.Command, len(kec.kymaConfig.extensions))
55+
func (kec *KymaExtensionsConfig) BuildExtensions(availableTemplateCommands *TemplateCommandsList, availableCoreCommands CoreCommandsMap, cmd *cobra.Command, config *KymaConfig) []*cobra.Command {
56+
var cmds []*cobra.Command
57+
58+
var cms, cmsError = getExtensionConfigMaps(config.Ctx, config.KubeClientConfig)
59+
if cmsError != nil {
60+
kec.parseErrors = cmsError
61+
return nil
62+
}
63+
64+
existingCommands := make(map[string]bool)
65+
for _, baseCmd := range cmd.Commands() {
66+
existingCommands[baseCmd.Name()] = true
67+
}
68+
69+
for _, cm := range cms.Items {
70+
extension, _ := parseResourceExtension(cm.Data)
71+
if existingCommands[extension.RootCommand.Name] {
72+
kec.parseErrors = errors.Join(
73+
kec.parseErrors,
74+
fmt.Errorf("failed to validate configmap '%s/%s': base command with name='%s' already exists",
75+
cm.GetNamespace(), cm.GetName(), extension.RootCommand.Name),
76+
)
77+
continue
78+
}
6279

63-
for i, extension := range kec.kymaConfig.extensions {
64-
cmds[i] = buildCommandFromExtension(kec.kymaConfig, &extension, availableTemplateCommands, availableCoreCommands)
80+
cmds = append(cmds, buildCommandFromExtension(kec.kymaConfig, extension, availableTemplateCommands, availableCoreCommands))
6581
}
6682

6783
return cmds
6884
}
6985

70-
func loadExtensionsFromCluster(ctx context.Context, clientConfig *KubeClientConfig) ([]Extension, error) {
86+
func (kec *KymaExtensionsConfig) DisplayExtensionsErrors(warningWriter io.Writer) {
87+
if kec.parseErrors != nil && getBoolFlagValue("--show-extensions-error") {
88+
// print error as warning if expected and continue
89+
fmt.Fprintf(warningWriter, "Extensions Warning:\n%s\n\n", kec.parseErrors.Error())
90+
} else if kec.parseErrors != nil {
91+
fmt.Fprintf(warningWriter, "Extensions Warning:\nfailed to fetch all extensions from the cluster. Use the '--show-extensions-error' flag to see more details.\n\n")
92+
}
93+
}
94+
95+
func getExtensionConfigMaps(ctx context.Context, clientConfig *KubeClientConfig) (*v1.ConfigMapList, error) {
7196
client, clientErr := clientConfig.GetKubeClient()
7297
if clientErr != nil {
7398
return nil, clientErr
@@ -81,15 +106,24 @@ func loadExtensionsFromCluster(ctx context.Context, clientConfig *KubeClientConf
81106
return nil, pkgerrors.Wrapf(err, "failed to load ConfigMaps from cluster with label %s", labelSelector)
82107
}
83108

109+
return cms, nil
110+
}
111+
112+
func loadExtensionsFromCluster(ctx context.Context, clientConfig *KubeClientConfig) ([]Extension, error) {
113+
var cms, cmsError = getExtensionConfigMaps(ctx, clientConfig)
114+
if cmsError != nil {
115+
return nil, cmsError
116+
}
117+
84118
var extensions []Extension
85-
var parseErr error
119+
var parseErrors error
86120
for _, cm := range cms.Items {
87121
extension, err := parseResourceExtension(cm.Data)
88122
if err != nil {
89123
// if the parse failed add an error to the errors list to take another extension
90124
// corrupted extension should not stop parsing the rest of the extensions
91-
parseErr = errors.Join(
92-
parseErr,
125+
parseErrors = errors.Join(
126+
parseErrors,
93127
pkgerrors.Wrapf(err, "failed to parse configmap '%s/%s'", cm.GetNamespace(), cm.GetName()),
94128
)
95129
continue
@@ -98,8 +132,8 @@ func loadExtensionsFromCluster(ctx context.Context, clientConfig *KubeClientConf
98132
if slices.ContainsFunc(extensions, func(e Extension) bool {
99133
return e.RootCommand.Name == extension.RootCommand.Name
100134
}) {
101-
parseErr = errors.Join(
102-
parseErr,
135+
parseErrors = errors.Join(
136+
parseErrors,
103137
fmt.Errorf("failed to validate configmap '%s/%s': extension with rootCommand.name='%s' already exists",
104138
cm.GetNamespace(), cm.GetName(), extension.RootCommand.Name),
105139
)
@@ -109,7 +143,7 @@ func loadExtensionsFromCluster(ctx context.Context, clientConfig *KubeClientConf
109143
extensions = append(extensions, *extension)
110144
}
111145

112-
return extensions, parseErr
146+
return extensions, parseErrors
113147
}
114148

115149
func parseResourceExtension(cmData map[string]string) (*Extension, error) {

internal/cmdcommon/extension_test.go

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmdcommon
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"fmt"
78
"os"
89
"testing"
@@ -17,7 +18,6 @@ import (
1718

1819
func TestListFromCluster(t *testing.T) {
1920
t.Run("list extensions from cluster", func(t *testing.T) {
20-
warnBuf := bytes.NewBuffer([]byte{})
2121
kubeClientConfig := &KubeClientConfig{
2222
KubeClient: &fake.KubeClient{
2323
TestKubernetesInterface: k8s_fake.NewSimpleClientset(
@@ -39,17 +39,12 @@ func TestListFromCluster(t *testing.T) {
3939
KubeClientConfig: kubeClientConfig,
4040
}
4141

42-
got := newExtensionsConfig(warnBuf, kymaConfig)
42+
got := newExtensionsConfig(kymaConfig)
4343
require.Equal(t, want, got.extensions)
44-
require.Empty(t, warnBuf.Bytes())
44+
require.Empty(t, got.parseErrors)
4545
})
4646

4747
t.Run("extensions duplications warning", func(t *testing.T) {
48-
oldArgs := os.Args
49-
os.Args = append(os.Args, "--show-extensions-error")
50-
defer func() { os.Args = oldArgs }()
51-
52-
warnBuf := bytes.NewBuffer([]byte{})
5348
kubeClientConfig := &KubeClientConfig{
5449
KubeClient: &fake.KubeClient{
5550
TestKubernetesInterface: k8s_fake.NewSimpleClientset(
@@ -69,17 +64,17 @@ func TestListFromCluster(t *testing.T) {
6964
KubeClientConfig: kubeClientConfig,
7065
}
7166

72-
wantWarning := "Extensions Warning:\n" +
67+
wantWarning :=
7368
"failed to validate configmap '/test-2': extension with rootCommand.name='test-1' already exists\n" +
74-
"failed to validate configmap '/test-3': extension with rootCommand.name='test-1' already exists\n\n"
69+
"failed to validate configmap '/test-3': extension with rootCommand.name='test-1' already exists"
70+
71+
got := newExtensionsConfig(kymaConfig)
7572

76-
got := newExtensionsConfig(warnBuf, kymaConfig)
7773
require.Equal(t, want, got.extensions)
78-
require.Equal(t, wantWarning, warnBuf.String())
74+
require.Equal(t, wantWarning, got.parseErrors.Error())
7975
})
8076

8177
t.Run("missing rootCommand error", func(t *testing.T) {
82-
warnBuf := bytes.NewBuffer([]byte{})
8378
kubeClientConfig := &KubeClientConfig{
8479
KubeClient: &fake.KubeClient{
8580
TestKubernetesInterface: k8s_fake.NewSimpleClientset(
@@ -96,21 +91,20 @@ func TestListFromCluster(t *testing.T) {
9691
},
9792
}
9893

99-
wantWarning := "Extensions Warning:\n" +
100-
"failed to fetch all extensions from the cluster. Use the '--show-extensions-error' flag to see more details.\n\n"
94+
wantWarning :=
95+
"failed to parse configmap '/bad-data': missing .data.rootCommand field"
10196

10297
kymaConfig := &KymaConfig{
10398
Ctx: context.Background(),
10499
KubeClientConfig: kubeClientConfig,
105100
}
106101

107-
got := newExtensionsConfig(warnBuf, kymaConfig)
108-
require.Equal(t, wantWarning, warnBuf.String())
102+
got := newExtensionsConfig(kymaConfig)
103+
require.Equal(t, wantWarning, got.parseErrors.Error())
109104
require.Empty(t, got.extensions)
110105
})
111106

112107
t.Run("skip optional fields", func(t *testing.T) {
113-
warnBuf := bytes.NewBuffer([]byte{})
114108
kubeClientConfig := &KubeClientConfig{
115109
KubeClient: &fake.KubeClient{
116110
TestKubernetesInterface: k8s_fake.NewSimpleClientset(
@@ -148,12 +142,50 @@ descriptionLong: test-description-long
148142
KubeClientConfig: kubeClientConfig,
149143
}
150144

151-
got := newExtensionsConfig(warnBuf, kymaConfig)
152-
require.Empty(t, warnBuf.Bytes())
145+
got := newExtensionsConfig(kymaConfig)
146+
require.Empty(t, got.parseErrors)
153147
require.Equal(t, want, got.extensions)
154148
})
155149
}
156150

151+
func TestDisplayExtensionsErrors(t *testing.T) {
152+
t.Run("extensions warning message display without '--show-extensions-error' flag", func(t *testing.T) {
153+
warnBuf := bytes.NewBuffer([]byte{})
154+
155+
wantWarning :=
156+
"Extensions Warning:\nfailed to fetch all extensions from the cluster. Use the '--show-extensions-error' flag to see more details.\n\n"
157+
158+
kymaExtensionsConfig := &KymaExtensionsConfig{
159+
kymaConfig: &KymaConfig{Ctx: nil, KubeClientConfig: nil},
160+
extensions: ExtensionList{},
161+
parseErrors: errors.New("failed to parse configmap '/bad-data': missing .data.rootCommand field"),
162+
}
163+
164+
kymaExtensionsConfig.DisplayExtensionsErrors(warnBuf)
165+
require.Equal(t, wantWarning, warnBuf.String())
166+
})
167+
168+
t.Run("extensions warning message display with '--show-extensions-error' flag", func(t *testing.T) {
169+
oldArgs := os.Args
170+
os.Args = append(os.Args, "--show-extensions-error")
171+
defer func() { os.Args = oldArgs }()
172+
173+
warnBuf := bytes.NewBuffer([]byte{})
174+
175+
wantWarning :=
176+
"Extensions Warning:\nfailed to parse configmap '/bad-data': missing .data.rootCommand field\n\n"
177+
178+
kymaExtensionsConfig := &KymaExtensionsConfig{
179+
kymaConfig: &KymaConfig{Ctx: nil, KubeClientConfig: nil},
180+
extensions: ExtensionList{},
181+
parseErrors: errors.New("failed to parse configmap '/bad-data': missing .data.rootCommand field"),
182+
}
183+
184+
kymaExtensionsConfig.DisplayExtensionsErrors(warnBuf)
185+
require.Equal(t, wantWarning, warnBuf.String())
186+
})
187+
}
188+
157189
func fixTestExtensionConfigmap(name string) *corev1.ConfigMap {
158190
return fixTestExtensionConfigmapWithCMName(name, name)
159191
}

internal/cmdcommon/kymaconfig.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmdcommon
33
import (
44
"context"
55

6-
"github.com/kyma-project/cli.v3/internal/clierror"
76
"github.com/spf13/cobra"
87
)
98

@@ -15,13 +14,13 @@ type KymaConfig struct {
1514
Ctx context.Context
1615
}
1716

18-
func NewKymaConfig(cmd *cobra.Command) (*KymaConfig, clierror.Error) {
17+
func NewKymaConfig(cmd *cobra.Command) *KymaConfig {
1918
ctx := context.Background()
2019

2120
kymaConfig := &KymaConfig{}
2221
kymaConfig.Ctx = ctx
2322
kymaConfig.KubeClientConfig = newKubeClientConfig(cmd)
24-
kymaConfig.KymaExtensionsConfig = newExtensionsConfig(cmd.OutOrStderr(), kymaConfig)
23+
kymaConfig.KymaExtensionsConfig = newExtensionsConfig(kymaConfig)
2524

26-
return kymaConfig, nil
25+
return kymaConfig
2726
}

0 commit comments

Comments
 (0)