Projects STRLCPY cdebug Commits e6cc1c7c
🤬
  • Local port forwarding works!

    Example: cdebug port-forward 98 -L 80 -L 18088:80 -L 0.0.0.0:18099:80 -L bridge:80 -L 127.0.0.1:19088:172.17.0.2:80.
    
    Multi-network targets are also supported!
  • Loading...
  • Ivan Velichko committed 1 year ago
    e6cc1c7c
    1 parent ee17491a
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
  • ■ ■ ■ ■ ■
    cmd/exec/exec.go
    skipped 148 lines
    149 149   return errors.New("target container found but it's not running")
    150 150   }
    151 151   
     152 + cli.PrintAux("Pulling debugger image...\n")
    152 153   if err := client.ImagePullEx(ctx, opts.image, types.ImagePullOptions{}); err != nil {
    153 154   return fmt.Errorf("cannot pull debugger image %q: %w", opts.image, err)
    154 155   }
    skipped 213 lines
  • ■ ■ ■ ■ ■ ■
    cmd/portforward/portforward.go
    skipped 3 lines
    4 4   "context"
    5 5   "errors"
    6 6   "fmt"
    7  - "net"
    8 7   "os"
    9 8   "os/signal"
    10 9   "strings"
    skipped 1 lines
    12 11   
    13 12   "github.com/docker/docker/api/types"
    14 13   "github.com/docker/docker/api/types/container"
     14 + dockerclient "github.com/docker/docker/client"
    15 15   "github.com/docker/go-connections/nat"
    16 16   "github.com/sirupsen/logrus"
    17 17   "github.com/spf13/cobra"
    skipped 5 lines
    23 23  )
    24 24   
    25 25  // TODO:
    26  -// - parse ports args
    27  -// - handle non-default network case
    28  -// - handle exposing localhost ports
     26 +// - Local port forwarding: handle exposing localhost ports
    29 27  // cdebug exec --name helper --image socat <target> <target-port> <proxy-port>
    30 28  // cdebug port-forward helper <host-port>:<proxy-port>
    31  - 
    32  -// Possible options (kinda sorta as in ssh -L):
    33  -// - TARGET_PORT # binds TARGET_IP:TARGET_PORT to a random port on localhost
    34  -// - TARGET_IP:TARGET_PORT # The second form is needed to:
     29 +//
     30 +// - Remote port forwarding: implement me!
     31 +//
     32 +// Local port forwarding's possible modes (kinda sorta as in ssh -L):
     33 +// - REMOTE_PORT # binds REMOTE_IP:REMOTE_PORT to a random port on localhost
     34 +// - REMOTE_<IP|ALIAS|NET>:REMOTE_PORT # The second form is needed to:
    35 35  // # 1) allow exposing target's localhost ports
    36 36  // # 2) specify a concrete IP for a multi-network target
    37 37  //
    38  -// - LOCAL_PORT:TARGET_PORT # binds TARGET_IP:TARGET_PORT to LOCAL_PORT on localhost
    39  -// - LOCAL_PORT:TARGET_IP:TARGET_PORT
     38 +// - LOCAL_PORT:REMOTE_PORT # binds REMOTE_IP:REMOTE_PORT to LOCAL_PORT on localhost
     39 +// - LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
    40 40  //
    41  -// - LOCAL_IP:LOCAL_PORT:TARGET_PORT # similar to LOCAL_PORT:TARGET_PORT but LOCAL_IP is used instead of localhost
    42  -// - LOCAL_IP:LOCAL_PORT:TARGET_IP:TARGET_PORT
     41 +// - LOCAL_IP:LOCAL_PORT:REMOTE_PORT # similar to LOCAL_PORT:REMOTE_PORT but LOCAL_IP is used instead of localhost
     42 +// - LOCAL_IP:LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
     43 +//
     44 +// Remote port forwarding's possible modes (kinda sorta as in ssh -R):
     45 +// - coming soon...
    43 46   
    44 47  const (
    45  - helperImage = "nixery.dev/shell/socat:latest"
     48 + forwarderImage = "nixery.dev/shell/socat:latest"
    46 49   
    47 50   outFormatText = "text"
    48 51   outFormatJSON = "json"
    49 52  )
    50 53   
     54 +var (
     55 + errNoAddr = errors.New("target container must have at least one IP address")
     56 + errBadLocalPort = errors.New("bad local port")
     57 + errBadRemotePort = errors.New("bad remote port")
     58 +)
     59 + 
    51 60  type options struct {
    52  - target string
    53  - forwardings []string
    54  - output string
    55  - quiet bool
     61 + target string
     62 + locals []string
     63 + remotes []string
     64 + output string
     65 + quiet bool
    56 66  }
    57 67   
    58 68  func NewCommand(cli cliutil.CLI) *cobra.Command {
    59 69   var opts options
    60 70   
    61 71   cmd := &cobra.Command{
    62  - Use: "port-forward CONTAINER [[LOCAL_IP:]LOCAL_PORT:]TARGET_PORT [...]",
    63  - Short: `"Publish" one or more ports of an already running container`,
    64  - Args: cobra.MinimumNArgs(2),
     72 + Use: "port-forward CONTAINER -L [LOCAL:]REMOTE [-L ...] | -R [REMOTE:]:LOCAL [-R ...]",
     73 + Short: `Forward one or more local or remote ports`,
     74 + Long: `While the implementation for sure differs, the behavior and semantic of the command
     75 +are meant to be similar to SSH local (-L) and remote (-R) port forwarding. The word "local" always
     76 +refers to the cdebug side. The word "remote" always refers to the target container side.`,
     77 + Args: cobra.ExactArgs(1),
    65 78   RunE: func(cmd *cobra.Command, args []string) error {
     79 + if len(opts.locals)+len(opts.remotes) == 0 {
     80 + return cliutil.NewStatusError(1, "at least one -L or -R flag must be provided")
     81 + }
     82 + if len(opts.remotes) > 0 {
     83 + // TODO: Implement me!
     84 + return cliutil.NewStatusError(1, "remote port forwarding is not implemented yet")
     85 + }
     86 + 
    66 87   cli.SetQuiet(opts.quiet)
    67 88   
    68 89   opts.target = args[0]
    69  - opts.forwardings = args[1:]
     90 + 
    70 91   return cliutil.WrapStatusError(runPortForward(context.Background(), cli, &opts))
    71 92   },
    72 93   }
    73 94   
    74 95   flags := cmd.Flags()
    75  - flags.SetInterspersed(false) // Instead of relying on --
     96 + 
     97 + flags.StringSliceVarP(
     98 + &opts.locals,
     99 + "local",
     100 + "L",
     101 + nil,
     102 + `Local port forwarding in the form [[LOCAL_IP:]LOCAL_PORT:][REMOTE_IP|REMOTE_NETWORK|REMOTE_ALIAS:]REMOTE_PORT`,
     103 + )
     104 + 
     105 + flags.StringSliceVarP(
     106 + &opts.remotes,
     107 + "remote",
     108 + "R",
     109 + nil,
     110 + `Remote port forwarding in the form [REMOTE_IP|REMOTE_NETWORK|REMOTE_ALIAS:]REMOTE_PORT:LOCAL_IP:LOCAL_PORT`,
     111 + )
    76 112   
    77 113   flags.BoolVarP(
    78 114   &opts.quiet,
    skipped 8 lines
    87 123   "output",
    88 124   "o",
    89 125   outFormatText,
    90  - `Output format (plain text or JSON)`,
     126 + `Output format ("text" | "json")`,
    91 127   )
    92 128   
    93 129   return cmd
    skipped 9 lines
    103 139   if err != nil {
    104 140   return err
    105 141   }
    106  - if target.State == nil || !target.State.Running {
    107  - return errors.New("target container found but it's not running")
     142 + if err := validateTarget(target); err != nil {
     143 + return err
    108 144   }
    109 145   
    110  - if err := client.ImagePullEx(ctx, helperImage, types.ImagePullOptions{}); err != nil {
    111  - return fmt.Errorf("cannot pull port-forwarder helper image %q: %w", helperImage, err)
     146 + cli.PrintAux("Pulling forwarder image...\n")
     147 + if err := client.ImagePullEx(ctx, forwarderImage, types.ImagePullOptions{}); err != nil {
     148 + return fmt.Errorf("cannot pull forwarder image %q: %w", forwarderImage, err)
    112 149   }
    113 150   
    114  - forwardings, err := parseForwardings(target, opts.forwardings)
     151 + locals, err := parseLocalForwardings(target, opts.locals)
    115 152   if err != nil {
    116 153   return err
    117 154   }
    118 155   
    119  - exposedPorts, portBindings, err := nat.ParsePortSpecs(forwardings.toDockerPortSpecs())
    120  - if err != nil {
    121  - return err
    122  - }
    123  - 
    124  - // TODO: Iterate over all forwardings.
    125  - resp, err := client.ContainerCreate(
    126  - ctx,
    127  - &container.Config{
    128  - Image: helperImage,
    129  - Entrypoint: []string{"socat"},
    130  - Cmd: []string{
    131  - fmt.Sprintf("TCP-LISTEN:%s,fork", forwardings[0].targetPort),
    132  - fmt.Sprintf("TCP-CONNECT:%s:%s", forwardings[0].targetIP, forwardings[0].targetPort),
    133  - },
    134  - ExposedPorts: exposedPorts,
    135  - },
    136  - &container.HostConfig{
    137  - AutoRemove: true,
    138  - PortBindings: portBindings,
    139  - },
    140  - nil,
    141  - nil,
    142  - "port-forwarder-"+uuid.ShortID(),
    143  - )
    144  - if err != nil {
    145  - return fmt.Errorf("cannot create port-forwarder container: %w", err)
    146  - }
     156 + // TODO: It's probably a good idea to monitor forwarders too.
     157 + for i, l := range locals {
     158 + forwarder, err := startLocalForwarder(ctx, client, target, l)
     159 + if err != nil {
     160 + return err
     161 + }
     162 + defer func() {
     163 + if err := client.ContainerKill(ctx, forwarder.ID, "KILL"); err != nil {
     164 + logrus.Debugf("Cannot kill forwarder container: %s", err)
     165 + }
     166 + }()
    147 167   
    148  - if err := client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
    149  - return fmt.Errorf("cannot start port-forwarder container: %w", err)
     168 + if len(l.localPort) == 0 {
     169 + for _, bindings := range forwarder.NetworkSettings.Ports {
     170 + locals[i].localPort = bindings[0].HostPort
     171 + break
     172 + }
     173 + }
    150 174   }
    151 175   
    152  - forwarder, err := client.ContainerInspect(ctx, resp.ID)
    153  - if err != nil {
    154  - return fmt.Errorf("cannot inspect forwarder container: %w", err)
     176 + switch opts.output {
     177 + case outFormatJSON:
     178 + cli.PrintOut(localForwardingsToJSON(locals) + "\n")
     179 + case outFormatText:
     180 + cli.PrintOut(localForwardingsToText(locals) + "\n")
     181 + default:
     182 + panic("unreachable!")
    155 183   }
    156 184   
    157  - output(cli, forwardings, forwarder, opts.output)
    158  - 
    159  - sigCh := make(chan os.Signal, 128)
    160  - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
    161  - defer close(sigCh)
    162  - 
    163  - go func() {
    164  - for _ = range sigCh {
    165  - cli.PrintAux("Exiting...")
    166  - 
    167  - if err := client.ContainerKill(ctx, resp.ID, "KILL"); err != nil {
    168  - logrus.Debugf("Cannot kill forwarder container: %s", err)
    169  - }
    170  - break
    171  - }
    172  - }()
     185 + signalCh := make(chan os.Signal, 128)
     186 + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
     187 + defer close(signalCh)
    173 188   
    174  - forwarderStatusCh, forwarderErrCh := client.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
    175  - // targetStatusCh, targetErrCh := client.ContainerWait(ctx, target.ID, container.WaitConditionNotRunning)
     189 + targetStatusCh, targetErrCh := client.ContainerWait(ctx, target.ID, container.WaitConditionNotRunning)
    176 190   select {
    177  - case err := <-forwarderErrCh:
     191 + case <-signalCh:
     192 + cli.PrintAux("Exiting...")
     193 + 
     194 + case err := <-targetErrCh:
    178 195   if err != nil {
    179  - return fmt.Errorf("waiting for port-forwarder container failed: %w", err)
     196 + return fmt.Errorf("waiting for target container failed: %w", err)
    180 197   }
    181  - case <-forwarderStatusCh:
     198 + 
     199 + case <-targetStatusCh:
    182 200   }
    183 201   
    184 202   return nil
    185 203  }
    186 204   
    187  -func output(
    188  - cli cliutil.CLI,
    189  - forwardings forwardingList,
    190  - forwarder types.ContainerJSON,
    191  - outFormat string,
    192  -) {
    193  - targetIP := target.NetworkSettings.Networks["bridge"].IPAddress
    194  - for remotePort, localBindings := range forwarder.NetworkSettings.Ports {
    195  - for _, binding := range localBindings {
    196  - switch opts.output {
    197  - case outFormatText:
    198  - local := net.JoinHostPort(binding.HostIP, binding.HostPort)
    199  - remote := targetIP + ":" + string(remotePort)
    200  - cli.PrintOut("Forwarding %s to %s's %s\n", local, target.Name[1:], remote)
    201  - case outFormatJSON:
    202  - cli.PrintOut(jsonutil.Dump(map[string]string{
    203  - "localHost": binding.HostIP,
    204  - "localPort": binding.HostPort,
    205  - "remoteHost": targetIP,
    206  - "remotePort": string(remotePort),
    207  - }))
    208  - default:
    209  - panic("unreachable!")
    210  - }
    211  - }
     205 +func validateTarget(target types.ContainerJSON) error {
     206 + if target.State == nil || !target.State.Running {
     207 + return errors.New("target container found but it's not running")
     208 + }
     209 + 
     210 + hasIP := false
     211 + for _, net := range target.NetworkSettings.Networks {
     212 + hasIP = hasIP || len(net.IPAddress) > 0
     213 + }
     214 + if !hasIP {
     215 + return errNoAddr
    212 216   }
     217 + 
     218 + return nil
    213 219  }
    214 220   
    215 221  type forwarding struct {
    216 222   localIP string
    217 223   localPort string
    218  - targetIP string
    219  - targetPort string
     224 + remoteIP string
     225 + remotePort string
    220 226  }
    221 227   
    222  -type forwardingList []forwarding
     228 +func (f forwarding) toDockerPortSpec() string {
     229 + // ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
     230 + return f.localIP + ":" + f.localPort + ":" + f.remotePort
     231 +}
    223 232   
    224  -func (list forwardingList) toDockerPortSpecs() []string {
    225  - // ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
    226  - var spec []string
    227  - for _, f := range list {
    228  - spec = append(spec, fmt.Sprintf("%s:%s:%s", f.localIP, f.localPort, f.targetPort))
     233 +func parseLocalForwardings(
     234 + target types.ContainerJSON,
     235 + locals []string,
     236 +) ([]forwarding, error) {
     237 + var parsed []forwarding
     238 + for _, l := range locals {
     239 + next, err := parseLocalForwarding(target, l)
     240 + if err != nil {
     241 + return nil, err
     242 + }
     243 + parsed = append(parsed, next)
    229 244   }
    230  - return spec
     245 + return parsed, nil
    231 246  }
    232 247   
    233  -func parseForwardings(
     248 +func parseLocalForwarding(
    234 249   target types.ContainerJSON,
    235  - forwardings []string,
    236  -) (forwardingList, error) {
    237  - var list forwardingList
     250 + local string,
     251 +) (forwarding, error) {
     252 + parts := strings.Split(local, ":")
     253 + if len(parts) == 1 {
     254 + // Case 1: REMOTE_PORT only
     255 + if _, err := nat.ParsePort(parts[0]); err != nil {
     256 + return forwarding{}, errBadRemotePort
     257 + }
    238 258   
    239  - targetIP := target.NetworkSettings.Networks["bridge"].IPAddress
     259 + remoteIP, err := unambiguousIP(target)
     260 + if err != nil {
     261 + return forwarding{}, err
     262 + }
    240 263   
    241  - for _, f := range forwardings {
    242  - parts := strings.Split(f, ":")
    243  - if len(parts) == 1 {
    244  - // Case 1: TARGET_PORT
     264 + // localPort will be later assigned by Docker dynamically
     265 + return forwarding{
     266 + localIP: "127.0.0.1",
     267 + remoteIP: remoteIP,
     268 + remotePort: parts[0],
     269 + }, nil
     270 + }
    245 271   
    246  - if _, err := nat.ParsePort(parts[0]); err != nil {
    247  - // TODO: Return a user-friendly error.
    248  - return nil, err
     272 + if len(parts) == 2 {
     273 + // Either LOCAL_PORT:REMOTE_PORT or REMOTE_<IP|ALIAS|NETWORK>:REMOTE_PORT
     274 + 
     275 + if _, err := nat.ParsePort(parts[1]); err != nil {
     276 + return forwarding{}, errBadRemotePort
     277 + }
     278 + 
     279 + if _, err := nat.ParsePort(parts[0]); err == nil {
     280 + // Case 2: LOCAL_PORT:REMOTE_PORT
     281 + remoteIP, err := unambiguousIP(target)
     282 + if err != nil {
     283 + return forwarding{}, err
     284 + }
     285 + 
     286 + return forwarding{
     287 + localIP: "127.0.0.1",
     288 + localPort: parts[0],
     289 + remoteIP: remoteIP,
     290 + remotePort: parts[1],
     291 + }, nil
     292 + }
     293 + 
     294 + // Case 3: REMOTE_<IP|ALIAS|NETWORK>:REMOTE_PORT
     295 + remoteIP, err := lookupTargetIP(target, parts[0])
     296 + if err != nil {
     297 + return forwarding{}, err
     298 + }
     299 + 
     300 + // localPort will be later assigned by Docker dynamically
     301 + return forwarding{
     302 + localIP: "127.0.0.1",
     303 + remotePort: parts[1],
     304 + remoteIP: remoteIP,
     305 + }, nil
     306 + }
     307 + 
     308 + if len(parts) == 3 {
     309 + // Either LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT or LOCAL_IP:LOCAL_PORT:REMOTE_PORT
     310 + 
     311 + if _, err := nat.ParsePort(parts[0]); err == nil {
     312 + // Case 4: LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
     313 + remoteIP, err := lookupTargetIP(target, parts[1])
     314 + if err != nil {
     315 + return forwarding{}, err
    249 316   }
    250 317   
    251  - // TODO: if "target has more than 1 IP" return err
     318 + if _, err := nat.ParsePort(parts[2]); err != nil {
     319 + return forwarding{}, errBadRemotePort
     320 + }
    252 321   
    253  - list = append(list, forwarding{
     322 + return forwarding{
    254 323   localIP: "127.0.0.1",
    255  - targetPort: parts[0],
    256  - targetIP: targetIP,
    257  - })
    258  - continue
     324 + localPort: parts[0],
     325 + remoteIP: remoteIP,
     326 + remotePort: parts[2],
     327 + }, nil
    259 328   }
    260 329   
    261  - if len(parts) == 2 {
    262  - if _, err := nat.ParsePort(parts[0]); err == nil {
    263  - // Case 2: LOCAL_PORT:TARGET_PORT
     330 + // Case 5: LOCAL_IP:LOCAL_PORT:REMOTE_PORT
     331 + remoteIP, err := unambiguousIP(target)
     332 + if err != nil {
     333 + return forwarding{}, err
     334 + }
    264 335   
    265  - // TODO: if "target has more than 1 IP" return err
     336 + return forwarding{
     337 + localIP: parts[0],
     338 + localPort: parts[1],
     339 + remoteIP: remoteIP,
     340 + remotePort: parts[2],
     341 + }, nil
     342 + }
    266 343   
    267  - list = append(list, forwarding{
    268  - localPort: parts[0],
    269  - localIP: "127.0.0.1",
    270  - targetPort: parts[1],
    271  - targetIP: targetIP,
    272  - })
    273  - } else {
    274  - // Case 3: TARGET_IP:TARGET_PORT
     344 + // Case 6: LOCAL_IP:LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
     345 + if _, err := nat.ParsePort(parts[1]); err != nil {
     346 + return forwarding{}, errBadLocalPort
     347 + }
     348 + if _, err := nat.ParsePort(parts[3]); err != nil {
     349 + return forwarding{}, errBadRemotePort
     350 + }
    275 351   
    276  - // TODO: if "parts[0] not in target IP list" return err
     352 + remoteIP, err := lookupTargetIP(target, parts[2])
     353 + if err != nil {
     354 + return forwarding{}, err
     355 + }
    277 356   
    278  - list = append(list, forwarding{
    279  - localIP: "127.0.0.1",
    280  - targetPort: parts[1],
    281  - targetIP: parts[0],
    282  - })
     357 + return forwarding{
     358 + localIP: parts[0],
     359 + localPort: parts[1],
     360 + remoteIP: remoteIP,
     361 + remotePort: parts[3],
     362 + }, nil
     363 +}
     364 + 
     365 +func unambiguousIP(target types.ContainerJSON) (string, error) {
     366 + var found string
     367 + for _, net := range target.NetworkSettings.Networks {
     368 + if len(net.IPAddress) > 0 {
     369 + if len(found) > 0 {
     370 + return "", errors.New("remote IP must be specified explicitly for targets with multiple network interfaces")
    283 371   }
     372 + found = net.IPAddress
     373 + }
     374 + }
     375 + 
     376 + if len(found) == 0 {
     377 + // This cannot really happen unless there is a mistake in validateTarget().
     378 + return "", errNoAddr
     379 + }
     380 + 
     381 + return found, nil
     382 +}
     383 + 
     384 +func lookupTargetIP(target types.ContainerJSON, ipAliasNetwork string) (string, error) {
     385 + for name, net := range target.NetworkSettings.Networks {
     386 + if len(net.IPAddress) == 0 {
    284 387   continue
    285 388   }
    286 389   
    287  - // TODO: other cases
    288  - return nil, errors.New("implement me")
     390 + if net.IPAddress == ipAliasNetwork {
     391 + return net.IPAddress, nil
     392 + }
     393 + 
     394 + for _, alias := range net.Aliases {
     395 + if alias == ipAliasNetwork {
     396 + return net.IPAddress, nil
     397 + }
     398 + }
     399 + 
     400 + if name == ipAliasNetwork {
     401 + return net.IPAddress, nil
     402 + }
     403 + }
     404 + 
     405 + return "", errors.New("cannot derive remote host")
     406 +}
     407 + 
     408 +func targetNetworkByIP(target types.ContainerJSON, ip string) (string, error) {
     409 + for name, net := range target.NetworkSettings.Networks {
     410 + if net.IPAddress == ip {
     411 + return name, nil
     412 + }
     413 + }
     414 + return "", errors.New("cannot deduce target network by IP")
     415 +}
     416 + 
     417 +func startLocalForwarder(
     418 + ctx context.Context,
     419 + client dockerclient.CommonAPIClient,
     420 + target types.ContainerJSON,
     421 + fwd forwarding,
     422 +) (types.ContainerJSON, error) {
     423 + exposedPorts, portBindings, err := nat.ParsePortSpecs([]string{fwd.toDockerPortSpec()})
     424 + if err != nil {
     425 + return types.ContainerJSON{}, err
     426 + }
     427 + 
     428 + network, err := targetNetworkByIP(target, fwd.remoteIP)
     429 + if err != nil {
     430 + return types.ContainerJSON{}, err
     431 + }
     432 + 
     433 + resp, err := client.ContainerCreate(
     434 + ctx,
     435 + &container.Config{
     436 + Image: forwarderImage,
     437 + Entrypoint: []string{"socat"},
     438 + Cmd: []string{
     439 + fmt.Sprintf("TCP-LISTEN:%s,fork", fwd.remotePort),
     440 + fmt.Sprintf("TCP-CONNECT:%s:%s", fwd.remoteIP, fwd.remotePort),
     441 + },
     442 + ExposedPorts: exposedPorts,
     443 + },
     444 + &container.HostConfig{
     445 + AutoRemove: true,
     446 + PortBindings: portBindings,
     447 + NetworkMode: container.NetworkMode(network),
     448 + },
     449 + nil,
     450 + nil,
     451 + "cdebug-fwd-"+uuid.ShortID(),
     452 + )
     453 + if err != nil {
     454 + return types.ContainerJSON{}, fmt.Errorf("cannot create forwarder container: %w", err)
     455 + }
     456 + 
     457 + if err := client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
     458 + return types.ContainerJSON{}, fmt.Errorf("cannot start forwarder container: %w", err)
     459 + }
     460 + 
     461 + forwarder, err := client.ContainerInspect(ctx, resp.ID)
     462 + if err != nil {
     463 + return forwarder, fmt.Errorf("cannot inspect forwarder container: %w", err)
     464 + }
     465 + return forwarder, nil
     466 +}
     467 + 
     468 +func localForwardingsToJSON(fwds []forwarding) string {
     469 + out := []map[string]string{}
     470 + for _, f := range fwds {
     471 + out = append(out, map[string]string{
     472 + "localHost": f.localIP,
     473 + "localPort": f.localPort,
     474 + "remoteHost": f.remoteIP,
     475 + "remotePort": f.remotePort,
     476 + })
    289 477   }
     478 + return jsonutil.DumpIndent(out)
     479 +}
    290 480   
    291  - return list, nil
     481 +func localForwardingsToText(fwds []forwarding) string {
     482 + out := []string{}
     483 + for _, f := range fwds {
     484 + out = append(out, fmt.Sprintf(
     485 + "Forwarding %s:%s to %s:%s",
     486 + f.localIP, f.localPort, f.remoteIP, f.remotePort,
     487 + ))
     488 + }
     489 + return strings.Join(out, "\n")
    292 490  }
    293 491   
  • ■ ■ ■ ■
    pkg/jsonutil/jsonutil.go
    skipped 12 lines
    13 13  }
    14 14   
    15 15  func DumpIndent(v any) string {
    16  - b, _ := json.MarshalIndent(v, "", " ")
     16 + b, err := json.MarshalIndent(v, "", " ")
     17 + if err != nil {
     18 + panic(err)
     19 + }
    17 20   return string(b)
    18 21  }
    19 22   
Please wait...
Page is in error, reload to recover