From 8a02ed0fb0b0acff9d1008b9c7fea0ac89e33f23 Mon Sep 17 00:00:00 2001 From: Hidetake Iwata Date: Fri, 5 Apr 2019 14:52:15 +0900 Subject: [PATCH] Split cli package into adaptors and use-cases (#44) --- Makefile | 2 +- adaptors/cmd.go | 66 +++++++++++++ adaptors/cmd_test.go | 35 +++++++ .../authserver/authserver.go | 0 .../authserver/handler.go | 0 .../authserver/testdata/.gitignore | 0 .../authserver/testdata/Makefile | 0 .../authserver/testdata/oidc-discovery.json | 0 .../authserver/testdata/oidc-jwks.json | 0 .../authserver/testdata/oidc-token.json | 0 .../authserver/testdata/openssl.cnf | 0 .../e2e_test.go => adaptors_test/cmd_test.go | 43 +++++---- .../kubeconfig/kubeconfig.go | 0 .../kubeconfig/testdata/kubeconfig.yaml | 0 cli/cli.go | 93 ------------------- cli/cli_test.go | 35 ------- main.go | 14 +-- usecases/interfaces/usecases.go | 14 +++ usecases/login.go | 55 +++++++++++ {cli => usecases}/tls.go | 6 +- 20 files changed, 204 insertions(+), 159 deletions(-) create mode 100644 adaptors/cmd.go create mode 100644 adaptors/cmd_test.go rename {cli_test => adaptors_test}/authserver/authserver.go (100%) rename {cli_test => adaptors_test}/authserver/handler.go (100%) rename {cli_test => adaptors_test}/authserver/testdata/.gitignore (100%) rename {cli_test => adaptors_test}/authserver/testdata/Makefile (100%) rename {cli_test => adaptors_test}/authserver/testdata/oidc-discovery.json (100%) rename {cli_test => adaptors_test}/authserver/testdata/oidc-jwks.json (100%) rename {cli_test => adaptors_test}/authserver/testdata/oidc-token.json (100%) rename {cli_test => adaptors_test}/authserver/testdata/openssl.cnf (100%) rename cli_test/e2e_test.go => adaptors_test/cmd_test.go (82%) rename {cli_test => adaptors_test}/kubeconfig/kubeconfig.go (100%) rename {cli_test => adaptors_test}/kubeconfig/testdata/kubeconfig.yaml (100%) delete mode 100644 cli/cli.go delete mode 100644 cli/cli_test.go create mode 100644 usecases/interfaces/usecases.go create mode 100644 usecases/login.go rename {cli => usecases}/tls.go (89%) diff --git a/Makefile b/Makefile index e22a9f4..d5b08f0 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ all: $(TARGET) check: golint go vet - $(MAKE) -C cli_test/authserver/testdata + $(MAKE) -C adaptors_test/authserver/testdata go test -v ./... $(TARGET): $(wildcard *.go) diff --git a/adaptors/cmd.go b/adaptors/cmd.go new file mode 100644 index 0000000..9b09180 --- /dev/null +++ b/adaptors/cmd.go @@ -0,0 +1,66 @@ +package adaptors + +import ( + "context" + "fmt" + "log" + + "github.com/int128/kubelogin/usecases/interfaces" + "github.com/jessevdk/go-flags" + "github.com/mitchellh/go-homedir" + "github.com/pkg/errors" +) + +type Cmd struct { + Login usecases.Login +} + +func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int { + var o cmdOptions + parser := flags.NewParser(&o, flags.HelpFlag) + parser.LongDescription = fmt.Sprintf(`Version %s + This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`, + version) + args, err := parser.ParseArgs(args[1:]) + if err != nil { + log.Printf("Error: %s", err) + return 1 + } + if len(args) > 0 { + log.Printf("Error: too many arguments") + return 1 + } + kubeConfig, err := o.ExpandKubeConfig() + if err != nil { + log.Printf("Error: invalid option: %s", err) + return 1 + } + + in := usecases.LoginIn{ + KubeConfig: kubeConfig, + ListenPort: o.ListenPort, + SkipTLSVerify: o.SkipTLSVerify, + SkipOpenBrowser: o.SkipOpenBrowser, + } + if err := cmd.Login.Do(ctx, in); err != nil { + log.Printf("Error: %s", err) + return 1 + } + return 0 +} + +type cmdOptions struct { + KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"` + ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"` + SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"` + SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."` +} + +// ExpandKubeConfig returns an expanded KubeConfig path. +func (c *cmdOptions) ExpandKubeConfig() (string, error) { + d, err := homedir.Expand(c.KubeConfig) + if err != nil { + return "", errors.Wrapf(err, "could not expand %s", c.KubeConfig) + } + return d, nil +} diff --git a/adaptors/cmd_test.go b/adaptors/cmd_test.go new file mode 100644 index 0000000..c152ba4 --- /dev/null +++ b/adaptors/cmd_test.go @@ -0,0 +1,35 @@ +package adaptors + +import ( + "context" + "testing" + + "github.com/int128/kubelogin/usecases/interfaces" +) + +//TODO: Use gomock +type mockLogin struct{} + +func (*mockLogin) Do(ctx context.Context, in usecases.LoginIn) error { + return nil +} + +func TestCmd_Run(t *testing.T) { + cmd := Cmd{ + Login: &mockLogin{}, + } + + t.Run("NoArg", func(t *testing.T) { + exitCode := cmd.Run(context.TODO(), []string{"kubelogin"}, "version") + if exitCode != 0 { + t.Errorf("exitCode wants 0 but %d", exitCode) + } + }) + + t.Run("TooManyArgs", func(t *testing.T) { + exitCode := cmd.Run(context.TODO(), []string{"kubelogin", "some"}, "version") + if exitCode != 1 { + t.Errorf("exitCode wants 1 but %d", exitCode) + } + }) +} diff --git a/cli_test/authserver/authserver.go b/adaptors_test/authserver/authserver.go similarity index 100% rename from cli_test/authserver/authserver.go rename to adaptors_test/authserver/authserver.go diff --git a/cli_test/authserver/handler.go b/adaptors_test/authserver/handler.go similarity index 100% rename from cli_test/authserver/handler.go rename to adaptors_test/authserver/handler.go diff --git a/cli_test/authserver/testdata/.gitignore b/adaptors_test/authserver/testdata/.gitignore similarity index 100% rename from cli_test/authserver/testdata/.gitignore rename to adaptors_test/authserver/testdata/.gitignore diff --git a/cli_test/authserver/testdata/Makefile b/adaptors_test/authserver/testdata/Makefile similarity index 100% rename from cli_test/authserver/testdata/Makefile rename to adaptors_test/authserver/testdata/Makefile diff --git a/cli_test/authserver/testdata/oidc-discovery.json b/adaptors_test/authserver/testdata/oidc-discovery.json similarity index 100% rename from cli_test/authserver/testdata/oidc-discovery.json rename to adaptors_test/authserver/testdata/oidc-discovery.json diff --git a/cli_test/authserver/testdata/oidc-jwks.json b/adaptors_test/authserver/testdata/oidc-jwks.json similarity index 100% rename from cli_test/authserver/testdata/oidc-jwks.json rename to adaptors_test/authserver/testdata/oidc-jwks.json diff --git a/cli_test/authserver/testdata/oidc-token.json b/adaptors_test/authserver/testdata/oidc-token.json similarity index 100% rename from cli_test/authserver/testdata/oidc-token.json rename to adaptors_test/authserver/testdata/oidc-token.json diff --git a/cli_test/authserver/testdata/openssl.cnf b/adaptors_test/authserver/testdata/openssl.cnf similarity index 100% rename from cli_test/authserver/testdata/openssl.cnf rename to adaptors_test/authserver/testdata/openssl.cnf diff --git a/cli_test/e2e_test.go b/adaptors_test/cmd_test.go similarity index 82% rename from cli_test/e2e_test.go rename to adaptors_test/cmd_test.go index 8f0ee03..b1ff4b1 100644 --- a/cli_test/e2e_test.go +++ b/adaptors_test/cmd_test.go @@ -1,4 +1,4 @@ -package cli_test +package adaptors_test import ( "context" @@ -11,9 +11,10 @@ import ( "testing" "time" - "github.com/int128/kubelogin/cli" - "github.com/int128/kubelogin/cli_test/authserver" - "github.com/int128/kubelogin/cli_test/kubeconfig" + "github.com/int128/kubelogin/adaptors" + "github.com/int128/kubelogin/adaptors_test/authserver" + "github.com/int128/kubelogin/adaptors_test/kubeconfig" + "github.com/int128/kubelogin/usecases" "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) @@ -25,16 +26,17 @@ import ( // 3. Open a request for port 8000. // 4. Wait for the CLI. // 5. Shutdown the auth server. -func TestE2E(t *testing.T) { +// +func TestCmd_Run(t *testing.T) { data := map[string]struct { kubeconfigValues kubeconfig.Values - cli cli.CLI + args []string serverConfig authserver.Config clientTLS *tls.Config }{ "NoTLS": { kubeconfig.Values{Issuer: "http://localhost:9000"}, - cli.CLI{}, + []string{"kubelogin"}, authserver.Config{Issuer: "http://localhost:9000"}, &tls.Config{}, }, @@ -43,7 +45,7 @@ func TestE2E(t *testing.T) { Issuer: "http://localhost:9000", ExtraScopes: "profile groups", }, - cli.CLI{}, + []string{"kubelogin"}, authserver.Config{ Issuer: "http://localhost:9000", Scope: "profile groups openid", @@ -52,7 +54,7 @@ func TestE2E(t *testing.T) { }, "SkipTLSVerify": { kubeconfig.Values{Issuer: "https://localhost:9000"}, - cli.CLI{SkipTLSVerify: true}, + []string{"kubelogin", "--insecure-skip-tls-verify"}, authserver.Config{ Issuer: "https://localhost:9000", Cert: authserver.ServerCert, @@ -65,7 +67,7 @@ func TestE2E(t *testing.T) { Issuer: "https://localhost:9000", IDPCertificateAuthority: authserver.CACert, }, - cli.CLI{}, + []string{"kubelogin"}, authserver.Config{ Issuer: "https://localhost:9000", Cert: authserver.ServerCert, @@ -78,7 +80,7 @@ func TestE2E(t *testing.T) { Issuer: "https://localhost:9000", IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, authserver.CACert)), }, - cli.CLI{}, + []string{"kubelogin"}, authserver.Config{ Issuer: "https://localhost:9000", Cert: authserver.ServerCert, @@ -89,9 +91,9 @@ func TestE2E(t *testing.T) { "InvalidCACertShouldBeSkipped": { kubeconfig.Values{ Issuer: "http://localhost:9000", - IDPCertificateAuthority: "e2e_test.go", + IDPCertificateAuthority: "cmd_test.go", }, - cli.CLI{}, + []string{"kubelogin"}, authserver.Config{Issuer: "http://localhost:9000"}, &tls.Config{}, }, @@ -100,7 +102,7 @@ func TestE2E(t *testing.T) { Issuer: "http://localhost:9000", IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("foo")), }, - cli.CLI{}, + []string{"kubelogin"}, authserver.Config{Issuer: "http://localhost:9000"}, &tls.Config{}, }, @@ -114,13 +116,18 @@ func TestE2E(t *testing.T) { defer server.Shutdown(ctx) kcfg := kubeconfig.Create(t, &c.kubeconfigValues) defer os.Remove(kcfg) - c.cli.KubeConfig = kcfg - c.cli.SkipOpenBrowser = true - c.cli.ListenPort = 8000 + args := append(c.args, "--kubeconfig", kcfg, "--skip-open-browser") + cmd := adaptors.Cmd{ + Login: &usecases.Login{}, + } var eg errgroup.Group eg.Go(func() error { - return c.cli.Run(ctx) + exitCode := cmd.Run(ctx, args, "HEAD") + if exitCode != 0 { + return errors.Errorf("exit status %d", exitCode) + } + return nil }) if err := openBrowserRequest(c.clientTLS); err != nil { cancel() diff --git a/cli_test/kubeconfig/kubeconfig.go b/adaptors_test/kubeconfig/kubeconfig.go similarity index 100% rename from cli_test/kubeconfig/kubeconfig.go rename to adaptors_test/kubeconfig/kubeconfig.go diff --git a/cli_test/kubeconfig/testdata/kubeconfig.yaml b/adaptors_test/kubeconfig/testdata/kubeconfig.yaml similarity index 100% rename from cli_test/kubeconfig/testdata/kubeconfig.yaml rename to adaptors_test/kubeconfig/testdata/kubeconfig.yaml diff --git a/cli/cli.go b/cli/cli.go deleted file mode 100644 index 9330aa7..0000000 --- a/cli/cli.go +++ /dev/null @@ -1,93 +0,0 @@ -package cli - -import ( - "context" - "fmt" - "log" - "net/http" - - "github.com/int128/kubelogin/auth" - "github.com/int128/kubelogin/kubeconfig" - flags "github.com/jessevdk/go-flags" - homedir "github.com/mitchellh/go-homedir" - "github.com/pkg/errors" -) - -// Parse parses command line arguments and returns a CLI instance. -func Parse(osArgs []string, version string) (*CLI, error) { - var cli CLI - parser := flags.NewParser(&cli, flags.HelpFlag) - parser.LongDescription = fmt.Sprintf(`Version %s - This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`, - version) - args, err := parser.ParseArgs(osArgs[1:]) - if err != nil { - return nil, err - } - if len(args) > 0 { - return nil, errors.Errorf("too many argument") - } - return &cli, nil -} - -// CLI represents an interface of this command. -type CLI struct { - KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"` - ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"` - SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"` - SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."` -} - -// ExpandKubeConfig returns an expanded KubeConfig path. -func (c *CLI) ExpandKubeConfig() (string, error) { - d, err := homedir.Expand(c.KubeConfig) - if err != nil { - return "", errors.Wrapf(err, "could not expand %s", c.KubeConfig) - } - return d, nil -} - -// Run performs this command. -func (c *CLI) Run(ctx context.Context) error { - log.Printf("Reading %s", c.KubeConfig) - path, err := c.ExpandKubeConfig() - if err != nil { - return err - } - cfg, err := kubeconfig.Read(path) - if err != nil { - return errors.Wrapf(err, "could not read kubeconfig") - } - log.Printf("Using current-context: %s", cfg.CurrentContext) - authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg) - if err != nil { - return errors.Wrapf(err, `could not find OIDC configuration in kubeconfig, - did you setup kubectl for OIDC authentication? - kubectl config set-credentials %s \ - --auth-provider oidc \ - --auth-provider-arg idp-issuer-url=https://issuer.example.com \ - --auth-provider-arg client-id=YOUR_CLIENT_ID \ - --auth-provider-arg client-secret=YOUR_CLIENT_SECRET`, - cfg.CurrentContext) - } - tlsConfig := c.tlsConfig(authProvider) - authConfig := &auth.Config{ - Issuer: authProvider.IDPIssuerURL(), - ClientID: authProvider.ClientID(), - ClientSecret: authProvider.ClientSecret(), - ExtraScopes: authProvider.ExtraScopes(), - Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}, - LocalServerPort: c.ListenPort, - SkipOpenBrowser: c.SkipOpenBrowser, - } - token, err := authConfig.GetTokenSet(ctx) - if err != nil { - return errors.Wrapf(err, "could not get token from OIDC provider") - } - - authProvider.SetIDToken(token.IDToken) - authProvider.SetRefreshToken(token.RefreshToken) - kubeconfig.Write(cfg, path) - log.Printf("Updated %s", c.KubeConfig) - return nil -} diff --git a/cli/cli_test.go b/cli/cli_test.go deleted file mode 100644 index f03fe05..0000000 --- a/cli/cli_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package cli - -import ( - "testing" -) - -func TestParse(t *testing.T) { - c, err := Parse([]string{"kubelogin"}, "version") - if err != nil { - t.Errorf("Parse returned error: %s", err) - } - if c == nil { - t.Errorf("Parse should return CLI but nil") - } -} - -func TestParse_TooManyArgs(t *testing.T) { - c, err := Parse([]string{"kubelogin", "some"}, "version") - if err == nil { - t.Errorf("Parse should return error but nil") - } - if c != nil { - t.Errorf("Parse should return nil but %+v", c) - } -} - -func TestParse_Help(t *testing.T) { - c, err := Parse([]string{"kubelogin", "--help"}, "version") - if err == nil { - t.Errorf("Parse should return error but nil") - } - if c != nil { - t.Errorf("Parse should return nil but %+v", c) - } -} diff --git a/main.go b/main.go index 54b3b07..b5f26e9 100644 --- a/main.go +++ b/main.go @@ -2,21 +2,17 @@ package main import ( "context" - "log" "os" - "github.com/int128/kubelogin/cli" + "github.com/int128/kubelogin/adaptors" + "github.com/int128/kubelogin/usecases" ) var version = "HEAD" func main() { - c, err := cli.Parse(os.Args, version) - if err != nil { - log.Fatal(err) - } - ctx := context.Background() - if err := c.Run(ctx); err != nil { - log.Fatalf("Error: %s", err) + cmd := adaptors.Cmd{ + Login: &usecases.Login{}, } + os.Exit(cmd.Run(context.Background(), os.Args, version)) } diff --git a/usecases/interfaces/usecases.go b/usecases/interfaces/usecases.go new file mode 100644 index 0000000..bce6f27 --- /dev/null +++ b/usecases/interfaces/usecases.go @@ -0,0 +1,14 @@ +package usecases + +import "context" + +type Login interface { + Do(ctx context.Context, in LoginIn) error +} + +type LoginIn struct { + KubeConfig string + SkipTLSVerify bool + SkipOpenBrowser bool + ListenPort int +} diff --git a/usecases/login.go b/usecases/login.go new file mode 100644 index 0000000..400a890 --- /dev/null +++ b/usecases/login.go @@ -0,0 +1,55 @@ +package usecases + +import ( + "context" + "log" + "net/http" + + "github.com/int128/kubelogin/auth" + "github.com/int128/kubelogin/kubeconfig" + "github.com/int128/kubelogin/usecases/interfaces" + "github.com/pkg/errors" +) + +type Login struct{} + +func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error { + cfg, err := kubeconfig.Read(in.KubeConfig) + if err != nil { + return errors.Wrapf(err, "could not read kubeconfig") + } + log.Printf("Using current-context: %s", cfg.CurrentContext) + authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg) + if err != nil { + return errors.Wrapf(err, `could not find OIDC configuration in kubeconfig, + did you setup kubectl for OIDC authentication? + kubectl config set-credentials %s \ + --auth-provider oidc \ + --auth-provider-arg idp-issuer-url=https://issuer.example.com \ + --auth-provider-arg client-id=YOUR_CLIENT_ID \ + --auth-provider-arg client-secret=YOUR_CLIENT_SECRET`, + cfg.CurrentContext) + } + tlsConfig := tlsConfig(authProvider, in.SkipTLSVerify) + authConfig := &auth.Config{ + Issuer: authProvider.IDPIssuerURL(), + ClientID: authProvider.ClientID(), + ClientSecret: authProvider.ClientSecret(), + ExtraScopes: authProvider.ExtraScopes(), + Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}, + LocalServerPort: in.ListenPort, + SkipOpenBrowser: in.SkipOpenBrowser, + } + token, err := authConfig.GetTokenSet(ctx) + if err != nil { + return errors.Wrapf(err, "could not get token from OIDC provider") + } + + authProvider.SetIDToken(token.IDToken) + authProvider.SetRefreshToken(token.RefreshToken) + if err := kubeconfig.Write(cfg, in.KubeConfig); err != nil { + return errors.Wrapf(err, "could not update the kubeconfig") + } + log.Printf("Updated %s", in.KubeConfig) + return nil +} diff --git a/cli/tls.go b/usecases/tls.go similarity index 89% rename from cli/tls.go rename to usecases/tls.go index 819b11d..6e36e27 100644 --- a/cli/tls.go +++ b/usecases/tls.go @@ -1,4 +1,4 @@ -package cli +package usecases import ( "crypto/tls" @@ -11,7 +11,7 @@ import ( "github.com/pkg/errors" ) -func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProvider) *tls.Config { +func tlsConfig(authProvider *kubeconfig.OIDCAuthProvider, skipTLSVerify bool) *tls.Config { p := x509.NewCertPool() if ca := authProvider.IDPCertificateAuthority(); ca != "" { if err := appendCertFile(p, ca); err != nil { @@ -27,7 +27,7 @@ func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProvider) *tls.Config { log.Printf("Using CA certificate of idp-certificate-authority-data") } } - cfg := &tls.Config{InsecureSkipVerify: c.SkipTLSVerify} + cfg := &tls.Config{InsecureSkipVerify: skipTLSVerify} if len(p.Subjects()) > 0 { cfg.RootCAs = p }