diff --git a/cli_tests/app.sh b/cli_tests/app.sh index b6596f24..6c1337a2 100755 --- a/cli_tests/app.sh +++ b/cli_tests/app.sh @@ -77,7 +77,7 @@ EOF } @test "framework export" { - run $KETCH framework export "$FRAMEWORK" + run $KETCH framework export "$FRAMEWORK" -f framework.yaml result=$(cat framework.yaml) echo "RECEIVED:" $result [[ $result =~ "name: $FRAMEWORK" ]] @@ -171,13 +171,13 @@ EOF } @test "app export" { - run $KETCH app export "$APP_NAME" + run $KETCH app export "$APP_NAME" -f app.yaml result=$(cat app.yaml) echo "RECEIVED:" $result [[ $result =~ "name: $APP_NAME" ]] [[ $result =~ "type: Application" ]] [[ $result =~ "framework: $FRAMEWORK" ]] - rm -f framework.yaml + rm -f app.yaml } @test "app stop" { diff --git a/cmd/ketch/app.go b/cmd/ketch/app.go index 007cf58d..82a734ed 100644 --- a/cmd/ketch/app.go +++ b/cmd/ketch/app.go @@ -40,7 +40,7 @@ func newAppCmd(cfg config, out io.Writer, packSvc *pack.Client, configDefaultBui cmd.AddCommand(newAppInfoCmd(cfg, out)) cmd.AddCommand(newAppStartCmd(cfg, out, appStart)) cmd.AddCommand(newAppStopCmd(cfg, out, appStop)) - cmd.AddCommand(newAppExportCmd(cfg, exportApp)) + cmd.AddCommand(newAppExportCmd(cfg, exportApp, out)) return cmd } diff --git a/cmd/ketch/app_export.go b/cmd/ketch/app_export.go index b8687513..86a428f9 100644 --- a/cmd/ketch/app_export.go +++ b/cmd/ketch/app_export.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "io" "os" "github.com/spf13/cobra" @@ -17,9 +18,9 @@ const appExportHelp = ` Export an application as a yaml file. ` -type appExportFn func(ctx context.Context, cfg config, options appExportOptions) error +type appExportFn func(ctx context.Context, cfg config, options appExportOptions, out io.Writer) error -func newAppExportCmd(cfg config, appExport appExportFn) *cobra.Command { +func newAppExportCmd(cfg config, appExport appExportFn, out io.Writer) *cobra.Command { options := appExportOptions{} cmd := &cobra.Command{ Use: "export APPNAME", @@ -28,13 +29,13 @@ func newAppExportCmd(cfg config, appExport appExportFn) *cobra.Command { Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { options.appName = args[0] - return appExport(cmd.Context(), cfg, options) + return appExport(cmd.Context(), cfg, options, out) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autoCompleteAppNames(cfg, toComplete) }, } - cmd.Flags().StringVarP(&options.filename, "file", "f", "app.yaml", "filename for app export") + cmd.Flags().StringVarP(&options.filename, "file", "f", "", "filename for app export") return cmd } @@ -43,26 +44,29 @@ type appExportOptions struct { filename string } -func exportApp(ctx context.Context, cfg config, options appExportOptions) error { +func exportApp(ctx context.Context, cfg config, options appExportOptions, out io.Writer) error { app := ketchv1.App{} if err := cfg.Client().Get(ctx, types.NamespacedName{Name: options.appName}, &app); err != nil { return fmt.Errorf("failed to get app: %w", err) } application := deploy.GetApplicationFromKetchApp(app) - // open file, err if exist, write application - _, err := os.Stat(options.filename) - if !os.IsNotExist(err) { - return errFileExists + if options.filename != "" { + // open file, err if exist, write application + _, err := os.Stat(options.filename) + if !os.IsNotExist(err) { + return errFileExists + } + f, err := os.Create(options.filename) + if err != nil { + return err + } + defer f.Close() + out = f } - f, err := os.Create(options.filename) - if err != nil { - return err - } - defer f.Close() b, err := yaml.Marshal(application) if err != nil { return err } - _, err = f.Write(b) + _, err = out.Write(b) return err } diff --git a/cmd/ketch/app_export_test.go b/cmd/ketch/app_export_test.go index 67a7dfda..7e00a974 100644 --- a/cmd/ketch/app_export_test.go +++ b/cmd/ketch/app_export_test.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "context" + "io" "os" "path/filepath" "testing" @@ -29,7 +31,7 @@ func Test_newAppExportCmd(t *testing.T) { { name: "happy path", args: []string{"foo-bar"}, - appExport: func(ctx context.Context, cfg config, options appExportOptions) error { + appExport: func(ctx context.Context, cfg config, options appExportOptions, out io.Writer) error { require.Equal(t, "foo-bar", options.appName) return nil }, @@ -42,7 +44,7 @@ func Test_newAppExportCmd(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmd := newAppExportCmd(nil, tt.appExport) + cmd := newAppExportCmd(nil, tt.appExport, &bytes.Buffer{}) cmd.SetArgs(tt.args) err := cmd.Execute() if tt.wantErr { @@ -112,6 +114,27 @@ func Test_appExport(t *testing.T) { }, }, }, + options: appExportOptions{ + appName: "dashboard", + filename: "app.yaml", + }, + wantOut: `framework: gke +name: dashboard +type: Application +version: v1 +`, + }, + { + name: "success - stdout", + cfg: &mocks.Configuration{ + CtrlClientObjects: []runtime.Object{dashboard, gke}, + StorageInstance: &mockStorage{ + OnGet: func(name string) (*templates.Templates, error) { + require.Equal(t, templates.IngressConfigMapName(ketchv1.IstioIngressControllerType.String()), name) + return &templates.Templates{}, nil + }, + }, + }, options: appExportOptions{ appName: "dashboard", }, @@ -136,15 +159,20 @@ version: v1 t.Run(tt.name, func(t *testing.T) { tt.options.filename = filepath.Join(t.TempDir(), "app.yaml") defer os.Remove(tt.options.filename) - - err := exportApp(context.Background(), tt.cfg, tt.options) + buf := &bytes.Buffer{} + err := exportApp(context.Background(), tt.cfg, tt.options, buf) if tt.wantErr != "" { require.Equal(t, err.Error(), tt.wantErr) + return } else { require.Nil(t, err) + } + if tt.options.filename != "" { b, err := os.ReadFile(tt.options.filename) require.Nil(t, err) require.Equal(t, tt.wantOut, string(b)) + } else { + require.Equal(t, tt.wantOut, buf.String()) } }) } diff --git a/cmd/ketch/framework.go b/cmd/ketch/framework.go index 84789d37..ee08c905 100644 --- a/cmd/ketch/framework.go +++ b/cmd/ketch/framework.go @@ -39,7 +39,7 @@ func newFrameworkCmd(cfg config, out io.Writer) *cobra.Command { cmd.AddCommand(newFrameworkAddCmd(cfg, out, addFramework)) cmd.AddCommand(newFrameworkRemoveCmd(cfg, out)) cmd.AddCommand(newFrameworkUpdateCmd(cfg, out)) - cmd.AddCommand(newFrameworkExportCmd(cfg)) + cmd.AddCommand(newFrameworkExportCmd(cfg, out)) return cmd } diff --git a/cmd/ketch/framework_add.go b/cmd/ketch/framework_add.go index a978b00b..beecd263 100644 --- a/cmd/ketch/framework_add.go +++ b/cmd/ketch/framework_add.go @@ -150,6 +150,7 @@ func newFrameworkFromArgs(options frameworkAddOptions) *ketchv1.Framework { Name: options.name, }, Spec: ketchv1.FrameworkSpec{ + Name: options.name, NamespaceName: namespace, AppQuotaLimit: &options.appQuotaLimit, IngressController: ketchv1.IngressControllerSpec{ diff --git a/cmd/ketch/framework_add_test.go b/cmd/ketch/framework_add_test.go index 6132a238..8ff87add 100644 --- a/cmd/ketch/framework_add_test.go +++ b/cmd/ketch/framework_add_test.go @@ -106,6 +106,7 @@ ingressController: ingressClusterIssuer: "le-production", }, wantFrameworkSpec: ketchv1.FrameworkSpec{ + Name: "hello", NamespaceName: "gke", AppQuotaLimit: conversions.IntPtr(5), IngressController: ketchv1.IngressControllerSpec{ @@ -135,6 +136,7 @@ ingressController: ingressClusterIssuer: "le-production", }, wantFrameworkSpec: ketchv1.FrameworkSpec{ + Name: "hello", NamespaceName: "gke", AppQuotaLimit: conversions.IntPtr(5), IngressController: ketchv1.IngressControllerSpec{ @@ -160,6 +162,7 @@ ingressController: ingressType: traefik, }, wantFrameworkSpec: ketchv1.FrameworkSpec{ + Name: "aws", NamespaceName: "ketch-aws", AppQuotaLimit: conversions.IntPtr(5), IngressController: ketchv1.IngressControllerSpec{ @@ -367,6 +370,7 @@ func TestNewFrameworkFromArgs(t *testing.T) { Name: "hello", }, Spec: ketchv1.FrameworkSpec{ + Name: "hello", NamespaceName: "my-namespace", AppQuotaLimit: conversions.IntPtr(5), IngressController: ketchv1.IngressControllerSpec{ @@ -389,6 +393,7 @@ func TestNewFrameworkFromArgs(t *testing.T) { Name: "hello", }, Spec: ketchv1.FrameworkSpec{ + Name: "hello", NamespaceName: "ketch-hello", AppQuotaLimit: conversions.IntPtr(5), IngressController: ketchv1.IngressControllerSpec{ diff --git a/cmd/ketch/framework_export.go b/cmd/ketch/framework_export.go index ddb18221..db60ab11 100644 --- a/cmd/ketch/framework_export.go +++ b/cmd/ketch/framework_export.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "io" "os" "github.com/spf13/cobra" @@ -21,7 +22,7 @@ const frameworkExportHelp = `Export a framework's configuration file.` var errFileExists = errors.New("file already exists") -func newFrameworkExportCmd(cfg config) *cobra.Command { +func newFrameworkExportCmd(cfg config, out io.Writer) *cobra.Command { var options frameworkExportOptions cmd := &cobra.Command{ @@ -31,34 +32,38 @@ func newFrameworkExportCmd(cfg config) *cobra.Command { Long: frameworkExportHelp, RunE: func(cmd *cobra.Command, args []string) error { options.frameworkName = args[0] - return exportFramework(cmd.Context(), cfg, options) + return exportFramework(cmd.Context(), cfg, options, out) }, } - cmd.Flags().StringVarP(&options.filename, "file", "f", "framework.yaml", "filename for framework export") + cmd.Flags().StringVarP(&options.filename, "file", "f", "", "filename for framework export") return cmd } -func exportFramework(ctx context.Context, cfg config, options frameworkExportOptions) error { +func exportFramework(ctx context.Context, cfg config, options frameworkExportOptions, out io.Writer) error { var framework ketchv1.Framework err := cfg.Client().Get(ctx, types.NamespacedName{Name: options.frameworkName}, &framework) if err != nil { return err } framework.Spec.Name = framework.Name - // open file, err if exist, write framework.Spec - _, err = os.Stat(options.filename) - if !os.IsNotExist(err) { - return errFileExists - } - f, err := os.Create(options.filename) - if err != nil { - return err + + if options.filename != "" { + // open file, err if exist, write framework.Spec + _, err = os.Stat(options.filename) + if !os.IsNotExist(err) { + return errFileExists + } + f, err := os.Create(options.filename) + if err != nil { + return err + } + defer f.Close() + out = f } - defer f.Close() b, err := yaml.Marshal(framework.Spec) if err != nil { return err } - _, err = f.Write(b) + _, err = out.Write(b) return err } diff --git a/cmd/ketch/framework_export_test.go b/cmd/ketch/framework_export_test.go index cf3b4371..cc4c1c32 100644 --- a/cmd/ketch/framework_export_test.go +++ b/cmd/ketch/framework_export_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "os" "testing" @@ -51,6 +52,24 @@ ingressController: name: myframework namespace: ketch-myframework version: v1 +`, + }, + { + name: "success - stdout", + cfg: &mocks.Configuration{ + CtrlClientObjects: []runtime.Object{mockFramework}, + DynamicClientObjects: []runtime.Object{}, + }, + options: frameworkExportOptions{frameworkName: "myframework"}, + expected: `appQuotaLimit: 1 +ingressController: + className: traefik + clusterIssuer: letsencrypt + serviceEndpoint: 10.10.20.30 + type: traefik +name: myframework +namespace: ketch-myframework +version: v1 `, }, { @@ -72,16 +91,21 @@ version: v1 if tt.before != nil { tt.before() } - err := exportFramework(context.Background(), tt.cfg, tt.options) + buf := &bytes.Buffer{} + err := exportFramework(context.Background(), tt.cfg, tt.options, buf) if tt.err != nil { require.Equal(t, tt.err, err) return } else { require.Nil(t, err) } - data, err := os.ReadFile(tt.options.filename) - require.Nil(t, err) - require.Equal(t, tt.expected, string(data)) + if tt.options.filename != "" { + data, err := os.ReadFile(tt.options.filename) + require.Nil(t, err) + require.Equal(t, tt.expected, string(data)) + } else { + require.Equal(t, tt.expected, buf.String()) + } }) } } diff --git a/internal/deploy/yaml.go b/internal/deploy/yaml.go index 79d5d64e..4bc14b7c 100644 --- a/internal/deploy/yaml.go +++ b/internal/deploy/yaml.go @@ -16,7 +16,7 @@ import ( // Application represents the fields in an application.yaml file that will be // transitioned to a ChangeSet. type Application struct { - Version *string `json:"version"` + Version *string `json:"version,omitempty"` Type *string `json:"type"` Name *string `json:"name"` Image *string `json:"image,omitempty"`