Repository: kubernetes-sigs/ingress2gateway (go)

Issue:
Add support for resource filtering by namespace
<!-- Please only use this template for submitting enhancement requests -->

**What would you like to be added**:
Add a --namespace / -n flag to support Gateway API object filtering by namespace.
In addition, a --all-namespaces / -A flag to indicate that all namespaces should be used.
If none of the flags is specified, the current namespace will be used.

Pull request:
Add support for resource filtering by namespace
**What type of PR is this?**
<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind test

Optionally add one or more of the following kinds if applicable:
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind feature
**What this PR does / why we need it**:
This PR introduces 2 new flags which will enable the user to filter Gateway API objects by namespace.
**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #30 

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, please enter a release note below:
-->
```release-note
Add a --namespace / -n flag to support Gateway API object filtering by namespace.
In addition, a --all-namespaces / -A flag to indicate that all namespaces should be used.
The current namespace will be used if none of the flags is specified.
```


Patch:
diff --git a/Makefile b/Makefile
index 2d164d6..e3a2250 100644
--- a/Makefile
+++ b/Makefile
@@ -50,4 +50,4 @@ build: vet;$(info $(M)...Build the binary.)  @ ## Build the binary.
 # Run static analysis.
 .PHONY: verify
 verify:
-	hack/verify-all.sh
+	hack/verify-all.sh -v
diff --git a/cmd/print.go b/cmd/print.go
index 7b7996b..d8d7d50 100644
--- a/cmd/print.go
+++ b/cmd/print.go
@@ -23,12 +23,22 @@ import (
 	"github.com/spf13/cobra"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/cli-runtime/pkg/printers"
+	"k8s.io/client-go/tools/clientcmd"
 )
 
 var (
 	// outputFormat contains currently set output format. Value assigned via --output/-o flag.
 	// Defaults to YAML.
 	outputFormat = "yaml"
+
+	// The namespace used to query Gateway API objects. Value assigned via
+	// --namespace/-n flag.
+	// On absence, the current user active namespace is used.
+	namespace string
+
+	// allNamespaces indicates whether all namespaces should be used. Value assigned via
+	// --all-namespaces/-A flag.
+	allNamespaces bool
 )
 
 // printCmd represents the print command. It prints HTTPRoutes and Gateways
@@ -41,7 +51,11 @@ var printCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		i2gw.Run(resourcePrinter)
+		namespaceFilter, err := getNamespaceFilter(namespace, allNamespaces)
+		if err != nil {
+			return err
+		}
+		i2gw.Run(resourcePrinter, namespaceFilter)
 		return nil
 	},
 }
@@ -59,6 +73,33 @@ func getResourcePrinter(outputFormat string) (printers.ResourcePrinter, error) {
 	}
 }
 
+// getNamespaceFilter returns a namespace filter, taking into consideration whether a specific
+// namespace is requested, or all of them are.
+func getNamespaceFilter(requestedNamespace string, useAllNamespaces bool) (string, error) {
+
+	// When we should use all namespaces, return an empty string.
+	// This is the first condition since it should override the requestedNamespace,
+	// if specified.
+	if useAllNamespaces {
+		return "", nil
+	}
+
+	if requestedNamespace == "" {
+		return getNamespaceInCurrentContext()
+	}
+	return requestedNamespace, nil
+}
+
+// getNamespaceInCurrentContext returns the namespace in the current active context of the user.
+func getNamespaceInCurrentContext() (string, error) {
+	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
+
+	kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
+	currentNamespace, _, err := kubeConfig.Namespace()
+
+	return currentNamespace, err
+}
+
 func init() {
 	var printFlags genericclioptions.JSONYamlPrintFlags
 	allowedFormats := printFlags.AllowedFormats()
@@ -66,5 +107,14 @@ func init() {
 	printCmd.Flags().StringVarP(&outputFormat, "output", "o", "yaml",
 		fmt.Sprintf(`Output format. One of: (%s)`, strings.Join(allowedFormats, ", ")))
 
+	printCmd.Flags().StringVarP(&namespace, "namespace", "n", "",
+		fmt.Sprintf(`If present, the namespace scope for this CLI request`))
+
+	printCmd.Flags().BoolVarP(&allNamespaces, "all-namespaces", "A", false,
+		fmt.Sprintf(`If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even
+if specified with --namespace.`))
+
+	printCmd.MarkFlagsMutuallyExclusive("namespace", "all-namespaces")
+
 	rootCmd.AddCommand(printCmd)
 }
diff --git a/go.mod b/go.mod
index 438b5e7..511d1de 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	k8s.io/api v0.25.2
 	k8s.io/apimachinery v0.25.2
 	k8s.io/cli-runtime v0.25.2
+	k8s.io/client-go v0.25.2
 	sigs.k8s.io/controller-runtime v0.13.0
 	sigs.k8s.io/gateway-api v0.5.0
 )
@@ -58,7 +59,6 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/client-go v0.25.2 // indirect
 	k8s.io/klog/v2 v2.70.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
 	k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
diff --git a/pkg/i2gw/ingress2gateway.go b/pkg/i2gw/ingress2gateway.go
index b0f82bd..47e3307 100644
--- a/pkg/i2gw/ingress2gateway.go
+++ b/pkg/i2gw/ingress2gateway.go
@@ -27,12 +27,19 @@ import (
 	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 )
 
