diff --git a/cmd/ocm/describe/cluster/cmd.go b/cmd/ocm/describe/cluster/cmd.go index 4c5563c3..86c31f75 100644 --- a/cmd/ocm/describe/cluster/cmd.go +++ b/cmd/ocm/describe/cluster/cmd.go @@ -20,13 +20,11 @@ import ( "bytes" "fmt" "os" - "regexp" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" "github.com/spf13/cobra" c "github.com/openshift-online/ocm-cli/pkg/cluster" - clusterpkg "github.com/openshift-online/ocm-cli/pkg/cluster" "github.com/openshift-online/ocm-cli/pkg/dump" "github.com/openshift-online/ocm-cli/pkg/ocm" ) @@ -75,7 +73,7 @@ func run(cmd *cobra.Command, argv []string) error { // Check that the cluster key (name, identifier or external identifier) given by the user // is reasonably safe so that there is no risk of SQL injection: key := argv[0] - if !keyRE.MatchString(key) { + if !c.IsValidClusterKey(key) { fmt.Fprintf( os.Stderr, "Cluster name, identifier or external identifier '%s' isn't valid: it "+ @@ -132,7 +130,7 @@ func run(cmd *cobra.Command, argv []string) error { } } else { - err = clusterpkg.PrintClusterDescription(connection, cluster) + err = c.PrintClusterDescription(connection, cluster) if err != nil { return err } @@ -140,7 +138,3 @@ func run(cmd *cobra.Command, argv []string) error { return nil } - -// Regular expression to check that the cluster key (name, identifier or external identifier) given -// by the user is reasonably safe and that there is no risk of SQL injection. -var keyRE = regexp.MustCompile(`^(\w|-)+$`) diff --git a/cmd/ocm/describe/cmd.go b/cmd/ocm/describe/cmd.go index 5af96768..b37526a0 100644 --- a/cmd/ocm/describe/cmd.go +++ b/cmd/ocm/describe/cmd.go @@ -15,6 +15,7 @@ package describe import ( "github.com/openshift-online/ocm-cli/cmd/ocm/describe/cluster" + "github.com/openshift-online/ocm-cli/cmd/ocm/describe/ingress" "github.com/spf13/cobra" ) @@ -26,4 +27,5 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(cluster.Cmd) + Cmd.AddCommand(ingress.Cmd) } diff --git a/cmd/ocm/describe/ingress/cmd.go b/cmd/ocm/describe/ingress/cmd.go new file mode 100644 index 00000000..69d07795 --- /dev/null +++ b/cmd/ocm/describe/ingress/cmd.go @@ -0,0 +1,167 @@ +package ingress + +import ( + "bytes" + "fmt" + "os" + + c "github.com/openshift-online/ocm-cli/pkg/cluster" + "github.com/openshift-online/ocm-cli/pkg/dump" + i "github.com/openshift-online/ocm-cli/pkg/ingress" + "github.com/openshift-online/ocm-cli/pkg/ocm" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + "github.com/spf13/cobra" +) + +var args struct { + json bool + output bool + ingressKey string +} + +var Cmd = &cobra.Command{ + Use: "ingress [flags] {CLUSTER_NAME|CLUSTER_ID|CLUSTER_EXTERNAL_ID} -i ingress_key", + Short: "Show details of an ingress", + Long: "Show details of an ingress identified by name, or identifier", + RunE: run, +} + +func init() { + // Add flags to rootCmd: + flags := Cmd.Flags() + flags.BoolVar( + &args.output, + "output", + false, + "Output result into JSON file.", + ) + flags.BoolVar( + &args.json, + "json", + false, + "Output the entire JSON structure", + ) + flags.StringVarP( + &args.ingressKey, + "ingress", + "i", + "", + "Ingress identifier", + ) +} + +func run(cmd *cobra.Command, argv []string) error { + // Check that there is exactly one cluster name, identifir or external identifier in the + // command line arguments: + if len(argv) != 1 { + fmt.Fprintf( + os.Stderr, + "Expected exactly one cluster name, identifier or external identifier "+ + "is required\n", + ) + os.Exit(1) + } + + // Check that the cluster key (name, identifier or external identifier) given by the user + // is reasonably safe so that there is no risk of SQL injection: + key := argv[0] + if !c.IsValidClusterKey(key) { + fmt.Fprintf( + os.Stderr, + "Cluster name, identifier or external identifier '%s' isn't valid: it "+ + "must contain only letters, digits, dashes and underscores\n", + key, + ) + os.Exit(1) + } + ingressKey := args.ingressKey + if ingressKey == "" { + fmt.Fprintf( + os.Stderr, + "Ingress identifier must be supplied\n", + ) + os.Exit(1) + } + + // Create the client for the OCM API: + connection, err := ocm.NewConnection().Build() + if err != nil { + return fmt.Errorf("Failed to create OCM connection: %v", err) + } + defer connection.Close() + + cluster, err := c.GetCluster(connection, key) + if err != nil { + return fmt.Errorf("Can't retrieve cluster for key '%s': %v", key, err) + } + + clusterId := cluster.ID() + response, err := connection.ClustersMgmt().V1(). + Clusters().Cluster(clusterId). + Ingresses(). + List().Page(1).Size(-1). + Send() + if err != nil { + return err + } + + ingresses := response.Items().Slice() + var ingress *cmv1.Ingress + for _, item := range ingresses { + if ingressKey == "apps" && item.Default() { + ingress = item + } + if ingressKey == "apps2" && !item.Default() { + ingress = item + } + if item.ID() == ingressKey { + ingress = item + } + } + if ingress == nil { + return fmt.Errorf("Failed to get ingress '%s' for cluster '%s'", ingressKey, clusterId) + } + + if args.output { + // Create a filename based on cluster name: + filename := fmt.Sprintf("ingress-%s-%s.json", cluster.ID(), ingress.ID()) + + // Attempt to create file: + myFile, err := os.Create(filename) + if err != nil { + return fmt.Errorf("Failed to create file: %v", err) + } + + // Dump encoder content into file: + err = cmv1.MarshalIngress(ingress, myFile) + if err != nil { + return fmt.Errorf("Failed to Marshal ingress into file: %v", err) + } + } + + // Get full API response (JSON): + if args.json { + // Buffer for pretty output: + buf := new(bytes.Buffer) + fmt.Println() + + // Convert cluster to JSON and dump to encoder: + err = cmv1.MarshalIngress(ingress, buf) + if err != nil { + return fmt.Errorf("Failed to Marshal ingress into JSON encoder: %v", err) + } + + err = dump.Pretty(os.Stdout, buf.Bytes()) + if err != nil { + return fmt.Errorf("Can't print body: %v", err) + } + + } else { + err = i.PrintIngressDescription(ingress, cluster) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/ingress/describe.go b/pkg/ingress/describe.go new file mode 100644 index 00000000..3ae289b9 --- /dev/null +++ b/pkg/ingress/describe.go @@ -0,0 +1,100 @@ +package ingress + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/openshift-online/ocm-cli/pkg/utils" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" +) + +func PrintIngressDescription(ingress *cmv1.Ingress, cluster *cmv1.Cluster) error { + entries := generateEntriesOutput(cluster, ingress) + ingressOutput := "" + keys := utils.MapKeys(entries) + sort.Strings(keys) + minWidth := getMinWidth(keys) + for _, key := range keys { + ingressOutput += fmt.Sprintf("%s: %s\n", key, strings.Repeat(" ", minWidth-len(key))+entries[key]) + } + fmt.Print(ingressOutput) + return nil +} + +// Min width is defined as the length of the longest string +func getMinWidth(keys []string) int { + minWidth := 0 + for _, key := range keys { + if len(key) > minWidth { + minWidth = len(key) + } + } + return minWidth +} + +func generateEntriesOutput(cluster *cmv1.Cluster, ingress *cmv1.Ingress) map[string]string { + private := false + if ingress.Listening() == cmv1.ListeningMethodInternal { + private = true + } + entries := map[string]string{ + "ID": ingress.ID(), + "Cluster ID": cluster.ID(), + "Default": strconv.FormatBool(ingress.Default()), + "Private": strconv.FormatBool(private), + "LB-Type": string(ingress.LoadBalancerType()), + } + // These are only available for ingress v2 + wildcardPolicy := string(ingress.RouteWildcardPolicy()) + if wildcardPolicy != "" { + entries["Wildcard Policy"] = string(ingress.RouteWildcardPolicy()) + } + namespaceOwnershipPolicy := string(ingress.RouteNamespaceOwnershipPolicy()) + if namespaceOwnershipPolicy != "" { + entries["Namespace Ownership Policy"] = namespaceOwnershipPolicy + } + routeSelectors := "" + if len(ingress.RouteSelectors()) > 0 { + routeSelectors = fmt.Sprintf("%v", ingress.RouteSelectors()) + } + if routeSelectors != "" { + entries["Route Selectors"] = routeSelectors + } + excludedNamespaces := utils.SliceToSortedString(ingress.ExcludedNamespaces()) + if excludedNamespaces != "" { + entries["Excluded Namespaces"] = excludedNamespaces + } + componentRoutes := "" + componentKeys := utils.MapKeys(ingress.ComponentRoutes()) + sort.Strings(componentKeys) + for _, component := range componentKeys { + value := ingress.ComponentRoutes()[component] + keys := utils.MapKeys(entries) + minWidth := getMinWidth(keys) + depth := 4 + componentRouteEntries := map[string]string{ + "Hostname": value.Hostname(), + "TLS Secret Ref": value.TlsSecretRef(), + } + componentRoutes += fmt.Sprintf("%s: \n", strings.Repeat(" ", depth)+component) + depth *= 2 + paramKeys := utils.MapKeys(componentRouteEntries) + sort.Strings(paramKeys) + for _, param := range paramKeys { + componentRoutes += fmt.Sprintf( + "%s: %s\n", + strings.Repeat(" ", depth)+param, + strings.Repeat(" ", minWidth-len(param)-depth)+componentRouteEntries[param], + ) + } + } + if componentRoutes != "" { + componentRoutes = fmt.Sprintf("\n%s", componentRoutes) + //remove extra \n at the end + componentRoutes = componentRoutes[:len(componentRoutes)-1] + entries["Component Routes"] = componentRoutes + } + return entries +} diff --git a/pkg/ingress/describe_test.go b/pkg/ingress/describe_test.go new file mode 100644 index 00000000..239bf1ad --- /dev/null +++ b/pkg/ingress/describe_test.go @@ -0,0 +1,47 @@ +package ingress + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + v1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" +) + +var _ = Describe("Get min width for output", func() { + It("retrieves the min width", func() { + minWidth := getMinWidth([]string{"a", "ab", "abc", "def"}) + Expect(minWidth).To(Equal(3)) + }) + When("empty slice", func() { + It("retrieves the min width as 0", func() { + minWidth := getMinWidth([]string{}) + Expect(minWidth).To(Equal(0)) + }) + }) +}) + +var _ = Describe("Retrieve map of entries for output", func() { + It("retrieves map", func() { + cluster, err := cmv1.NewCluster().ID("123").Build() + Expect(err).To(BeNil()) + ingress, err := cmv1.NewIngress(). + ID("123"). + Default(true). + Listening(cmv1.ListeningMethodExternal). + LoadBalancerType(cmv1.LoadBalancerFlavorNlb). + RouteWildcardPolicy(cmv1.WildcardPolicyWildcardsAllowed). + RouteNamespaceOwnershipPolicy(cmv1.NamespaceOwnershipPolicyStrict). + RouteSelectors(map[string]string{ + "test-route": "test-selector", + }). + ExcludedNamespaces("test", "test2"). + ComponentRoutes(map[string]*cmv1.ComponentRouteBuilder{ + string(cmv1.ComponentRouteTypeOauth): v1.NewComponentRoute(). + Hostname("oauth-hostname").TlsSecretRef("oauth-secret"), + }). + Build() + Expect(err).To(BeNil()) + mapOutput := generateEntriesOutput(cluster, ingress) + Expect(mapOutput).To(HaveLen(10)) + }) +}) diff --git a/pkg/ingress/main_test.go b/pkg/ingress/main_test.go new file mode 100644 index 00000000..df4ce79e --- /dev/null +++ b/pkg/ingress/main_test.go @@ -0,0 +1,13 @@ +package ingress + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestEditCluster(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Describe ingress suite") +} diff --git a/pkg/utils/strings.go b/pkg/utils/strings.go index f121c783..856537ae 100644 --- a/pkg/utils/strings.go +++ b/pkg/utils/strings.go @@ -22,3 +22,11 @@ func SortStringRespectLength(s []string) { return s[i] < s[j] }) } + +func MapKeys[K comparable, V any](m map[K]V) []K { + r := make([]K, 0, len(m)) + for k := range m { + r = append(r, k) + } + return r +}