diff --git a/pkg/cmd/hgctl/completion.go b/pkg/cmd/hgctl/completion.go new file mode 100644 index 000000000..d56d4c3fc --- /dev/null +++ b/pkg/cmd/hgctl/completion.go @@ -0,0 +1,232 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hgctl + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +const completionDesc = ` +Generate autocompletion scripts for hgctl for the specified shell. +` + +const bashCompDesc = ` +Generate the autocompletion script for the bash shell. + +This script depends on the 'bash-completion' package. +If it is not installed already, you can install it via your OS's package manager. + +To load completions in your current shell session: + + source <(hgctl completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + hgctl completion bash > /etc/bash_completion.d/hgctl + +#### macOS: + + hgctl completion bash > $(brew --prefix)/etc/bash_completion.d/hgctl + +You will need to start a new shell for this setup to take effect. +` + +const zshCompDesc = ` +Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(hgctl completion zsh); compdef _hgctl hgctl + +To load completions for every new session, execute once: + +#### Linux: + + hgctl completion zsh > "${fpath[1]}/_hgctl" + +#### macOS: + + hgctl completion zsh > $(brew --prefix)/share/zsh/site-functions/_hgctl + +You will need to start a new shell for this setup to take effect. +` + +const fishCompDesc = ` +Generate the autocompletion script for the fish shell. + +To load completions in your current shell session: + + hgctl completion fish | source + +To load completions for every new session, execute once: + + hgctl completion fish > ~/.config/fish/completions/hgctl.fish + +You will need to start a new shell for this setup to take effect. +` + +const powershellCompDesc = ` +Generate the autocompletion script for powershell. + +To load completions in your current shell session: + + hgctl completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. +` + +const ( + noDescFlagName = "no-descriptions" + noDescFlagText = "disable completion descriptions" +) + +var disableCompDescriptions bool + +// newCompletionCmd creates a new completion command for hgctl +func newCompletionCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "completion", + Short: "generate autocompletion scripts for the specified shell", + Long: completionDesc, + Args: cobra.NoArgs, + } + + bash := &cobra.Command{ + Use: "bash", + Short: "generate autocompletion script for bash", + Long: bashCompDesc, + Args: cobra.NoArgs, + ValidArgsFunction: noCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return runCompletionBash(out, cmd) + }, + } + bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) + + zsh := &cobra.Command{ + Use: "zsh", + Short: "generate autocompletion script for zsh", + Long: zshCompDesc, + Args: cobra.NoArgs, + ValidArgsFunction: noCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return runCompletionZsh(out, cmd) + }, + } + zsh.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) + + fish := &cobra.Command{ + Use: "fish", + Short: "generate autocompletion script for fish", + Long: fishCompDesc, + Args: cobra.NoArgs, + ValidArgsFunction: noCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return runCompletionFish(out, cmd) + }, + } + fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) + + powershell := &cobra.Command{ + Use: "powershell", + Short: "generate autocompletion script for powershell", + Long: powershellCompDesc, + Args: cobra.NoArgs, + ValidArgsFunction: noCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return runCompletionPowershell(out, cmd) + }, + } + powershell.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) + + cmd.AddCommand(bash, zsh, fish, powershell) + + return cmd +} + +func runCompletionBash(out io.Writer, cmd *cobra.Command) error { + err := cmd.Root().GenBashCompletionV2(out, !disableCompDescriptions) + + // In case the user renamed the hgctl binary, we hook the new binary name to the completion function + if binary := filepath.Base(os.Args[0]); binary != "hgctl" { + renamedBinaryHook := ` +# Hook the command used to generate the completion script +# to the hgctl completion function to handle the case where +# the user renamed the hgctl binary +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_hgctl %[1]s +else + complete -o default -o nospace -F __start_hgctl %[1]s +fi +` + fmt.Fprintf(out, renamedBinaryHook, binary) + } + + return err +} + +func runCompletionZsh(out io.Writer, cmd *cobra.Command) error { + var err error + if disableCompDescriptions { + err = cmd.Root().GenZshCompletionNoDesc(out) + } else { + err = cmd.Root().GenZshCompletion(out) + } + + // In case the user renamed the hgctl binary, we hook the new binary name to the completion function + if binary := filepath.Base(os.Args[0]); binary != "hgctl" { + renamedBinaryHook := ` +# Hook the command used to generate the completion script +# to the hgctl completion function to handle the case where +# the user renamed the hgctl binary +compdef _hgctl %[1]s +` + fmt.Fprintf(out, renamedBinaryHook, binary) + } + + // Cobra doesn't source zsh completion file, explicitly doing it here + fmt.Fprintf(out, "compdef _hgctl hgctl") + + return err +} + +func runCompletionFish(out io.Writer, cmd *cobra.Command) error { + return cmd.Root().GenFishCompletion(out, !disableCompDescriptions) +} + +func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error { + if disableCompDescriptions { + return cmd.Root().GenPowerShellCompletion(out) + } + return cmd.Root().GenPowerShellCompletionWithDesc(out) +} + +// Function to disable file completion +func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp +} diff --git a/pkg/cmd/hgctl/config_retriever.go b/pkg/cmd/hgctl/config_retriever.go index e52cb3f73..d0e8760b0 100644 --- a/pkg/cmd/hgctl/config_retriever.go +++ b/pkg/cmd/hgctl/config_retriever.go @@ -96,7 +96,7 @@ func portForwarder(nn types.NamespacedName) (kubernetes.PortForwarder, error) { return nil, fmt.Errorf("pod %s is not running", nn) } - fw, err := kubernetes.NewLocalPortForwarder(c, nn, 0, defaultProxyAdminPort) + fw, err := kubernetes.NewLocalPortForwarder(c, nn, 0, defaultProxyAdminPort, bindAddress) if err != nil { return nil, err } diff --git a/pkg/cmd/hgctl/config_test.go b/pkg/cmd/hgctl/config_test.go index f3ffdaa88..9a515af7e 100644 --- a/pkg/cmd/hgctl/config_test.go +++ b/pkg/cmd/hgctl/config_test.go @@ -38,7 +38,7 @@ type fakePortForwarder struct { } func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) { - p, err := kubernetes.LocalAvailablePort() + p, err := kubernetes.LocalAvailablePort("localhost") if err != nil { return nil, err } diff --git a/pkg/cmd/hgctl/dashboard.go b/pkg/cmd/hgctl/dashboard.go index aff3b8413..67ba07ea1 100644 --- a/pkg/cmd/hgctl/dashboard.go +++ b/pkg/cmd/hgctl/dashboard.go @@ -15,15 +15,20 @@ package hgctl import ( + "context" "fmt" "io" "os" "os/exec" "os/signal" "runtime" + "strings" "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes" "github.com/alibaba/higress/pkg/cmd/options" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/flags" + types2 "github.com/docker/docker/api/types" "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" @@ -49,6 +54,8 @@ var ( envoyDashNs = "" proxyAdminPort int + + docker = false ) const ( @@ -81,6 +88,7 @@ func newDashboardCmd() *cobra.Command { "Default is true which means hgctl dashboard will always open a browser to view the dashboard.") dashboardCmd.PersistentFlags().StringVarP(&addonNamespace, "namespace", "n", "higress-system", "Namespace where the addon is running, if not specified, higress-system would be used") + dashboardCmd.PersistentFlags().StringVarP(&bindAddress, "listen", "l", "localhost", "The address to bind to") prom := promDashCmd() prom.PersistentFlags().IntVar(&promPort, "ui-port", defaultPrometheusPort, "The component dashboard UI port.") @@ -99,6 +107,7 @@ func newDashboardCmd() *cobra.Command { consoleCmd := consoleDashCmd() consoleCmd.PersistentFlags().IntVar(&consolePort, "ui-port", defaultConsolePort, "The component dashboard UI port.") + consoleCmd.PersistentFlags().BoolVar(&docker, "docker", false, "Search higress console from docker") dashboardCmd.AddCommand(consoleCmd) controllerDebugCmd := controllerDebugCmd() @@ -156,18 +165,23 @@ func consoleDashCmd() *cobra.Command { hgctl dash console hgctl d console`, RunE: func(cmd *cobra.Command, args []string) error { + if docker { + return accessDocker(cmd) + } client, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader()) if err != nil { - return fmt.Errorf("build CLI client fail: %w", err) + fmt.Printf("build kubernetes CLI client fail: %v\ntry to access docker container\n", err) + return accessDocker(cmd) } - pl, err := client.PodsForSelector(addonNamespace, "app.kubernetes.io/name=higress-console") if err != nil { - return fmt.Errorf("not able to locate console pod: %v", err) + fmt.Printf("build kubernetes CLI client fail: %v\ntry to access docker container\n", err) + return accessDocker(cmd) } if len(pl.Items) < 1 { - return errors.New("no higress console pods found") + fmt.Printf("no higress console pods found\ntry to access docker container\n") + return accessDocker(cmd) } // only use the first pod in the list @@ -179,6 +193,32 @@ func consoleDashCmd() *cobra.Command { return cmd } +// accessDocker access docker container +func accessDocker(cmd *cobra.Command) error { + dockerCli, err := command.NewDockerCli(command.WithCombinedStreams(os.Stdout)) + if err != nil { + return fmt.Errorf("build docker CLI client fail: %w", err) + } + err = dockerCli.Initialize(flags.NewClientOptions()) + if err != nil { + return fmt.Errorf("docker client initialize fail: %w", err) + } + apiClient := dockerCli.Client() + list, err := apiClient.ContainerList(context.Background(), types2.ContainerListOptions{}) + for _, container := range list { + for i, name := range container.Names { + if strings.Contains(name, "higress-console") { + port := container.Ports[i].PublicPort + // not support define ip address + url := fmt.Sprintf("http://localhost:%d", port) + openBrowser(url, cmd.OutOrStdout(), browser) + return nil + } + } + } + return errors.New("no higress console container found") +} + // port-forward to Higress System Grafana; open browser func grafanaDashCmd() *cobra.Command { cmd := &cobra.Command{ @@ -324,7 +364,7 @@ func portForward(podName, namespace, flavor, urlFormat, localAddress string, rem var err error for _, localPort := range portPrefs { var fw kubernetes.PortForwarder - fw, err = kubernetes.NewLocalPortForwarder(client, types.NamespacedName{Namespace: namespace, Name: podName}, localPort, remotePort) + fw, err = kubernetes.NewLocalPortForwarder(client, types.NamespacedName{Namespace: namespace, Name: podName}, localPort, remotePort, bindAddress) if err != nil { return fmt.Errorf("could not build port forwarder for %s: %v", flavor, err) } @@ -378,6 +418,7 @@ func openBrowser(url string, writer io.Writer, browser bool) { default: fmt.Fprintf(writer, "Unsupported platform %q; open %s in your browser.\n", runtime.GOOS, url) } + } func openCommand(writer io.Writer, command string, args ...string) { diff --git a/pkg/cmd/hgctl/kubernetes/port-forwarder.go b/pkg/cmd/hgctl/kubernetes/port-forwarder.go index 5495345e4..e81481a80 100644 --- a/pkg/cmd/hgctl/kubernetes/port-forwarder.go +++ b/pkg/cmd/hgctl/kubernetes/port-forwarder.go @@ -28,12 +28,8 @@ import ( "k8s.io/client-go/transport/spdy" ) -const ( - DefaultLocalAddress = "localhost" -) - -func LocalAvailablePort() (int, error) { - l, err := net.Listen("tcp", fmt.Sprintf("%s:0", DefaultLocalAddress)) +func LocalAvailablePort(localAddress string) (int, error) { + l, err := net.Listen("tcp", fmt.Sprintf("%s:0", localAddress)) if err != nil { return 0, err } @@ -59,23 +55,25 @@ type localForwarder struct { types.NamespacedName CLIClient - localPort int - podPort int + localPort int + podPort int + localAddress string stopCh chan struct{} } -func NewLocalPortForwarder(client CLIClient, namespacedName types.NamespacedName, localPort, podPort int) (PortForwarder, error) { +func NewLocalPortForwarder(client CLIClient, namespacedName types.NamespacedName, localPort, podPort int, bindAddress string) (PortForwarder, error) { f := &localForwarder{ stopCh: make(chan struct{}), CLIClient: client, NamespacedName: namespacedName, localPort: localPort, podPort: podPort, + localAddress: bindAddress, } if f.localPort == 0 { // get a random port - p, err := LocalAvailablePort() + p, err := LocalAvailablePort(bindAddress) if err != nil { return nil, errors.Wrapf(err, "failed to get a local available port") } @@ -136,7 +134,7 @@ func (f *localForwarder) buildKubernetesPortForwarder(readyCh chan struct{}) (*p dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, serverURL) fw, err := portforward.NewOnAddresses(dialer, - []string{DefaultLocalAddress}, + []string{f.localAddress}, []string{fmt.Sprintf("%d:%d", f.localPort, f.podPort)}, f.stopCh, readyCh, @@ -154,7 +152,7 @@ func (f *localForwarder) Stop() { } func (f *localForwarder) Address() string { - return fmt.Sprintf("%s:%d", DefaultLocalAddress, f.localPort) + return fmt.Sprintf("%s:%d", f.localAddress, f.localPort) } func (f *localForwarder) WaitForStop() { diff --git a/pkg/cmd/hgctl/root.go b/pkg/cmd/hgctl/root.go index f1179e42f..786f3d179 100644 --- a/pkg/cmd/hgctl/root.go +++ b/pkg/cmd/hgctl/root.go @@ -17,6 +17,7 @@ package hgctl import ( "github.com/alibaba/higress/pkg/cmd/hgctl/plugin" "github.com/spf13/cobra" + "os" ) // GetRootCommand returns the root cobra command to be executed @@ -38,6 +39,7 @@ func GetRootCommand() *cobra.Command { rootCmd.AddCommand(newDashboardCmd()) rootCmd.AddCommand(newManifestCmd()) rootCmd.AddCommand(plugin.NewCommand()) + rootCmd.AddCommand(newCompletionCmd(os.Stdout)) return rootCmd }