Skip to content

Commit

Permalink
Extensions create (#2320)
Browse files Browse the repository at this point in the history
  • Loading branch information
pPrecel authored Jan 21, 2025
1 parent 50e5878 commit a5f7df3
Show file tree
Hide file tree
Showing 13 changed files with 649 additions and 50 deletions.
1 change: 1 addition & 0 deletions internal/cmd/alpha/alpha.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func NewAlphaCMD() (*cobra.Command, clierror.Error) {
cmds := kymaConfig.BuildExtensions(&cmdcommon.TemplateCommandsList{
// list of template commands deffinitions
Explain: templates.BuildExplainCommand,
Create: templates.BuildCreateCommand,
}, cmdcommon.CoreCommandsMap{
// map of available core commands
"registry_config": config.NewConfigCMD,
Expand Down
142 changes: 142 additions & 0 deletions internal/cmd/alpha/templates/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package templates

import (
"context"
"fmt"
"io"
"os"
"strings"

"github.com/kyma-project/cli.v3/internal/clierror"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/templates/parameters"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/templates/types"
"github.com/kyma-project/cli.v3/internal/kube"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)

type KubeClientGetter interface {
GetKubeClientWithClierr() (kube.Client, clierror.Error)
}

type CreateOptions struct {
types.CreateCommand
ResourceInfo types.ResourceInfo
}

func BuildCreateCommand(clientGetter KubeClientGetter, createOptions *CreateOptions) *cobra.Command {
return buildCreateCommand(os.Stdout, clientGetter, createOptions)
}

func buildCreateCommand(out io.Writer, clientGetter KubeClientGetter, createOptions *CreateOptions) *cobra.Command {
extraValues := []parameters.Value{}
cmd := &cobra.Command{
Use: "create",
Short: createOptions.Description,
Long: createOptions.DescriptionLong,
Run: func(cmd *cobra.Command, args []string) {
clierror.Check(createResource(&createArgs{
out: out,
ctx: cmd.Context(),
clientGetter: clientGetter,
createOptions: createOptions,
extraValues: extraValues,
}))
},
}

flags := append(createOptions.CustomFlags, buildDefaultFlags(createOptions.ResourceInfo.Scope)...)
for _, flag := range flags {
value := parameters.NewTyped(flag.Type, flag.Path, flag.DefaultValue)
cmd.Flags().VarP(value, flag.Name, flag.Shorthand, flag.Description)
if flag.Required {
_ = cmd.MarkFlagRequired(flag.Name)
}
extraValues = append(extraValues, value)
}

return cmd
}

type createArgs struct {
out io.Writer
ctx context.Context
clientGetter KubeClientGetter
createOptions *CreateOptions
extraValues []parameters.Value
}

func createResource(args *createArgs) clierror.Error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: args.createOptions.ResourceInfo.Group,
Version: args.createOptions.ResourceInfo.Version,
Kind: args.createOptions.ResourceInfo.Kind,
})

client, clierr := args.clientGetter.GetKubeClientWithClierr()
if clierr != nil {
return clierr
}

for _, extraValue := range args.extraValues {
value := extraValue.GetValue()
if value == nil {
// value is not set and has no default value
continue
}

fields := strings.Split(
// remove optional dot at the beginning of the path
strings.TrimPrefix(extraValue.GetPath(), "."),
".",
)

err := unstructured.SetNestedField(u.Object, value, fields...)
if err != nil {
return clierror.Wrap(err, clierror.New(
fmt.Sprintf("failed to set value %v for path %s", value, extraValue.GetPath()),
))
}
}

err := client.RootlessDynamic().Apply(args.ctx, u)
if err != nil {
return clierror.Wrap(err, clierror.New("failed to create resource"))
}

fmt.Fprintf(args.out, "resource %s applied\n", getResourceName(args.createOptions.ResourceInfo.Scope, u))
return nil
}

func buildDefaultFlags(resourceScope types.Scope) []types.CreateCustomFlag {
params := []types.CreateCustomFlag{
{
Name: "name",
Type: types.StringCustomFlagType,
Description: "name of the resource",
Path: ".metadata.name",
Required: true,
},
}
if resourceScope == types.NamespaceScope {
params = append(params, types.CreateCustomFlag{
Name: "namespace",
Type: types.StringCustomFlagType,
Description: "resource namespace",
Path: ".metadata.namespace",
DefaultValue: "default",
})
}

return params
}

func getResourceName(scope types.Scope, u *unstructured.Unstructured) string {
if scope == types.NamespaceScope {
return fmt.Sprintf("%s/%s", u.GetNamespace(), u.GetName())
}

return u.GetName()
}
149 changes: 149 additions & 0 deletions internal/cmd/alpha/templates/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package templates

import (
"bytes"
"context"
"errors"
"io"
"testing"

"github.com/kyma-project/cli.v3/internal/clierror"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/templates/parameters"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/templates/types"
"github.com/kyma-project/cli.v3/internal/kube"
"github.com/kyma-project/cli.v3/internal/kube/fake"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func Test_create(t *testing.T) {
t.Run("build proper command", func(t *testing.T) {
cmd := fixCreateCommand(bytes.NewBuffer([]byte{}), &mockGetter{})

require.Equal(t, "create", cmd.Use)
require.Equal(t, "create test deploy", cmd.Short)
require.Equal(t, "use this to create test deploy", cmd.Long)

require.NotNil(t, cmd.Flag("name"))
require.NotNil(t, cmd.Flag("namespace"))
require.NotNil(t, cmd.Flag("replicas"))

})

t.Run("create custom resource", func(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
fakeClient := &fake.RootlessDynamicClient{}
mock := mockGetter{
client: &fake.KubeClient{
TestRootlessDynamicInterface: fakeClient,
},
}
cmd := fixCreateCommand(buf, &mock)

cmd.SetArgs([]string{"--name", "test-deploy", "--namespace", "test-namespace", "--replicas", "2"})
err := cmd.Execute()
require.NoError(t, err)

require.Equal(t, "resource test-namespace/test-deploy applied\n", buf.String())

require.Len(t, fakeClient.ApplyObjs, 1)
require.Equal(t, fixUnstructuredDeployment(), fakeClient.ApplyObjs[0])
})

t.Run("failed to get client from getter", func(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
mock := mockGetter{
clierror: clierror.New("test error"),
client: nil,
}

err := createResource(&createArgs{
out: buf,
ctx: context.Background(),
clientGetter: &mock,
createOptions: fixCreateOptions(),
})
require.Equal(t, clierror.New("test error"), err)
})

t.Run("failed to apply resource", func(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
fakeClient := &fake.RootlessDynamicClient{
ReturnErr: errors.New("test error"),
}
mock := mockGetter{
client: &fake.KubeClient{
TestRootlessDynamicInterface: fakeClient,
},
}

err := createResource(&createArgs{
out: buf,
ctx: context.Background(),
clientGetter: &mock,
createOptions: fixCreateOptions(),
extraValues: []parameters.Value{
parameters.NewTyped(types.StringCustomFlagType, ".metadata.name", "test-name"),
parameters.NewTyped(types.StringCustomFlagType, ".metadata.namespace", "test-namespace"),
parameters.NewTyped(types.IntCustomFlagType, ".spec.replicas", 1),
},
})
require.Equal(t, clierror.Wrap(errors.New("test error"), clierror.New("failed to create resource")), err)
})
}

type mockGetter struct {
clierror clierror.Error
client kube.Client
}

func (m *mockGetter) GetKubeClientWithClierr() (kube.Client, clierror.Error) {
return m.client, m.clierror
}

func fixCreateCommand(writer io.Writer, clietGetter KubeClientGetter) *cobra.Command {
return buildCreateCommand(writer, clietGetter, fixCreateOptions())
}

func fixCreateOptions() *CreateOptions {
return &CreateOptions{
ResourceInfo: types.ResourceInfo{
Scope: types.NamespaceScope,
Kind: "Deployment",
Group: "apps",
Version: "v1",
},
CreateCommand: types.CreateCommand{
Description: "create test deploy",
DescriptionLong: "use this to create test deploy",
CustomFlags: []types.CreateCustomFlag{
{
Type: types.IntCustomFlagType,
Name: "replicas",
Description: "test flag",
Shorthand: "r",
Path: ".spec.replicas",
DefaultValue: 3,
Required: false,
},
},
},
}
}

func fixUnstructuredDeployment() unstructured.Unstructured {
return unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deploy",
"namespace": "test-namespace",
},
"spec": map[string]interface{}{
"replicas": int64(2),
},
},
}
}
17 changes: 11 additions & 6 deletions internal/cmd/alpha/templates/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,28 @@ package templates

