■ ■ ■ ■ ■ ■
cmd/portforward/portforward.go
| skipped 1 lines |
2 | 2 | | |
3 | 3 | | import ( |
4 | 4 | | "context" |
| 5 | + | "errors" |
5 | 6 | | "fmt" |
6 | 7 | | "io" |
| 8 | + | "strings" |
7 | 9 | | |
8 | 10 | | "github.com/docker/docker/api/types" |
9 | 11 | | "github.com/docker/docker/api/types/container" |
| skipped 15 lines |
25 | 27 | | // Possible options (kinda sorta as in ssh -L): |
26 | 28 | | // - TARGET_PORT # binds TARGET_IP:TARGET_PORT to a random port on localhost |
27 | 29 | | // - TARGET_IP:TARGET_PORT # The second form is needed to: |
28 | | - | // # 1) allow target's localhost ports expose |
29 | | - | // # 2) specify a concrete IP if a multi-network target listens on 0.0.0.0 |
| 30 | + | // # 1) allow exposing target's localhost ports |
| 31 | + | // # 2) specify a concrete IP for a multi-network target |
30 | 32 | | // |
31 | 33 | | // - LOCAL_PORT:TARGET_PORT # binds TARGET_IP:TARGET_PORT to LOCAL_PORT on localhost |
32 | 34 | | // - LOCAL_PORT:TARGET_IP:TARGET_PORT |
33 | 35 | | // |
34 | | - | // - LOCAL_IP:LOCAL_PORT:TARGET_PORT # binds TARGET_IP:TARGET_PORT to LOCAL_PORT on LOCAL_IP |
| 36 | + | // - LOCAL_IP:LOCAL_PORT:TARGET_PORT # similar to LOCAL_PORT:TARGET_PORT but LOCAL_IP is used instead of localhost |
35 | 37 | | // - LOCAL_IP:LOCAL_PORT:TARGET_IP:TARGET_PORT |
36 | 38 | | |
37 | 39 | | const ( |
38 | | - | helperImage = "nixery.dev/socat:latest" |
| 40 | + | helperImage = "nixery.dev/shell/socat:latest" |
39 | 41 | | ) |
40 | 42 | | |
41 | 43 | | type options struct { |
42 | | - | target string |
43 | | - | address string |
44 | | - | ports []string |
| 44 | + | target string |
| 45 | + | forwardings []string |
45 | 46 | | } |
46 | 47 | | |
47 | 48 | | func NewCommand(cli cmd.CLI) *cobra.Command { |
48 | 49 | | var opts options |
49 | 50 | | |
50 | 51 | | cmd := &cobra.Command{ |
51 | | - | Use: "port-forward [OPTIONS] CONTAINER [LOCAL_PORT:]TARGET_PORT [...[LOCAL_PORT_N:]TARGET_PORT_N]", |
52 | | - | Short: "Publish a port of an already running container (kind of)", |
| 52 | + | Use: "port-forward CONTAINER [[LOCAL_IP:]LOCAL_PORT:]TARGET_PORT [...]", |
| 53 | + | Short: `"Publish" one or more ports of an already running container`, |
53 | 54 | | Args: cobra.MinimumNArgs(2), |
54 | 55 | | RunE: func(cmd *cobra.Command, args []string) error { |
55 | 56 | | opts.target = args[0] |
56 | | - | if len(args) > 1 { |
57 | | - | opts.ports = args[1:] |
58 | | - | } |
| 57 | + | opts.forwardings = args[1:] |
59 | 58 | | return runPortForward(context.Background(), cli, &opts) |
60 | 59 | | }, |
61 | 60 | | } |
62 | 61 | | |
63 | | - | flags := cmd.Flags() |
64 | | - | flags.SetInterspersed(false) // Instead of relying on -- |
65 | | - | |
66 | | - | flags.StringVar( |
67 | | - | &opts.address, |
68 | | - | "address", |
69 | | - | "127.0.0.1", |
70 | | - | "Host's interface address to bind the port to", |
71 | | - | ) |
72 | | - | |
73 | 62 | | return cmd |
74 | 63 | | } |
75 | 64 | | |
| skipped 15 lines |
91 | 80 | | return err |
92 | 81 | | } |
93 | 82 | | |
94 | | - | ports, portBindings, err := nat.ParsePortSpecs([]string{"8080:80"}) |
| 83 | + | forwardings, err := prepareForwardings(target, opts.forwardings) |
95 | 84 | | if err != nil { |
96 | 85 | | return err |
97 | 86 | | } |
98 | 87 | | |
99 | | - | contIP := target.NetworkSettings.Networks["bridge"].IPAddress |
| 88 | + | exposedPorts, portBindings, err := nat.ParsePortSpecs(forwardings.toDockerPortSpecs()) |
| 89 | + | if err != nil { |
| 90 | + | return err |
| 91 | + | } |
| 92 | + | |
| 93 | + | // TODO: Iterate over all forwardings. |
100 | 94 | | resp, err := client.ContainerCreate( |
101 | 95 | | ctx, |
102 | 96 | | &container.Config{ |
103 | 97 | | Image: helperImage, |
104 | 98 | | Entrypoint: []string{"socat"}, |
105 | 99 | | Cmd: []string{ |
106 | | - | "TCP-LISTEN:80,fork", |
107 | | - | fmt.Sprintf("TCP-CONNECT:%s:%d", contIP, 80), |
| 100 | + | fmt.Sprintf("TCP-LISTEN:%s,fork", forwardings[0].targetPort), |
| 101 | + | fmt.Sprintf("TCP-CONNECT:%s:%d", forwardings[0].targetIP, forwardings[0].targetPort), |
108 | 102 | | }, |
109 | | - | ExposedPorts: ports, |
| 103 | + | ExposedPorts: exposedPorts, |
110 | 104 | | }, |
111 | 105 | | &container.HostConfig{ |
112 | 106 | | AutoRemove: true, |
| skipped 11 lines |
124 | 118 | | return fmt.Errorf("cannot start port-forwarder container: %w", err) |
125 | 119 | | } |
126 | 120 | | |
| 121 | + | // TODO: Handle ctrl + C and other signals. |
127 | 122 | | forwarderStatusCh, forwarderErrCh := client.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) |
128 | 123 | | // targetStatusCh, targetErrCh := client.ContainerWait(ctx, target.ID, container.WaitConditionNotRunning) |
129 | 124 | | select { |
| skipped 23 lines |
153 | 148 | | return err |
154 | 149 | | } |
155 | 150 | | |
| 151 | + | type forwarding struct { |
| 152 | + | localIP string |
| 153 | + | localPort string |
| 154 | + | targetIP string |
| 155 | + | targetPort string |
| 156 | + | } |
| 157 | + | |
| 158 | + | type forwardingList []forwarding |
| 159 | + | |
| 160 | + | func (list forwardingList) toDockerPortSpecs() []string { |
| 161 | + | // ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort |
| 162 | + | var spec []string |
| 163 | + | for _, f := range list { |
| 164 | + | spec = append(spec, fmt.Sprintf("%s:%s:%s", f.localIP, f.localPort, f.targetPort)) |
| 165 | + | } |
| 166 | + | return spec |
| 167 | + | } |
| 168 | + | |
| 169 | + | func prepareForwardings( |
| 170 | + | target types.ContainerJSON, |
| 171 | + | forwardings []string, |
| 172 | + | ) (forwardingList, error) { |
| 173 | + | var list forwardingList |
| 174 | + | |
| 175 | + | targetIP := target.NetworkSettings.Networks["bridge"].IPAddress |
| 176 | + | |
| 177 | + | for _, f := range forwardings { |
| 178 | + | parts := strings.Split(f, ":") |
| 179 | + | if len(parts) == 1 { |
| 180 | + | // Case 1: TARGET_PORT |
| 181 | + | |
| 182 | + | if _, err := nat.ParsePort(parts[0]); err != nil { |
| 183 | + | // TODO: Return a user-friendly error. |
| 184 | + | return nil, err |
| 185 | + | } |
| 186 | + | |
| 187 | + | // TODO: if "target has more than 1 IP" return err |
| 188 | + | |
| 189 | + | list = append(list, forwarding{ |
| 190 | + | localIP: "127.0.0.1", |
| 191 | + | targetPort: parts[0], |
| 192 | + | targetIP: targetIP, |
| 193 | + | }) |
| 194 | + | continue |
| 195 | + | } |
| 196 | + | |
| 197 | + | if len(parts) == 2 { |
| 198 | + | if _, err := nat.ParsePort(parts[0]); err == nil { |
| 199 | + | // Case 2: LOCAL_PORT:TARGET_PORT |
| 200 | + | |
| 201 | + | // TODO: if "target has more than 1 IP" return err |
| 202 | + | |
| 203 | + | list = append(list, forwarding{ |
| 204 | + | localPort: parts[0], |
| 205 | + | localIP: "127.0.0.1", |
| 206 | + | targetPort: parts[1], |
| 207 | + | targetIP: targetIP, |
| 208 | + | }) |
| 209 | + | } else { |
| 210 | + | // Case 3: TARGET_IP:TARGET_PORT |
| 211 | + | |
| 212 | + | // TODO: if "parts[0] not in target IP list" return err |
| 213 | + | |
| 214 | + | list = append(list, forwarding{ |
| 215 | + | localIP: "127.0.0.1", |
| 216 | + | targetPort: parts[1], |
| 217 | + | targetIP: parts[0], |
| 218 | + | }) |
| 219 | + | } |
| 220 | + | continue |
| 221 | + | } |
| 222 | + | |
| 223 | + | // TODO: other cases |
| 224 | + | return nil, errors.New("implement me") |
| 225 | + | } |
| 226 | + | |
| 227 | + | return list, nil |
| 228 | + | } |
| 229 | + | |