diff --git a/hack/tools/genkarmadactldocs/gen_karmadactl_docs.go b/hack/tools/genkarmadactldocs/gen_karmadactl_docs.go index 5a4dbce77335..628acf6daeb1 100644 --- a/hack/tools/genkarmadactldocs/gen_karmadactl_docs.go +++ b/hack/tools/genkarmadactldocs/gen_karmadactl_docs.go @@ -71,7 +71,7 @@ func GenMarkdownTreeForIndex(cmd *cobra.Command, dir string) error { return err } - for _, tp := range []string{util.GroupBasic, util.GroupClusterRegistration, util.GroupClusterManagement, util.GroupClusterTroubleshootingAndDebugging, util.GroupAdvancedCommands} { + for _, tp := range []string{util.GroupBasic, util.GroupClusterRegistration, util.GroupClusterManagement, util.GroupClusterTroubleshootingAndDebugging, util.GroupAdvancedCommands, util.GroupSettingsCommands, util.GroupOtherCommands} { // write header of type _, err = io.WriteString(f, "## "+tp+"\n\n") if err != nil { diff --git a/pkg/karmadactl/annotate/annotate.go b/pkg/karmadactl/annotate/annotate.go index 934d17769561..dcd752d96dbc 100644 --- a/pkg/karmadactl/annotate/annotate.go +++ b/pkg/karmadactl/annotate/annotate.go @@ -51,5 +51,8 @@ var ( func NewCmdAnnotate(f util.Factory, parentCommand string, ioStreams genericiooptions.IOStreams) *cobra.Command { cmd := kubectlannotate.NewCmdAnnotate(parentCommand, f, ioStreams) cmd.Example = fmt.Sprintf(annotateExample, parentCommand) + cmd.Annotations = map[string]string{ + util.TagCommandGroup: util.GroupSettingsCommands, + } return cmd } diff --git a/pkg/karmadactl/apiresources/apiresources.go b/pkg/karmadactl/apiresources/apiresources.go index a5be361f7edf..2645048513f9 100644 --- a/pkg/karmadactl/apiresources/apiresources.go +++ b/pkg/karmadactl/apiresources/apiresources.go @@ -65,6 +65,9 @@ func NewCmdAPIResources(f util.Factory, parentCommand string, ioStreams generici cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunAPIResources()) }, + Annotations: map[string]string{ + util.TagCommandGroup: util.GroupOtherCommands, + }, } o.OperationScope = options.KarmadaControlPlane diff --git a/pkg/karmadactl/apiresources/apiversions.go b/pkg/karmadactl/apiresources/apiversions.go index 1726a4277f1e..3aede352f83b 100644 --- a/pkg/karmadactl/apiresources/apiversions.go +++ b/pkg/karmadactl/apiresources/apiversions.go @@ -53,6 +53,9 @@ func NewCmdAPIVersions(f util.Factory, parentCommand string, ioStreams genericio cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunAPIVersions()) }, + Annotations: map[string]string{ + util.TagCommandGroup: util.GroupOtherCommands, + }, } o.OperationScope = options.KarmadaControlPlane diff --git a/pkg/karmadactl/attach/attach.go b/pkg/karmadactl/attach/attach.go index 9676bfab1784..d92227254a48 100644 --- a/pkg/karmadactl/attach/attach.go +++ b/pkg/karmadactl/attach/attach.go @@ -70,6 +70,9 @@ func NewCmdAttach(f util.Factory, parentCommand string, streams genericiooptions cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, + Annotations: map[string]string{ + util.TagCommandGroup: util.GroupClusterTroubleshootingAndDebugging, + }, } cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout) diff --git a/pkg/karmadactl/create/create.go b/pkg/karmadactl/create/create.go index 7f5a1213ff0d..10b22a7cb3da 100644 --- a/pkg/karmadactl/create/create.go +++ b/pkg/karmadactl/create/create.go @@ -49,5 +49,8 @@ func NewCmdCreate(f util.Factory, parentCommnd string, ioStreams genericiooption cmd := kubectlcreate.NewCmdCreate(f, ioStreams) cmd.Long = fmt.Sprintf(createLong, parentCommnd) cmd.Example = fmt.Sprintf(createExample, parentCommnd) + cmd.Annotations = map[string]string{ + util.TagCommandGroup: util.GroupBasic, + } return cmd } diff --git a/pkg/karmadactl/delete/delete.go b/pkg/karmadactl/delete/delete.go index 1a48c1b49fd6..aa007daa003c 100644 --- a/pkg/karmadactl/delete/delete.go +++ b/pkg/karmadactl/delete/delete.go @@ -92,5 +92,8 @@ func NewCmdDelete(f util.Factory, parentCommnd string, ioStreams genericiooption cmd := kubectldelete.NewCmdDelete(f, ioStreams) cmd.Long = fmt.Sprintf(deleteLong, parentCommnd) cmd.Example = fmt.Sprintf(deleteExample, parentCommnd) + cmd.Annotations = map[string]string{ + util.TagCommandGroup: util.GroupBasic, + } return cmd } diff --git a/pkg/karmadactl/edit/edit.go b/pkg/karmadactl/edit/edit.go index e90828f1f1a4..6487d5cfec54 100644 --- a/pkg/karmadactl/edit/edit.go +++ b/pkg/karmadactl/edit/edit.go @@ -50,5 +50,8 @@ var ( func NewCmdEdit(f util.Factory, parentCommand string, ioStreams genericiooptions.IOStreams) *cobra.Command { cmd := kubectledit.NewCmdEdit(f, ioStreams) cmd.Example = fmt.Sprintf(editExample, parentCommand) + cmd.Annotations = map[string]string{ + util.TagCommandGroup: util.GroupBasic, + } return cmd } diff --git a/pkg/karmadactl/explain/explain.go b/pkg/karmadactl/explain/explain.go index 7b5bbfbac58a..52378a78a74f 100644 --- a/pkg/karmadactl/explain/explain.go +++ b/pkg/karmadactl/explain/explain.go @@ -74,6 +74,9 @@ func NewCmdExplain(f util.Factory, parentCommand string, streams genericiooption cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, + Annotations: map[string]string{ + util.TagCommandGroup: util.GroupBasic, + }, } flags := cmd.Flags() diff --git a/pkg/karmadactl/label/label.go b/pkg/karmadactl/label/label.go index aa308bd25c56..fafb7b9283b1 100644 --- a/pkg/karmadactl/label/label.go +++ b/pkg/karmadactl/label/label.go @@ -50,5 +50,8 @@ var ( func NewCmdLabel(f util.Factory, parentCommand string, ioStreams genericiooptions.IOStreams) *cobra.Command { cmd := kubectllabel.NewCmdLabel(f, ioStreams) cmd.Example = fmt.Sprintf(labelExample, parentCommand) + cmd.Annotations = map[string]string{ + util.TagCommandGroup: util.GroupSettingsCommands, + } return cmd } diff --git a/pkg/karmadactl/patch/patch.go b/pkg/karmadactl/patch/patch.go index 859f878e8b87..09d8e3be085d 100644 --- a/pkg/karmadactl/patch/patch.go +++ b/pkg/karmadactl/patch/patch.go @@ -46,5 +46,8 @@ var ( func NewCmdPatch(f util.Factory, parentCommand string, ioStreams genericiooptions.IOStreams) *cobra.Command { cmd := kubectlpatch.NewCmdPatch(f, ioStreams) cmd.Example = fmt.Sprintf(patchExample, parentCommand) + cmd.Annotations = map[string]string{ + util.TagCommandGroup: util.GroupAdvancedCommands, + } return cmd } diff --git a/pkg/karmadactl/portforward/portforward.go b/pkg/karmadactl/portforward/portforward.go new file mode 100644 index 000000000000..bd39a2c07c72 --- /dev/null +++ b/pkg/karmadactl/portforward/portforward.go @@ -0,0 +1,171 @@ +/* +Copyright 2024 The Karmada Authors. +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 portforward + +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericiooptions" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + kubectlportforward "k8s.io/kubectl/pkg/cmd/portforward" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/completion" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/karmada-io/karmada/pkg/karmadactl/options" + "github.com/karmada-io/karmada/pkg/karmadactl/util" +) + +const ( + // Amount of time to wait until at least one pod is running + defaultPodPortForwardWaitTimeout = 60 * time.Second +) + +var ( + portforwardLong = templates.LongDesc(` + Forward one or more local ports to a pod. + + Use resource type/name such as deployment/mydeployment to select a pod. Resource type defaults to 'pod' if omitted. + + If there are multiple pods matching the criteria, a pod will be selected automatically. The + forwarding session ends when the selected pod terminates, and a rerun of the command is needed + to resume forwarding.`) + + portforwardExample = templates.Examples(` + # Listen on ports 5000 and 6000 locally, forwarding data to/from ports 5000 and 6000 in the pod + %[1]s port-forward pod/mypod 5000 6000 + + # Listen on ports 5000 and 6000 locally, forwarding data to/from ports 5000 and 6000 in a pod selected by the deployment + %[1]s port-forward deployment/mydeployment 5000 6000 + + # Listen on port 8443 locally, forwarding to the targetPort of the service's port named "https" in a pod selected by the service + %[1]s port-forward service/myservice 8443:https + + # Listen on port 8888 locally, forwarding to 5000 in the pod + %[1]s port-forward pod/mypod 8888:5000 + + # Listen on port 8888 on all addresses, forwarding to 5000 in the pod + %[1]s port-forward --address 0.0.0.0 pod/mypod 8888:5000 + + # Listen on port 8888 on localhost and selected IP, forwarding to 5000 in the pod + %[1]s port-forward --address localhost,10.19.21.23 pod/mypod 8888:5000 + + # Listen on a random port locally, forwarding to 5000 in the pod + %[1]s port-forward pod/mypod :5000`) +) + +// NewCmdPortForWard new port-forward command. +func NewCmdPortForWard(f util.Factory, parentCommand string, streams genericiooptions.IOStreams) *cobra.Command { + opts := &CommandPortForwardOptions{ + KubectlPortForwardOptions: &kubectlportforward.PortForwardOptions{ + PortForwarder: &defaultPortForwarder{ + IOStreams: streams, + }, + }} + + cmd := &cobra.Command{ + Use: "port-forward TYPE/NAME [options] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]", + DisableFlagsInUseLine: true, + Short: "Forward one or more local ports to a pod", + Long: portforwardLong, + Example: fmt.Sprintf(portforwardExample, parentCommand), + ValidArgsFunction: completion.PodResourceNameCompletionFunc(f), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(opts.PreCheck()) + cmdutil.CheckErr(opts.Complete(f, cmd, args)) + cmdutil.CheckErr(opts.Validate()) + cmdutil.CheckErr(opts.Run(context.Background())) + }, + } + cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodPortForwardWaitTimeout) + cmd.Flags().StringSliceVar(&opts.KubectlPortForwardOptions.Address, "address", []string{"localhost"}, "Addresses to listen on (comma separated). Only accepts IP addresses or localhost as a value. When localhost is supplied, kubectl will try to bind on both 127.0.0.1 and ::1 and will fail if neither of these addresses are available to bind.") + cmd.Flags().StringVarP(options.DefaultConfigFlags.Namespace, "namespace", "n", *options.DefaultConfigFlags.Namespace, "If present, the namespace scope for this CLI request") + opts.OperationScope = options.KarmadaControlPlane + cmd.Flags().Var(&opts.OperationScope, "operation-scope", "Used to control the operation scope of the command. The optional values are karmada and members. Defaults to karmada.") + cmd.Flags().StringVarP(&opts.Cluster, "cluster", "C", "", "Used to specify a target member cluster and only takes effect when the command's operation scope is members, for example: --operation-scope=members --cluster=member1") + + return cmd +} + +// CommandPortForwardOptions contains the input to the port-forward command. +type CommandPortForwardOptions struct { + KubectlPortForwardOptions *kubectlportforward.PortForwardOptions + Cluster string + OperationScope options.OperationScope +} + +type defaultPortForwarder struct { + genericiooptions.IOStreams +} + +// ForwardPorts port-forward to specify url. +func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts kubectlportforward.PortForwardOptions) error { + transport, upgrader, err := spdy.RoundTripperFor(opts.Config) + if err != nil { + return err + } + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) + fw, err := portforward.NewOnAddresses(dialer, opts.Address, opts.Ports, opts.StopChannel, opts.ReadyChannel, f.Out, f.ErrOut) + if err != nil { + return err + } + return fw.ForwardPorts() +} + +// PreCheck precondition check operation-scope and other flags. +func (o *CommandPortForwardOptions) PreCheck() error { + err := options.VerifyOperationScopeFlags(o.OperationScope, options.KarmadaControlPlane, options.Members) + if err != nil { + return err + } + if o.OperationScope == options.Members && len(o.Cluster) == 0 { + return fmt.Errorf("must specify a member cluster") + } + return nil +} + +// Complete ensures that options are valid and marshals them if necessary +func (o *CommandPortForwardOptions) Complete(f util.Factory, cmd *cobra.Command, args []string) error { + var portForwardFactory cmdutil.Factory = f + if o.OperationScope == options.KarmadaControlPlane { + portForwardFactory = f + } + if o.OperationScope == options.Members && len(o.Cluster) != 0 { + memberFactory, err := f.FactoryForMemberCluster(o.Cluster) + if err != nil { + return err + } + portForwardFactory = memberFactory + } + + return o.KubectlPortForwardOptions.Complete(portForwardFactory, cmd, args) +} + +// Validate checks if the parameters are valid +func (o *CommandPortForwardOptions) Validate() error { + return o.KubectlPortForwardOptions.Validate() +} + +// Run implements all the necessary functionality for port-forward cmd. +// It ends portforwarding when an error is received from the backend, or an os.Interrupt +// signal is received, or the provided context is done. +func (o *CommandPortForwardOptions) Run(ctx context.Context) error { + return o.KubectlPortForwardOptions.RunPortForwardContext(ctx) +} diff --git a/pkg/karmadactl/top/top.go b/pkg/karmadactl/top/top.go index 235b81727f7c..1241102b1035 100644 --- a/pkg/karmadactl/top/top.go +++ b/pkg/karmadactl/top/top.go @@ -58,6 +58,9 @@ func NewCmdTop(f util.Factory, parentCommand string, streams genericiooptions.IO Short: "Display resource (CPU/memory) usage of member clusters", Long: topLong, Run: cmdutil.DefaultSubCommandRun(streams.ErrOut), + Annotations: map[string]string{ + util.TagCommandGroup: util.GroupAdvancedCommands, + }, } // create subcommands diff --git a/pkg/karmadactl/util/command_group.go b/pkg/karmadactl/util/command_group.go index 4899d4f4f32d..c9eb67ed26fa 100644 --- a/pkg/karmadactl/util/command_group.go +++ b/pkg/karmadactl/util/command_group.go @@ -36,4 +36,10 @@ const ( // GroupAdvancedCommands means the command belongs to Group "Advanced Commands" GroupAdvancedCommands = "Advanced Commands" + + // GroupSettingsCommands means the command belongs to Group "Settings Commands" + GroupSettingsCommands = "Settings Commands" + + // GroupOtherCommands means the command belongs to Group "Other Commands" + GroupOtherCommands = "Other Commands" )