Split cli package into adaptors and use-cases (#44)

This commit is contained in:
Hidetake Iwata
2019-04-05 14:52:15 +09:00
committed by GitHub
parent 3485c5408e
commit 8a02ed0fb0
20 changed files with 204 additions and 159 deletions

View File

@@ -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)

66
adaptors/cmd.go Normal file
View File

@@ -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
}

35
adaptors/cmd_test.go Normal file
View File

@@ -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)
}
})
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)
}
}

14
main.go
View File

@@ -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))
}

View File

@@ -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
}

55
usecases/login.go Normal file
View File

@@ -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
}

View File

@@ -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
}