Skip to content
This repository was archived by the owner on Mar 16, 2024. It is now read-only.

Commit 1fc80f7

Browse files
Merge pull request #1574 from ibuildthecloud/main
Add acorn port-forward
2 parents 1f8c830 + f9284a2 commit 1fc80f7

File tree

22 files changed

+550
-45
lines changed

22 files changed

+550
-45
lines changed

docs/docs/100-reference/01-command-line/acorn.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ acorn [flags]
4242
* [acorn logout](acorn_logout.md) - Remove registry credentials
4343
* [acorn logs](acorn_logs.md) - Log all workloads from an app
4444
* [acorn offerings](acorn_offerings.md) - Show infrastructure offerings
45+
* [acorn port-forward](acorn_port-forward.md) - Forward a container port locally
4546
* [acorn project](acorn_project.md) - Manage projects
4647
* [acorn pull](acorn_pull.md) - Pull an image from a remote registry
4748
* [acorn push](acorn_push.md) - Push an image to a remote registry
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
title: "acorn port-forward"
3+
---
4+
## acorn port-forward
5+
6+
Forward a container port locally
7+
8+
### Synopsis
9+
10+
Forward a container port locally
11+
12+
```
13+
acorn port-forward [flags] APP_NAME|CONTAINER_NAME PORT
14+
```
15+
16+
### Options
17+
18+
```
19+
--address string The IP address to listen on (default "127.0.0.1")
20+
-c, --container string Name of container to port forward into
21+
-h, --help help for port-forward
22+
```
23+
24+
### Options inherited from parent commands
25+
26+
```
27+
-A, --all-projects Use all known projects
28+
--debug Enable debug logging
29+
--debug-level int Debug log level (valid 0-9) (default 7)
30+
--kubeconfig string Explicitly use kubeconfig file, overriding current project
31+
-j, --project string Project to work in
32+
```
33+
34+
### SEE ALSO
35+
36+
* [acorn](acorn.md) -
37+

