Skip to content
This repository was archived by the owner on Mar 16, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/100-reference/01-command-line/acorn.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ acorn [flags]
* [acorn logout](acorn_logout.md) - Remove registry credentials
* [acorn logs](acorn_logs.md) - Log all workloads from an app
* [acorn offerings](acorn_offerings.md) - Show infrastructure offerings
* [acorn port-forward](acorn_port-forward.md) - Forward a container port locally
* [acorn project](acorn_project.md) - Manage projects
* [acorn pull](acorn_pull.md) - Pull an image from a remote registry
* [acorn push](acorn_push.md) - Push an image to a remote registry
Expand Down
37 changes: 37 additions & 0 deletions docs/docs/100-reference/01-command-line/acorn_port-forward.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: "acorn port-forward"
---
## acorn port-forward

Forward a container port locally

### Synopsis

Forward a container port locally

```
acorn port-forward [flags] APP_NAME|CONTAINER_NAME PORT
```

### Options

```
--address string The IP address to listen on (default "127.0.0.1")
-c, --container string Name of container to port forward into
-h, --help help for port-forward
```

### Options inherited from parent commands

```
-A, --all-projects Use all known projects
--debug Enable debug logging
--debug-level int Debug log level (valid 0-9) (default 7)
--kubeconfig string Explicitly use kubeconfig file, overriding current project
-j, --project string Project to work in
```

### SEE ALSO

* [acorn](acorn.md) -