import (
"fmt"
"io"
"os"

"github.com/kyma-project/cli.v3/internal/cmd/alpha/templates/types"
"github.com/spf13/cobra"
)

type ExplainOptions struct {
Short string
Long string
Output string
types.ExplainCommand
}

func BuildExplainCommand(explainOptions *ExplainOptions) *cobra.Command {
return buildExplainCommand(os.Stdout, explainOptions)
}

func buildExplainCommand(out io.Writer, explainOptions *ExplainOptions) *cobra.Command {
return &cobra.Command{
Use: "explain",
Short: explainOptions.Short,
Long: explainOptions.Long,
Short: explainOptions.Description,
Long: explainOptions.DescriptionLong,
Run: func(_ *cobra.Command, _ []string) {
fmt.Println(explainOptions.Output)
fmt.Fprintln(out, explainOptions.Output)
},
}
}
31 changes: 31 additions & 0 deletions internal/cmd/alpha/templates/explain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package templates

import (
"bytes"
"testing"

"github.com/kyma-project/cli.v3/internal/cmd/alpha/templates/types"
"github.com/stretchr/testify/require"
)

func Test_explain(t *testing.T) {
t.Run("print output string", func(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
cmd := buildExplainCommand(buf, &ExplainOptions{
ExplainCommand: types.ExplainCommand{
Description: "test explain command",
DescriptionLong: "this is test explain command",
Output: "test output",
},
})

require.Equal(t, "explain", cmd.Use)
require.Equal(t, "test explain command", cmd.Short)
require.Equal(t, "this is test explain command", cmd.Long)

err := cmd.Execute()
require.NoError(t, err)

require.Equal(t, "test output\n", buf.String())
})
}
26 changes: 26 additions & 0 deletions internal/cmd/alpha/templates/parameters/default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package parameters

// this func makes sure that types used as an default follows YAML standards
// converts:
// int8,int16,int32,int64,int to int64
// string, []byte to string
func sanitizeDefaultValue(defaultValue interface{}) interface{} {
switch value := defaultValue.(type) {
case int8:
return int64(value)
case int16:
return int64(value)
case int32:
return int64(value)
case int64:
return value
case int:
return int64(value)
case string:
return value
case []byte:
return string(value)
default:
return nil
}
}
Loading

0 comments on commit a5f7df3

Please sign in to comment.