pkg/apis/api.acorn.io/v1/conversion.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,18 @@ func convert_url_Values_To__LogOptions(in *url.Values, out *LogOptions, s conver
5858
func Convert_url_Values_To__LogOptions(in, out interface{}, s conversion.Scope) error {
5959
return convert_url_Values_To__LogOptions(in.(*url.Values), out.(*LogOptions), s)
6060
}
61+
62+
func convert_url_Values_To__ContainerReplicaPortForwardOptions(in *url.Values, out *ContainerReplicaPortForwardOptions, s conversion.Scope) error {
63+
if values, ok := map[string][]string(*in)["port"]; ok && len(values) > 0 {
64+
if err := runtime.Convert_Slice_string_To_int(&values, &out.Port, s); err != nil {
65+
return err
66+
}
67+
} else {
68+
out.Port = 0
69+
}
70+
return nil
71+
}
72+
73+
func Convert_url_Values_To__ContainerReplicaPortForwardOptions(in, out interface{}, s conversion.Scope) error {
74+
return convert_url_Values_To__ContainerReplicaPortForwardOptions(in.(*url.Values), out.(*ContainerReplicaPortForwardOptions), s)
75+
}

pkg/apis/api.acorn.io/v1/scheme.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func AddToSchemeWithGV(scheme *runtime.Scheme, schemeGroupVersion schema.GroupVe
4747
&ContainerReplica{},
4848
&ContainerReplicaList{},
4949
&ContainerReplicaExecOptions{},
50+
&ContainerReplicaPortForwardOptions{},
5051
&Secret{},
5152
&SecretList{},
5253
&Service{},
@@ -70,6 +71,9 @@ func AddToSchemeWithGV(scheme *runtime.Scheme, schemeGroupVersion schema.GroupVe
7071
// Add the watch version that applies
7172
metav1.AddToGroupVersion(scheme, schemeGroupVersion)
7273

74+
if err := scheme.AddConversionFunc((*url.Values)(nil), (*ContainerReplicaPortForwardOptions)(nil), Convert_url_Values_To__ContainerReplicaPortForwardOptions); err != nil {
75+
return err
76+
}
7377
if err := scheme.AddConversionFunc((*url.Values)(nil), (*ContainerReplicaExecOptions)(nil), Convert_url_Values_To__ContainerReplicaExecOptions); err != nil {
7478
return err
7579
}

pkg/apis/api.acorn.io/v1/types.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@ type LogOptions struct {
170170
Since string `json:"since,omitempty"`
171171
}
172172

173+
type PortForwardOptions struct {
174+
metav1.TypeMeta `json:",inline"`
175+
176+
Tail *int64 `json:"tailLines,omitempty"`
177+
Follow bool `json:"follow,omitempty"`
178+
ContainerReplica string `json:"containerReplica,omitempty"`
179+
Since string `json:"since,omitempty"`
180+
}
181+
173182
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
174183

175184
type AppPullImage struct {
@@ -291,6 +300,15 @@ type ContainerReplicaExecOptions struct {
291300
DebugImage string `json:"debugImage,omitempty"`
292301
}
293302

303+
// +k8s:conversion-gen:explicit-from=net/url.Values
304+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
305+
306+
type ContainerReplicaPortForwardOptions struct {
307+
metav1.TypeMeta `json:",inline"`
308+
309+
Port int `json:"port,omitempty"`
310+
}
311+
294312
const (
295313
SecretTypeCredential = "acorn.io/credential"
296314
SecretTypeContext = "acorn.io/context"

pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cli/acorn.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func New() *cobra.Command {
4545
NewDev(cmdContext),
4646
NewRender(cmdContext),
4747
NewExec(cmdContext),
48+
NewPortForward(cmdContext),
4849
NewFmt(cmdContext),
4950
NewImage(cmdContext),
5051
NewInstall(cmdContext),

pkg/cli/exec.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,50 +80,59 @@ func appAndArgs(ctx context.Context, c client.Client, args []string) (string, []
8080
return appName, nil, err
8181
}
8282

83-
func (s *Exec) filterContainers(containers []apiv1.ContainerReplica) (result []apiv1.ContainerReplica) {
83+
func filterContainers(containerName string, containers []apiv1.ContainerReplica) (result []apiv1.ContainerReplica) {
8484
for _, c := range containers {
85-
if s.Container == "" {
85+
if containerName == "" {
8686
result = append(result, c)
87-
} else if c.Spec.ContainerName == s.Container {
87+
} else if c.Spec.ContainerName == containerName {
8888
result = append(result, c)
8989
break
90-
} else if c.Spec.ContainerName+"."+c.Spec.SidecarName == s.Container {
90+
} else if c.Name == containerName {
9191
result = append(result, c)
9292
break
9393
}
9494
}
9595
return result
9696
}
9797

98-
func (s *Exec) execApp(ctx context.Context, c client.Client, app *apiv1.App, args []string) error {
98+
func getContainerForApp(ctx context.Context, c client.Client, app *apiv1.App, containerName string, first bool) (string, error) {
9999
containers, err := c.ContainerReplicaList(ctx, &client.ContainerReplicaListOptions{
100100
App: app.Name,
101101
})
102102
if err != nil {
103-
return err
103+
return "", err
104104
}
105105

106106
var (
107107
displayNames []string
108108
names = map[string]string{}
109109
)
110110

111-
containers = s.filterContainers(containers)
111+
containers = filterContainers(containerName, containers)
112112

113113
for _, container := range containers {
114+
if container.Status.Columns.State == "stopped" {
115+
continue
116+
}
114117
displayName := fmt.Sprintf("%s (%s %s)", container.Name, container.Status.Columns.State, table.FormatCreated(container.CreationTimestamp))
115118
displayNames = append(displayNames, displayName)
116119
names[displayName] = container.Name
117120
}
118121

122+
if first && containerName != "" && len(containers) > 0 {
123+
for _, name := range names {
124+
return name, nil
125+
}
126+
}
127+
119128
if len(containers) == 0 {
120-
return fmt.Errorf("failed to find any containers for app %s", app.Name)
129+
return "", fmt.Errorf("failed to find any containers for app %s", app.Name)
121130
}
122131

123132
var choice string
124133
switch len(displayNames) {
125134
case 0:
126-
return fmt.Errorf("failed to find any containers for app %s", app.Name)
135+
return "", fmt.Errorf("failed to find any containers for app %s", app.Name)
127136
case 1:
128137
choice = displayNames[0]
129138
default:
@@ -133,11 +142,11 @@ func (s *Exec) execApp(ctx context.Context, c client.Client, app *apiv1.App, arg
133142
Default: displayNames[0],
134143
}, &choice)
135144
if err != nil {
136-
return err
145+
return "", err
137146
}
138147
}
139148

140-
return s.execContainer(ctx, c, names[choice], args)
149+
return names[choice], nil
141150
}
142151

143152
func (s *Exec) execContainer(ctx context.Context, c client.Client, containerName string, args []string) error {
@@ -171,7 +180,10 @@ func (s *Exec) Run(cmd *cobra.Command, args []string) error {
171180

172181
app, appErr := c.AppGet(ctx, name)
173182
if appErr == nil {
174-
return s.execApp(ctx, c, app, args)
183+
name, err = getContainerForApp(ctx, c, app, s.Container, false)
184+
if err != nil {
185+
return err
186+
}
175187
}
176188
return s.execContainer(ctx, c, name, args)
177189
}

pkg/cli/port_forward.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"strconv"
8+
"strings"
9+
10+
cli "github.com/acorn-io/acorn/pkg/cli/builder"
11+
"github.com/acorn-io/acorn/pkg/client"
12+
"github.com/spf13/cobra"
13+
"inet.af/tcpproxy"
14+
)
15+
16+
func NewPortForward(c CommandContext) *cobra.Command {
17+
exec := &PortForward{client: c.ClientFactory}
18+
cmd := cli.Command(exec, cobra.Command{
19+
Use: "port-forward [flags] APP_NAME|CONTAINER_NAME PORT",
20+
SilenceUsage: true,
21+
Short: "Forward a container port locally",
22+
Long: "Forward a container port locally",
23+
ValidArgsFunction: newCompletion(c.ClientFactory, onlyAppsWithAcornContainer(exec.Container)).complete,
24+
Args: cobra.ExactArgs(2),
25+
})
26+
27+
// This will produce an error if the container flag doesn't exist or a completion function has already
28+
// been registered for this flag. Not returning the error since neither of these is likely occur.
29+
if err := cmd.RegisterFlagCompletionFunc("container", newCompletion(c.ClientFactory, acornContainerCompletion).complete); err != nil {
30+
cmd.Printf("Error registering completion function for -c flag: %v\n", err)
31+
}
32+
33+
return cmd
34+
}
35+
36+
type PortForward struct {
37+
Container string `usage:"Name of container to port forward into" short:"c"`
38+
Address string `usage:"The IP address to listen on" default:"127.0.0.1"`
39+
client ClientFactory
40+
}
41+
42+
func (s *PortForward) forwardPort(ctx context.Context, c client.Client, containerName string, portDef string) error {
43+
src, dest, ok := strings.Cut(portDef, ":")
44+
if !ok {
45+
dest = src
46+
}
47+
48+
port, err := strconv.Atoi(dest)
49+
if err != nil {
50+
return err
51+
}
52+
53+
dialer, err := c.ContainerReplicaPortForward(ctx, containerName, port)
54+
if err != nil {
55+
return err
56+
}
57+
58+
p := tcpproxy.Proxy{}
59+
p.AddRoute(s.Address+":"+src, &tcpproxy.DialProxy{
60+
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
61+
return dialer(ctx)
62+
},
63+
})
64+
p.ListenFunc = func(_, laddr string) (net.Listener, error) {
65+
l, err := net.Listen("tcp", laddr)
66+
if err != nil {
67+
return nil, err
68+
}
69+
fmt.Printf("Forwarding %s => %d\n", l.Addr().String(), port)
70+
return l, err
71+
}
72+
go func() {
73+
<-ctx.Done()
74+
_ = p.Close()
75+
}()
76+
if err := p.Start(); err != nil {
77+
return err
78+
}
79+
return p.Wait()
80+
}
81+
82+
func (s *PortForward) Run(cmd *cobra.Command, args []string) error {
83+
ctx := cmd.Context()
84+
c, err := s.client.CreateDefault()
85+
if err != nil {
86+
return err
87+
}
88+
89+
name, portDef := args[0], args[1]
90+
if err != nil {
91+
return err
92+
}
93+
94+
app, appErr := c.AppGet(ctx, name)
95+
if appErr == nil {
96+
name, err = getContainerForApp(ctx, c, app, s.Container, true)
97+
if err != nil {
98+
return err
99+
}
100+
}
101+
return s.forwardPort(ctx, c, name, portDef)
102+
}

pkg/cli/testdata/MockClient.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,10 @@ func (m *MockClient) ContainerReplicaExec(ctx context.Context, name string, args
448448
return nil, nil
449449
}
450450

451+
func (m *MockClient) ContainerReplicaPortForward(ctx context.Context, name string, port int) (client.PortForwardDialer, error) {
452+
return nil, nil
453+
}
454+
451455
func (m *MockClient) VolumeList(ctx context.Context) ([]apiv1.Volume, error) {
452456
if m.Volumes != nil {
453457
return m.Volumes, nil

0 commit comments

Comments
 (0)