mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-14 16:39:51 +00:00
Split cli package into adaptors and use-cases (#44)
This commit is contained in:
2
Makefile
2
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)
|
||||
|
||||
66
adaptors/cmd.go
Normal file
66
adaptors/cmd.go
Normal 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
35
adaptors/cmd_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
93
cli/cli.go
93
cli/cli.go
@@ -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
|
||||
}
|
||||
@@ -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
14
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))
|
||||
}
|
||||
|
||||
14
usecases/interfaces/usecases.go
Normal file
14
usecases/interfaces/usecases.go
Normal 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
55
usecases/login.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user