mirror of
https://github.com/int128/kubelogin.git
synced 2026-04-22 09:16:37 +00:00
Run as a client-go credential plugin (#118)
This commit is contained in:
248
README.md
248
README.md
@@ -3,7 +3,7 @@
|
||||
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 provides browser based authentication and writes an ID token and refresh token to the kubeconfig.
|
||||
Kubelogin integrates browser based authentication with kubectl.
|
||||
|
||||
|
||||
## Getting Started
|
||||
@@ -24,17 +24,92 @@ unzip kubelogin_linux_amd64.zip
|
||||
ln -s kubelogin kubectl-oidc_login
|
||||
```
|
||||
|
||||
You need to configure the OIDC provider, Kubernetes API server, kubectl authentication and role binding.
|
||||
You need to configure the OIDC provider, Kubernetes API server, kubeconfig and role binding.
|
||||
See the following documents for more:
|
||||
|
||||
- [Getting Started with Keycloak](docs/keycloak.md)
|
||||
- [Getting Started with Google Identity Platform](docs/google.md)
|
||||
- [Team Operation](docs/team_ops.md)
|
||||
|
||||
You can run kubelogin as the following methods:
|
||||
|
||||
### Login by the command
|
||||
- Run as a credential plugin
|
||||
- Run as a standalone command
|
||||
- Wrap kubectl (deprecated)
|
||||
|
||||
Just run the command:
|
||||
|
||||
### 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.
|
||||
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: keycloak
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
args:
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://issuer.example.com
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Run kubectl.
|
||||
|
||||
```sh
|
||||
kubectl get pods
|
||||
```
|
||||
|
||||
Kubectl executes kubelogin before calling the Kubernetes APIs.
|
||||
Kubelogin automatically opens the browser and you can log in to the provider.
|
||||
|
||||
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
|
||||
|
||||
After authentication, kubelogin returns the credentials to kubectl and finally kubectl calls the Kubernetes APIs with the credential.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-18 10:28:51 +0900 JST
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: YOUR_CLIENT_ID
|
||||
client-secret: YOUR_CLIENT_SECRET
|
||||
idp-issuer-url: https://issuer.example.com
|
||||
name: oidc
|
||||
```
|
||||
|
||||
Run kubelogin:
|
||||
|
||||
```sh
|
||||
kubelogin
|
||||
@@ -47,7 +122,7 @@ It automatically opens the browser and you can log in to the provider.
|
||||
|
||||
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
|
||||
|
||||
After authentication, kubelogin writes an ID token and refresh token to the kubeconfig.
|
||||
After authentication, kubelogin writes the ID token and refresh token to the kubeconfig.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
@@ -75,7 +150,9 @@ 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 and login transparently
|
||||
### Wrap kubectl
|
||||
|
||||
Status: DEPRECATED and will be removed in kubelogin v1.15.0.
|
||||
|
||||
You can wrap kubectl to transparently login to the provider.
|
||||
|
||||
@@ -119,25 +196,77 @@ If the current auth provider is not `oidc`, kubelogin just executes kubectl.
|
||||
This document is for the development version.
|
||||
If you are looking for a specific version, see [the release tags](https://github.com/int128/kubelogin/tags).
|
||||
|
||||
|
||||
### Run as a credential plugin
|
||||
|
||||
Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
% kubelogin get-token -h
|
||||
Run as a kubectl credential plugin
|
||||
|
||||
Usage:
|
||||
kubelogin get-token [flags]
|
||||
|
||||
Flags:
|
||||
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
--oidc-issuer-url string Issuer URL of the provider (mandatory)
|
||||
--oidc-client-id string Client ID of the provider (mandatory)
|
||||
--oidc-client-secret string Client secret of the provider
|
||||
--oidc-extra-scope strings Scopes to request to the provider
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
-v, --v int If set to 1 or greater, it shows debug log
|
||||
--token-cache string Path to a file for caching the token (default "~/.kube/oidc-login.token-cache")
|
||||
-h, --help help for get-token
|
||||
```
|
||||
|
||||
#### Extra scopes
|
||||
|
||||
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
|
||||
|
||||
```yaml
|
||||
- --oidc-extra-scope=email
|
||||
- --oidc-extra-scope=profile
|
||||
```
|
||||
|
||||
#### CA Certificates
|
||||
|
||||
You can use your self-signed certificates for the provider.
|
||||
|
||||
```yaml
|
||||
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
|
||||
```
|
||||
|
||||
|
||||
### Run as a standalone command
|
||||
|
||||
Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
% kubelogin -h
|
||||
Login to the OpenID Connect provider and update the kubeconfig
|
||||
|
||||
Usage:
|
||||
kubelogin [flags]
|
||||
kubelogin [command]
|
||||
|
||||
Examples:
|
||||
# Login to the provider using authorization code grant.
|
||||
# Login to the provider using the authorization code flow.
|
||||
kubelogin
|
||||
|
||||
# Login to the provider using resource owner password credentials grant.
|
||||
# Login to the provider using the resource owner password credentials flow.
|
||||
kubelogin --username USERNAME --password PASSWORD
|
||||
|
||||
# Wrap kubectl and login transparently
|
||||
alias kubectl='kubelogin exec -- kubectl'
|
||||
# Run as a credential plugin.
|
||||
kubelogin get-token --oidc-issuer-url=https://issuer.example.com
|
||||
|
||||
Available Commands:
|
||||
exec Login transparently and execute the kubectl command
|
||||
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
|
||||
|
||||
@@ -155,22 +284,7 @@ Flags:
|
||||
-h, --help help for kubelogin
|
||||
```
|
||||
|
||||
It supports the following keys of `auth-provider` in a kubeconfig.
|
||||
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
|
||||
|
||||
Key | Direction | Value
|
||||
----|-----------|------
|
||||
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
|
||||
`client-id` | Read (Mandatory) | Client ID of the provider.
|
||||
`client-secret` | Read (Mandatory) | Client Secret of the provider.
|
||||
`idp-certificate-authority` | Read | CA certificate path of the provider.
|
||||
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
|
||||
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
|
||||
`id-token` | Write | ID token got from the provider.
|
||||
`refresh-token` | Write | Refresh token got from the provider.
|
||||
|
||||
|
||||
### Kubeconfig
|
||||
#### Kubeconfig
|
||||
|
||||
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
|
||||
It defaults to `~/.kube/config`.
|
||||
@@ -185,6 +299,50 @@ KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
|
||||
|
||||
If you set multiple files, kubelogin will find the file which has the current authentication (i.e. `user` and `auth-provider`) and write a token to it.
|
||||
|
||||
Kubelogin supports the following keys of `auth-provider` in a kubeconfig.
|
||||
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
|
||||
|
||||
Key | Direction | Value
|
||||
----|-----------|------
|
||||
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
|
||||
`client-id` | Read (Mandatory) | Client ID of the provider.
|
||||
`client-secret` | Read (Mandatory) | Client Secret of the provider.
|
||||
`idp-certificate-authority` | Read | CA certificate path of the provider.
|
||||
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
|
||||
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
|
||||
`id-token` | Write | ID token got from the provider.
|
||||
`refresh-token` | Write | Refresh token got from the provider.
|
||||
|
||||
#### Extra scopes
|
||||
|
||||
You can set the extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
|
||||
```
|
||||
|
||||
Currently kubectl does not accept multiple scopes, so you need to edit the kubeconfig as like:
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
|
||||
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
|
||||
```
|
||||
|
||||
#### CA Certificates
|
||||
|
||||
You can use your self-signed certificates for the provider.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak \
|
||||
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
|
||||
```
|
||||
|
||||
|
||||
### HTTP Proxy
|
||||
|
||||
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
|
||||
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
|
||||
|
||||
|
||||
### Authentication flows
|
||||
|
||||
@@ -201,7 +359,11 @@ You need to register the following redirect URIs to the provider:
|
||||
You can change the ports by the option:
|
||||
|
||||
```sh
|
||||
# run as a standalone command
|
||||
kubelogin --listen-port 12345 --listen-port 23456
|
||||
|
||||
# run as a credential plugin
|
||||
kubelogin get-token --listen-port 12345 --listen-port 23456
|
||||
```
|
||||
|
||||
|
||||
@@ -225,38 +387,6 @@ Password:
|
||||
```
|
||||
|
||||
|
||||
### Extra scopes
|
||||
|
||||
You can set the extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
|
||||
```
|
||||
|
||||
Currently kubectl does not accept multiple scopes, so you need to edit the kubeconfig as like:
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
|
||||
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
|
||||
```
|
||||
|
||||
|
||||
### CA Certificates
|
||||
|
||||
You can use your self-signed certificates for the provider.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak \
|
||||
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
|
||||
```
|
||||
|
||||
|
||||
### HTTP Proxy
|
||||
|
||||
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
|
||||
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
|
||||
|
||||
|
||||
## Contributions
|
||||
|
||||
This is an open source software licensed under Apache License 2.0.
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Cmd.
|
||||
@@ -20,20 +21,22 @@ var Set = wire.NewSet(
|
||||
wire.Bind(new(adaptors.Cmd), new(*Cmd)),
|
||||
)
|
||||
|
||||
const examples = ` # Login to the provider using authorization code grant.
|
||||
const examples = ` # Login to the provider using the authorization code flow.
|
||||
%[1]s
|
||||
|
||||
# Login to the provider using resource owner password credentials grant.
|
||||
# Login to the provider using the resource owner password credentials flow.
|
||||
%[1]s --username USERNAME --password PASSWORD
|
||||
|
||||
# Wrap kubectl and login transparently
|
||||
alias kubectl='%[1]s exec -- kubectl'`
|
||||
# Run as a credential plugin.
|
||||
%[1]s get-token --oidc-issuer-url=https://issuer.example.com`
|
||||
|
||||
var defaultListenPort = []int{8000, 18000}
|
||||
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
|
||||
}
|
||||
@@ -75,9 +78,10 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
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",
|
||||
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)
|
||||
@@ -125,6 +129,9 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
o.kubeloginOptions.register(execCmd.Flags())
|
||||
rootCmd.AddCommand(&execCmd)
|
||||
|
||||
getTokenCmd := newGetTokenCmd(ctx, cmd)
|
||||
rootCmd.AddCommand(getTokenCmd)
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
@@ -178,3 +185,72 @@ func (o *kubeloginOptions) register(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
|
||||
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
|
||||
}
|
||||
|
||||
// getTokenOptions represents the options for get-token command.
|
||||
type getTokenOptions struct {
|
||||
kubeloginOptions
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
CertificateAuthority string
|
||||
SkipTLSVerify bool
|
||||
Verbose int
|
||||
TokenCacheFilename string
|
||||
}
|
||||
|
||||
func (o *getTokenOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
o.kubeloginOptions.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")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
|
||||
f.StringVar(&o.TokenCacheFilename, "token-cache", defaultTokenCache, "Path to a file for caching the token")
|
||||
}
|
||||
|
||||
func newGetTokenCmd(ctx context.Context, cmd *Cmd) *cobra.Command {
|
||||
var o getTokenOptions
|
||||
c := &cobra.Command{
|
||||
Use: "get-token [flags]",
|
||||
Short: "Run as a kubectl credential plugin",
|
||||
Args: func(c *cobra.Command, args []string) error {
|
||||
if err := cobra.NoArgs(c, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.IssuerURL == "" {
|
||||
return xerrors.New("--oidc-issuer-url is missing")
|
||||
}
|
||||
if o.ClientID == "" {
|
||||
return xerrors.New("--oidc-client-id is missing")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
ListenPort: o.ListenPort,
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
TokenCacheFilename: o.TokenCacheFilename,
|
||||
}
|
||||
if err := cmd.GetToken.Do(ctx, in); err != nil {
|
||||
return xerrors.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c.SilenceUsage = true
|
||||
o.register(c.Flags())
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -265,4 +265,112 @@ func TestCmd_Run(t *testing.T) {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
getToken := mock_usecases.NewMockGetToken(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, usecases.GetTokenIn{
|
||||
ListenPort: defaultListenPort,
|
||||
TokenCacheFilename: defaultTokenCache,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
|
||||
|
||||
cmd := Cmd{
|
||||
GetToken: getToken,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/FullOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
getToken := mock_usecases.NewMockGetToken(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, usecases.GetTokenIn{
|
||||
TokenCacheFilename: defaultTokenCache,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
CACertFilename: "/path/to/cacert",
|
||||
SkipTLSVerify: true,
|
||||
ListenPort: []int{10080, 20080},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
|
||||
|
||||
cmd := Cmd{
|
||||
GetToken: getToken,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/MissingMandatoryOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
cmd := Cmd{
|
||||
GetToken: mock_usecases.NewMockGetToken(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/TooManyArgs", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
cmd := Cmd{
|
||||
GetToken: mock_usecases.NewMockGetToken(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
40
adaptors/credentialplugin/credential_plugin.go
Normal file
40
adaptors/credentialplugin/credential_plugin.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Package credentialplugin provides interaction with kubectl for a credential plugin.
|
||||
package credentialplugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
)
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Interaction), "*"),
|
||||
wire.Bind(new(adaptors.CredentialPluginInteraction), new(*Interaction)),
|
||||
)
|
||||
|
||||
type Interaction struct{}
|
||||
|
||||
// Write writes the ExecCredential to standard output for kubectl.
|
||||
func (*Interaction) Write(out credentialplugin.Output) error {
|
||||
ec := &v1beta1.ExecCredential{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Kind: "ExecCredential",
|
||||
},
|
||||
Status: &v1beta1.ExecCredentialStatus{
|
||||
Token: out.Token,
|
||||
ExpirationTimestamp: &v1.Time{Time: out.Expiry},
|
||||
},
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
if err := e.Encode(ec); err != nil {
|
||||
return xerrors.Errorf("could not write the ExecCredential: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors Kubeconfig,OIDC,OIDCClient,Env,Logger
|
||||
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,Env,Logger
|
||||
|
||||
type Cmd interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
@@ -18,6 +19,15 @@ type Kubeconfig interface {
|
||||
UpdateAuthProvider(auth *kubeconfig.AuthProvider) error
|
||||
}
|
||||
|
||||
type TokenCacheRepository interface {
|
||||
Read(filename string) (*credentialplugin.TokenCache, error)
|
||||
Write(filename string, tc credentialplugin.TokenCache) error
|
||||
}
|
||||
|
||||
type CredentialPluginInteraction interface {
|
||||
Write(out credentialplugin.Output) error
|
||||
}
|
||||
|
||||
type OIDC interface {
|
||||
New(ctx context.Context, config OIDCClientConfig) (OIDCClient, error)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/adaptors (interfaces: Kubeconfig,OIDC,OIDCClient,Env,Logger)
|
||||
// Source: github.com/int128/kubelogin/adaptors (interfaces: Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,Env,Logger)
|
||||
|
||||
// Package mock_adaptors is a generated GoMock package.
|
||||
package mock_adaptors
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
adaptors "github.com/int128/kubelogin/adaptors"
|
||||
credentialplugin "github.com/int128/kubelogin/models/credentialplugin"
|
||||
kubeconfig "github.com/int128/kubelogin/models/kubeconfig"
|
||||
reflect "reflect"
|
||||
)
|
||||
@@ -60,6 +61,89 @@ func (mr *MockKubeconfigMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomo
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockKubeconfig)(nil).UpdateAuthProvider), arg0)
|
||||
}
|
||||
|
||||
// MockTokenCacheRepository is a mock of TokenCacheRepository interface
|
||||
type MockTokenCacheRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTokenCacheRepositoryMockRecorder
|
||||
}
|
||||
|
||||
// MockTokenCacheRepositoryMockRecorder is the mock recorder for MockTokenCacheRepository
|
||||
type MockTokenCacheRepositoryMockRecorder struct {
|
||||
mock *MockTokenCacheRepository
|
||||
}
|
||||
|
||||
// NewMockTokenCacheRepository creates a new mock instance
|
||||
func NewMockTokenCacheRepository(ctrl *gomock.Controller) *MockTokenCacheRepository {
|
||||
mock := &MockTokenCacheRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockTokenCacheRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockTokenCacheRepository) EXPECT() *MockTokenCacheRepositoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Read mocks base method
|
||||
func (m *MockTokenCacheRepository) Read(arg0 string) (*credentialplugin.TokenCache, error) {
|
||||
ret := m.ctrl.Call(m, "Read", arg0)
|
||||
ret0, _ := ret[0].(*credentialplugin.TokenCache)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Read indicates an expected call of Read
|
||||
func (mr *MockTokenCacheRepositoryMockRecorder) Read(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTokenCacheRepository)(nil).Read), arg0)
|
||||
}
|
||||
|
||||
// Write mocks base method
|
||||
func (m *MockTokenCacheRepository) Write(arg0 string, arg1 credentialplugin.TokenCache) error {
|
||||
ret := m.ctrl.Call(m, "Write", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Write indicates an expected call of Write
|
||||
func (mr *MockTokenCacheRepositoryMockRecorder) Write(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockTokenCacheRepository)(nil).Write), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockCredentialPluginInteraction is a mock of CredentialPluginInteraction interface
|
||||
type MockCredentialPluginInteraction struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockCredentialPluginInteractionMockRecorder
|
||||
}
|
||||
|
||||
// MockCredentialPluginInteractionMockRecorder is the mock recorder for MockCredentialPluginInteraction
|
||||
type MockCredentialPluginInteractionMockRecorder struct {
|
||||
mock *MockCredentialPluginInteraction
|
||||
}
|
||||
|
||||
// NewMockCredentialPluginInteraction creates a new mock instance
|
||||
func NewMockCredentialPluginInteraction(ctrl *gomock.Controller) *MockCredentialPluginInteraction {
|
||||
mock := &MockCredentialPluginInteraction{ctrl: ctrl}
|
||||
mock.recorder = &MockCredentialPluginInteractionMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockCredentialPluginInteraction) EXPECT() *MockCredentialPluginInteractionMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Write mocks base method
|
||||
func (m *MockCredentialPluginInteraction) Write(arg0 credentialplugin.Output) error {
|
||||
ret := m.ctrl.Call(m, "Write", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Write indicates an expected call of Write
|
||||
func (mr *MockCredentialPluginInteractionMockRecorder) Write(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockCredentialPluginInteraction)(nil).Write), arg0)
|
||||
}
|
||||
|
||||
// MockOIDC is a mock of OIDC interface
|
||||
type MockOIDC struct {
|
||||
ctrl *gomock.Controller
|
||||
|
||||
46
adaptors/tokencache/tokencache.go
Normal file
46
adaptors/tokencache/tokencache.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package tokencache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Repository), "*"),
|
||||
wire.Bind(new(adaptors.TokenCacheRepository), new(*Repository)),
|
||||
)
|
||||
|
||||
type Repository struct{}
|
||||
|
||||
func (*Repository) Read(filename string) (*credentialplugin.TokenCache, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not open file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
d := json.NewDecoder(f)
|
||||
var c credentialplugin.TokenCache
|
||||
if err := d.Decode(&c); err != nil {
|
||||
return nil, xerrors.Errorf("could not decode json file %s: %w", filename, err)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (*Repository) Write(filename string, tc credentialplugin.TokenCache) error {
|
||||
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not create file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
e := json.NewEncoder(f)
|
||||
if err := e.Encode(&tc); err != nil {
|
||||
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
73
adaptors/tokencache/tokencache_test.go
Normal file
73
adaptors/tokencache/tokencache_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package tokencache
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
)
|
||||
|
||||
func TestRepository_Read(t *testing.T) {
|
||||
var r Repository
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "kube")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a temp dir: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Errorf("could not clean up the temp dir: %s", err)
|
||||
}
|
||||
}()
|
||||
json := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}`
|
||||
filename := filepath.Join(dir, "token-cache")
|
||||
if err := ioutil.WriteFile(filename, []byte(json), 0600); err != nil {
|
||||
t.Fatalf("could not write to the temp file: %s", err)
|
||||
}
|
||||
|
||||
tokenCache, err := r.Read(filename)
|
||||
if err != nil {
|
||||
t.Errorf("err wants nil but %+v", err)
|
||||
}
|
||||
want := &credentialplugin.TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
|
||||
if diff := deep.Equal(tokenCache, want); diff != nil {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_Write(t *testing.T) {
|
||||
var r Repository
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "kube")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a temp dir: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Errorf("could not clean up the temp dir: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
filename := filepath.Join(dir, "token-cache")
|
||||
tokenCache := credentialplugin.TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
|
||||
if err := r.Write(filename, tokenCache); err != nil {
|
||||
t.Errorf("err wants nil but %+v", err)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the token cache file: %s", err)
|
||||
}
|
||||
want := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}
|
||||
`
|
||||
if diff := deep.Equal(string(b), want); diff != nil {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
17
di/di.go
17
di/di.go
@@ -7,12 +7,15 @@ import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/cmd"
|
||||
credentialPluginAdaptor "github.com/int128/kubelogin/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/adaptors/env"
|
||||
"github.com/int128/kubelogin/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors/logger"
|
||||
"github.com/int128/kubelogin/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/auth"
|
||||
credentialPluginUseCase "github.com/int128/kubelogin/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases/login"
|
||||
)
|
||||
|
||||
@@ -22,24 +25,32 @@ func NewCmd() adaptors.Cmd {
|
||||
auth.Set,
|
||||
auth.ExtraSet,
|
||||
login.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
tokencache.Set,
|
||||
credentialPluginAdaptor.Set,
|
||||
oidc.Set,
|
||||
logger.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCmdWith returns an instance of adaptors.Cmd with given dependencies.
|
||||
// This is only for e2e tests.
|
||||
func NewCmdWith(adaptors.Logger, usecases.LoginShowLocalServerURL) adaptors.Cmd {
|
||||
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
|
||||
func NewCmdForHeadless(
|
||||
adaptors.Logger,
|
||||
usecases.LoginShowLocalServerURL,
|
||||
adaptors.CredentialPluginInteraction,
|
||||
) adaptors.Cmd {
|
||||
wire.Build(
|
||||
auth.Set,
|
||||
login.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
tokencache.Set,
|
||||
oidc.Set,
|
||||
)
|
||||
return nil
|
||||
|
||||
@@ -8,12 +8,15 @@ package di
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/cmd"
|
||||
"github.com/int128/kubelogin/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/adaptors/env"
|
||||
"github.com/int128/kubelogin/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors/logger"
|
||||
"github.com/int128/kubelogin/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/auth"
|
||||
credentialplugin2 "github.com/int128/kubelogin/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases/login"
|
||||
)
|
||||
|
||||
@@ -40,6 +43,14 @@ func NewCmd() adaptors.Cmd {
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
repository := &tokencache.Repository{}
|
||||
interaction := &credentialplugin.Interaction{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
Authentication: authentication,
|
||||
TokenCacheRepository: repository,
|
||||
Interaction: interaction,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
exec := &login.Exec{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
@@ -48,13 +59,14 @@ func NewCmd() adaptors.Cmd {
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
LoginAndExec: exec,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
|
||||
func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL) adaptors.Cmd {
|
||||
func NewCmdForHeadless(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL, credentialPluginInteraction adaptors.CredentialPluginInteraction) adaptors.Cmd {
|
||||
factory := &oidc.Factory{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
@@ -71,6 +83,13 @@ func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
repository := &tokencache.Repository{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
Authentication: authentication,
|
||||
TokenCacheRepository: repository,
|
||||
Interaction: credentialPluginInteraction,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
exec := &login.Exec{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
@@ -79,6 +98,7 @@ func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
LoginAndExec: exec,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ Open [Google APIs Console](https://console.developers.google.com/apis/credential
|
||||
|
||||
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
|
||||
```
|
||||
--oidc-issuer-url=https://accounts.google.com
|
||||
--oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
```
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
@@ -46,49 +51,33 @@ subjects:
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
|
||||
## 4. Setup kubectl
|
||||
## 4. Setup kubeconfig
|
||||
|
||||
Configure `kubectl` for the OIDC authentication.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials KUBECONTEXT \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
|
||||
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## 5. Run kubelogin
|
||||
|
||||
Run `kubelogin`.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
- name: google
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
idp-issuer-url: https://accounts.google.com
|
||||
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
client-secret: YOUR_SECRET
|
||||
id-token: ey... # kubelogin will update ID token here
|
||||
refresh-token: ey... # kubelogin will update refresh token here
|
||||
name: oidc
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
args:
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://accounts.google.com
|
||||
- --oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## 5. Run kubectl
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
|
||||
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
|
||||
|
||||
@@ -32,6 +32,12 @@ For example, if you have the `admin` role of the client, you will get a JWT with
|
||||
|
||||
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
|
||||
```
|
||||
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
--oidc-client-id=kubernetes
|
||||
--oidc-groups-claim=groups
|
||||
```
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
|
||||
|
||||
```yaml
|
||||
@@ -62,49 +68,33 @@ subjects:
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
|
||||
## 4. Setup kubectl
|
||||
## 4. Setup kubeconfig
|
||||
|
||||
Configure `kubectl` for the OIDC authentication.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials KUBECONTEXT \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
|
||||
--auth-provider-arg client-id=kubernetes \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## 5. Run kubelogin
|
||||
|
||||
Run `kubelogin`.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
idp-issuer-url: https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
client-id: kubernetes
|
||||
client-secret: YOUR_SECRET
|
||||
id-token: ey... # kubelogin will update ID token here
|
||||
refresh-token: ey... # kubelogin will update refresh token here
|
||||
name: oidc
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
args:
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
- --oidc-client-id=kubernetes
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## 5. Run kubectl
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
|
||||
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Team Operation
|
||||
# Team on-boarding
|
||||
|
||||
## kops
|
||||
|
||||
@@ -29,12 +29,14 @@ preferences: {}
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
user:
|
||||
auth-provider:
|
||||
name: oidc
|
||||
config:
|
||||
client-id: YOUR_CLIEND_ID
|
||||
client-secret: YOUR_CLIENT_SECRET
|
||||
idp-issuer-url: YOUR_ISSUER
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
args:
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for easy onboarding.
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
74
e2e_test/credetial_plugin_test.go
Normal file
74
e2e_test/credetial_plugin_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
)
|
||||
|
||||
// Run the integration tests of the credential plugin use-case.
|
||||
//
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for the local server.
|
||||
// 4. Verify the output.
|
||||
//
|
||||
func TestCmd_Run_CredentialPlugin(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()
|
||||
|
||||
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)
|
||||
|
||||
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
|
||||
credentialPluginInteraction.EXPECT().
|
||||
Write(gomock.Any()).
|
||||
Do(func(out credentialplugin.Output) {
|
||||
if out.Token != idToken {
|
||||
t.Errorf("Token wants %s but %s", idToken, out.Token)
|
||||
}
|
||||
if out.Expiry != tokenExpiryFuture {
|
||||
t.Errorf("Expiry wants %v but %v", tokenExpiryFuture, out.Expiry)
|
||||
}
|
||||
})
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runGetTokenCmd(t, ctx, req, credentialPluginInteraction,
|
||||
"--skip-open-browser",
|
||||
"--listen-port", "0",
|
||||
"--token-cache", "/dev/null",
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
)
|
||||
req.wait()
|
||||
})
|
||||
}
|
||||
|
||||
func runGetTokenCmd(t *testing.T, ctx context.Context, s usecases.LoginShowLocalServerURL, interaction adaptors.CredentialPluginInteraction, args ...string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdForHeadless(logger.New(t), s, interaction)
|
||||
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "get-token", "--v=1"}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,19 @@ import (
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
)
|
||||
|
||||
// Run the integration tests.
|
||||
var (
|
||||
tokenExpiryFuture = time.Now().Add(time.Hour).Round(time.Second)
|
||||
tokenExpiryPast = time.Now().Add(-time.Hour).Round(time.Second)
|
||||
)
|
||||
|
||||
// Run the integration tests of the Login use-case.
|
||||
//
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for the local server.
|
||||
// 4. Verify the kuneconfig.
|
||||
// 4. Verify the kubeconfig.
|
||||
//
|
||||
func TestCmd_Run(t *testing.T) {
|
||||
func TestCmd_Run_Login(t *testing.T) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
@@ -66,7 +71,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "", time.Hour)
|
||||
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").
|
||||
@@ -203,7 +208,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
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", time.Hour)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
|
||||
@@ -231,7 +236,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
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", time.Hour)
|
||||
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").
|
||||
@@ -239,7 +244,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", -time.Hour), // expired
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
@@ -269,7 +274,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", -time.Hour), // expired
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
@@ -283,7 +288,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func newIDToken(t *testing.T, issuer, nonce string, expiration time.Duration) string {
|
||||
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
|
||||
t.Helper()
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
@@ -295,7 +300,7 @@ func newIDToken(t *testing.T, issuer, nonce string, expiration time.Duration) st
|
||||
Audience: "kubernetes",
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: time.Now().Add(expiration).Unix(),
|
||||
ExpiresAt: expiry.Unix(),
|
||||
}
|
||||
claims.Nonce = nonce
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
@@ -318,14 +323,14 @@ func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, server
|
||||
})
|
||||
service.EXPECT().Exchange("YOUR_AUTH_CODE").
|
||||
DoAndReturn(func(string) (*idp.TokenResponse, error) {
|
||||
*idToken = newIDToken(t, serverURL, nonce, time.Hour)
|
||||
*idToken = newIDToken(t, serverURL, nonce, tokenExpiryFuture)
|
||||
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
|
||||
})
|
||||
}
|
||||
|
||||
func runCmd(t *testing.T, ctx context.Context, s usecases.LoginShowLocalServerURL, args ...string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdWith(logger.New(t), s)
|
||||
cmd := di.NewCmdForHeadless(logger.New(t), s, nil)
|
||||
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
1
go.mod
1
go.mod
@@ -17,5 +17,6 @@ require (
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
|
||||
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
|
||||
)
|
||||
|
||||
16
models/credentialplugin/credential_plugin.go
Normal file
16
models/credentialplugin/credential_plugin.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Package credentialplugin provides models for the credential plugin.
|
||||
package credentialplugin
|
||||
|
||||
import "time"
|
||||
|
||||
// TokenCache represents a token object cached.
|
||||
type TokenCache struct {
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
}
|
||||
|
||||
// Output represents an output object of the credential plugin.
|
||||
type Output struct {
|
||||
Token string
|
||||
Expiry time.Time
|
||||
}
|
||||
@@ -55,13 +55,13 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
|
||||
}
|
||||
|
||||
if in.OIDCConfig.IDToken != "" {
|
||||
u.Logger.Debugf(1, "Verifying the token in the kubeconfig")
|
||||
u.Logger.Debugf(1, "Verifying the existing token")
|
||||
out, err := client.Verify(ctx, adaptors.OIDCVerifyIn{IDToken: in.OIDCConfig.IDToken})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("invalid ID token in the kubeconfig, you need to remove it manually: %w", err)
|
||||
return nil, xerrors.Errorf("you need to remove the existing token manually: %w", err)
|
||||
}
|
||||
if out.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
|
||||
u.Logger.Debugf(1, "You already have a valid token in the kubeconfig")
|
||||
u.Logger.Debugf(1, "You already have a valid token")
|
||||
return &usecases.AuthenticationOut{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: in.OIDCConfig.IDToken,
|
||||
|
||||
73
usecases/credentialplugin/get_token.go
Normal file
73
usecases/credentialplugin/get_token.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Package credentialplugin provides the use-cases for running as a client-go credentials plugin.
|
||||
//
|
||||
// See https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
||||
package credentialplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(GetToken), "*"),
|
||||
wire.Bind(new(usecases.GetToken), new(*GetToken)),
|
||||
)
|
||||
|
||||
type GetToken struct {
|
||||
Authentication usecases.Authentication
|
||||
TokenCacheRepository adaptors.TokenCacheRepository
|
||||
Interaction adaptors.CredentialPluginInteraction
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
|
||||
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
|
||||
|
||||
tokenCache, err := u.TokenCacheRepository.Read(in.TokenCacheFilename)
|
||||
if err != nil {
|
||||
u.Logger.Debugf(1, "could not read the token cache file: %s", err)
|
||||
tokenCache = &credentialplugin.TokenCache{}
|
||||
}
|
||||
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
IDToken: tokenCache.IDToken,
|
||||
RefreshToken: tokenCache.RefreshToken,
|
||||
},
|
||||
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 got a valid token until %s", out.IDTokenExpiry)
|
||||
if err := u.TokenCacheRepository.Write(in.TokenCacheFilename, credentialplugin.TokenCache{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
}); err != nil {
|
||||
return xerrors.Errorf("could not write the token cache: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := u.Interaction.Write(credentialplugin.Output{Token: out.IDToken, Expiry: out.IDTokenExpiry}); err != nil {
|
||||
return xerrors.Errorf("could not write a credential object: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
167
usecases/credentialplugin/get_token_test.go
Normal file
167
usecases/credentialplugin/get_token_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package credentialplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func TestGetToken_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.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
}
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
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)
|
||||
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
Read("/path/to/token-cache").
|
||||
Return(nil, xerrors.New("file not found"))
|
||||
tokenCacheRepository.EXPECT().
|
||||
Write("/path/to/token-cache", credentialplugin.TokenCache{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
|
||||
credentialPluginInteraction.EXPECT().
|
||||
Write(credentialplugin.Output{
|
||||
Token: "YOUR_ID_TOKEN",
|
||||
Expiry: futureTime,
|
||||
})
|
||||
u := GetToken{
|
||||
Authentication: mockAuthentication,
|
||||
TokenCacheRepository: tokenCacheRepository,
|
||||
Interaction: credentialPluginInteraction,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, in); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasValidIDToken", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
}
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
},
|
||||
}).
|
||||
Return(&usecases.AuthenticationOut{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
Read("/path/to/token-cache").
|
||||
Return(&credentialplugin.TokenCache{
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
}, nil)
|
||||
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
|
||||
credentialPluginInteraction.EXPECT().
|
||||
Write(credentialplugin.Output{
|
||||
Token: "VALID_ID_TOKEN",
|
||||
Expiry: futureTime,
|
||||
})
|
||||
u := GetToken{
|
||||
Authentication: mockAuthentication,
|
||||
TokenCacheRepository: tokenCacheRepository,
|
||||
Interaction: credentialPluginInteraction,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, in); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AuthenticationError", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
}
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
}).
|
||||
Return(nil, xerrors.New("authentication error"))
|
||||
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
Read("/path/to/token-cache").
|
||||
Return(nil, xerrors.New("file not found"))
|
||||
u := GetToken{
|
||||
Authentication: mockAuthentication,
|
||||
TokenCacheRepository: tokenCacheRepository,
|
||||
Interaction: mock_adaptors.NewMockCredentialPluginInteraction(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, in); err == nil {
|
||||
t.Errorf("err wants non-nil but nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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,Authentication
|
||||
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,LoginAndExec,GetToken,Authentication
|
||||
|
||||
type Login interface {
|
||||
Do(ctx context.Context, in LoginIn) error
|
||||
@@ -32,6 +32,25 @@ type LoginShowLocalServerURL interface {
|
||||
ShowLocalServerURL(url string)
|
||||
}
|
||||
|
||||
type GetToken interface {
|
||||
Do(ctx context.Context, in GetTokenIn) error
|
||||
}
|
||||
|
||||
// GetTokenIn represents an input DTO of the GetToken use-case.
|
||||
type GetTokenIn struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string // optional
|
||||
SkipOpenBrowser bool
|
||||
ListenPort []int
|
||||
Username string // If set, perform the resource owner password credentials grant
|
||||
Password string // If empty, read a password using Env.ReadPassword()
|
||||
CACertFilename string // If set, use the CA cert
|
||||
SkipTLSVerify bool
|
||||
TokenCacheFilename string
|
||||
}
|
||||
|
||||
type LoginAndExec interface {
|
||||
Do(ctx context.Context, in LoginAndExecIn) (*LoginAndExecOut, error)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,LoginAndExec,Authentication)
|
||||
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,LoginAndExec,GetToken,Authentication)
|
||||
|
||||
// Package mock_usecases is a generated GoMock package.
|
||||
package mock_usecases
|
||||
@@ -82,6 +82,41 @@ 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
|
||||
recorder *MockGetTokenMockRecorder
|
||||
}
|
||||
|
||||
// MockGetTokenMockRecorder is the mock recorder for MockGetToken
|
||||
type MockGetTokenMockRecorder struct {
|
||||
mock *MockGetToken
|
||||
}
|
||||
|
||||
// NewMockGetToken creates a new mock instance
|
||||
func NewMockGetToken(ctrl *gomock.Controller) *MockGetToken {
|
||||
mock := &MockGetToken{ctrl: ctrl}
|
||||
mock.recorder = &MockGetTokenMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockGetToken) EXPECT() *MockGetTokenMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Do mocks base method
|
||||
func (m *MockGetToken) Do(arg0 context.Context, arg1 usecases.GetTokenIn) error {
|
||||
ret := m.ctrl.Call(m, "Do", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Do indicates an expected call of Do
|
||||
func (mr *MockGetTokenMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockGetToken)(nil).Do), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockAuthentication is a mock of Authentication interface
|
||||
type MockAuthentication struct {
|
||||
ctrl *gomock.Controller
|
||||
|
||||
Reference in New Issue
Block a user