diff --git a/pkg/kapp/cmd/app/app_flags.go b/pkg/kapp/cmd/app/app_flags.go index a4ed5f7b0..3b4d6e4ed 100644 --- a/pkg/kapp/cmd/app/app_flags.go +++ b/pkg/kapp/cmd/app/app_flags.go @@ -12,10 +12,12 @@ import ( type Flags struct { NamespaceFlags cmdcore.NamespaceFlags Name string + AppNamespace string } func (s *Flags) Set(cmd *cobra.Command, flagsFactory cmdcore.FlagsFactory) { s.NamespaceFlags.Set(cmd, flagsFactory) cmd.Flags().StringVarP(&s.Name, "app", "a", s.Name, "Set app name (or label selector) (format: name, label:key=val, !key)") + cmd.Flags().StringVar(&s.AppNamespace, "app-namespace", s.AppNamespace, "Set app namespace (to store app state)") } diff --git a/pkg/kapp/cmd/app/factory.go b/pkg/kapp/cmd/app/factory.go index 474d9ce77..2308fd1b2 100644 --- a/pkg/kapp/cmd/app/factory.go +++ b/pkg/kapp/cmd/app/factory.go @@ -18,9 +18,13 @@ type FactorySupportObjs struct { Apps ctlapp.Apps } -func FactoryClients(depsFactory cmdcore.DepsFactory, nsFlags cmdcore.NamespaceFlags, +func FactoryClients(depsFactory cmdcore.DepsFactory, nsFlags cmdcore.NamespaceFlags, appNamespace string, resTypesFlags ResourceTypesFlags, logger logger.Logger) (FactorySupportObjs, error) { + if appNamespace == "" { + appNamespace = nsFlags.Name + } + coreClient, err := depsFactory.CoreClient() if err != nil { return FactorySupportObjs{}, err @@ -56,7 +60,7 @@ func FactoryClients(depsFactory cmdcore.DepsFactory, nsFlags cmdcore.NamespaceFl CoreClient: coreClient, ResourceTypes: resTypes, IdentifiedResources: identifiedResources, - Apps: ctlapp.NewApps(nsFlags.Name, coreClient, identifiedResources, logger), + Apps: ctlapp.NewApps(appNamespace, coreClient, identifiedResources, logger), } return result, nil @@ -65,7 +69,7 @@ func FactoryClients(depsFactory cmdcore.DepsFactory, nsFlags cmdcore.NamespaceFl func Factory(depsFactory cmdcore.DepsFactory, appFlags Flags, resTypesFlags ResourceTypesFlags, logger logger.Logger) (ctlapp.App, FactorySupportObjs, error) { - supportingObjs, err := FactoryClients(depsFactory, appFlags.NamespaceFlags, resTypesFlags, logger) + supportingObjs, err := FactoryClients(depsFactory, appFlags.NamespaceFlags, appFlags.AppNamespace, resTypesFlags, logger) if err != nil { return nil, FactorySupportObjs{}, err } diff --git a/pkg/kapp/cmd/app/list.go b/pkg/kapp/cmd/app/list.go index 99801a587..31654e024 100644 --- a/pkg/kapp/cmd/app/list.go +++ b/pkg/kapp/cmd/app/list.go @@ -56,7 +56,7 @@ func (o *ListOptions) Run() error { nsHeader.Hidden = false } - supportObjs, err := FactoryClients(o.depsFactory, o.NamespaceFlags, ResourceTypesFlags{}, o.logger) + supportObjs, err := FactoryClients(o.depsFactory, o.NamespaceFlags, "", ResourceTypesFlags{}, o.logger) if err != nil { return err } diff --git a/pkg/kapp/cmd/appgroup/app_group_flags.go b/pkg/kapp/cmd/appgroup/app_group_flags.go index 01630b683..f8c430657 100644 --- a/pkg/kapp/cmd/appgroup/app_group_flags.go +++ b/pkg/kapp/cmd/appgroup/app_group_flags.go @@ -11,10 +11,12 @@ import ( type Flags struct { NamespaceFlags cmdcore.NamespaceFlags Name string + AppNamespace string } func (s *Flags) Set(cmd *cobra.Command, flagsFactory cmdcore.FlagsFactory) { s.NamespaceFlags.Set(cmd, flagsFactory) cmd.Flags().StringVarP(&s.Name, "group", "g", "", "Set app group name") + cmd.Flags().StringVar(&s.AppNamespace, "app-namespace", s.AppNamespace, "Set app namespace (to store app state)") } diff --git a/pkg/kapp/cmd/appgroup/delete.go b/pkg/kapp/cmd/appgroup/delete.go index c67298699..651d78b6d 100644 --- a/pkg/kapp/cmd/appgroup/delete.go +++ b/pkg/kapp/cmd/appgroup/delete.go @@ -55,7 +55,7 @@ func (o *DeleteOptions) Run() error { return fmt.Errorf("Expected group name to be non-empty") } - supportObjs, err := cmdapp.FactoryClients(o.depsFactory, o.AppGroupFlags.NamespaceFlags, cmdapp.ResourceTypesFlags{}, o.logger) + supportObjs, err := cmdapp.FactoryClients(o.depsFactory, o.AppGroupFlags.NamespaceFlags, o.AppGroupFlags.AppNamespace, cmdapp.ResourceTypesFlags{}, o.logger) if err != nil { return err } @@ -77,15 +77,23 @@ func (o *DeleteOptions) Run() error { func (o *DeleteOptions) deleteApp(name string) error { o.ui.PrintLinef("--- deleting app '%s' (namespace: %s)", - name, o.AppGroupFlags.NamespaceFlags.Name) + name, o.appNamespace()) deleteOpts := cmdapp.NewDeleteOptions(o.ui, o.depsFactory, o.logger) deleteOpts.AppFlags = cmdapp.Flags{ Name: name, NamespaceFlags: o.AppGroupFlags.NamespaceFlags, + AppNamespace: o.AppGroupFlags.AppNamespace, } deleteOpts.DiffFlags = o.AppFlags.DiffFlags deleteOpts.ApplyFlags = o.AppFlags.ApplyFlags return deleteOpts.Run() } + +func (o *DeleteOptions) appNamespace() string { + if o.AppGroupFlags.AppNamespace != "" { + return o.AppGroupFlags.AppNamespace + } + return o.AppGroupFlags.NamespaceFlags.Name +} diff --git a/pkg/kapp/cmd/appgroup/deploy.go b/pkg/kapp/cmd/appgroup/deploy.go index 3004d8943..6464c94d4 100644 --- a/pkg/kapp/cmd/appgroup/deploy.go +++ b/pkg/kapp/cmd/appgroup/deploy.go @@ -84,7 +84,7 @@ func (o *DeployOptions) Run() error { } } - supportObjs, err := cmdapp.FactoryClients(o.depsFactory, o.AppGroupFlags.NamespaceFlags, cmdapp.ResourceTypesFlags{}, o.logger) + supportObjs, err := cmdapp.FactoryClients(o.depsFactory, o.AppGroupFlags.NamespaceFlags, o.AppGroupFlags.AppNamespace, cmdapp.ResourceTypesFlags{}, o.logger) if err != nil { return err } @@ -142,12 +142,13 @@ func (o *DeployOptions) appsToUpdate() (map[string]appGroupApp, error) { func (o *DeployOptions) deployApp(app appGroupApp) error { o.ui.PrintLinef("--- deploying app '%s' (namespace: %s) from %s", - app.Name, o.AppGroupFlags.NamespaceFlags.Name, app.Path) + app.Name, o.appNamespace(), app.Path) deployOpts := cmdapp.NewDeployOptions(o.ui, o.depsFactory, o.logger) deployOpts.AppFlags = cmdapp.Flags{ Name: app.Name, NamespaceFlags: o.AppGroupFlags.NamespaceFlags, + AppNamespace: o.AppGroupFlags.AppNamespace, } deployOpts.FileFlags = cmdtools.FileFlags{ Files: []string{app.Path}, @@ -167,15 +168,23 @@ func (o *DeployOptions) deployApp(app appGroupApp) error { func (o *DeployOptions) deleteApp(name string) error { o.ui.PrintLinef("--- deleting app '%s' (namespace: %s)", - name, o.AppGroupFlags.NamespaceFlags.Name) + name, o.appNamespace()) deleteOpts := cmdapp.NewDeleteOptions(o.ui, o.depsFactory, o.logger) deleteOpts.AppFlags = cmdapp.Flags{ Name: name, NamespaceFlags: o.AppGroupFlags.NamespaceFlags, + AppNamespace: o.AppGroupFlags.AppNamespace, } deleteOpts.DiffFlags = o.AppFlags.DiffFlags deleteOpts.ApplyFlags = o.AppFlags.DeleteApplyFlags return deleteOpts.Run() } + +func (o *DeployOptions) appNamespace() string { + if o.AppGroupFlags.AppNamespace != "" { + return o.AppGroupFlags.AppNamespace + } + return o.AppGroupFlags.NamespaceFlags.Name +} diff --git a/test/e2e/app_namespace_test.go b/test/e2e/app_namespace_test.go new file mode 100644 index 000000000..6cd9dcf7a --- /dev/null +++ b/test/e2e/app_namespace_test.go @@ -0,0 +1,71 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAppNamespace(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + kubectl := Kubectl{t, env.Namespace, logger} + + name := "test-app-namespace" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", name}) + } + + cleanUp() + defer cleanUp() + + yaml := ` +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +` + + logger.Section("deploy app with -n flag", func() { + kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name, "-n", env.Namespace}, RunOpts{NoNamespace: true, StdinReader: strings.NewReader(yaml)}) + + // both app meta configmap and the resources should be present in the -n namespace + NewPresentClusterResource("configmap", name, env.Namespace, kubectl) + NewPresentClusterResource("configmap", "test-cm", env.Namespace, kubectl) + }) + + logger.Section("deploy same app with both -n and --app-namespace flag", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name, "-n", env.Namespace, "--app-namespace", env.Namespace, "--diff-run", "--diff-exit-status"}, + RunOpts{NoNamespace: true, AllowError: true, StdinReader: strings.NewReader(yaml)}) + + require.Errorf(t, err, "Expected to receive error") + + require.Containsf(t, err.Error(), "Exiting after diffing with no pending changes (exit status 2)", "Expected to find stderr output") + require.Containsf(t, err.Error(), "exit code: '2'", "Expected to find exit code") + }) + + logger.Section("deploy same app with --app-namespace flag only", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name, "--app-namespace", env.Namespace, "--diff-run", "--diff-exit-status"}, + RunOpts{NoNamespace: true, AllowError: true, StdinReader: strings.NewReader(yaml)}) + + require.Errorf(t, err, "Expected to receive error") + // Resources would get created in the default namespace from kubeconfig since -n is not provided + require.Containsf(t, err.Error(), "Exiting after diffing with pending changes (exit status 3)", "Expected to find stderr output") + require.Containsf(t, err.Error(), "exit code: '3'", "Expected to find exit code") + }) + + logger.Section("delete app with --app-namespace flag only", func() { + kapp.RunWithOpts([]string{"delete", "-a", name, "--app-namespace", env.Namespace}, + RunOpts{NoNamespace: true}) + + NewMissingClusterResource(t, "configmap", name, env.Namespace, kubectl) + NewMissingClusterResource(t, "configmap", "test-cm", env.Namespace, kubectl) + }) +}