From 62e69733a3b6abd743d379aafbd9cbad8637c8fe Mon Sep 17 00:00:00 2001 From: Tim Middleton Date: Fri, 11 Oct 2024 10:52:57 +0800 Subject: [PATCH] Add custom panels for monitor cluster command (#235) --- docs/reference/99_monitor_clusters.adoc | 49 ++++++- pkg/cmd/completions.go | 9 ++ pkg/cmd/formatting.go | 24 +++- pkg/cmd/monitor_cluster.go | 8 ++ pkg/cmd/panel.go | 165 ++++++++++++++++++++++++ pkg/cmd/root.go | 12 ++ scripts/generate-doc-snippets.sh | 5 + test/common/common.go | 37 ++++++ test/e2e/standalone/run_test.go | 7 +- 9 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 pkg/cmd/panel.go diff --git a/docs/reference/99_monitor_clusters.adoc b/docs/reference/99_monitor_clusters.adoc index 3eea9929..f4fe2d0a 100644 --- a/docs/reference/99_monitor_clusters.adoc +++ b/docs/reference/99_monitor_clusters.adoc @@ -17,6 +17,9 @@ NOTE: The `monitor cluster` command is currently experimental only and may be ch * <> - monitors the cluster using text based UI * <> - shows all available panels +* <> - displays the panels that have been created +* <> - adds a panel to the list of panels that can be displayed +* <> - removes a panel that has been created [#monitor-cluster] ==== Monitor Cluster @@ -183,8 +186,6 @@ Coherence CLI: 2024-05-06 11:13:59 - Monitoring cluster local (22.06.8) ESC to q NOTE: Any of the panels or layouts that specify `cache-*` or `service-*` must have the cache or service specified using `-C` or `-S` respectively. - - [#monitor-cluster-panels] ==== Monitor Cluster Show Panels @@ -196,6 +197,50 @@ Output: include::../../build/_output/docs-gen/monitor_cluster_panels.adoc[tag=text] +[#get-panels] +==== Get Panels + +include::../../build/_output/docs-gen/get_panels.adoc[tag=text] + +[source,bash] +---- +cohctl get panels + +PANEL LAYOUT +caches caches:services +test caches,services:persistence +---- + +NOTE: Added panels cant be used by specifying the `-l` option in the `monitor cluster` command. + +[#add-panel] +==== Add Panel + +include::../../build/_output/docs-gen/add_panel.adoc[tag=text] + +[source,bash] +---- +cohctl add panel my-panel -l "caches:services,persistence" + +Are you sure you want to add the panel my-panel with layout of [caches:services,persistence]? (y/n) y +panel my-panel was added with layout [caches:services,persistence] +---- + +NOTE: Added panels cant be used by specifying the `-l` option on `monitor cluster` command. + +[#remove-panel] +==== Remove Panel + +include::../../build/_output/docs-gen/remove_panel.adoc[tag=text] + +[source,bash] +---- +cohctl remove panel my-panel + +Are you sure you want to remove the panel my-panel? (y/n) y +panel my-panel was removed +---- + === See Also * <> diff --git a/pkg/cmd/completions.go b/pkg/cmd/completions.go index ad54d774..e3e02fc4 100644 --- a/pkg/cmd/completions.go +++ b/pkg/cmd/completions.go @@ -40,6 +40,15 @@ func completionAllProfiles(_ *cobra.Command, _ []string, _ string) ([]string, co return profiles, cobra.ShellCompDirectiveNoFileComp } +// completionAllPanels provides a completion function to return all panels. +func completionAllPanels(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + panels := make([]string, 0) + for _, p := range Config.Panels { + panels = append(panels, p.Name) + } + return panels, cobra.ShellCompDirectiveNoFileComp +} + // completionCaches provides a completion function to return all cache names. func completionCaches(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { var ( diff --git a/pkg/cmd/formatting.go b/pkg/cmd/formatting.go index 5c9140bf..ea084514 100644 --- a/pkg/cmd/formatting.go +++ b/pkg/cmd/formatting.go @@ -1140,14 +1140,13 @@ func FormatDiscoveredClusters(clusters []discovery.DiscoveredCluster) string { // FormatProfiles returns the profiles in a column formatted output. func FormatProfiles(profiles []ProfileValue) string { - var ( - profileCount = len(profiles) - ) + var profileCount = len(profiles) + if profileCount == 0 { return "" } - table := newFormattedTable().WithHeader("PROFILE", "VALUE") + table := newFormattedTable().WithHeader("PROFILE", "VALUE").WithSortingColumn("PROFILE") for _, value := range profiles { table.AddRow(value.Name, value.Value) @@ -1156,6 +1155,23 @@ func FormatProfiles(profiles []ProfileValue) string { return table.String() } +// FormatPanels returns the panels in a column formatted output. +func FormatPanels(panels []Panel) string { + var panelCount = len(panels) + + if panelCount == 0 { + return "" + } + + table := newFormattedTable().WithHeader("PANEL", "LAYOUT").WithSortingColumn("PANEL") + + for _, value := range panels { + table.AddRow(value.Name, value.Layout) + } + + return table.String() +} + // FormatClusterConnections returns the cluster information in a column formatted output. func FormatClusterConnections(clusters []ClusterConnection) string { var ( diff --git a/pkg/cmd/monitor_cluster.go b/pkg/cmd/monitor_cluster.go index 8633ab6d..7f7136cd 100644 --- a/pkg/cmd/monitor_cluster.go +++ b/pkg/cmd/monitor_cluster.go @@ -173,6 +173,14 @@ Use --show-panels to show all available panels.`, padMaxHeightParam = false } + // check for custom panel layouts added by "add panel" + if layoutParam != "" { + if l := getPanelLayout(layoutParam); l != "" { + layoutParam = l + + } + } + // check for default layouts if l, ok := defaultMap[layoutParam]; ok { layoutParam = l diff --git a/pkg/cmd/panel.go b/pkg/cmd/panel.go new file mode 100644 index 00000000..41c7f79f --- /dev/null +++ b/pkg/cmd/panel.go @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl. + */ + +package cmd + +import ( + "errors" + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const providePanelName = "you must provide a panel name" + +var panelLayout string + +// addPanelCmd represents the add profile command. +var addPanelCmd = &cobra.Command{ + Use: "panel panel-name", + Short: "add a panel and layout for displaying in monitor clusters.", + Long: `The 'add panel' command adds a panel to the list of panels that can be displayed +byt the 'monitor clusters' command.`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + displayErrorAndExit(cmd, providePanelName) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var ( + panelName = args[0] + err error + panels = Config.Panels + ) + + // validate panel name + if err = validateProfileName(panelName); err != nil { + return err + } + + if len(panelLayout) == 0 { + return errors.New("you must provide a value for the panel") + } + + if getPanelLayout(panelName) != "" { + return fmt.Errorf("the panel '%s' already exists", panelName) + } + + if panelAlreadyExists(panelName) { + return fmt.Errorf("the panel '%s' already exists in the list of panels in monitor cluster", panelName) + } + + // confirm the operation + if !confirmOperation(cmd, fmt.Sprintf("Are you sure you want to add the panel %s with layout of [%s]? (y/n) ", panelName, panelLayout)) { + return nil + } + + panels = append(panels, Panel{Name: panelName, Layout: panelLayout}) + + viper.Set(panelsKey, panels) + err = WriteConfig() + if err != nil { + return err + } + cmd.Printf("panel %s was added with layout [%s]\n", panelName, panelLayout) + return nil + }, +} + +// removePanelCmd represents the remove panel command. +var removePanelCmd = &cobra.Command{ + Use: "panel panel-name", + Short: "remove a panel from the list of panels", + Long: `The 'remove panel' command removes a panel from the list of panels.`, + ValidArgsFunction: completionAllPanels, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + displayErrorAndExit(cmd, providePanelName) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var ( + panelName = args[0] + err error + panels = Config.Panels + ) + + if getPanelLayout(panelName) == "" { + return fmt.Errorf("a panel with the name %s does not exist", panelName) + } + + // confirm the operation + if !confirmOperation(cmd, fmt.Sprintf("Are you sure you want to remove the panel %s? (y/n) ", panelName)) { + return nil + } + + newPanels := make([]Panel, 0) + + // loop though the list of panels + for _, v := range panels { + if v.Name != panelName { + newPanels = append(newPanels, v) + } + } + + viper.Set(panelsKey, newPanels) + err = WriteConfig() + if err != nil { + return err + } + cmd.Printf("panel %s was removed\n", panelName) + return nil + }, +} + +// panelAlreadyExists returns true if the panel exists in either the default panel or +// in the list of panels +func panelAlreadyExists(panelName string) bool { + // also check for other panels + if validatePanels([]string{panelName}) == nil { + return true + } + + // validate that the panel is not in the list of known default panels in the monitor cluster command + if _, ok := defaultMap[panelName]; ok { + return true + } + + return false +} + +// getProfilesCmd represents the get profiles command. +var getPanelsCmd = &cobra.Command{ + Use: "panels", + Short: "display the panels that have been created", + Long: `The 'get panels' displays the panels that have been created.`, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, _ []string) error { + cmd.Println(FormatPanels(Config.Panels)) + return nil + }, +} + +func init() { + addPanelCmd.Flags().StringVarP(&panelLayout, "layout", "l", "", "panel layout") + _ = addPanelCmd.MarkFlagRequired("layout") + addPanelCmd.Flags().BoolVarP(&automaticallyConfirm, "yes", "y", false, confirmOptionMessage) + + removePanelCmd.Flags().BoolVarP(&automaticallyConfirm, "yes", "y", false, confirmOptionMessage) +} + +// getPanelLayout returns the layout for a given panel or "" if the panel doesn't exist. +func getPanelLayout(panelName string) string { + for _, v := range Config.Panels { + if v.Name == panelName { + return v.Layout + } + } + + return "" +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 2664988c..2d20f31a 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -66,8 +66,10 @@ const ( ignoreCertsContextKey = "ignoreInvalidCerts" requestTimeoutKey = "requestTimeout" defaultBytesFormatKey = "defaultBytesFormat" + defaultLayoutKey = "defaultLayout" defaultHeapKey = "defaultHeap" profilesKey = "profiles" + panelsKey = "panels" confirmOptionMessage = "automatically confirm the operation" timeoutMessage = "timeout in seconds for NS Lookup requests" @@ -171,6 +173,7 @@ type CoherenceCLIConfig struct { DefaultHeap string `json:"defaultHeap"` UseGradle bool `json:"useGradle"` Profiles []ProfileValue `mapstructure:"profiles"` + Panels []Panel `mapstructure:"panels"` } // ProfileValue describes a profile to be used for creating and starting clusters. @@ -179,6 +182,12 @@ type ProfileValue struct { Value string `json:"value"` } +// Panel describes a panel and layout that can be used by the monitor cluster command. +type Panel struct { + Name string `json:"name"` + Layout string `json:"layout"` +} + // ClusterConnection describes an individual connection to a cluster. type ClusterConnection struct { Name string `json:"name"` // the name the user gives to the cluster connection @@ -538,6 +547,7 @@ func Initialize(command *cobra.Command) *cobra.Command { getCmd.AddCommand(getViewCachesCmd) getCmd.AddCommand(getCachePartitionsCmd) getCmd.AddCommand(getFederationIncomingCmd) + getCmd.AddCommand(getPanelsCmd) // set command command.AddCommand(setCmd) @@ -572,6 +582,7 @@ func Initialize(command *cobra.Command) *cobra.Command { // add command.AddCommand(addCmd) addCmd.AddCommand(addClusterCmd) + addCmd.AddCommand(addPanelCmd) // replicate command.AddCommand(replicateCmd) @@ -618,6 +629,7 @@ func Initialize(command *cobra.Command) *cobra.Command { removeCmd.AddCommand(removeClusterCmd) removeCmd.AddCommand(removeSnapshotCmd) removeCmd.AddCommand(removeProfileCmd) + removeCmd.AddCommand(removePanelCmd) // describe command.AddCommand(describeCmd) diff --git a/scripts/generate-doc-snippets.sh b/scripts/generate-doc-snippets.sh index 92a3d766..e9ae6716 100755 --- a/scripts/generate-doc-snippets.sh +++ b/scripts/generate-doc-snippets.sh @@ -48,6 +48,11 @@ create_doc $DOCS_DIR/discover_clusters "${COHCTL} discover clusters --help" create_doc $DOCS_DIR/get_cluster_config "${COHCTL} get cluster-config --help" create_doc $DOCS_DIR/get_cluster_description "${COHCTL} get cluster-description --help" create_doc $DOCS_DIR/monitor_cluster "${COHCTL} monitor cluster --help" +create_doc $DOCS_DIR/add_panel "${COHCTL} add panel --help" +create_doc $DOCS_DIR/get_panels "${COHCTL} get panels --help" +create_doc $DOCS_DIR/remove_panel "${COHCTL} remove panel --help" + + ( echo "// # tag::text[]" echo "----" diff --git a/test/common/common.go b/test/common/common.go index 9ff1bf8b..de2e34b6 100644 --- a/test/common/common.go +++ b/test/common/common.go @@ -2060,6 +2060,43 @@ func RunTestProfileCommands(t *testing.T) { "profile2", "-y") } +// RunTestPanelsCommands tests panels commands. +func RunTestPanelsCommands(t *testing.T) { + g := NewGomegaWithT(t) + + file := initializeTestFile(t) + + cliCmd := cmd.Initialize(nil) + + // set the debug to true + test_utils.EnsureCommandContains(g, t, cliCmd, "on", configArg, file, "set", "debug", "on") + + // get panels + test_utils.EnsureCommandContains(g, t, cliCmd, "", configArg, file, "get", "panels") + + // add panels + test_utils.EnsureCommandContains(g, t, cliCmd, "panel test", configArg, file, "add", "panel", + "test", "-l", "caches", "-y") + + // get panels + test_utils.EnsureCommandContainsAll(g, t, cliCmd, "PANEL,LAYOUT,test,caches", configArg, file, "get", "panels") + + // set a second panel + test_utils.EnsureCommandContains(g, t, cliCmd, "panel test2", configArg, file, "add", "panel", + "test2", "-l", "default-federation", "-y") + + // get profiles + test_utils.EnsureCommandContainsAll(g, t, cliCmd, "PANEL,LAYOUT,test,caches,test2", configArg, file, "get", "panels") + + // add a panel that already exists + test_utils.EnsureCommandErrorContains(g, t, cliCmd, "already exists in the list", configArg, file, "add", "panel", + "caches", "-l", "default-federation", "-y") + + // remove the panels + test_utils.EnsureCommandContains(g, t, cliCmd, "panel test was removed", configArg, file, "remove", "panel", "test", "-y") + test_utils.EnsureCommandContains(g, t, cliCmd, "panel test2 was removed", configArg, file, "remove", "panel", "test2", "-y") +} + // RunTestResetCommands tests various reset commands. func RunTestResetCommands(t *testing.T) { var ( diff --git a/test/e2e/standalone/run_test.go b/test/e2e/standalone/run_test.go index a078d4c6..d547dcce 100644 --- a/test/e2e/standalone/run_test.go +++ b/test/e2e/standalone/run_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. */ @@ -145,6 +145,11 @@ func TestProfileCommands(t *testing.T) { common.RunTestProfileCommands(t) } +// TestPanelsCommands tests panels command. +func TestPanelsCommands(t *testing.T) { + common.RunTestPanelsCommands(t) +} + // TestResetCommands tests reset commands. func TestResetCommands(t *testing.T) { common.RunTestResetCommands(t)