mirror of
https://github.com/int128/kubelogin.git
synced 2026-03-01 00:10:20 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f566a7b32 | ||
|
|
5158159bdd | ||
|
|
3a2aa0c6c0 | ||
|
|
56b17efae1 | ||
|
|
3e5be43d8a | ||
|
|
1ffa927432 | ||
|
|
5c6b461f37 | ||
|
|
6f96ccae62 | ||
|
|
dc88948d88 | ||
|
|
31609a3ed3 | ||
|
|
9685507d7d | ||
|
|
1c66099496 | ||
|
|
681faa07ca |
@@ -11,15 +11,13 @@ jobs:
|
||||
curl -L -o ~/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/linux/amd64/kubectl
|
||||
chmod +x ~/bin/kubectl
|
||||
- run: |
|
||||
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.3.0/ghcp_linux_amd64
|
||||
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.5.0/ghcp_linux_amd64
|
||||
chmod +x ~/bin/ghcp
|
||||
- run: |
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.16.0
|
||||
- run: go get github.com/int128/goxzst
|
||||
- run: go get github.com/tcnksm/ghr
|
||||
- checkout
|
||||
# workaround for https://github.com/golang/go/issues/27925
|
||||
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
|
||||
- run: make check
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- run: make run
|
||||
|
||||
@@ -24,10 +24,6 @@ As well as it respects the environment variable `KUBECONFIG`.
|
||||
|
||||
TODO
|
||||
|
||||
### Wrap kubectl and login transparently
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
5
Makefile
5
Makefile
@@ -29,10 +29,13 @@ diagram: docs/authn.png
|
||||
dist:
|
||||
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
|
||||
mv dist/gh/kubelogin.rb dist/
|
||||
mkdir -p dist/plugins
|
||||
cp dist/gh/oidc-login.yaml dist/plugins/oidc-login.yaml
|
||||
|
||||
release: dist
|
||||
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
|
||||
ghcp -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
|
||||
ghcp commit -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
|
||||
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(CIRCLE_TAG)" -m "Bump oidc-login to $(CIRCLE_TAG)" -C dist/ plugins/oidc-login.yaml
|
||||
|
||||
clean:
|
||||
-rm $(TARGET)
|
||||
|
||||
285
README.md
285
README.md
@@ -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 provides browser based authentication and writes 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,22 +19,96 @@ brew install kubelogin
|
||||
kubectl krew install oidc-login
|
||||
|
||||
# GitHub Releases
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.12.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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Just run the command:
|
||||
|
||||
### Run as a credential plugin
|
||||
|
||||
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: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- 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 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 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
|
||||
|
||||
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 +121,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, it 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,69 +149,81 @@ 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
|
||||
|
||||
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, it 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, it 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`, it just executes kubectl.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
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
|
||||
get-token Run as a kubectl credential plugin
|
||||
help Help about any command
|
||||
version Print the version information
|
||||
|
||||
@@ -155,22 +241,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 +256,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 +316,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 +344,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,112 +21,39 @@ 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
|
||||
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())
|
||||
|
||||
execCmd := cobra.Command{
|
||||
Use: "exec [flags] -- kubectl [args]",
|
||||
Short: "Login transparently and execute the kubectl command",
|
||||
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
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
getTokenCmd := newGetTokenCmd(ctx, cmd)
|
||||
rootCmd.AddCommand(getTokenCmd)
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
Args: cobra.NoArgs,
|
||||
@@ -133,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.
|
||||
@@ -163,18 +92,120 @@ 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")
|
||||
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")
|
||||
}
|
||||
|
||||
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 {
|
||||
loginOptions
|
||||
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.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")
|
||||
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
|
||||
},
|
||||
}
|
||||
o.register(c.Flags())
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -96,173 +96,111 @@ func TestCmd_Run(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("loginAndExec/Defaults", func(t *testing.T) {
|
||||
t.Run("get-token/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)
|
||||
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{
|
||||
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,
|
||||
GetToken: getToken,
|
||||
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",
|
||||
"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("loginAndExec/OverrideOptions", func(t *testing.T) {
|
||||
t.Run("get-token/FullOptions", 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)
|
||||
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(2))
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
|
||||
|
||||
cmd := Cmd{
|
||||
LoginAndExec: loginAndExec,
|
||||
Logger: logger,
|
||||
GetToken: getToken,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
// kubelogin options in the first args should be mapped to the options
|
||||
"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",
|
||||
"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/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
|
||||
}
|
||||
18
adaptors/env/env.go
vendored
18
adaptors/env/env.go
vendored
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -77,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 {
|
||||
|
||||
@@ -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
|
||||
@@ -194,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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
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,21 +43,23 @@ func NewCmd() adaptors.Cmd {
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
exec := &login.Exec{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
repository := &tokencache.Repository{}
|
||||
interaction := &credentialplugin.Interaction{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
Authentication: authentication,
|
||||
TokenCacheRepository: repository,
|
||||
Interaction: interaction,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
LoginAndExec: exec,
|
||||
Logger: adaptorsLogger,
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
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,16 +76,17 @@ func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
exec := &login.Exec{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
repository := &tokencache.Repository{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
Authentication: authentication,
|
||||
TokenCacheRepository: repository,
|
||||
Interaction: credentialPluginInteraction,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
LoginAndExec: exec,
|
||||
Logger: adaptorsLogger,
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang/mock/gomock"
|
||||
"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/keys"
|
||||
"github.com/int128/kubelogin/e2e_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
)
|
||||
|
||||
// Run the integration tests.
|
||||
//
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for the local server.
|
||||
// 4. Verify the kuneconfig.
|
||||
//
|
||||
func TestCmd_Run(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)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
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",
|
||||
})
|
||||
})
|
||||
|
||||
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, "", time.Hour)
|
||||
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)
|
||||
|
||||
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("env:KUBECONFIG", 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)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", 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, "profile groups openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
ExtraScopes: "profile,groups",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
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",
|
||||
})
|
||||
})
|
||||
|
||||
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", time.Hour)
|
||||
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", time.Hour)
|
||||
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", -time.Hour), // 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", -time.Hour), // 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, expiration time.Duration) string {
|
||||
t.Helper()
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
Nonce string `json:"nonce"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
claims.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: time.Now().Add(expiration).Unix(),
|
||||
}
|
||||
claims.Nonce = nonce
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(keys.JWSKeyPair)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
|
||||
var nonce string
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
|
||||
DoAndReturn(func(_, gotNonce string) (string, error) {
|
||||
nonce = gotNonce
|
||||
return "YOUR_AUTH_CODE", nil
|
||||
})
|
||||
service.EXPECT().Exchange("YOUR_AUTH_CODE").
|
||||
DoAndReturn(func(string) (*idp.TokenResponse, error) {
|
||||
*idToken = newIDToken(t, serverURL, nonce, time.Hour)
|
||||
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)
|
||||
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
type nopBrowserRequest struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (r *nopBrowserRequest) ShowLocalServerURL(url string) {
|
||||
r.t.Errorf("ShowLocalServerURL must not be called")
|
||||
}
|
||||
|
||||
type browserRequest struct {
|
||||
t *testing.T
|
||||
urlCh chan<- string
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
func (r *browserRequest) ShowLocalServerURL(url string) {
|
||||
defer close(r.urlCh)
|
||||
r.t.Logf("Open %s for authentication", url)
|
||||
r.urlCh <- url
|
||||
}
|
||||
|
||||
func (r *browserRequest) wait() {
|
||||
r.wg.Wait()
|
||||
}
|
||||
|
||||
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) *browserRequest {
|
||||
t.Helper()
|
||||
urlCh := make(chan string)
|
||||
var wg sync.WaitGroup
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case url := <-urlCh:
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a request: %s", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("could not send a request: %s", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
case err := <-ctx.Done():
|
||||
t.Errorf("context done while waiting for URL prompt: %s", err)
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
return &browserRequest{t, urlCh, &wg}
|
||||
}
|
||||
|
||||
func setenv(t *testing.T, key, value string) {
|
||||
t.Helper()
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func unsetenv(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatalf("Could not unset the env var %s: %s", key, err)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
388
e2e_test/login_test.go
Normal file
388
e2e_test/login_test.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang/mock/gomock"
|
||||
"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/keys"
|
||||
"github.com/int128/kubelogin/e2e_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
)
|
||||
|
||||
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 kubeconfig.
|
||||
//
|
||||
func TestCmd_Run_Login(t *testing.T) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
type testParameter struct {
|
||||
startServer func(t *testing.T, h http.Handler) (string, localserver.Shutdowner)
|
||||
kubeconfigIDPCertificateAuthority string
|
||||
clientTLSConfig *tls.Config
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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,
|
||||
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",
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", 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, "profile groups openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
ExtraScopes: "profile,groups",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
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",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
|
||||
t.Helper()
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
Nonce string `json:"nonce"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
claims.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: expiry.Unix(),
|
||||
}
|
||||
claims.Nonce = nonce
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(keys.JWSKeyPair)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
|
||||
var nonce string
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
|
||||
DoAndReturn(func(_, gotNonce string) (string, error) {
|
||||
nonce = gotNonce
|
||||
return "YOUR_AUTH_CODE", nil
|
||||
})
|
||||
service.EXPECT().Exchange("YOUR_AUTH_CODE").
|
||||
DoAndReturn(func(string) (*idp.TokenResponse, error) {
|
||||
*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.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)
|
||||
}
|
||||
}
|
||||
|
||||
type nopBrowserRequest struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (r *nopBrowserRequest) ShowLocalServerURL(url string) {
|
||||
r.t.Errorf("ShowLocalServerURL must not be called")
|
||||
}
|
||||
|
||||
type browserRequest struct {
|
||||
t *testing.T
|
||||
urlCh chan<- string
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
func (r *browserRequest) ShowLocalServerURL(url string) {
|
||||
defer close(r.urlCh)
|
||||
r.t.Logf("Open %s for authentication", url)
|
||||
r.urlCh <- url
|
||||
}
|
||||
|
||||
func (r *browserRequest) wait() {
|
||||
r.wg.Wait()
|
||||
}
|
||||
|
||||
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) *browserRequest {
|
||||
t.Helper()
|
||||
urlCh := make(chan string)
|
||||
var wg sync.WaitGroup
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case url := <-urlCh:
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a request: %s", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("could not send a request: %s", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
case err := <-ctx.Done():
|
||||
t.Errorf("context done while waiting for URL prompt: %s", err)
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
return &browserRequest{t, urlCh, &wg}
|
||||
}
|
||||
|
||||
func setenv(t *testing.T, key, value string) {
|
||||
t.Helper()
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func unsetenv(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatalf("Could not unset the env var %s: %s", key, err)
|
||||
}
|
||||
}
|
||||
30
go.mod
30
go.mod
@@ -1,32 +1,22 @@
|
||||
module github.com/int128/kubelogin
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-test/deep v1.0.1
|
||||
github.com/gogo/protobuf v1.2.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda
|
||||
github.com/go-test/deep v1.0.2
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||
github.com/google/wire v0.3.0
|
||||
github.com/imdario/mergo v0.3.7 // indirect
|
||||
github.com/int128/oauth2cli v1.4.1
|
||||
github.com/json-iterator/go v1.1.6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
|
||||
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/api v0.0.0-20190222213804-5cb15d344471 // indirect
|
||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 // indirect
|
||||
k8s.io/client-go v10.0.0+incompatible
|
||||
k8s.io/klog v0.2.0 // indirect
|
||||
sigs.k8s.io/yaml v1.1.0 // indirect
|
||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
|
||||
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
|
||||
)
|
||||
|
||||
124
go.sum
124
go.sum
@@ -1,4 +1,5 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
@@ -7,127 +8,138 @@ github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda h1:NyywMz59neOoVRFDz+ccfKWxn784fiHMDnZSy6T+JXY=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
||||
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
|
||||
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60=
|
||||
github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
|
||||
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
|
||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/int128/oauth2cli v1.4.1 h1:IsaYMafEDS1jyArxYdmksw+nMsNxiYCQzdkPj3QF9BY=
|
||||
github.com/int128/oauth2cli v1.4.1/go.mod h1:CMJjyUSgKiobye1M/9byFACOjtB2LRo2mo7boklEKlI=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE=
|
||||
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774 h1:a4tQYYYuK9QdeO/+kEvNYyuR21S+7ve5EANok6hABhI=
|
||||
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd h1:sMHc2rZHuzQmrbVoSpt9HgerkXPyIeCSO6k0zUMGfFk=
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
|
||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 h1:jIOcLT9BZzyJ9ce+IwwZ+aF9yeCqzrR+NrD68a/SHKw=
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d h1:TnM+PKb3ylGmZvyPXmo9m/wktg7Jn/a/fNmr33HSj8g=
|
||||
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
|
||||
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE=
|
||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
|
||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg=
|
||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/client-go v10.0.0+incompatible h1:+xQQxwjrcIPWDMJBAS+1G2FNk1McoPnb53xkvcDiDqE=
|
||||
k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
|
||||
k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
|
||||
k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
|
||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 h1:uV4S5IB5g4Nvi+TBVNf3e9L4wrirlwYJ6w88jUQxTUw=
|
||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
|
||||
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g=
|
||||
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k=
|
||||
k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
|
||||
k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
|
||||
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
||||
k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4=
|
||||
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +46,7 @@ type Authentication struct {
|
||||
|
||||
func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
|
||||
client, err := u.OIDC.New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.CurrentAuthProvider.OIDCConfig,
|
||||
Config: in.OIDCConfig,
|
||||
CACertFilename: in.CACertFilename,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
})
|
||||
@@ -54,18 +54,18 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
|
||||
return nil, xerrors.Errorf("could not create an OIDC client: %w", err)
|
||||
}
|
||||
|
||||
if in.CurrentAuthProvider.OIDCConfig.IDToken != "" {
|
||||
u.Logger.Debugf(1, "Verifying the token in the kubeconfig")
|
||||
out, err := client.Verify(ctx, adaptors.OIDCVerifyIn{IDToken: in.CurrentAuthProvider.OIDCConfig.IDToken})
|
||||
if in.OIDCConfig.IDToken != "" {
|
||||
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.CurrentAuthProvider.OIDCConfig.IDToken,
|
||||
RefreshToken: in.CurrentAuthProvider.OIDCConfig.RefreshToken,
|
||||
IDToken: in.OIDCConfig.IDToken,
|
||||
RefreshToken: in.OIDCConfig.RefreshToken,
|
||||
IDTokenExpiry: out.IDTokenExpiry,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
}, nil
|
||||
@@ -73,10 +73,10 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
|
||||
u.Logger.Debugf(1, "You have an expired token at %s", out.IDTokenExpiry)
|
||||
}
|
||||
|
||||
if in.CurrentAuthProvider.OIDCConfig.RefreshToken != "" {
|
||||
if in.OIDCConfig.RefreshToken != "" {
|
||||
u.Logger.Debugf(1, "Refreshing the token")
|
||||
out, err := client.Refresh(ctx, adaptors.OIDCRefreshIn{
|
||||
RefreshToken: in.CurrentAuthProvider.OIDCConfig.RefreshToken,
|
||||
RefreshToken: in.OIDCConfig.RefreshToken,
|
||||
})
|
||||
if err == nil {
|
||||
return &usecases.AuthenticationOut{
|
||||
|
||||
@@ -28,11 +28,9 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
SkipOpenBrowser: true,
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
CurrentAuthProvider: &kubeconfig.AuthProvider{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
@@ -50,7 +48,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.CurrentAuthProvider.OIDCConfig,
|
||||
Config: in.OIDCConfig,
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
}).
|
||||
@@ -83,11 +81,9 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
CurrentAuthProvider: &kubeconfig.AuthProvider{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
@@ -105,7 +101,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.CurrentAuthProvider.OIDCConfig,
|
||||
Config: in.OIDCConfig,
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
}).
|
||||
@@ -135,11 +131,9 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
in := usecases.AuthenticationIn{
|
||||
Username: "USER",
|
||||
CurrentAuthProvider: &kubeconfig.AuthProvider{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
@@ -157,7 +151,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.CurrentAuthProvider.OIDCConfig,
|
||||
Config: in.OIDCConfig,
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
mockEnv := mock_adaptors.NewMockEnv(ctrl)
|
||||
@@ -188,17 +182,15 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
in := usecases.AuthenticationIn{
|
||||
Username: "USER",
|
||||
CurrentAuthProvider: &kubeconfig.AuthProvider{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
}
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.CurrentAuthProvider.OIDCConfig,
|
||||
Config: in.OIDCConfig,
|
||||
}).
|
||||
Return(mock_adaptors.NewMockOIDCClient(ctrl), nil)
|
||||
mockEnv := mock_adaptors.NewMockEnv(ctrl)
|
||||
@@ -222,12 +214,10 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.AuthenticationIn{
|
||||
CurrentAuthProvider: &kubeconfig.AuthProvider{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
},
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
@@ -240,7 +230,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.CurrentAuthProvider.OIDCConfig,
|
||||
Config: in.OIDCConfig,
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
u := Authentication{
|
||||
@@ -267,13 +257,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.AuthenticationIn{
|
||||
CurrentAuthProvider: &kubeconfig.AuthProvider{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "EXPIRED_ID_TOKEN",
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
},
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "EXPIRED_ID_TOKEN",
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
@@ -296,7 +284,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.CurrentAuthProvider.OIDCConfig,
|
||||
Config: in.OIDCConfig,
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
u := Authentication{
|
||||
@@ -324,13 +312,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
in := usecases.AuthenticationIn{
|
||||
ListenPort: []int{10000},
|
||||
CurrentAuthProvider: &kubeconfig.AuthProvider{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "EXPIRED_ID_TOKEN",
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
},
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "EXPIRED_ID_TOKEN",
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
@@ -358,7 +344,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.CurrentAuthProvider.OIDCConfig,
|
||||
Config: in.OIDCConfig,
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
u := Authentication{
|
||||
|
||||
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,GetToken,Authentication
|
||||
|
||||
type Login interface {
|
||||
Do(ctx context.Context, in LoginIn) error
|
||||
@@ -32,19 +32,23 @@ type LoginShowLocalServerURL interface {
|
||||
ShowLocalServerURL(url string)
|
||||
}
|
||||
|
||||
type LoginAndExec interface {
|
||||
Do(ctx context.Context, in LoginAndExecIn) (*LoginAndExecOut, error)
|
||||
type GetToken interface {
|
||||
Do(ctx context.Context, in GetTokenIn) 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
|
||||
// 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 Authentication interface {
|
||||
@@ -53,13 +57,13 @@ type Authentication interface {
|
||||
|
||||
// AuthenticationIn represents an input DTO of the Authentication use-case.
|
||||
type AuthenticationIn struct {
|
||||
CurrentAuthProvider *kubeconfig.AuthProvider
|
||||
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
|
||||
OIDCConfig kubeconfig.OIDCConfig
|
||||
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
|
||||
}
|
||||
|
||||
// AuthenticationIn represents an output DTO of the Authentication use-case.
|
||||
|
||||
@@ -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{
|
||||
CurrentAuthProvider: authProvider,
|
||||
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
|
||||
}
|
||||
@@ -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{
|
||||
CurrentAuthProvider: currentAuthProvider,
|
||||
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{CurrentAuthProvider: currentAuthProvider}).
|
||||
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{CurrentAuthProvider: currentAuthProvider}).
|
||||
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{CurrentAuthProvider: currentAuthProvider}).
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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?
|
||||
@@ -48,13 +46,13 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
|
||||
u.Logger.Debugf(1, "A token will be written to %s", authProvider.LocationOfOrigin)
|
||||
|
||||
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
|
||||
CurrentAuthProvider: authProvider,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
ListenPort: in.ListenPort,
|
||||
Username: in.Username,
|
||||
Password: in.Password,
|
||||
CACertFilename: in.CACertFilename,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
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)
|
||||
|
||||
@@ -60,13 +60,13 @@ func TestLogin_Do(t *testing.T) {
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{
|
||||
CurrentAuthProvider: currentAuthProvider,
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
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",
|
||||
@@ -104,7 +104,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
Return(currentAuthProvider, nil)
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
|
||||
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
|
||||
Return(&usecases.AuthenticationOut{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
@@ -161,7 +161,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
Return(currentAuthProvider, nil)
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
|
||||
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
|
||||
Return(nil, xerrors.New("authentication error"))
|
||||
u := Login{
|
||||
Authentication: mockAuthentication,
|
||||
@@ -206,7 +206,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
Return(xerrors.New("I/O error"))
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{CurrentAuthProvider: currentAuthProvider}).
|
||||
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
|
||||
Return(&usecases.AuthenticationOut{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
|
||||
@@ -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,GetToken,Authentication)
|
||||
|
||||
// Package mock_usecases is a generated GoMock package.
|
||||
package mock_usecases
|
||||
@@ -46,40 +46,39 @@ 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 {
|
||||
// MockGetToken is a mock of GetToken interface
|
||||
type MockGetToken struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoginAndExecMockRecorder
|
||||
recorder *MockGetTokenMockRecorder
|
||||
}
|
||||
|
||||
// MockLoginAndExecMockRecorder is the mock recorder for MockLoginAndExec
|
||||
type MockLoginAndExecMockRecorder struct {
|
||||
mock *MockLoginAndExec
|
||||
// MockGetTokenMockRecorder is the mock recorder for MockGetToken
|
||||
type MockGetTokenMockRecorder struct {
|
||||
mock *MockGetToken
|
||||
}
|
||||
|
||||
// NewMockLoginAndExec creates a new mock instance
|
||||
func NewMockLoginAndExec(ctrl *gomock.Controller) *MockLoginAndExec {
|
||||
mock := &MockLoginAndExec{ctrl: ctrl}
|
||||
mock.recorder = &MockLoginAndExecMockRecorder{mock}
|
||||
// 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 *MockLoginAndExec) EXPECT() *MockLoginAndExecMockRecorder {
|
||||
func (m *MockGetToken) EXPECT() *MockGetTokenMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Do mocks base method
|
||||
func (m *MockLoginAndExec) Do(arg0 context.Context, arg1 usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
|
||||
func (m *MockGetToken) Do(arg0 context.Context, arg1 usecases.GetTokenIn) error {
|
||||
ret := m.ctrl.Call(m, "Do", arg0, arg1)
|
||||
ret0, _ := ret[0].(*usecases.LoginAndExecOut)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user