diff --git a/docs/docs/100-reference/01-command-line/acorn.md b/docs/docs/100-reference/01-command-line/acorn.md index 50af9b7de..7d8507e07 100644 --- a/docs/docs/100-reference/01-command-line/acorn.md +++ b/docs/docs/100-reference/01-command-line/acorn.md @@ -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 diff --git a/docs/docs/100-reference/01-command-line/acorn_port-forward.md b/docs/docs/100-reference/01-command-line/acorn_port-forward.md new file mode 100644 index 000000000..9b2991518 --- /dev/null +++ b/docs/docs/100-reference/01-command-line/acorn_port-forward.md @@ -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) - + diff --git a/pkg/apis/api.acorn.io/v1/conversion.go b/pkg/apis/api.acorn.io/v1/conversion.go index 0503ff91c..6ef186484 100644 --- a/pkg/apis/api.acorn.io/v1/conversion.go +++ b/pkg/apis/api.acorn.io/v1/conversion.go @@ -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) +} diff --git a/pkg/apis/api.acorn.io/v1/scheme.go b/pkg/apis/api.acorn.io/v1/scheme.go index 574ef883d..6c84c5745 100644 --- a/pkg/apis/api.acorn.io/v1/scheme.go +++ b/pkg/apis/api.acorn.io/v1/scheme.go @@ -47,6 +47,7 @@ func AddToSchemeWithGV(scheme *runtime.Scheme, schemeGroupVersion schema.GroupVe &ContainerReplica{}, &ContainerReplicaList{}, &ContainerReplicaExecOptions{}, + &ContainerReplicaPortForwardOptions{}, &Secret{}, &SecretList{}, &Service{}, @@ -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 } diff --git a/pkg/apis/api.acorn.io/v1/types.go b/pkg/apis/api.acorn.io/v1/types.go index 18778a309..ad38a238c 100644 --- a/pkg/apis/api.acorn.io/v1/types.go +++ b/pkg/apis/api.acorn.io/v1/types.go @@ -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 { @@ -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" diff --git a/pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go b/pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go index 5b7d40b37..3c9124b91 100644 --- a/pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go +++ b/pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go @@ -576,6 +576,30 @@ func (in *ContainerReplicaList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerReplicaPortForwardOptions) DeepCopyInto(out *ContainerReplicaPortForwardOptions) { + *out = *in + out.TypeMeta = in.TypeMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerReplicaPortForwardOptions. +func (in *ContainerReplicaPortForwardOptions) DeepCopy() *ContainerReplicaPortForwardOptions { + if in == nil { + return nil + } + out := new(ContainerReplicaPortForwardOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ContainerReplicaPortForwardOptions) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ContainerReplicaSpec) DeepCopyInto(out *ContainerReplicaSpec) { *out = *in @@ -1129,6 +1153,27 @@ func (in *LogOptions) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PortForwardOptions) DeepCopyInto(out *PortForwardOptions) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Tail != nil { + in, out := &in.Tail, &out.Tail + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PortForwardOptions. +func (in *PortForwardOptions) DeepCopy() *PortForwardOptions { + if in == nil { + return nil + } + out := new(PortForwardOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Project) DeepCopyInto(out *Project) { *out = *in diff --git a/pkg/cli/acorn.go b/pkg/cli/acorn.go index 9c8314a7a..5cd094dbf 100644 --- a/pkg/cli/acorn.go +++ b/pkg/cli/acorn.go @@ -45,6 +45,7 @@ func New() *cobra.Command { NewDev(cmdContext), NewRender(cmdContext), NewExec(cmdContext), + NewPortForward(cmdContext), NewFmt(cmdContext), NewImage(cmdContext), NewInstall(cmdContext), diff --git a/pkg/cli/exec.go b/pkg/cli/exec.go index 6b532e6b1..a2ce07f0e 100644 --- a/pkg/cli/exec.go +++ b/pkg/cli/exec.go @@ -80,14 +80,14 @@ 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 } @@ -95,12 +95,12 @@ func (s *Exec) filterContainers(containers []apiv1.ContainerReplica) (result []a 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 ( @@ -108,22 +108,31 @@ func (s *Exec) execApp(ctx context.Context, c client.Client, app *apiv1.App, arg 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: @@ -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 { @@ -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) } diff --git a/pkg/cli/port_forward.go b/pkg/cli/port_forward.go new file mode 100644 index 000000000..b251d0049 --- /dev/null +++ b/pkg/cli/port_forward.go @@ -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) +} diff --git a/pkg/cli/testdata/MockClient.go b/pkg/cli/testdata/MockClient.go index 01b743e31..3fde37272 100644 --- a/pkg/cli/testdata/MockClient.go +++ b/pkg/cli/testdata/MockClient.go @@ -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 diff --git a/pkg/cli/testdata/acorn/acorn_test_info.txt b/pkg/cli/testdata/acorn/acorn_test_info.txt index 19f839655..a873e4c9a 100644 --- a/pkg/cli/testdata/acorn/acorn_test_info.txt +++ b/pkg/cli/testdata/acorn/acorn_test_info.txt @@ -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 diff --git a/pkg/client/client.go b/pkg/client/client.go index 7c90dc7ab..a27fc3727 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -2,6 +2,7 @@ package client import ( "context" + "net" "os" apiv1 "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1" @@ -183,6 +184,8 @@ type ImageDetails struct { ParseError string `json:"parseError,omitempty"` } +type PortForwardDialer func(ctx context.Context) (net.Conn, error) + type Client interface { AppList(ctx context.Context) ([]apiv1.App, error) AppDelete(ctx context.Context, name string) (*apiv1.App, error) @@ -212,6 +215,7 @@ type Client interface { ContainerReplicaGet(ctx context.Context, name string) (*apiv1.ContainerReplica, error) ContainerReplicaDelete(ctx context.Context, name string) (*apiv1.ContainerReplica, error) ContainerReplicaExec(ctx context.Context, name string, args []string, tty bool, opts *ContainerReplicaExecOptions) (*term.ExecIO, error) + ContainerReplicaPortForward(ctx context.Context, name string, port int) (PortForwardDialer, error) VolumeList(ctx context.Context) ([]apiv1.Volume, error) VolumeGet(ctx context.Context, name string) (*apiv1.Volume, error) diff --git a/pkg/client/deferred.go b/pkg/client/deferred.go index 3e8acc80f..29c108d51 100644 --- a/pkg/client/deferred.go +++ b/pkg/client/deferred.go @@ -206,6 +206,13 @@ func (d *DeferredClient) ContainerReplicaExec(ctx context.Context, name string, return d.Client.ContainerReplicaExec(ctx, name, args, tty, opts) } +func (d *DeferredClient) ContainerReplicaPortForward(ctx context.Context, containerName string, port int) (PortForwardDialer, error) { + if err := d.create(); err != nil { + return nil, err + } + return d.Client.ContainerReplicaPortForward(ctx, containerName, port) +} + func (d *DeferredClient) VolumeList(ctx context.Context) ([]apiv1.Volume, error) { if err := d.create(); err != nil { return nil, err diff --git a/pkg/client/exec.go b/pkg/client/exec.go index 0269e99f3..262892d67 100644 --- a/pkg/client/exec.go +++ b/pkg/client/exec.go @@ -7,7 +7,6 @@ import ( "github.com/acorn-io/acorn/pkg/client/term" "github.com/acorn-io/acorn/pkg/scheme" "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func (c *DefaultClient) execContainer(ctx context.Context, container *apiv1.ContainerReplica, args []string, tty bool, opts *ContainerReplicaExecOptions) (*term.ExecIO, error) { @@ -32,14 +31,6 @@ func (c *DefaultClient) execContainer(ctx context.Context, container *apiv1.Cont } func (c *DefaultClient) ContainerReplicaExec(ctx context.Context, containerName string, args []string, tty bool, opts *ContainerReplicaExecOptions) (*term.ExecIO, error) { - if containerName == "_" && opts != nil && opts.DebugImage != "" { - return c.execContainer(ctx, &apiv1.ContainerReplica{ - ObjectMeta: metav1.ObjectMeta{ - Name: "_", - Namespace: c.Namespace, - }, - }, args, tty, opts) - } con, err := c.ContainerReplicaGet(ctx, containerName) if err != nil { return nil, err diff --git a/pkg/client/ignore.go b/pkg/client/ignore.go index e744e7b28..0545cafd8 100644 --- a/pkg/client/ignore.go +++ b/pkg/client/ignore.go @@ -143,6 +143,10 @@ func (c IgnoreUninstalled) ContainerReplicaExec(ctx context.Context, name string return c.Client.ContainerReplicaExec(ctx, name, args, tty, opts) } +func (c IgnoreUninstalled) ContainerReplicaPortForward(ctx context.Context, name string, port int) (PortForwardDialer, error) { + return c.Client.ContainerReplicaPortForward(ctx, name, port) +} + func (c IgnoreUninstalled) VolumeList(ctx context.Context) ([]apiv1.Volume, error) { return ignoreUninstalled(c.Client.VolumeList(ctx)) } diff --git a/pkg/client/multi.go b/pkg/client/multi.go index 14c02fcae..70da180d2 100644 --- a/pkg/client/multi.go +++ b/pkg/client/multi.go @@ -305,6 +305,14 @@ func (m *MultiClient) ContainerReplicaExec(ctx context.Context, name string, arg return exec, err } +func (m *MultiClient) ContainerReplicaPortForward(ctx context.Context, name string, port int) (dialer PortForwardDialer, err error) { + _, err = onOne(ctx, m.Factory, name, func(name string, c Client) (*apiv1.ContainerReplica, error) { + dialer, err = c.ContainerReplicaPortForward(ctx, name, port) + return &apiv1.ContainerReplica{}, err + }) + return dialer, err +} + func (m *MultiClient) VolumeList(ctx context.Context) ([]apiv1.Volume, error) { return aggregate(ctx, m.Factory, func(c Client) ([]apiv1.Volume, error) { return c.VolumeList(ctx) diff --git a/pkg/client/port_forward.go b/pkg/client/port_forward.go new file mode 100644 index 000000000..0f86201f0 --- /dev/null +++ b/pkg/client/port_forward.go @@ -0,0 +1,40 @@ +package client + +import ( + "context" + "net" + + apiv1 "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1" + "github.com/acorn-io/acorn/pkg/scheme" + "github.com/sirupsen/logrus" +) + +func (c *DefaultClient) execPortForward(container *apiv1.ContainerReplica, port int) (PortForwardDialer, error) { + req := c.RESTClient.Get(). + Namespace(container.Namespace). + Resource("containerreplicas"). + Name(container.Name). + SubResource("portforward"). + VersionedParams(&apiv1.ContainerReplicaPortForwardOptions{ + Port: port, + }, scheme.ParameterCodec) + + url := req.URL().String() + logrus.Debugf("Exec URL: %s", url) + return func(ctx context.Context) (net.Conn, error) { + conn, err := c.Dialer.DialMultiplexed(ctx, url, nil) + if err != nil { + return nil, err + } + return conn.ForStream(0), nil + }, nil +} + +func (c *DefaultClient) ContainerReplicaPortForward(ctx context.Context, containerName string, port int) (PortForwardDialer, error) { + con, err := c.ContainerReplicaGet(ctx, containerName) + if err != nil { + return nil, err + } + + return c.execPortForward(con, port) +} diff --git a/pkg/k8schannel/dialer.go b/pkg/k8schannel/dialer.go index 66a541873..db09ba7ed 100644 --- a/pkg/k8schannel/dialer.go +++ b/pkg/k8schannel/dialer.go @@ -46,6 +46,14 @@ func (d *Dialer) DialWebsocket(ctx context.Context, url string, headers http.Hea return conn, resp, nil } +func (d *Dialer) DialMultiplexed(ctx context.Context, url string, headers http.Header) (*Connection, error) { + conn, _, err := d.DialWebsocket(ctx, url, headers) + if err != nil { + return nil, err + } + return NewConnection(conn, true), nil +} + func (d *Dialer) DialContext(ctx context.Context, url string, headers http.Header) (*Connection, error) { conn, _, err := d.DialWebsocket(ctx, url, headers) if err != nil { diff --git a/pkg/mocks/mock_client.go b/pkg/mocks/mock_client.go index ca5bf01af..20b3415c7 100644 --- a/pkg/mocks/mock_client.go +++ b/pkg/mocks/mock_client.go @@ -335,6 +335,21 @@ func (mr *MockClientMockRecorder) ContainerReplicaList(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerReplicaList", reflect.TypeOf((*MockClient)(nil).ContainerReplicaList), arg0, arg1) } +// ContainerReplicaPortForward mocks base method. +func (m *MockClient) ContainerReplicaPortForward(arg0 context.Context, arg1 string, arg2 int) (client.PortForwardDialer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ContainerReplicaPortForward", arg0, arg1, arg2) + ret0, _ := ret[0].(client.PortForwardDialer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ContainerReplicaPortForward indicates an expected call of ContainerReplicaPortForward. +func (mr *MockClientMockRecorder) ContainerReplicaPortForward(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerReplicaPortForward", reflect.TypeOf((*MockClient)(nil).ContainerReplicaPortForward), arg0, arg1, arg2) +} + // CredentialCreate mocks base method. func (m *MockClient) CredentialCreate(arg0 context.Context, arg1, arg2, arg3 string, arg4 bool) (*v1.Credential, error) { m.ctrl.T.Helper() diff --git a/pkg/openapi/generated/openapi_generated.go b/pkg/openapi/generated/openapi_generated.go index 90f144e51..ff947854f 100644 --- a/pkg/openapi/generated/openapi_generated.go +++ b/pkg/openapi/generated/openapi_generated.go @@ -40,6 +40,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.ContainerReplicaColumns": schema_pkg_apis_apiacornio_v1_ContainerReplicaColumns(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.ContainerReplicaExecOptions": schema_pkg_apis_apiacornio_v1_ContainerReplicaExecOptions(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.ContainerReplicaList": schema_pkg_apis_apiacornio_v1_ContainerReplicaList(ref), + "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.ContainerReplicaPortForwardOptions": schema_pkg_apis_apiacornio_v1_ContainerReplicaPortForwardOptions(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.ContainerReplicaSpec": schema_pkg_apis_apiacornio_v1_ContainerReplicaSpec(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.ContainerReplicaStatus": schema_pkg_apis_apiacornio_v1_ContainerReplicaStatus(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.Credential": schema_pkg_apis_apiacornio_v1_Credential(ref), @@ -58,6 +59,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.InfoSpec": schema_pkg_apis_apiacornio_v1_InfoSpec(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.LogMessage": schema_pkg_apis_apiacornio_v1_LogMessage(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.LogOptions": schema_pkg_apis_apiacornio_v1_LogOptions(ref), + "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.PortForwardOptions": schema_pkg_apis_apiacornio_v1_PortForwardOptions(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.Project": schema_pkg_apis_apiacornio_v1_Project(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.ProjectList": schema_pkg_apis_apiacornio_v1_ProjectList(ref), "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1.ProjectSpec": schema_pkg_apis_apiacornio_v1_ProjectSpec(ref), @@ -1926,6 +1928,38 @@ func schema_pkg_apis_apiacornio_v1_ContainerReplicaList(ref common.ReferenceCall } } +func schema_pkg_apis_apiacornio_v1_ContainerReplicaPortForwardOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "port": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_apiacornio_v1_ContainerReplicaSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -3028,6 +3062,56 @@ func schema_pkg_apis_apiacornio_v1_LogOptions(ref common.ReferenceCallback) comm } } +func schema_pkg_apis_apiacornio_v1_PortForwardOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "tailLines": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int64", + }, + }, + "follow": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "containerReplica": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "since": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_apiacornio_v1_Project(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/server/registry/apigroups/acorn/apigroup.go b/pkg/server/registry/apigroups/acorn/apigroup.go index abb0554c7..2c63a9d58 100644 --- a/pkg/server/registry/apigroups/acorn/apigroup.go +++ b/pkg/server/registry/apigroups/acorn/apigroup.go @@ -56,6 +56,11 @@ func Stores(c kclient.WithWatch, cfg, localCfg *clientgo.Config) (map[string]res return nil, err } + portForward, err := containers.NewPortForward(c, cfg) + if err != nil { + return nil, err + } + appsStorage := apps.NewStorage(c, clientFactory) logsStorage, err := apps.NewLogs(c, cfg) @@ -66,30 +71,31 @@ func Stores(c kclient.WithWatch, cfg, localCfg *clientgo.Config) (map[string]res volumesStorage := volumes.NewStorage(c) stores := map[string]rest.Storage{ - "acornimagebuilds": buildsStorage, - "apps": appsStorage, - "apps/log": logsStorage, - "apps/confirmupgrade": apps.NewConfirmUpgrade(c), - "apps/pullimage": apps.NewPullAppImage(c), - "builders": buildersStorage, - "builders/port": buildersPort, - "images": imagesStorage, - "images/tag": images.NewTagStorage(c), - "images/push": images.NewImagePush(c, transport), - "images/pull": images.NewImagePull(c, clientFactory, transport), - "images/details": images.NewImageDetails(c, transport), - "projects": projects.NewStorage(c), - "volumes": volumesStorage, - "volumeclasses": class.NewClassStorage(c), - "containerreplicas": containersStorage, - "containerreplicas/exec": containerExec, - "credentials": credentials.NewStore(c), - "secrets": secrets.NewStorage(c), - "secrets/reveal": secrets.NewReveal(c), - "infos": info.NewStorage(c), - "computeclasses": computeclass.NewAggregateStorage(c), - "regions": regions.NewStorage(c), - "imageallowrules": imageallowrules.NewStorage(c), + "acornimagebuilds": buildsStorage, + "apps": appsStorage, + "apps/log": logsStorage, + "apps/confirmupgrade": apps.NewConfirmUpgrade(c), + "apps/pullimage": apps.NewPullAppImage(c), + "builders": buildersStorage, + "builders/port": buildersPort, + "images": imagesStorage, + "images/tag": images.NewTagStorage(c), + "images/push": images.NewImagePush(c, transport), + "images/pull": images.NewImagePull(c, clientFactory, transport), + "images/details": images.NewImageDetails(c, transport), + "projects": projects.NewStorage(c), + "volumes": volumesStorage, + "volumeclasses": class.NewClassStorage(c), + "containerreplicas": containersStorage, + "containerreplicas/exec": containerExec, + "containerreplicas/portforward": portForward, + "credentials": credentials.NewStore(c), + "secrets": secrets.NewStorage(c), + "secrets/reveal": secrets.NewReveal(c), + "infos": info.NewStorage(c), + "computeclasses": computeclass.NewAggregateStorage(c), + "regions": regions.NewStorage(c), + "imageallowrules": imageallowrules.NewStorage(c), } return stores, nil diff --git a/pkg/server/registry/apigroups/acorn/containers/port_forward.go b/pkg/server/registry/apigroups/acorn/containers/port_forward.go new file mode 100644 index 000000000..713b8710b --- /dev/null +++ b/pkg/server/registry/apigroups/acorn/containers/port_forward.go @@ -0,0 +1,98 @@ +package containers + +import ( + "context" + "net/http" + "net/http/httputil" + "time" + + apiv1 "github.com/acorn-io/acorn/pkg/apis/api.acorn.io/v1" + "github.com/acorn-io/acorn/pkg/k8sclient" + "github.com/acorn-io/baaah/pkg/restconfig" + "github.com/acorn-io/mink/pkg/strategy" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + registryrest "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + kclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +type PortForward struct { + *strategy.DestroyAdapter + client kclient.WithWatch + t *Translator + proxy httputil.ReverseProxy + RESTClient rest.Interface +} + +func NewPortForward(client kclient.WithWatch, cfg *rest.Config) (*PortForward, error) { + cfg = rest.CopyConfig(cfg) + restconfig.SetScheme(cfg, scheme.Scheme) + + k8s, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + + transport, err := rest.TransportFor(cfg) + if err != nil { + return nil, err + } + + return &PortForward{ + t: &Translator{ + client: client, + }, + client: client, + proxy: httputil.ReverseProxy{ + FlushInterval: 200 * time.Millisecond, + Transport: transport, + Director: func(request *http.Request) {}, + }, + RESTClient: k8s.CoreV1().RESTClient(), + }, nil +} + +func (c *PortForward) New() runtime.Object { + return &apiv1.ContainerReplicaExecOptions{} +} + +func (c *PortForward) connect(podName, podNamespace string, execOpt *apiv1.ContainerReplicaPortForwardOptions) (http.Handler, error) { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + req := c.RESTClient.Get(). + Namespace(podNamespace). + Resource("pods"). + Name(podName). + SubResource("portforward"). + VersionedParams(&corev1.PodPortForwardOptions{ + Ports: []int32{int32(execOpt.Port)}, + }, scheme.ParameterCodec) + request.URL = req.URL() + c.proxy.ServeHTTP(writer, request) + }), nil +} + +func (c *PortForward) Connect(ctx context.Context, id string, options runtime.Object, r registryrest.Responder) (http.Handler, error) { + forwardOpts := options.(*apiv1.ContainerReplicaPortForwardOptions) + + container := &apiv1.ContainerReplica{} + ns, _ := request.NamespaceFrom(ctx) + + err := c.client.Get(ctx, k8sclient.ObjectKey{Namespace: ns, Name: id}, container) + if err != nil { + return nil, err + } + + return c.connect(container.Status.PodName, container.Status.PodNamespace, forwardOpts) +} + +func (c *PortForward) NewConnectOptions() (runtime.Object, bool, string) { + return &apiv1.ContainerReplicaPortForwardOptions{}, false, "" +} + +func (c *PortForward) ConnectMethods() []string { + return []string{"GET"} +}