Compare commits

...

7 Commits

Author SHA1 Message Date
Hidetake Iwata
4f566a7b32 Refactor: use RunE and root error handler (#127) 2019-08-01 10:54:03 +09:00
Hidetake Iwata
5158159bdd Fix stdout of browser launcher breaks credential json (#126) 2019-08-01 10:50:36 +09:00
Hidetake Iwata
3a2aa0c6c0 Fix TLS certificate on refreshing token (#125)
* Add test of refreshing token with TLS cert

* Fix TLS certificate on refreshing token (#123)
2019-08-01 10:19:58 +09:00
Hidetake Iwata
56b17efae1 Remove LoginAndExec use-case (#120) 2019-07-30 09:57:22 +09:00
Hidetake Iwata
3e5be43d8a Change description in krew yaml 2019-07-28 16:52:26 +09:00
Hidetake Iwata
1ffa927432 Update README.md 2019-07-27 13:42:40 +09:00
Hidetake Iwata
5c6b461f37 Update README.md 2019-07-27 13:10:21 +09:00
16 changed files with 258 additions and 998 deletions

View File

@@ -24,10 +24,6 @@ As well as it respects the environment variable `KUBECONFIG`.
TODO
### Wrap kubectl and login transparently
TODO
## Architecture

View File

@@ -2,8 +2,8 @@
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens), also known as `kubectl oidc-login`.
In Kubernetes OIDC authentication, kubectl does not provide actual authentication and we need to manually set an ID token and refresh token to the kubeconfig.
Kubelogin integrates browser based authentication with kubectl.
You do not need to manually set an ID token and refresh token to the kubeconfig.
## Getting Started
@@ -19,7 +19,7 @@ brew install kubelogin
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.13.0/kubelogin_linux_amd64.zip
curl -LO https://github.com/int128/kubelogin/releases/download/v1.14.0/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
@@ -35,13 +35,10 @@ You can run kubelogin as the following methods:
- Run as a credential plugin
- Run as a standalone command
- Wrap kubectl (deprecated)
### Run as a credential plugin
Status: beta since kubelogin v1.14.0.
You can run kubelogin as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
This provides transparent login without manually running `kubelogin` command.
@@ -53,8 +50,9 @@ users:
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubelogin
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://issuer.example.com
- --oidc-client-id=YOUR_CLIENT_ID
@@ -82,17 +80,18 @@ NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
Kubelogin writes the ID token and refresh token to the cache file.
Kubelogin writes the ID token and refresh token to the token cache file.
If the cached ID token is valid, kubelogin just returns it.
If the cached ID token has expired, kubelogin will refresh the token using the refresh token.
If the refresh token has expired, kubelogin will proceed the authentication.
If the refresh token has expired, kubelogin will perform reauthentication.
You can log out by removing the token cache file (default `~/.kube/oidc-login.token-cache`).
Kubelogin will perform authentication if the token cache file does not exist.
### Run as a standalone command
Status: stable.
You can run kubelogin as a standalone command.
In this method, you need to manually run the command before running kubectl.
@@ -150,47 +149,6 @@ If the ID token has expired, kubelogin will refresh the token using the refresh
If the refresh token has expired, kubelogin will proceed the authentication.
### Wrap kubectl
Status: DEPRECATED and will be removed in kubelogin v1.15.0.
You can wrap kubectl to transparently login to the provider.
```sh
alias kubectl='kubelogin exec -- kubectl'
# or run as a kubectl plugin
alias kubectl='kubectl oidc-login exec -- kubectl'
```
If the token expired, kubelogin updates the kubeconfig and executes kubectl.
```
% kubectl get pods
Open http://localhost:8000 for authentication
You got a valid token until 2019-06-05 19:05:34 +0900 JST
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
If the ID token is valid, kubelogin just executes kubectl.
```
% kubectl get pods
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
If the ID token has expired, kubelogin will refresh the token using the refresh token in the kubeconfig.
If the refresh token has expired, kubelogin will proceed the authentication.
Kubelogin respects kubectl options passed to the extra arguments.
For example, if you run `kubectl --kubeconfig .kubeconfig`,
it will update `.kubeconfig` and execute kubectl.
If the current auth provider is not `oidc`, kubelogin just executes kubectl.
## Configuration
This document is for the development version.
@@ -265,7 +223,6 @@ Examples:
kubelogin get-token --oidc-issuer-url=https://issuer.example.com
Available Commands:
exec Login transparently and execute the kubectl command (deprecated)
get-token Run as a kubectl credential plugin
help Help about any command
version Print the version information

View File

@@ -35,104 +35,25 @@ var defaultTokenCache = homedir.HomeDir() + "/.kube/oidc-login.token-cache"
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
Login usecases.Login
GetToken usecases.GetToken
LoginAndExec usecases.LoginAndExec
Logger adaptors.Logger
Login usecases.Login
GetToken usecases.GetToken
Logger adaptors.Logger
}
// Run parses the command line arguments and executes the specified use-case.
// It returns an exit code, that is 0 on success or 1 on error.
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
var exitCode int
executable := filepath.Base(args[0])
var o struct {
kubectlOptions
kubeloginOptions
}
rootCmd := cobra.Command{
Use: executable,
Short: "Login to the OpenID Connect provider and update the kubeconfig",
Example: fmt.Sprintf(examples, executable),
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.LoginIn{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
}
if err := cmd.Login.Do(ctx, in); err != nil {
cmd.Logger.Printf("error: %s", err)
exitCode = 1
return
}
},
}
o.kubectlOptions.register(rootCmd.Flags())
o.kubeloginOptions.register(rootCmd.Flags())
//TODO: deprecated
execCmd := cobra.Command{
Use: "exec [flags] -- kubectl [args]",
Short: "Login transparently and execute the kubectl command (deprecated)",
Args: func(execCmd *cobra.Command, args []string) error {
if execCmd.ArgsLenAtDash() == -1 {
return xerrors.Errorf("double dash is missing, please run as %s exec -- kubectl", executable)
}
if len(args) < 1 {
return xerrors.New("too few arguments")
}
return nil
},
Run: func(execCmd *cobra.Command, args []string) {
// parse the extra args and override the kubectl options
f := pflag.NewFlagSet(execCmd.Name(), pflag.ContinueOnError)
o.kubectlOptions.register(f)
// ignore unknown flags and help flags (-h/--help)
f.ParseErrorsWhitelist.UnknownFlags = true
f.BoolP("help", "h", false, "ignore help flags")
if err := f.Parse(args); err != nil {
cmd.Logger.Debugf(1, "error while parsing the extra arguments: %s", err)
}
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
},
Executable: args[0],
Args: args[1:],
}
out, err := cmd.LoginAndExec.Do(ctx, in)
if err != nil {
cmd.Logger.Printf("error: %s", err)
exitCode = 1
return
}
exitCode = out.ExitCode
},
}
o.kubeloginOptions.register(execCmd.Flags())
rootCmd.AddCommand(&execCmd)
rootCmd := newRootCmd(ctx, executable, cmd)
rootCmd.Version = version
rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
getTokenCmd := newGetTokenCmd(ctx, cmd)
rootCmd.AddCommand(getTokenCmd)
versionCmd := cobra.Command{
versionCmd := &cobra.Command{
Use: "version",
Short: "Print the version information",
Args: cobra.NoArgs,
@@ -140,14 +61,15 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
cmd.Logger.Printf("%s version %s", executable, version)
},
}
rootCmd.AddCommand(&versionCmd)
rootCmd.AddCommand(versionCmd)
rootCmd.SetArgs(args[1:])
if err := rootCmd.Execute(); err != nil {
cmd.Logger.Debugf(1, "error while parsing the arguments: %s", err)
cmd.Logger.Printf("error: %s", err)
cmd.Logger.Debugf(1, "stacktrace: %+v", err)
return 1
}
return exitCode
return 0
}
// kubectlOptions represents kubectl specific options.
@@ -170,15 +92,15 @@ func (o *kubectlOptions) register(f *pflag.FlagSet) {
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
}
// kubeloginOptions represents application specific options.
type kubeloginOptions struct {
// loginOptions represents the options for Login use-case.
type loginOptions struct {
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
func (o *kubeloginOptions) register(f *pflag.FlagSet) {
func (o *loginOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
@@ -186,9 +108,43 @@ func (o *kubeloginOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
}
func newRootCmd(ctx context.Context, executable string, cmd *Cmd) *cobra.Command {
var o struct {
kubectlOptions
loginOptions
}
rootCmd := &cobra.Command{
Use: executable,
Short: "Login to the OpenID Connect provider and update the kubeconfig",
Example: fmt.Sprintf(examples, executable),
Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error {
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.LoginIn{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
}
if err := cmd.Login.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)
}
return nil
},
}
o.kubectlOptions.register(rootCmd.Flags())
o.loginOptions.register(rootCmd.Flags())
return rootCmd
}
// getTokenOptions represents the options for get-token command.
type getTokenOptions struct {
kubeloginOptions
loginOptions
IssuerURL string
ClientID string
ClientSecret string
@@ -201,7 +157,7 @@ type getTokenOptions struct {
func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
o.kubeloginOptions.register(f)
o.loginOptions.register(f)
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)")
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
@@ -250,7 +206,6 @@ func newGetTokenCmd(ctx context.Context, cmd *Cmd) *cobra.Command {
return nil
},
}
c.SilenceUsage = true
o.register(c.Flags())
return c
}

View File

@@ -96,176 +96,6 @@ func TestCmd_Run(t *testing.T) {
}
})
t.Run("loginAndExec/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: defaultListenPort,
},
Executable: "kubectl",
Args: []string{"dummy"},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable, "exec", "--", "kubectl", "dummy"}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("loginAndExec/OptionsInExtraArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig2",
KubeconfigContext: "hello2.k8s.local",
KubeconfigUser: "google2",
CACertFilename: "/path/to/cacert2",
SkipTLSVerify: true,
ListenPort: defaultListenPort,
},
Executable: "kubectl",
Args: []string{
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
"--help",
},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(2))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"exec",
"--",
"kubectl",
// kubectl options in the extra args should be mapped to the options
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
// kubelogin options in the extra args should not affect
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
"--help",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("loginAndExec/OverrideOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig2",
KubeconfigContext: "hello2.k8s.local",
KubeconfigUser: "google2",
CACertFilename: "/path/to/cacert2",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
},
Executable: "kubectl",
Args: []string{
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(2))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
// kubelogin options in the first args should be mapped to the options
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
"exec",
"--",
"kubectl",
// kubectl options in the extra args should be mapped to the options
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
// kubelogin options in the extra args should not affect
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("get-token/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

18
adaptors/env/env.go vendored
View File

@@ -1,10 +1,8 @@
package env
import (
"context"
"fmt"
"os"
"os/exec"
"syscall"
"github.com/google/wire"
@@ -36,19 +34,3 @@ func (*Env) ReadPassword(prompt string) (string, error) {
}
return string(b), nil
}
// Exec executes the command and returns the exit code.
// Unlike the exec package, this does not return an error even if the command exited with non-zero code.
func (*Env) Exec(ctx context.Context, executable string, args []string) (int, error) {
c := exec.CommandContext(ctx, executable, args...)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
if err, ok := err.(*exec.ExitError); ok {
return err.ExitCode(), nil
}
return 0, xerrors.Errorf("could not execute the command: %w", err)
}
return 0, nil
}

