diff --git a/commands/live/apply/cmdapply.go b/commands/live/apply/cmdapply.go index e9a45cad80..c00fe8dadd 100644 --- a/commands/live/apply/cmdapply.go +++ b/commands/live/apply/cmdapply.go @@ -28,9 +28,11 @@ import ( alphaprinterstable "github.com/kptdev/kpt/pkg/printer/table" "github.com/kptdev/kpt/pkg/status" "github.com/spf13/cobra" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" "k8s.io/kubectl/pkg/cmd/util" "sigs.k8s.io/cli-utils/cmd/flagutils" "sigs.k8s.io/cli-utils/pkg/apply" @@ -54,6 +56,13 @@ func NewRunner( applyRunner: runApply, alpha: alpha, } + r.namespaceChecker = func(namespace string) error { + clientset, err := factory.KubernetesClientSet() + if err != nil { + return err + } + return checkNamespaceExists(clientset, namespace) + } c := &cobra.Command{ Use: "apply [PKG_PATH | -]", RunE: r.runE, @@ -124,6 +133,7 @@ type Runner struct { applyRunner func(r *Runner, invInfo inventory.Info, objs []*unstructured.Unstructured, dryRunStrategy common.DryRunStrategy) error + namespaceChecker func(namespace string) error } func (r *Runner) preRunE(cmd *cobra.Command, _ []string) error { @@ -191,6 +201,12 @@ func (r *Runner) runE(c *cobra.Command, args []string) error { return err } + if !live.NamespaceInObjects(objs, inv.Namespace) { + if err := r.namespaceChecker(inv.Namespace); err != nil { + return err + } + } + invInfo, err := live.ToInventoryInfo(inv) if err != nil { return err @@ -289,3 +305,14 @@ func runApply(r *Runner, invInfo inventory.Info, objs []*unstructured.Unstructur } return printer.Print(ch, dryRunStrategy, r.printStatusEvents) } + +func checkNamespaceExists(clientset kubernetes.Interface, namespace string) error { + if namespace == "" { + namespace = "default" + } + _, err := clientset.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return fmt.Errorf("inventory namespace %q does not exist", namespace) + } + return err +} diff --git a/commands/live/apply/cmdapply_test.go b/commands/live/apply/cmdapply_test.go index a6eb85d311..74c39a9c87 100644 --- a/commands/live/apply/cmdapply_test.go +++ b/commands/live/apply/cmdapply_test.go @@ -23,13 +23,57 @@ import ( "github.com/kptdev/kpt/pkg/kptfile/kptfileutil" "github.com/kptdev/kpt/pkg/printer/fake" "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/cli-runtime/pkg/genericclioptions" + k8sfake "k8s.io/client-go/kubernetes/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/inventory" ) +func TestCheckNamespaceExistsWithClient(t *testing.T) { + testCases := map[string]struct { + namespace string + existingNs string + expectedErrMsg string + }{ + "namespace exists": { + namespace: "my-ns", + existingNs: "my-ns", + }, + "namespace does not exist": { + namespace: "missing-ns", + existingNs: "other-ns", + expectedErrMsg: `inventory namespace "missing-ns" does not exist`, + }, + "empty namespace defaults to default and exists": { + namespace: "", + existingNs: "default", + }, + "empty namespace defaults to default but default missing": { + namespace: "", + existingNs: "other-ns", + expectedErrMsg: `inventory namespace "default" does not exist`, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + clientset := k8sfake.NewSimpleClientset(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: tc.existingNs}, + }) + err := checkNamespaceExists(clientset, tc.namespace) + if tc.expectedErrMsg != "" { + assert.EqualError(t, err, tc.expectedErrMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestCmd(t *testing.T) { testCases := map[string]struct { args []string @@ -148,6 +192,7 @@ func TestCmd(t *testing.T) { runner := NewRunner(fake.CtxWithDefaultPrinter(), tf, ioStreams, false) runner.Command.SetArgs(tc.args) + runner.namespaceChecker = func(_ string) error { return nil } runner.applyRunner = func(_ *Runner, inv inventory.Info, _ []*unstructured.Unstructured, _ common.DryRunStrategy) error { tc.applyCallbackFunc(t, runner, inv) diff --git a/pkg/live/load.go b/pkg/live/load.go index a7bf9c9aa8..7d39bcf7ea 100644 --- a/pkg/live/load.go +++ b/pkg/live/load.go @@ -321,6 +321,16 @@ func validateInventory(inventory kptfilev1.Inventory) error { return nil } +// NamespaceInObjects return true if any object in objs is a Namespace with the given name +func NamespaceInObjects(objs []*unstructured.Unstructured, namespace string) bool { + for _, obj := range objs { + if obj.GetKind() == "Namespace" && obj.GetName() == namespace { + return true + } + } + return false +} + func generateInventoryObj(inv kptfilev1.Inventory) *unstructured.Unstructured { // Create and return ResourceGroup custom resource as inventory object.