@@ -3,6 +3,7 @@ package portforward
33import (
44 "context"
55 "fmt"
6+ "strconv"
67
78 "github.com/spf13/cobra"
89 "golang.org/x/sync/errgroup"
@@ -11,6 +12,16 @@ import (
1112 "github.com/iximiuz/labctl/internal/portforward"
1213)
1314
15+ // runRestorePortForwards restores saved port forwards and blocks until done.
16+ func runRestorePortForwards (ctx context.Context , cli labcli.CLI , opts * options ) error {
17+ resultCh , err := portforward .RestoreSavedForwards (ctx , cli .Client (), opts .playID , cli )
18+ if err != nil {
19+ return err
20+ }
21+
22+ return <- resultCh
23+ }
24+
1425type options struct {
1526 playID string
1627 machine string
@@ -21,6 +32,11 @@ type options struct {
2132 remotes []string
2233
2334 quiet bool
35+
36+ // New flags
37+ list bool
38+ restore bool
39+ remove int
2440}
2541
2642// Local port forwarding's possible modes (kinda sorta as in ssh -L):
@@ -32,18 +48,48 @@ type options struct {
3248// - LOCAL_HOST:LOCAL_PORT:REMOTE_HOST:REMOTE_PORT # the most explicit form
3349
3450func NewCommand (cli labcli.CLI ) * cobra.Command {
35- var opts options
51+ opts := options {
52+ remove : - 1 , // -1 means not set
53+ }
3654
3755 cmd := & cobra.Command {
38- Use : "port-forward <playground> [-m machine] -L [LOCAL:]REMOTE [-L ...] | -R [REMOTE:]:LOCAL [-R ...] " ,
56+ Use : "port-forward <playground> [-m machine] -L [LOCAL:]REMOTE [-L ...] | --list | --restore | --remove <index> " ,
3957 Short : `Forward one or more local or remote ports to a running playground` ,
40- Long : `While the implementation for sure differs, the behavior and semantic of the command
58+ Long : `Forward one or more local or remote ports to a running playground.
59+
60+ While the implementation differs significantly, the behavior and semantic of the command
4161are meant to be similar to SSH local (-L) and remote (-R) port forwarding. The word "local" always
42- refers to the labctl side. The word "remote" always refers to the target playground side.` ,
62+ refers to the labctl side. The word "remote" always refers to the target playground side.
63+
64+ The command also supports managing "saved" port forwards:
65+ --list List all "should be forwarded" ports for the playground
66+ --restore Forward all "should be forwarded" ports (handy after a persistent playground restart)
67+ --remove Remove a "should be forwarded" port from the playground's config by its index (0-based)
68+
69+ When using -L|-R flags, port forwards are automatically saved to the playground's config for later restoration.` ,
4370 Args : cobra .ExactArgs (1 ),
4471 RunE : func (cmd * cobra.Command , args []string ) error {
72+ opts .playID = args [0 ]
73+ cli .SetQuiet (opts .quiet )
74+
75+ // Handle list mode
76+ if opts .list {
77+ return labcli .WrapStatusError (runListPortForwards (cmd .Context (), cli , & opts ))
78+ }
79+
80+ // Handle remove mode
81+ if opts .remove >= 0 {
82+ return labcli .WrapStatusError (runRemovePortForward (cmd .Context (), cli , & opts ))
83+ }
84+
85+ // Handle restore mode
86+ if opts .restore {
87+ return labcli .WrapStatusError (runRestorePortForwards (cmd .Context (), cli , & opts ))
88+ }
89+
90+ // Regular port forwarding mode
4591 if len (opts .locals )+ len (opts .remotes ) == 0 {
46- return labcli .NewStatusError (1 , "at least one -L or -R flag must be provided" )
92+ return labcli .NewStatusError (1 , "at least one -L or -R flag must be provided (or use --list, --restore, --remove) " )
4793 }
4894 if len (opts .remotes ) > 0 {
4995 // TODO: Implement me!
@@ -58,10 +104,6 @@ refers to the labctl side. The word "remote" always refers to the target playgro
58104 opts .localsParsed = append (opts .localsParsed , parsed )
59105 }
60106
61- cli .SetQuiet (opts .quiet )
62-
63- opts .playID = args [0 ]
64-
65107 return labcli .WrapStatusError (runPortForward (cmd .Context (), cli , & opts ))
66108 },
67109 }
@@ -97,9 +139,68 @@ refers to the labctl side. The word "remote" always refers to the target playgro
97139 `Suppress verbose output` ,
98140 )
99141
142+ flags .BoolVar (& opts .list , "list" , false , `List saved port forwards ("saved" means "should be forwarded")` )
143+ flags .BoolVar (& opts .restore , "restore" , false , `Forward all "should be forwarded" ports for the playground` )
144+ flags .IntVar (& opts .remove , "remove" , - 1 , `Remove a "should be forwarded" port from the playground's config by index (0-based)` )
145+
100146 return cmd
101147}
102148
149+ func runListPortForwards (ctx context.Context , cli labcli.CLI , opts * options ) error {
150+ forwards , err := cli .Client ().ListPortForwards (ctx , opts .playID )
151+ if err != nil {
152+ return fmt .Errorf ("couldn't list port forwards: %w" , err )
153+ }
154+
155+ if len (forwards ) == 0 {
156+ cli .PrintAux ("No saved port forwards found.\n " )
157+ return nil
158+ }
159+
160+ cli .PrintAux ("Saved port forwards:\n " )
161+ for i , pf := range forwards {
162+ localPart := ""
163+ if pf .LocalHost != "" || pf .LocalPort > 0 {
164+ if pf .LocalHost != "" {
165+ localPart = pf .LocalHost
166+ }
167+ if pf .LocalPort > 0 {
168+ if localPart != "" {
169+ localPart += ":"
170+ }
171+ localPart += strconv .Itoa (pf .LocalPort )
172+ }
173+ localPart += " -> "
174+ }
175+
176+ remotePart := ""
177+ if pf .RemoteHost != "" {
178+ remotePart = pf .RemoteHost + ":"
179+ }
180+ if pf .RemotePort > 0 {
181+ remotePart += strconv .Itoa (pf .RemotePort )
182+ }
183+
184+ kindLabel := pf .Kind
185+ if kindLabel == "" {
186+ kindLabel = "local"
187+ }
188+
189+ cli .PrintAux (" [%d] %s (%s): %s%s\n " , i , pf .Machine , kindLabel , localPart , remotePart )
190+ }
191+
192+ return nil
193+ }
194+
195+ func runRemovePortForward (ctx context.Context , cli labcli.CLI , opts * options ) error {
196+ if err := cli .Client ().RemovePortForward (ctx , opts .playID , opts .remove ); err != nil {
197+ return fmt .Errorf ("couldn't remove port forward: %w" , err )
198+ }
199+
200+ cli .PrintAux ("Port forward at index %d removed.\n " , opts .remove )
201+ return nil
202+ }
203+
103204func runPortForward (ctx context.Context , cli labcli.CLI , opts * options ) error {
104205 p , err := cli .Client ().GetPlay (ctx , opts .playID )
105206 if err != nil {
@@ -114,11 +215,20 @@ func runPortForward(ctx context.Context, cli labcli.CLI, opts *options) error {
114215 }
115216 }
116217
218+ // Save port forwards to play's config
219+ for _ , spec := range opts .localsParsed {
220+ pf , err := spec .ToPortForward (opts .machine )
221+ if err != nil {
222+ return fmt .Errorf ("couldn't convert port forwarding spec to API port forward model: %w" , err )
223+ }
224+ if _ , err := cli .Client ().AddPortForward (ctx , p .ID , * pf ); err != nil {
225+ cli .PrintErr ("Warning: couldn't save port forward: %v\n " , err )
226+ }
227+ }
228+
117229 tunnel , err := portforward .StartTunnel (ctx , cli .Client (), portforward.TunnelOptions {
118- PlayID : p .ID ,
119- FactoryID : p .FactoryID (),
120- Machine : opts .machine ,
121- PlaysDir : cli .Config ().PlaysDir ,
230+ PlayID : p .ID ,
231+ Machine : opts .machine ,
122232 })
123233 if err != nil {
124234 return fmt .Errorf ("couldn't start tunnel: %w" , err )
0 commit comments