View File

@@ -87,7 +87,6 @@ type OIDCRefreshIn struct {
type Env interface {
ReadPassword(prompt string) (string, error)
Exec(ctx context.Context, executable string, args []string) (int, error)
}
type Logger interface {

View File

@@ -278,19 +278,6 @@ func (m *MockEnv) EXPECT() *MockEnvMockRecorder {
return m.recorder
}
// Exec mocks base method
func (m *MockEnv) Exec(arg0 context.Context, arg1 string, arg2 []string) (int, error) {
ret := m.ctrl.Call(m, "Exec", arg0, arg1, arg2)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exec indicates an expected call of Exec
func (mr *MockEnvMockRecorder) Exec(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockEnv)(nil).Exec), arg0, arg1, arg2)
}
// ReadPassword mocks base method
func (m *MockEnv) ReadPassword(arg0 string) (string, error) {
ret := m.ctrl.Call(m, "ReadPassword", arg0)

View File

@@ -6,6 +6,7 @@ import (
"encoding/binary"
"fmt"
"net/http"
"os"
"time"
"github.com/coreos/go-oidc"
@@ -14,10 +15,18 @@ import (
"github.com/int128/kubelogin/adaptors/oidc/logging"
"github.com/int128/kubelogin/adaptors/oidc/tls"
"github.com/int128/oauth2cli"
"github.com/pkg/browser"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
func init() {
// In credential plugin mode, some browser launcher writes a message to stdout
// and it may break the credential json for client-go.
// This prevents the browser launcher from breaking the credential json.
browser.Stdout = os.Stderr
}
// Set provides an implementation and interface for OIDC.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
@@ -71,11 +80,16 @@ type client struct {
logger adaptors.Logger
}
// AuthenticateByCode performs the authorization code flow.
func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
func (c *client) wrapContext(ctx context.Context) context.Context {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
return ctx
}
// AuthenticateByCode performs the authorization code flow.
func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
ctx = c.wrapContext(ctx)
nonce, err := newNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce parameter")
@@ -125,9 +139,7 @@ func newNonce() (string, error) {
// AuthenticateByPassword performs the resource owner password credentials flow.
func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
ctx = c.wrapContext(ctx)
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, in.Username, in.Password)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
@@ -156,9 +168,7 @@ func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAut
// Verify checks client ID and signature of the ID token.
// This does not check the expiration and caller should check it.
func (c *client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
ctx = c.wrapContext(ctx)
verifier := c.provider.Verifier(&oidc.Config{
ClientID: c.oauth2Config.ClientID,
SkipExpiryCheck: true,
@@ -179,6 +189,7 @@ func (c *client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*adaptor
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
ctx = c.wrapContext(ctx)
currentToken := &oauth2.Token{
Expiry: time.Now(),
RefreshToken: in.RefreshToken,

View File

@@ -51,17 +51,10 @@ func NewCmd() adaptors.Cmd {
Interaction: interaction,
Logger: adaptorsLogger,
}
exec := &login.Exec{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Env: envEnv,
Logger: adaptorsLogger,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
GetToken: getToken,
LoginAndExec: exec,
Logger: adaptorsLogger,
Login: loginLogin,
GetToken: getToken,
Logger: adaptorsLogger,
}
return cmdCmd
}
@@ -90,17 +83,10 @@ func NewCmdForHeadless(adaptorsLogger adaptors.Logger, loginShowLocalServerURL u
Interaction: credentialPluginInteraction,
Logger: adaptorsLogger,
}
exec := &login.Exec{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Env: envEnv,
Logger: adaptorsLogger,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
GetToken: getToken,
LoginAndExec: exec,
Logger: adaptorsLogger,
Login: loginLogin,
GetToken: getToken,
Logger: adaptorsLogger,
}
return cmdCmd
}

View File

@@ -36,56 +36,181 @@ var (
func TestCmd_Run_Login(t *testing.T) {
timeout := 1 * time.Second
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
type testParameter struct {
startServer func(t *testing.T, h http.Handler) (string, localserver.Shutdowner)
kubeconfigIDPCertificateAuthority string
clientTLSConfig *tls.Config
}
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
testParameters := map[string]testParameter{
"NoTLS": {
startServer: localserver.Start,
},
"CACert": {
startServer: func(t *testing.T, h http.Handler) (string, localserver.Shutdowner) {
return localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, h)
},
kubeconfigIDPCertificateAuthority: keys.TLSCACert,
clientTLSConfig: keys.TLSCACertAsConfig,
},
}
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
defer os.Remove(kubeConfigFilename)
runTest := func(t *testing.T, p testParameter) {
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, p.clientTLSConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
defer os.Remove(kubeConfigFilename)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
})
t.Run("HasValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "VALID_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "NEW_REFRESH_TOKEN",
})
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "EXPIRED_REFRESH_TOKEN",
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, p.clientTLSConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
}
for name, p := range testParameters {
t.Run(name, func(t *testing.T) {
runTest(t, p)
})
}
t.Run("env:KUBECONFIG", func(t *testing.T) {
t.Parallel()
@@ -141,151 +266,6 @@ func TestCmd_Run_Login(t *testing.T) {
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("CACert", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthority: keys.TLSCACert,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("CACertData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
t.Run("HasValidRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "VALID_REFRESH_TOKEN",
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "NEW_REFRESH_TOKEN",
})
})
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
RefreshToken: "EXPIRED_REFRESH_TOKEN",
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
})
})
}
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {

View File

@@ -3,26 +3,16 @@ kind: Plugin
metadata:
name: oidc-login
spec:
shortDescription: Login for OpenID Connect authentication
homepage: https://github.com/int128/kubelogin
shortDescription: kubectl integration for OpenID Connect authentication
description: |
This plugin gets a token from the OIDC provider and writes it to the kubeconfig.
Just run:
% kubectl oidc-login
It opens the browser and you can log in to the provider.
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
Kubelogin integrates browser based authentication with kubectl.
You do not need to manually set an ID token and refresh token to the kubeconfig.
caveats: |
You need to setup the following components:
* OIDC provider
* Kubernetes API server
* Role for your group or user
* kubectl authentication
You need to setup the OIDC provider, Kubernetes API server, role binding and kubeconfig.
See https://github.com/int128/kubelogin for more.
homepage: https://github.com/int128/kubelogin
version: {{ env "VERSION" }}
platforms:
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip

View File

@@ -7,7 +7,7 @@ import (
"github.com/int128/kubelogin/models/kubeconfig"
)
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,LoginAndExec,GetToken,Authentication
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,GetToken,Authentication
type Login interface {
Do(ctx context.Context, in LoginIn) error
@@ -51,21 +51,6 @@ type GetTokenIn struct {
TokenCacheFilename string
}
type LoginAndExec interface {
Do(ctx context.Context, in LoginAndExecIn) (*LoginAndExecOut, error)
}
// LoginAndExecInIn represents an input DTO of the LoginAndExec use-case.
type LoginAndExecIn struct {
LoginIn LoginIn
Executable string
Args []string
}
type LoginAndExecOut struct {
ExitCode int
}
type Authentication interface {
Do(ctx context.Context, in AuthenticationIn) (*AuthenticationOut, error)
}

View File

@@ -1,77 +0,0 @@
package login
import (
"context"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"golang.org/x/xerrors"
)
// Exec provide the use case of transparently executing kubectl.
//
// If the current auth provider is not oidc, just run kubectl.
// If the kubeconfig has a valid token, just run kubectl.
// Otherwise, update the kubeconfig and run kubectl.
//
type Exec struct {
Authentication usecases.Authentication
Kubeconfig adaptors.Kubeconfig
Env adaptors.Env
Logger adaptors.Logger
}
func (u *Exec) Do(ctx context.Context, in usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
if err := u.login(ctx, in.LoginIn); err != nil {
return nil, xerrors.Errorf("could not log in to the provider: %w", err)
}
u.Logger.Debugf(1, "Executing the command %s %s", in.Executable, in.Args)
exitCode, err := u.Env.Exec(ctx, in.Executable, in.Args)
if err != nil {
return nil, xerrors.Errorf("could not execute kubectl: %w", err)
}
u.Logger.Debugf(1, "The command exited with status %d", exitCode)
return &usecases.LoginAndExecOut{ExitCode: exitCode}, nil
}
func (u *Exec) login(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
authProvider, err := u.Kubeconfig.GetCurrentAuthProvider(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
u.Logger.Debugf(1, "The current authentication provider is not oidc: %s", err)
return nil
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", authProvider.UserName)
u.Logger.Debugf(1, "A token will be written to %s", authProvider.LocationOfOrigin)
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
OIDCConfig: authProvider.OIDCConfig,
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
Username: in.Username,
Password: in.Password,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims {
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
}
if out.AlreadyHasValidIDToken {
u.Logger.Printf("You already have a valid token until %s", out.IDTokenExpiry)
return nil
}
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
authProvider.OIDCConfig.IDToken = out.IDToken
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
return xerrors.Errorf("could not write the token to the kubeconfig: %w", err)
}
return nil
}

View File

@@ -1,283 +0,0 @@
package login
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/mock_usecases"
"golang.org/x/xerrors"
)
func TestExec_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{
Executable: "kubectl",
Args: []string{"foo", "bar"},
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
},
}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
})
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(123, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{
OIDCConfig: currentAuthProvider.OIDCConfig,
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(&usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Exec{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.LoginAndExecOut{
ExitCode: 123,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{
Executable: "kubectl",
Args: []string{"foo", "bar"},
}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
}
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(0, nil)
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Exec{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.LoginAndExecOut{
ExitCode: 0,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("NoOIDCConfig", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{
Executable: "kubectl",
Args: []string{"foo", "bar"},
LoginIn: usecases.LoginIn{},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, xerrors.New("no oidc config"))
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(0, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
u := Exec{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.LoginAndExecOut{
ExitCode: 0,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("AuthenticationError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(nil, xerrors.New("authentication error"))
u := Exec{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mock_adaptors.NewMockEnv(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
t.Run("WriteError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}).
Return(xerrors.New("I/O error"))
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&usecases.AuthenticationOut{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Exec{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Env: mock_adaptors.NewMockEnv(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err == nil {
t.Errorf("err wants non-nil but nil")
}
if out != nil {
t.Errorf("out wants nil but %+v", out)
}
})
}

View File

@@ -12,9 +12,7 @@ import (
// Set provides the use-cases of logging in.
var Set = wire.NewSet(
wire.Struct(new(Login), "*"),
wire.Struct(new(Exec), "*"),
wire.Bind(new(usecases.Login), new(*Login)),
wire.Bind(new(usecases.LoginAndExec), new(*Exec)),
)
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,LoginAndExec,GetToken,Authentication)
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,GetToken,Authentication)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
@@ -46,42 +46,6 @@ func (mr *MockLoginMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLogin)(nil).Do), arg0, arg1)
}
// MockLoginAndExec is a mock of LoginAndExec interface
type MockLoginAndExec struct {
ctrl *gomock.Controller
recorder *MockLoginAndExecMockRecorder
}
// MockLoginAndExecMockRecorder is the mock recorder for MockLoginAndExec
type MockLoginAndExecMockRecorder struct {
mock *MockLoginAndExec
}
// NewMockLoginAndExec creates a new mock instance
func NewMockLoginAndExec(ctrl *gomock.Controller) *MockLoginAndExec {
mock := &MockLoginAndExec{ctrl: ctrl}
mock.recorder = &MockLoginAndExecMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLoginAndExec) EXPECT() *MockLoginAndExecMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockLoginAndExec) Do(arg0 context.Context, arg1 usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(*usecases.LoginAndExecOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do
func (mr *MockLoginAndExecMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLoginAndExec)(nil).Do), arg0, arg1)
}
// MockGetToken is a mock of GetToken interface
type MockGetToken struct {
ctrl *gomock.Controller