-func Run(printer printers.ResourcePrinter) {
-	cl, err := client.New(config.GetConfigOrDie(), client.Options{})
+func Run(printer printers.ResourcePrinter, namespace string) {
+	conf, err := config.GetConfig()
+	if err != nil {
+		fmt.Println("failed to get client config")
+		os.Exit(1)
+	}
+
+	cl, err := client.New(conf, client.Options{})
 	if err != nil {
 		fmt.Println("failed to create client")
 		os.Exit(1)
 	}
+	cl = client.NewNamespacedClient(cl, namespace)
 
 	ingressList := &networkingv1.IngressList{}
 
@@ -42,6 +49,16 @@ func Run(printer printers.ResourcePrinter) {
 		os.Exit(1)
 	}
 
+	if len(ingressList.Items) == 0 {
+		msg := "No resources found"
+		if namespace != "" {
+			fmt.Printf("%s in %s namespace\n", msg, namespace)
+		} else {
+			fmt.Println(msg)
+		}
+		return
+	}
+
 	httpRoutes, gateways, errors := ingresses2GatewaysAndHTTPRoutes(ingressList.Items)
 	if len(errors) > 0 {
 		fmt.Printf("# Encountered %d errors\n", len(errors))


Test patch:
diff --git a/cmd/print_test.go b/cmd/print_test.go
index 216a775..7aff8d3 100644
--- a/cmd/print_test.go
+++ b/cmd/print_test.go
@@ -16,6 +16,10 @@ limitations under the License.
 package cmd
 
 import (
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
 	"reflect"
 	"testing"
 
@@ -73,3 +77,156 @@ func Test_getResourcePrinter(t *testing.T) {
 
 	}
 }
+
+func Test_getNamespaceFilter(t *testing.T) {
+	testCases := []struct {
+		name                      string
+		namespace                 string
+		allNamespaces             bool
+		expectedNamespaceFilter   string
+		expectingError            bool
+		expectingCurrentNamespace bool
+	}{
+		{
+			name:                      "Only namespace is specified",
+			namespace:                 "default",
+			allNamespaces:             false,
+			expectedNamespaceFilter:   "default",
+			expectingError:            false,
+			expectingCurrentNamespace: false,
+		},
+		{
+			name:                      "All namespaces overrides a specific namespace",
+			namespace:                 "default",
+			allNamespaces:             true,
+			expectedNamespaceFilter:   "",
+			expectingError:            false,
+			expectingCurrentNamespace: false,
+		},
+		{
+			name:                      "Current namespace used when nothing specified",
+			namespace:                 "",
+			allNamespaces:             false,
+			expectedNamespaceFilter:   "_",
+			expectingError:            false,
+			expectingCurrentNamespace: true,
+		},
+	}
+
+	destroy, err := setupKubeConfig()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer destroy()
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			actualNamespaceFilter, err := getNamespaceFilter(tc.namespace, tc.allNamespaces)
+
+			if tc.expectingError && err == nil {
+				t.Errorf("Expected error but got none")
+			}
+			if !tc.expectingError && err != nil {
+				t.Errorf("Expected no error but got %v", err)
+			}
+
+			if tc.expectingCurrentNamespace {
+				tc.expectedNamespaceFilter, _ = getNamespaceFilter(tc.namespace, tc.allNamespaces)
+			}
+
+			if actualNamespaceFilter != tc.expectedNamespaceFilter {
+				t.Errorf(`getNamespaceFilter("%s", %v) = %v, expected %v`,
+					tc.namespace, tc.allNamespaces, actualNamespaceFilter, tc.expectedNamespaceFilter)
+			}
+		})
+
+	}
+}
+
+func setupKubeConfig() (func(), error) {
+
+	// Clean up from the last test, just in case...
+	cleanupFunc := func() {
+		globPattern := filepath.Join(os.TempDir(), "*-kube")
+		matches, err := filepath.Glob(globPattern)
+		if err != nil {
+			log.Fatalf("Failed to match %q: %v", globPattern, err)
+		}
+		for _, match := range matches {
+			if err = os.RemoveAll(match); err != nil {
+				log.Printf("Failed to remove %q: %v", match, err)
+			}
+		}
+	}
+	cleanupFunc()
+
+	content := []byte(`
+apiVersion: v1
+clusters:
+- cluster:
+    server: https://127.0.0.1:6443
+  name: example
+- cluster:
+    server: https://127.0.0.1:54873
+  name: kind-i2gw
+contexts:
+- context:
+    cluster: example
+    namespace: non-default-ns
+    user: example
+  name: example
+- context:
+    cluster: kind-i2gw
+    user: kind-i2gw
+  name: kind-i2gw
+current-context: example
+kind: Config
+preferences: {}
+`)
+
+	dir, err := os.MkdirTemp(os.TempDir(), "*-kube")
+	if err != nil {
+		log.Println(err)
+	}
+
+	kubeConfigFile := fmt.Sprintf("%s/config", dir)
+
+	f, err := os.Create(kubeConfigFile)
+	if err != nil {
+		return nil, err
+	}
+
+	if _, err = f.Write(content); err != nil {
+		os.Remove(kubeConfigFile)
+		return nil, err
+	}
+	if err = f.Close(); err != nil {
+		os.Remove(kubeConfigFile)
+		return nil, err
+	}
+
+	if err = os.Setenv("KUBECONFIG", kubeConfigFile); err != nil {
+		return nil, err
+	}
+
+	return cleanupFunc, nil
+}
+
+func Test_getNamespaceInCurrentContext(t *testing.T) {
+	destroy, err := setupKubeConfig()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer destroy()
+
+	expectedNamespace := "non-default-ns" // according to the kube-config at setupKubeConfig()
+	actualNamespace, err := getNamespaceInCurrentContext()
+	if err != nil {
+		t.Fatalf("Expected no error but got %v", err)
+	}
+
+	if expectedNamespace != actualNamespace {
+		t.Errorf(`getNamespaceInCurrentContext() = "%s", %v, expected %s, %v`,
+			actualNamespace, err, expectedNamespace, nil)
+	}
+}