15 changes: 15 additions & 0 deletions pkg/apis/api.acorn.io/v1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,18 @@ func convert_url_Values_To__LogOptions(in *url.Values, out *LogOptions, s conver
func Convert_url_Values_To__LogOptions(in, out interface{}, s conversion.Scope) error {
return convert_url_Values_To__LogOptions(in.(*url.Values), out.(*LogOptions), s)
}

func convert_url_Values_To__ContainerReplicaPortForwardOptions(in *url.Values, out *ContainerReplicaPortForwardOptions, s conversion.Scope) error {
if values, ok := map[string][]string(*in)["port"]; ok && len(values) > 0 {
if err := runtime.Convert_Slice_string_To_int(&values, &out.Port, s); err != nil {
return err
}
} else {
out.Port = 0
}
return nil
}

func Convert_url_Values_To__ContainerReplicaPortForwardOptions(in, out interface{}, s conversion.Scope) error {
return convert_url_Values_To__ContainerReplicaPortForwardOptions(in.(*url.Values), out.(*ContainerReplicaPortForwardOptions), s)
}
4 changes: 4 additions & 0 deletions pkg/apis/api.acorn.io/v1/scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func AddToSchemeWithGV(scheme *runtime.Scheme, schemeGroupVersion schema.GroupVe
&ContainerReplica{},
&ContainerReplicaList{},
&ContainerReplicaExecOptions{},
&ContainerReplicaPortForwardOptions{},
&Secret{},
&SecretList{},
&Service{},
Expand All @@ -70,6 +71,9 @@ func AddToSchemeWithGV(scheme *runtime.Scheme, schemeGroupVersion schema.GroupVe
// Add the watch version that applies
metav1.AddToGroupVersion(scheme, schemeGroupVersion)

if err := scheme.AddConversionFunc((*url.Values)(nil), (*ContainerReplicaPortForwardOptions)(nil), Convert_url_Values_To__ContainerReplicaPortForwardOptions); err != nil {
return err
}
if err := scheme.AddConversionFunc((*url.Values)(nil), (*ContainerReplicaExecOptions)(nil), Convert_url_Values_To__ContainerReplicaExecOptions); err != nil {
return err
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/apis/api.acorn.io/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ type LogOptions struct {
Since string `json:"since,omitempty"`
}

type PortForwardOptions struct {
metav1.TypeMeta `json:",inline"`

Tail *int64 `json:"tailLines,omitempty"`
Follow bool `json:"follow,omitempty"`
ContainerReplica string `json:"containerReplica,omitempty"`
Since string `json:"since,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type AppPullImage struct {
Expand Down Expand Up @@ -291,6 +300,15 @@ type ContainerReplicaExecOptions struct {
DebugImage string `json:"debugImage,omitempty"`
}

// +k8s:conversion-gen:explicit-from=net/url.Values
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type ContainerReplicaPortForwardOptions struct {
metav1.TypeMeta `json:",inline"`

Port int `json:"port,omitempty"`
}

const (
SecretTypeCredential = "acorn.io/credential"
SecretTypeContext = "acorn.io/context"
Expand Down
45 changes: 45 additions & 0 deletions pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkg/cli/acorn.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func New() *cobra.Command {
NewDev(cmdContext),
NewRender(cmdContext),
NewExec(cmdContext),
NewPortForward(cmdContext),
NewFmt(cmdContext),
NewImage(cmdContext),
NewInstall(cmdContext),
Expand Down
36 changes: 24 additions & 12 deletions pkg/cli/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,50 +80,59 @@ func appAndArgs(ctx context.Context, c client.Client, args []string) (string, []
return appName, nil, err
}

func (s *Exec) filterContainers(containers []apiv1.ContainerReplica) (result []apiv1.ContainerReplica) {
func filterContainers(containerName string, containers []apiv1.ContainerReplica) (result []apiv1.ContainerReplica) {
for _, c := range containers {
if s.Container == "" {
if containerName == "" {
result = append(result, c)
} else if c.Spec.ContainerName == s.Container {
} else if c.Spec.ContainerName == containerName {
result = append(result, c)
break
} else if c.Spec.ContainerName+"."+c.Spec.SidecarName == s.Container {
} else if c.Name == containerName {
result = append(result, c)
break
}
}
return result
}

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

var (
displayNames []string
names = map[string]string{}
)

containers = s.filterContainers(containers)
containers = filterContainers(containerName, containers)

for _, container := range containers {
if container.Status.Columns.State == "stopped" {
continue
}
displayName := fmt.Sprintf("%s (%s %s)", container.Name, container.Status.Columns.State, table.FormatCreated(container.CreationTimestamp))
displayNames = append(displayNames, displayName)
names[displayName] = container.Name
}

if first && containerName != "" && len(containers) > 0 {
for _, name := range names {
return name, nil
}
}

if len(containers) == 0 {
return fmt.Errorf("failed to find any containers for app %s", app.Name)
return "", fmt.Errorf("failed to find any containers for app %s", app.Name)
}

var choice string
switch len(displayNames) {
case 0:
return fmt.Errorf("failed to find any containers for app %s", app.Name)
return "", fmt.Errorf("failed to find any containers for app %s", app.Name)
case 1:
choice = displayNames[0]
default:
Expand All @@ -133,11 +142,11 @@ func (s *Exec) execApp(ctx context.Context, c client.Client, app *apiv1.App, arg
Default: displayNames[0],
}, &choice)
if err != nil {
return err
return "", err
}
}

return s.execContainer(ctx, c, names[choice], args)
return names[choice], nil
}

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

app, appErr := c.AppGet(ctx, name)
if appErr == nil {
return s.execApp(ctx, c, app, args)
name, err = getContainerForApp(ctx, c, app, s.Container, false)
if err != nil {
return err
}
}
return s.execContainer(ctx, c, name, args)
}
Expand Down
102 changes: 102 additions & 0 deletions pkg/cli/port_forward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cli

import (
"context"
"fmt"
"net"
"strconv"
"strings"

cli "github.com/acorn-io/acorn/pkg/cli/builder"
"github.com/acorn-io/acorn/pkg/client"
"github.com/spf13/cobra"
"inet.af/tcpproxy"
)

func NewPortForward(c CommandContext) *cobra.Command {
exec := &PortForward{client: c.ClientFactory}
cmd := cli.Command(exec, cobra.Command{
Use: "port-forward [flags] APP_NAME|CONTAINER_NAME PORT",
SilenceUsage: true,
Short: "Forward a container port locally",
Long: "Forward a container port locally",
ValidArgsFunction: newCompletion(c.ClientFactory, onlyAppsWithAcornContainer(exec.Container)).complete,
Args: cobra.ExactArgs(2),
})

// This will produce an error if the container flag doesn't exist or a completion function has already
// been registered for this flag. Not returning the error since neither of these is likely occur.
if err := cmd.RegisterFlagCompletionFunc("container", newCompletion(c.ClientFactory, acornContainerCompletion).complete); err != nil {
cmd.Printf("Error registering completion function for -c flag: %v\n", err)
}

return cmd
}

type PortForward struct {
Container string `usage:"Name of container to port forward into" short:"c"`
Address string `usage:"The IP address to listen on" default:"127.0.0.1"`
client ClientFactory
}

func (s *PortForward) forwardPort(ctx context.Context, c client.Client, containerName string, portDef string) error {
src, dest, ok := strings.Cut(portDef, ":")
if !ok {
dest = src
}

port, err := strconv.Atoi(dest)
if err != nil {
return err
}

dialer, err := c.ContainerReplicaPortForward(ctx, containerName, port)
if err != nil {
return err
}

p := tcpproxy.Proxy{}
p.AddRoute(s.Address+":"+src, &tcpproxy.DialProxy{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return dialer(ctx)
},
})
p.ListenFunc = func(_, laddr string) (net.Listener, error) {
l, err := net.Listen("tcp", laddr)
if err != nil {
return nil, err
}
fmt.Printf("Forwarding %s => %d\n", l.Addr().String(), port)
return l, err
}
go func() {
<-ctx.Done()
_ = p.Close()
}()
if err := p.Start(); err != nil {
return err
}
return p.Wait()
}

func (s *PortForward) Run(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
c, err := s.client.CreateDefault()
if err != nil {
return err
}

name, portDef := args[0], args[1]
if err != nil {
return err
}

app, appErr := c.AppGet(ctx, name)
if appErr == nil {
name, err = getContainerForApp(ctx, c, app, s.Container, true)
if err != nil {
return err
}
}
return s.forwardPort(ctx, c, name, portDef)
}
4 changes: 4 additions & 0 deletions pkg/cli/testdata/MockClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@ func (m *MockClient) ContainerReplicaExec(ctx context.Context, name string, args
return nil, nil
}

func (m *MockClient) ContainerReplicaPortForward(ctx context.Context, name string, port int) (client.PortForwardDialer, error) {
return nil, nil
}

func (m *MockClient) VolumeList(ctx context.Context) ([]apiv1.Volume, error) {
if m.Volumes != nil {
return m.Volumes, nil
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/testdata/acorn/acorn_test_info.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Available Commands:
logout Remove registry credentials
logs Log all workloads from an app
offerings Show infrastructure offerings
port-forward Forward a container port locally
project Manage projects
pull Pull an image from a remote registry
push Push an image to a remote registry
Expand Down
Loading