github.com/oam-dev/kubevela@v1.9.11/references/cli/portforward.go (about) 1 /* 2 Copyright 2021 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package cli 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 "net/url" 24 "strconv" 25 "strings" 26 27 "github.com/pkg/errors" 28 "github.com/spf13/cobra" 29 "k8s.io/cli-runtime/pkg/genericclioptions" 30 "k8s.io/client-go/kubernetes" 31 "k8s.io/client-go/rest" 32 "k8s.io/client-go/tools/portforward" 33 "k8s.io/client-go/transport/spdy" 34 cmdpf "k8s.io/kubectl/pkg/cmd/portforward" 35 k8scmdutil "k8s.io/kubectl/pkg/cmd/util" 36 "k8s.io/utils/pointer" 37 "sigs.k8s.io/controller-runtime/pkg/client" 38 39 pkgmulticluster "github.com/kubevela/pkg/multicluster" 40 41 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 42 "github.com/oam-dev/kubevela/apis/types" 43 "github.com/oam-dev/kubevela/pkg/multicluster" 44 "github.com/oam-dev/kubevela/pkg/utils/common" 45 "github.com/oam-dev/kubevela/pkg/utils/util" 46 querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types" 47 "github.com/oam-dev/kubevela/references/appfile" 48 ) 49 50 // VelaPortForwardOptions for vela port-forward 51 type VelaPortForwardOptions struct { 52 Cmd *cobra.Command 53 Args []string 54 ioStreams util.IOStreams 55 ClusterName string 56 ComponentName string 57 ResourceName string 58 ResourceType string 59 60 Ctx context.Context 61 VelaC common.Args 62 63 namespace string 64 App *v1beta1.Application 65 targetResource struct { 66 kind string 67 name string 68 cluster string 69 namespace string 70 } 71 targetPort int 72 73 f k8scmdutil.Factory 74 kcPortForwardOptions *cmdpf.PortForwardOptions 75 ClientSet kubernetes.Interface 76 Client client.Client 77 } 78 79 // NewPortForwardCommand is vela port-forward command 80 func NewPortForwardCommand(c common.Args, order string, ioStreams util.IOStreams) *cobra.Command { 81 o := &VelaPortForwardOptions{ 82 ioStreams: ioStreams, 83 kcPortForwardOptions: &cmdpf.PortForwardOptions{ 84 PortForwarder: &defaultPortForwarder{ioStreams}, 85 }, 86 } 87 cmd := &cobra.Command{ 88 Use: "port-forward", 89 Short: "Forward local ports to container/service port of vela application.", 90 Long: "Forward local ports to container/service port of vela application.", 91 Example: "port-forward APP_NAME [options] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]", 92 PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 93 o.VelaC = c 94 return nil 95 }, 96 RunE: func(cmd *cobra.Command, args []string) error { 97 if len(args) < 1 { 98 ioStreams.Error("Please specify application name.") 99 return nil 100 } 101 if o.ResourceType != "pod" && o.ResourceType != "service" { 102 o.ResourceType = "service" 103 } 104 if o.ResourceType == "pod" && len(args) < 2 { 105 return errors.New("not port specified for port-forward") 106 } 107 var err error 108 o.namespace, err = GetFlagNamespaceOrEnv(cmd, c) 109 if err != nil { 110 return err 111 } 112 113 newClient, err := o.VelaC.GetClient() 114 if err != nil { 115 return err 116 } 117 o.Client = newClient 118 if err := o.Init(context.Background(), cmd, args); err != nil { 119 return err 120 } 121 if err := o.Complete(); err != nil { 122 return err 123 } 124 if err := o.Run(); err != nil { 125 return err 126 } 127 return nil 128 }, 129 Annotations: map[string]string{ 130 types.TagCommandOrder: order, 131 types.TagCommandType: types.TypeApp, 132 }, 133 } 134 135 cmd.Flags().StringSliceVar(&o.kcPortForwardOptions.Address, "address", []string{"localhost"}, "Addresses to listen on (comma separated). Only accepts IP addresses or localhost as a value. When localhost is supplied, vela will try to bind on both 127.0.0.1 and ::1 and will fail if neither of these addresses are available to bind.") 136 cmd.Flags().Duration(podRunningTimeoutFlag, defaultPodExecTimeout, 137 "The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running", 138 ) 139 cmd.Flags().StringVarP(&o.ComponentName, "component", "c", "", "filter the pod by the component name") 140 cmd.Flags().StringVarP(&o.ClusterName, "cluster", "", "", "filter the pod by the cluster name") 141 cmd.Flags().StringVarP(&o.ResourceName, "resource-name", "", "", "specify the resource name") 142 cmd.Flags().StringVarP(&o.ResourceType, "resource-type", "t", "", "specify the resource type, support the service, and pod") 143 addNamespaceAndEnvArg(cmd) 144 return cmd 145 } 146 147 // Init will initialize 148 func (o *VelaPortForwardOptions) Init(ctx context.Context, cmd *cobra.Command, argsIn []string) error { 149 o.Ctx = ctx 150 o.Cmd = cmd 151 o.Args = argsIn 152 153 app, err := appfile.LoadApplication(o.namespace, o.Args[0], o.VelaC) 154 if err != nil { 155 return err 156 } 157 o.App = app 158 159 if o.ResourceType == "service" { 160 var selectService *querytypes.ResourceItem 161 services, err := GetApplicationServices(o.Ctx, o.App.Name, o.namespace, o.VelaC, Filter{ 162 Component: o.ComponentName, 163 Cluster: o.ClusterName, 164 }) 165 if err != nil { 166 return fmt.Errorf("failed to load the application services: %w", err) 167 } 168 169 if o.ResourceName != "" { 170 for i, service := range services { 171 if service.Object.GetName() == o.ResourceName { 172 selectService = &services[i] 173 break 174 } 175 } 176 if selectService == nil { 177 fmt.Println("The Service you specified does not exist, please select it from the list.") 178 } 179 } 180 if len(services) > 0 { 181 if selectService == nil { 182 selectService, o.targetPort, err = AskToChooseOneService(services, len(o.Args) < 2) 183 if err != nil { 184 return err 185 } 186 } 187 if selectService != nil { 188 o.targetResource.cluster = selectService.Cluster 189 o.targetResource.name = selectService.Object.GetName() 190 o.targetResource.namespace = selectService.Object.GetNamespace() 191 o.targetResource.kind = selectService.Object.GetKind() 192 } 193 } else if o.ResourceName == "" { 194 // If users do not specify the resource name and there is no service, switch to query the pod 195 o.ResourceType = "pod" 196 } 197 } 198 199 if o.ResourceType == "pod" { 200 var selectPod *querytypes.PodBase 201 pods, err := GetApplicationPods(o.Ctx, o.App.Name, o.namespace, o.VelaC, Filter{ 202 Component: o.ComponentName, 203 Cluster: o.ClusterName, 204 }) 205 if err != nil { 206 return fmt.Errorf("failed to load the application services: %w", err) 207 } 208 209 if o.ResourceName != "" { 210 for i, pod := range pods { 211 if pod.Metadata.Name == o.ResourceName { 212 selectPod = &pods[i] 213 break 214 } 215 } 216 if selectPod == nil { 217 fmt.Println("The Service you specified does not exist, please select it from the list.") 218 } 219 } 220 if selectPod == nil { 221 selectPod, err = AskToChooseOnePod(pods) 222 if err != nil { 223 return err 224 } 225 } 226 if selectPod != nil { 227 o.targetResource.cluster = selectPod.Cluster 228 o.targetResource.name = selectPod.Metadata.Name 229 o.targetResource.namespace = selectPod.Metadata.Namespace 230 o.targetResource.kind = "Pod" 231 } 232 } 233 234 cf := genericclioptions.NewConfigFlags(true) 235 cf.Namespace = pointer.String(o.targetResource.namespace) 236 cf.WrapConfigFn = func(cfg *rest.Config) *rest.Config { 237 cfg.Wrap(pkgmulticluster.NewTransportWrapper(pkgmulticluster.ForCluster(o.targetResource.cluster))) 238 return cfg 239 } 240 o.f = k8scmdutil.NewFactory(k8scmdutil.NewMatchVersionFlags(cf)) 241 o.Ctx = multicluster.ContextWithClusterName(ctx, o.targetResource.cluster) 242 config, err := o.VelaC.GetConfig() 243 if err != nil { 244 return err 245 } 246 config.Wrap(pkgmulticluster.NewTransportWrapper()) 247 forwardClient, err := client.New(config, client.Options{Scheme: common.Scheme}) 248 if err != nil { 249 return err 250 } 251 o.VelaC.SetClient(forwardClient) 252 if o.ClientSet == nil { 253 c, err := kubernetes.NewForConfig(config) 254 if err != nil { 255 return err 256 } 257 o.ClientSet = c 258 } 259 return nil 260 } 261 262 // Complete will complete the config of port-forward 263 func (o *VelaPortForwardOptions) Complete() error { 264 var forwardTypeName string 265 switch o.targetResource.kind { 266 case "Service": 267 forwardTypeName = "svc/" + o.targetResource.name 268 case "Pod": 269 forwardTypeName = "pod/" + o.targetResource.name 270 } 271 272 if len(o.Args) < 2 { 273 formatPort := func(p int) string { 274 val := strconv.Itoa(p) 275 if val == "80" { 276 val = "8080:80" 277 } else if val == "443" { 278 val = "8443:443" 279 } 280 return val 281 } 282 pt := o.targetPort 283 if pt == 0 { 284 return errors.New("not port specified for port-forward") 285 } 286 o.Args = append(o.Args, formatPort(pt)) 287 } 288 args := make([]string, len(o.Args)) 289 copy(args, o.Args) 290 args[0] = forwardTypeName 291 o.kcPortForwardOptions.Namespace = o.targetResource.namespace 292 o.ioStreams.Infof("trying to connect the remote endpoint %s ..", strings.Join(args, " ")) 293 return o.kcPortForwardOptions.Complete(o.f, o.Cmd, args) 294 } 295 296 // Run will execute port-forward 297 func (o *VelaPortForwardOptions) Run() error { 298 go func() { 299 <-o.kcPortForwardOptions.ReadyChannel 300 o.ioStreams.Info("\nForward successfully! Opening browser ...") 301 local, _ := splitPort(o.Args[1]) 302 var url = "http://127.0.0.1:" + local 303 if err := OpenBrowser(url); err != nil { 304 o.ioStreams.Errorf("\nFailed to open browser: %v", err) 305 } 306 }() 307 308 return o.kcPortForwardOptions.RunPortForward() 309 } 310 311 func splitPort(port string) (local, remote string) { 312 parts := strings.Split(port, ":") 313 if len(parts) == 2 { 314 return parts[0], parts[1] 315 } 316 return parts[0], parts[0] 317 } 318 319 type defaultPortForwarder struct { 320 util.IOStreams 321 } 322 323 func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts cmdpf.PortForwardOptions) error { 324 transport, upgrader, err := spdy.RoundTripperFor(opts.Config) 325 if err != nil { 326 return err 327 } 328 dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) 329 fw, err := portforward.NewOnAddresses(dialer, opts.Address, opts.Ports, opts.StopChannel, opts.ReadyChannel, f.Out, f.ErrOut) 330 if err != nil { 331 return err 332 } 333 return fw.ForwardPorts() 334 }