Skip to content

Commit

Permalink
Allocate a random port by default (#10)
Browse files Browse the repository at this point in the history
* Allocate a random port by default

* Refactor
  • Loading branch information
int128 authored Aug 26, 2019
1 parent 8a5d904 commit 720757e
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 81 deletions.
83 changes: 52 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Take a look at the concept:
+--------------------------------+
| Browser |
+--------------------------------+
↓ http://localhost:8000
↓ http://localhost:random_port
+--------------------------------+ +-----------------------------+
| kubectl auth-proxy | <-- token -- | client-go credential plugin |
+--------------------------------+ +-----------------------------+
Expand Down Expand Up @@ -50,11 +50,15 @@ You need to [configure the kubeconfig](https://docs.aws.amazon.com/eks/latest/us

To run an authentication proxy to the service:

```sh
kubectl auth-proxy -n kube-system https://kubernetes-dashboard.svc
```
% kubectl auth-proxy -n kube-system https://kubernetes-dashboard.svc
Starting an authentication proxy for pod/kubernetes-dashboard-57fc4fcb74-jjg77:8443
Open http://127.0.0.1:57867
Forwarding from 127.0.0.1:57866 -> 8443
Forwarding from [::1]:57866 -> 8443
```

Open http://localhost:8000 and you can access the Kubernetes Dashboard with the token.
Open the URL and you can access the Kubernetes Dashboard with the token.


### Kubernetes Dashboard with OpenID Connect authentication
Expand All @@ -63,11 +67,15 @@ You need to configure the kubeconfig to use [kubelogin](https://github.com/int12

Run the following command,

```sh
kubectl auth-proxy -n kube-system https://kubernetes-dashboard.svc
```
% kubectl auth-proxy -n kube-system https://kubernetes-dashboard.svc
Starting an authentication proxy for pod/kubernetes-dashboard-57fc4fcb74-jjg77:8443
Open http://127.0.0.1:57867
Forwarding from 127.0.0.1:57866 -> 8443
Forwarding from [::1]:57866 -> 8443
```

Open http://localhost:8000 and you can access the Kubernetes Dashboard with the token.
Open the URL and you can access the Kubernetes Dashboard with the token.


### Kibana with OpenID Connect authentication
Expand All @@ -76,11 +84,15 @@ You need to configure the kubeconfig to use [kubelogin](https://github.com/int12

Run the following command,

```sh
kubectl auth-proxy https://kibana
```
% kubectl auth-proxy https://kibana
Starting an authentication proxy for pod/kibana-57fc4fcb74-jjg77:8443
Open http://127.0.0.1:57867
Forwarding from 127.0.0.1:57866 -> 8443
Forwarding from [::1]:57866 -> 8443
```

Open http://localhost:8000 and you can access the Kibana with the token.
Open the URL and you can access the Kibana with the token.


## Known Issues
Expand All @@ -92,36 +104,45 @@ Open http://localhost:8000 and you can access the Kibana with the token.

```
Forward a local port to a pod or service via authentication proxy.
To forward a local port to a service, set a service name with .svc suffix. e.g. http://service-name.svc
To forward a local port to a pod, set a pod name. e.g. http://pod-name
LOCAL_ADDR defaults to localhost:8000.
Usage:
kubectl auth-proxy REMOTE_URL [LOCAL_ADDR] [flags]
kubectl auth-proxy POD_OR_SERVICE_URL [flags]
Examples:
kubectl auth-proxy https://kubernetes-dashboard.svc
Flags:
--as string Username to impersonate for the operation
--as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
--cache-dir string Default HTTP cache directory (default "~/.kube/http-cache")
--certificate-authority string Path to a cert file for the certificate authority
--client-certificate string Path to a client certificate file for TLS
--client-key string Path to a client key file for TLS
--cluster string The name of the kubeconfig cluster to use
--context string The name of the kubeconfig context to use
-h, --help help for kubectl
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--kubeconfig string Path to the kubeconfig file to use for CLI requests.
-n, --namespace string If present, the namespace scope for this CLI request
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
-s, --server string The address and port of the Kubernetes API server
--token string Bearer token for authentication to the API server
--user string The name of the kubeconfig user to use
--version version for kubectl
--address string The address on which to run the proxy. Default to a random port of localhost. (default "localhost:0")
--alsologtostderr log to standard error as well as files
--as string Username to impersonate for the operation
--as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
--cache-dir string Default HTTP cache directory (default "~/.kube/http-cache")
--certificate-authority string Path to a cert file for the certificate authority
--client-certificate string Path to a client certificate file for TLS
--client-key string Path to a client key file for TLS
--cluster string The name of the kubeconfig cluster to use
--context string The name of the kubeconfig context to use
-h, --help help for kubectl
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--kubeconfig string Path to the kubeconfig file to use for CLI requests.
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
-n, --namespace string If present, the namespace scope for this CLI request
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
-s, --server string The address and port of the Kubernetes API server
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
--token string Bearer token for authentication to the API server
--user string The name of the kubeconfig user to use
-v, --v Level number for the log level verbosity
--version version for kubectl
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```


Expand Down
43 changes: 19 additions & 24 deletions pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package cmd

import (
"context"
"net"
"net/url"

"github.com/google/wire"
"github.com/int128/kauthproxy/pkg/logger"
"github.com/int128/kauthproxy/pkg/usecases"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
Expand Down Expand Up @@ -42,29 +42,31 @@ func (cmd *Cmd) Run(ctx context.Context, osArgs []string, version string) int {
}

type rootCmdOptions struct {
*genericclioptions.ConfigFlags
k8sOptions *genericclioptions.ConfigFlags
address string
}

func (o *rootCmdOptions) addFlags(f *pflag.FlagSet) {
o.k8sOptions.AddFlags(f)
f.StringVar(&o.address, "address", "localhost:0", "The address on which to run the proxy. Default to a random port of localhost.")
}

func (cmd *Cmd) newRootCmd(ctx context.Context) *cobra.Command {
var o rootCmdOptions
o.ConfigFlags = genericclioptions.NewConfigFlags(false)
o.k8sOptions = genericclioptions.NewConfigFlags(false)
c := &cobra.Command{
Use: "kubectl auth-proxy REMOTE_URL [LOCAL_ADDR]",
Use: "kubectl auth-proxy POD_OR_SERVICE_URL",
Short: "Forward a local port to a pod or service via authentication proxy",
Long: `Forward a local port to a pod or service via authentication proxy.
To forward a local port to a service, set a service name with .svc suffix. e.g. http://service-name.svc
To forward a local port to a pod, set a pod name. e.g. http://pod-name
LOCAL_ADDR defaults to localhost:8000.
`,
To forward a local port to a pod, set a pod name. e.g. http://pod-name`,
Example: ` kubectl auth-proxy https://kubernetes-dashboard.svc`,
Args: cobra.RangeArgs(1, 2),
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
return cmd.runRootCmd(ctx, o, args)
},
}
o.ConfigFlags.AddFlags(c.Flags())
o.addFlags(c.Flags())
cmd.Logger.AddFlags(c.PersistentFlags())
return c
}
Expand All @@ -74,26 +76,19 @@ func (cmd *Cmd) runRootCmd(ctx context.Context, o rootCmdOptions, args []string)
if err != nil {
return xerrors.Errorf("invalid remote URL: %w", err)
}
localAddr := "localhost:8000"
if len(args) == 2 {
if _, _, err := net.SplitHostPort(args[1]); err != nil {
return xerrors.Errorf("invalid local address: %w", err)
}
localAddr = args[1]
}
config, err := o.ConfigFlags.ToRESTConfig()
config, err := o.k8sOptions.ToRESTConfig()
if err != nil {
return xerrors.Errorf("could not load the config: %w", err)
}
namespace, _, err := o.ConfigFlags.ToRawKubeConfigLoader().Namespace()
namespace, _, err := o.k8sOptions.ToRawKubeConfigLoader().Namespace()
if err != nil {
return xerrors.Errorf("could not determine the namespace: %w", err)
}
authProxyOptions := usecases.AuthProxyOptions{
Config: config,
Namespace: namespace,
RemoteURL: remoteURL,
LocalAddr: localAddr,
Config: config,
Namespace: namespace,
TargetURL: remoteURL,
BindAddress: o.address,
}
if err := cmd.AuthProxy.Do(ctx, authProxyOptions); err != nil {
return xerrors.Errorf("could not run an authentication proxy: %w", err)
Expand Down
7 changes: 5 additions & 2 deletions pkg/reverseproxy/mock_reverseproxy/mock_reverseproxy.go

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

16 changes: 11 additions & 5 deletions pkg/reverseproxy/reverse_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package reverseproxy
import (
"context"
"fmt"
"net"
"net/http"
"net/http/httputil"

Expand All @@ -21,7 +22,7 @@ var Set = wire.NewSet(
//go:generate mockgen -destination mock_reverseproxy/mock_reverseproxy.go github.com/int128/kauthproxy/pkg/reverseproxy Interface

type Interface interface {
Start(ctx context.Context, eg *errgroup.Group, o Options)
Start(ctx context.Context, eg *errgroup.Group, o Options) (string, error)
}

// ReverseProxy provides a reverse proxy.
Expand All @@ -48,10 +49,9 @@ type Target struct {
Port int
}

// Start starts a reverse proxy in goroutines.
func (rp *ReverseProxy) Start(ctx context.Context, eg *errgroup.Group, o Options) {
// Start starts a reverse proxy in goroutines and returns the bound address.
func (rp *ReverseProxy) Start(ctx context.Context, eg *errgroup.Group, o Options) (string, error) {
server := &http.Server{
Addr: o.Source.Address,
Handler: &httputil.ReverseProxy{
Transport: o.Transport,
Director: func(r *http.Request) {
Expand All @@ -61,9 +61,14 @@ func (rp *ReverseProxy) Start(ctx context.Context, eg *errgroup.Group, o Options
},
},
}

listener, err := net.Listen("tcp", o.Source.Address)
if err != nil {
return "", xerrors.Errorf("could not bind address %s: %w", o.Source.Address, err)
}
eg.Go(func() error {
rp.Logger.V(1).Infof("starting a reverse proxy for %s -> %s:%d", o.Source.Address, o.Target.Host, o.Target.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
return xerrors.Errorf("could not start a reverse proxy: %w", err)
}
rp.Logger.V(1).Infof("stopped the reverse proxy")
Expand All @@ -77,4 +82,5 @@ func (rp *ReverseProxy) Start(ctx context.Context, eg *errgroup.Group, o Options
}
return nil
})
return listener.Addr().String(), nil
}
27 changes: 16 additions & 11 deletions pkg/usecases/auth_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ type AuthProxy struct {

// AuthProxyOptions represents an option of AuthProxy.
type AuthProxyOptions struct {
Config *rest.Config
Namespace string
RemoteURL *url.URL
LocalAddr string
Config *rest.Config
Namespace string
TargetURL *url.URL
BindAddress string
}

// Do runs the use-case.
Expand All @@ -49,7 +49,7 @@ func (u *AuthProxy) Do(ctx context.Context, o AuthProxyOptions) error {
if err != nil {
return xerrors.Errorf("could not create a resolver: %w", err)
}
pod, containerPort, err := parseRemoteURL(rsv, o.Namespace, o.RemoteURL)
pod, containerPort, err := parseTargetURL(rsv, o.Namespace, o.TargetURL)
if err != nil {
return xerrors.Errorf("could not find the pod and container port: %w", err)
}
Expand All @@ -64,18 +64,20 @@ func (u *AuthProxy) Do(ctx context.Context, o AuthProxyOptions) error {
return xerrors.Errorf("could not create a transport for reverse proxy: %w", err)
}

u.Logger.Printf("Open http://%s", o.LocalAddr)
eg, ctx := errgroup.WithContext(ctx)
u.ReverseProxy.Start(ctx, eg,
bindAddress, err := u.ReverseProxy.Start(ctx, eg,
reverseproxy.Options{
Transport: transport,
Source: reverseproxy.Source{Address: o.LocalAddr},
Source: reverseproxy.Source{Address: o.BindAddress},
Target: reverseproxy.Target{
Scheme: o.RemoteURL.Scheme,
Scheme: o.TargetURL.Scheme,
Host: "localhost",
Port: transitPort,
},
})
if err != nil {
return xerrors.Errorf("could not start a reverse proxy: %w", err)
}
if err := u.PortForwarder.Start(ctx, eg,
portforwarder.Options{
Config: o.Config,
Expand All @@ -87,13 +89,16 @@ func (u *AuthProxy) Do(ctx context.Context, o AuthProxyOptions) error {
}); err != nil {
return xerrors.Errorf("could not start a port forwarder: %w", err)
}
u.Logger.Printf("Starting an authentication proxy for pod/%s:%d", pod.Name, containerPort)
u.Logger.Printf("Open http://%s", bindAddress)

if err := eg.Wait(); err != nil {
return xerrors.Errorf("error while port-forwarding: %w", err)
return xerrors.Errorf("error while running the authentication proxy: %w", err)
}
return nil
}

func parseRemoteURL(r resolver.Interface, namespace string, u *url.URL) (*v1.Pod, int, error) {
func parseTargetURL(r resolver.Interface, namespace string, u *url.URL) (*v1.Pod, int, error) {
h := u.Hostname()
if strings.HasSuffix(h, ".svc") {
serviceName := strings.TrimSuffix(h, ".svc")
Expand Down
16 changes: 8 additions & 8 deletions pkg/usecases/auth_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ func TestAuthProxy_Do(t *testing.T) {
Logger: mock_logger.New(t),
}
o := AuthProxyOptions{
Config: c,
Namespace: "NAMESPACE",
RemoteURL: parseURL(t, "https://podname"),
LocalAddr: "localhost:8888",
Config: c,
Namespace: "NAMESPACE",
TargetURL: parseURL(t, "https://podname"),
BindAddress: "localhost:8888",
}
if err := u.Do(context.Background(), o); err != nil {
t.Errorf("err wants nil but was %+v", err)
Expand Down Expand Up @@ -138,10 +138,10 @@ func TestAuthProxy_Do(t *testing.T) {
Logger: mock_logger.New(t),
}
o := AuthProxyOptions{
Config: c,
Namespace: "NAMESPACE",
RemoteURL: parseURL(t, "https://servicename.svc"),
LocalAddr: "localhost:9999",
Config: c,
Namespace: "NAMESPACE",
TargetURL: parseURL(t, "https://servicename.svc"),
BindAddress: "localhost:9999",
}
if err := u.Do(context.Background(), o); err != nil {
t.Errorf("err wants nil but was %+v", err)
Expand Down

0 comments on commit 720757e

Please sign in to comment.