From 64cc3ab83fdc4f0e0c735077150bcefbdfb00dd6 Mon Sep 17 00:00:00 2001 From: Jacob Colvin Date: Thu, 2 Jan 2025 23:18:31 -0500 Subject: [PATCH] Refactor cmd and pkg/plugin for easier testing --- cmd/kclx/main.go | 41 ++++----- {cmd/kclx => internal/cli}/chart.go | 2 +- internal/cli/root.go | 31 +++++++ internal/cli/root_test.go | 63 ++++++++++++++ internal/cli/testdata/simple.k | 1 + {cmd/kclx => internal/cli}/version.go | 2 +- pkg/plugin/helm/plugin.go | 114 +++++++++++++------------- pkg/plugin/helm/plugin_test.go | 4 +- pkg/plugin/http/plugin.go | 56 ++++++------- pkg/plugin/http/plugin_test.go | 4 +- pkg/plugin/os/plugin.go | 58 ++++++------- pkg/plugin/os/plugin_test.go | 4 +- 12 files changed, 231 insertions(+), 149 deletions(-) rename {cmd/kclx => internal/cli}/chart.go (99%) create mode 100644 internal/cli/root.go create mode 100644 internal/cli/root_test.go create mode 100644 internal/cli/testdata/simple.k rename {cmd/kclx => internal/cli}/version.go (97%) diff --git a/cmd/kclx/main.go b/cmd/kclx/main.go index bb64b63..c1a68d4 100644 --- a/cmd/kclx/main.go +++ b/cmd/kclx/main.go @@ -13,10 +13,11 @@ import ( kclcmd "kcl-lang.io/cli/cmd/kcl/commands" "kcl-lang.io/cli/pkg/plugin" + "github.com/MacroPower/kclx/internal/cli" "github.com/MacroPower/kclx/pkg/log" - _ "github.com/MacroPower/kclx/pkg/plugin/helm" - _ "github.com/MacroPower/kclx/pkg/plugin/http" - _ "github.com/MacroPower/kclx/pkg/plugin/os" + helmplugin "github.com/MacroPower/kclx/pkg/plugin/helm" + httpplugin "github.com/MacroPower/kclx/pkg/plugin/http" + osplugin "github.com/MacroPower/kclx/pkg/plugin/os" ) func init() { @@ -25,7 +26,8 @@ func init() { } const ( - cmdName = "kcl" + cmdName = "kcl" + shortDesc = "The KCL Extended Command Line Interface (CLI)." longDesc = `The KCL Extended Command Line Interface (CLI). @@ -36,29 +38,18 @@ scenarios. The KCL website: https://kcl-lang.io ) func main() { - cmd := &cobra.Command{ - Use: cmdName, - Short: shortDesc, - Long: longDesc, - SilenceUsage: true, - SilenceErrors: true, - Version: GetVersionString(), + if strings.ToLower(os.Getenv("KCLX_HELM_PLUGIN_DISABLED")) != "true" { + helmplugin.Register() + } + if strings.ToLower(os.Getenv("KCLX_HTTP_PLUGIN_DISABLED")) != "true" { + httpplugin.Register() + } + if strings.ToLower(os.Getenv("KCLX_OS_PLUGIN_DISABLED")) != "true" { + osplugin.Register() } - cmd.AddCommand(kclcmd.NewRunCmd()) - cmd.AddCommand(kclcmd.NewLintCmd()) - cmd.AddCommand(kclcmd.NewDocCmd()) - cmd.AddCommand(kclcmd.NewFmtCmd()) - cmd.AddCommand(kclcmd.NewTestCmd()) - cmd.AddCommand(kclcmd.NewVetCmd()) - cmd.AddCommand(kclcmd.NewCleanCmd()) - cmd.AddCommand(kclcmd.NewImportCmd()) - cmd.AddCommand(kclcmd.NewModCmd()) - cmd.AddCommand(kclcmd.NewRegistryCmd()) - cmd.AddCommand(kclcmd.NewServerCmd()) - cmd.AddCommand(NewVersionCmd()) - cmd.AddCommand(NewChartCmd()) - bootstrapCmdPlugin(cmd, plugin.NewDefaultPluginHandler([]string{cmdName})) + cmd := cli.NewRootCmd(cmdName, shortDesc, longDesc) + bootstrapCmdPlugin(cmd, plugin.NewDefaultPluginHandler([]string{cmdName})) if err := cmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, strings.TrimLeft(err.Error(), "\n")) os.Exit(1) diff --git a/cmd/kclx/chart.go b/internal/cli/chart.go similarity index 99% rename from cmd/kclx/chart.go rename to internal/cli/chart.go index 3707260..3ce563b 100644 --- a/cmd/kclx/chart.go +++ b/internal/cli/chart.go @@ -1,4 +1,4 @@ -package main +package cli import ( "errors" diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 0000000..e005067 --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,31 @@ +package cli + +import ( + "github.com/spf13/cobra" + kclcmd "kcl-lang.io/cli/cmd/kcl/commands" +) + +func NewRootCmd(name, shortDesc, longDesc string) *cobra.Command { + cmd := &cobra.Command{ + Use: name, + Short: shortDesc, + Long: longDesc, + SilenceUsage: true, + SilenceErrors: true, + Version: GetVersionString(), + } + cmd.AddCommand(kclcmd.NewRunCmd()) + cmd.AddCommand(kclcmd.NewLintCmd()) + cmd.AddCommand(kclcmd.NewDocCmd()) + cmd.AddCommand(kclcmd.NewFmtCmd()) + cmd.AddCommand(kclcmd.NewTestCmd()) + cmd.AddCommand(kclcmd.NewVetCmd()) + cmd.AddCommand(kclcmd.NewCleanCmd()) + cmd.AddCommand(kclcmd.NewImportCmd()) + cmd.AddCommand(kclcmd.NewModCmd()) + cmd.AddCommand(kclcmd.NewRegistryCmd()) + cmd.AddCommand(kclcmd.NewServerCmd()) + cmd.AddCommand(NewVersionCmd()) + cmd.AddCommand(NewChartCmd()) + return cmd +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 0000000..4302ce0 --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,63 @@ +package cli_test + +import ( + "bytes" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MacroPower/kclx/internal/cli" +) + +var testDataDir string + +func init() { + //nolint:dogsled + _, filename, _, _ := runtime.Caller(0) + dir := filepath.Dir(filename) + testDataDir = filepath.Join(dir, "testdata") + + os.Setenv("KCLX_HELM_PLUGIN_DISABLED", "true") + os.Setenv("KCLX_OS_PLUGIN_DISABLED", "true") + os.Setenv("KCLX_HTTP_PLUGIN_DISABLED", "true") +} + +func TestRun(t *testing.T) { + t.Parallel() + + tc := cli.NewRootCmd("test", "", "") + out := bytes.NewBufferString("") + outFile := filepath.Join(testDataDir, "got/simple.json") + err := os.MkdirAll(filepath.Dir(outFile), 0o755) + require.NoError(t, err) + + tc.SetArgs([]string{"run", filepath.Join(testDataDir, "simple.k"), "--format=json", "--output", outFile}) + tc.SetOut(out) + + err = tc.Execute() + require.NoError(t, err) + require.Empty(t, out.String()) + + outData, err := os.ReadFile(outFile) + require.NoError(t, err) + + require.JSONEq(t, `{"a":1}`, string(outData)) +} + +func BenchmarkRun(b *testing.B) { + for i := 0; i < b.N; i++ { + tc := cli.NewRootCmd("test", "", "") + + //plugin.RegisterPlugin(helm.HelmPlugin) + + out := bytes.NewBufferString("") + tc.SetArgs([]string{"run", filepath.Join(testDataDir, "simple.k"), "--output=/dev/null"}) + tc.SetOut(out) + err := tc.Execute() + require.NoError(b, err) + require.Empty(b, out.String()) + } +} diff --git a/internal/cli/testdata/simple.k b/internal/cli/testdata/simple.k new file mode 100644 index 0000000..1337a53 --- /dev/null +++ b/internal/cli/testdata/simple.k @@ -0,0 +1 @@ +a = 1 diff --git a/cmd/kclx/version.go b/internal/cli/version.go similarity index 97% rename from cmd/kclx/version.go rename to internal/cli/version.go index 59a7fb1..4ed6e86 100644 --- a/cmd/kclx/version.go +++ b/internal/cli/version.go @@ -1,4 +1,4 @@ -package main +package cli import ( "fmt" diff --git a/pkg/plugin/helm/plugin.go b/pkg/plugin/helm/plugin.go index c36d902..4ed89a7 100644 --- a/pkg/plugin/helm/plugin.go +++ b/pkg/plugin/helm/plugin.go @@ -11,72 +11,70 @@ import ( kclutil "github.com/MacroPower/kclx/pkg/kclutil" ) -func init() { - if strings.ToLower(os.Getenv("KCLX_HELM_PLUGIN_DISABLED")) == "true" { - return - } +func Register() { + plugin.RegisterPlugin(Plugin) +} - plugin.RegisterPlugin(plugin.Plugin{ - Name: "helm", - MethodMap: map[string]plugin.MethodSpec{ - "template": { - Type: &plugin.MethodType{ - KwArgsType: map[string]string{ - "chart": "str", - "target_revision": "str", - "repo_url": "str", - "release_name": "str", - "namespace": "str", - "helm_version": "str", - "skip_crds": "bool", - "skip_schema_validation": "bool", - "pass_credentials": "bool", - "values": "{str:any}", - }, - ResultType: "[{str:any}]", +var Plugin = plugin.Plugin{ + Name: "helm", + MethodMap: map[string]plugin.MethodSpec{ + "template": { + Type: &plugin.MethodType{ + KwArgsType: map[string]string{ + "chart": "str", + "target_revision": "str", + "repo_url": "str", + "release_name": "str", + "namespace": "str", + "helm_version": "str", + "skip_crds": "bool", + "skip_schema_validation": "bool", + "pass_credentials": "bool", + "values": "{str:any}", }, - Body: func(args *plugin.MethodArgs) (*plugin.MethodResult, error) { - safeArgs := kclutil.SafeMethodArgs{Args: args} + ResultType: "[{str:any}]", + }, + Body: func(args *plugin.MethodArgs) (*plugin.MethodResult, error) { + safeArgs := kclutil.SafeMethodArgs{Args: args} - chartName := args.StrKwArg("chart") - targetRevision := args.StrKwArg("target_revision") - repoURL := args.StrKwArg("repo_url") + chartName := args.StrKwArg("chart") + targetRevision := args.StrKwArg("target_revision") + repoURL := args.StrKwArg("repo_url") - // https://argo-cd.readthedocs.io/en/stable/user-guide/build-environment/ - // https://github.com/argoproj/argo-cd/pull/15186 - project := os.Getenv("ARGOCD_APP_PROJECT_NAME") - namespace := safeArgs.StrKwArg("namespace", os.Getenv("ARGOCD_APP_NAMESPACE")) - kubeVersion := os.Getenv("KUBE_VERSION") - kubeAPIVersions := os.Getenv("KUBE_API_VERSIONS") + // https://argo-cd.readthedocs.io/en/stable/user-guide/build-environment/ + // https://github.com/argoproj/argo-cd/pull/15186 + project := os.Getenv("ARGOCD_APP_PROJECT_NAME") + namespace := safeArgs.StrKwArg("namespace", os.Getenv("ARGOCD_APP_NAMESPACE")) + kubeVersion := os.Getenv("KUBE_VERSION") + kubeAPIVersions := os.Getenv("KUBE_API_VERSIONS") - helmClient, err := helm.NewClient(helm.NewTempPaths(os.TempDir(), helm.NewBase64PathEncoder()), project, "10M") - if err != nil { - return nil, fmt.Errorf("failed to create helm client: %w", err) - } + helmClient, err := helm.NewClient(helm.NewTempPaths(os.TempDir(), helm.NewBase64PathEncoder()), project, "10M") + if err != nil { + return nil, fmt.Errorf("failed to create helm client: %w", err) + } - helmChart := helm.NewChart(helmClient, helm.TemplateOpts{ - ChartName: chartName, - TargetRevision: targetRevision, - RepoURL: repoURL, - ReleaseName: safeArgs.StrKwArg("release_name", chartName), - Namespace: namespace, - HelmVersion: safeArgs.StrKwArg("helm_version", "v3"), - SkipCRDs: safeArgs.BoolKwArg("skip_crds", false), - SkipSchemaValidation: safeArgs.BoolKwArg("skip_schema_validation", true), - PassCredentials: safeArgs.BoolKwArg("pass_credentials", false), - ValuesObject: safeArgs.MapKwArg("values", map[string]any{}), - KubeVersion: kubeVersion, - APIVersions: strings.Split(kubeAPIVersions, ","), - }) + helmChart := helm.NewChart(helmClient, helm.TemplateOpts{ + ChartName: chartName, + TargetRevision: targetRevision, + RepoURL: repoURL, + ReleaseName: safeArgs.StrKwArg("release_name", chartName), + Namespace: namespace, + HelmVersion: safeArgs.StrKwArg("helm_version", "v3"), + SkipCRDs: safeArgs.BoolKwArg("skip_crds", false), + SkipSchemaValidation: safeArgs.BoolKwArg("skip_schema_validation", true), + PassCredentials: safeArgs.BoolKwArg("pass_credentials", false), + ValuesObject: safeArgs.MapKwArg("values", map[string]any{}), + KubeVersion: kubeVersion, + APIVersions: strings.Split(kubeAPIVersions, ","), + }) - objs, err := helmChart.Template() - if err != nil { - return nil, fmt.Errorf("failed to template '%s': %w", chartName, err) - } + objs, err := helmChart.Template() + if err != nil { + return nil, fmt.Errorf("failed to template '%s': %w", chartName, err) + } - return &plugin.MethodResult{V: objs}, nil - }, + return &plugin.MethodResult{V: objs}, nil }, }, - }) + }, } diff --git a/pkg/plugin/helm/plugin_test.go b/pkg/plugin/helm/plugin_test.go index e984397..35bb93a 100644 --- a/pkg/plugin/helm/plugin_test.go +++ b/pkg/plugin/helm/plugin_test.go @@ -11,7 +11,7 @@ import ( "kcl-lang.io/lib/go/native" "github.com/MacroPower/kclx/pkg/log" - _ "github.com/MacroPower/kclx/pkg/plugin/helm" + helmplugin "github.com/MacroPower/kclx/pkg/plugin/helm" ) var testDataDir string @@ -28,6 +28,8 @@ func init() { func TestPluginHelmTemplate(t *testing.T) { t.Parallel() + helmplugin.Register() + tcs := map[string]struct { kclFile string resultsFile string diff --git a/pkg/plugin/http/plugin.go b/pkg/plugin/http/plugin.go index b4084d5..047b0a1 100644 --- a/pkg/plugin/http/plugin.go +++ b/pkg/plugin/http/plugin.go @@ -2,8 +2,6 @@ package http import ( "fmt" - "os" - "strings" "time" "kcl-lang.io/lib/go/plugin" @@ -12,37 +10,35 @@ import ( "github.com/MacroPower/kclx/pkg/kclutil" ) -func init() { - if strings.ToLower(os.Getenv("KCLX_HTTP_PLUGIN_DISABLED")) == "true" { - return - } +func Register() { + plugin.RegisterPlugin(Plugin) +} - plugin.RegisterPlugin(plugin.Plugin{ - Name: "http", - MethodMap: map[string]plugin.MethodSpec{ - "get": { - // http.get(url, timeout="30s") - Body: func(args *plugin.MethodArgs) (*plugin.MethodResult, error) { - safeArgs := kclutil.SafeMethodArgs{Args: args} +var Plugin = plugin.Plugin{ + Name: "http", + MethodMap: map[string]plugin.MethodSpec{ + "get": { + // http.get(url, timeout="30s") + Body: func(args *plugin.MethodArgs) (*plugin.MethodResult, error) { + safeArgs := kclutil.SafeMethodArgs{Args: args} - urlArg := args.StrArg(0) - timeout := safeArgs.StrKwArg("timeout", "30s") - timeoutDuration, err := time.ParseDuration(timeout) - if err != nil { - return nil, fmt.Errorf("failed to parse timeout %s: %w", timeout, err) - } - client := http.NewClient(timeoutDuration) - body, status, err := client.Get(urlArg) - if err != nil { - return nil, fmt.Errorf("failed to get '%s': %w", urlArg, err) - } + urlArg := args.StrArg(0) + timeout := safeArgs.StrKwArg("timeout", "30s") + timeoutDuration, err := time.ParseDuration(timeout) + if err != nil { + return nil, fmt.Errorf("failed to parse timeout %s: %w", timeout, err) + } + client := http.NewClient(timeoutDuration) + body, status, err := client.Get(urlArg) + if err != nil { + return nil, fmt.Errorf("failed to get '%s': %w", urlArg, err) + } - return &plugin.MethodResult{V: map[string]any{ - "status": status, - "body": string(body), - }}, nil - }, + return &plugin.MethodResult{V: map[string]any{ + "status": status, + "body": string(body), + }}, nil }, }, - }) + }, } diff --git a/pkg/plugin/http/plugin_test.go b/pkg/plugin/http/plugin_test.go index 032e4f9..b19720f 100644 --- a/pkg/plugin/http/plugin_test.go +++ b/pkg/plugin/http/plugin_test.go @@ -8,12 +8,14 @@ import ( "kcl-lang.io/kcl-go/pkg/spec/gpyrpc" "kcl-lang.io/lib/go/native" - _ "github.com/MacroPower/kclx/pkg/http" + httpplugin "github.com/MacroPower/kclx/pkg/plugin/http" ) func TestPluginHttp(t *testing.T) { t.Parallel() + httpplugin.Register() + code := ` import kcl_plugin.http diff --git a/pkg/plugin/os/plugin.go b/pkg/plugin/os/plugin.go index 0b24795..5a49537 100644 --- a/pkg/plugin/os/plugin.go +++ b/pkg/plugin/os/plugin.go @@ -2,47 +2,43 @@ package os import ( "fmt" - goos "os" - "strings" "kcl-lang.io/kcl-go/pkg/plugin" "github.com/MacroPower/kclx/pkg/os" ) -func init() { - if strings.ToLower(goos.Getenv("KCLX_OS_PLUGIN_DISABLED")) == "true" { - return - } +func Register() { + plugin.RegisterPlugin(Plugin) +} - plugin.RegisterPlugin(plugin.Plugin{ - Name: "os", - MethodMap: map[string]plugin.MethodSpec{ - "exec": { - Body: func(args *plugin.MethodArgs) (*plugin.MethodResult, error) { - name := args.StrArg(0) - strArgs := []string{} - for _, v := range args.ListArg(1) { - strArgs = append(strArgs, fmt.Sprint(v)) - } - strEnvs := []string{} - if _, ok := args.KwArgs["env"]; ok { - for k, v := range args.MapKwArg("env") { - strEnvs = append(strEnvs, fmt.Sprintf("%s=%s", k, v)) - } +var Plugin = plugin.Plugin{ + Name: "os", + MethodMap: map[string]plugin.MethodSpec{ + "exec": { + Body: func(args *plugin.MethodArgs) (*plugin.MethodResult, error) { + name := args.StrArg(0) + strArgs := []string{} + for _, v := range args.ListArg(1) { + strArgs = append(strArgs, fmt.Sprint(v)) + } + strEnvs := []string{} + if _, ok := args.KwArgs["env"]; ok { + for k, v := range args.MapKwArg("env") { + strEnvs = append(strEnvs, fmt.Sprintf("%s=%s", k, v)) } + } - exec, err := os.Exec(name, strArgs, strEnvs) - if err != nil { - return nil, fmt.Errorf("failed to exec %s: %w", name, err) - } + exec, err := os.Exec(name, strArgs, strEnvs) + if err != nil { + return nil, fmt.Errorf("failed to exec %s: %w", name, err) + } - return &plugin.MethodResult{V: map[string]string{ - "stdout": exec.Stdout, - "stderr": exec.Stderr, - }}, nil - }, + return &plugin.MethodResult{V: map[string]string{ + "stdout": exec.Stdout, + "stderr": exec.Stderr, + }}, nil }, }, - }) + }, } diff --git a/pkg/plugin/os/plugin_test.go b/pkg/plugin/os/plugin_test.go index 3783fb3..4c7a1e0 100644 --- a/pkg/plugin/os/plugin_test.go +++ b/pkg/plugin/os/plugin_test.go @@ -7,12 +7,14 @@ import ( "kcl-lang.io/kcl-go/pkg/spec/gpyrpc" "kcl-lang.io/lib/go/native" - _ "github.com/MacroPower/kclx/pkg/plugin/os" + osplugin "github.com/MacroPower/kclx/pkg/plugin/os" ) func TestPluginExecStdout(t *testing.T) { t.Parallel() + osplugin.Register() + code := ` import kcl_plugin.os