Compare commits

...

24 Commits

Author SHA1 Message Date
Hidetake Iwata
c8967faf6b Fix krew yaml (#134) 2019-08-18 16:57:47 +09:00
Hidetake Iwata
315d6151d7 Refactor (#133)
* Refactor: change debug messages to lowercase

* Refactor: add debug messages

* Refactor Makefile

* Refactor: add keys and certificates of e2e tests
2019-08-18 15:14:07 +09:00
Hidetake Iwata
1ff03fdfb3 Skip verification of cached token to reduce time (#132) 2019-08-17 21:40:14 +09:00
Hidetake Iwata
5e0fc7f399 Save token cache for each issuer and client ID (#131) 2019-08-14 14:52:58 +09:00
Hidetake Iwata
9423a65f46 Add dex documentation (#130) 2019-08-11 16:09:53 +09:00
Hidetake Iwata
45417a18fd Refactor docs 2019-08-10 15:25:06 +09:00
Hidetake Iwata
760416fd04 Update README.md 2019-08-09 17:01:36 +09:00
Hidetake Iwata
0a4ebb26c2 Refactor packages structure (#129) 2019-08-09 10:15:17 +09:00
Hidetake Iwata
de9f7a2a01 Fix typo (#128)
* Fix typo

* Update google.md

* Update keycloak.md
2019-08-03 20:05:13 +09:00
Hidetake Iwata
0006cdda2d Update README.md 2019-08-02 14:10:20 +09:00
Hidetake Iwata
c89a8a1823 Update README.md 2019-08-01 11:00:55 +09:00
Hidetake Iwata
4f566a7b32 Refactor: use RunE and root error handler (#127) 2019-08-01 10:54:03 +09:00
Hidetake Iwata
5158159bdd Fix stdout of browser launcher breaks credential json (#126) 2019-08-01 10:50:36 +09:00
Hidetake Iwata
3a2aa0c6c0 Fix TLS certificate on refreshing token (#125)
* Add test of refreshing token with TLS cert

* Fix TLS certificate on refreshing token (#123)
2019-08-01 10:19:58 +09:00
Hidetake Iwata
56b17efae1 Remove LoginAndExec use-case (#120) 2019-07-30 09:57:22 +09:00
Hidetake Iwata
3e5be43d8a Change description in krew yaml 2019-07-28 16:52:26 +09:00
Hidetake Iwata
1ffa927432 Update README.md 2019-07-27 13:42:40 +09:00
Hidetake Iwata
5c6b461f37 Update README.md 2019-07-27 13:10:21 +09:00
Hidetake Iwata
6f96ccae62 Make commit for PR for krew-index on release 2019-07-26 19:35:45 +09:00
Hidetake Iwata
dc88948d88 Run as a client-go credential plugin (#118) 2019-07-25 17:01:34 +09:00
Hidetake Iwata
31609a3ed3 Move to k8s.io/client-go@v12.0.0 (#119)
See https://github.com/kubernetes/client-go/blob/master/INSTALL.md
2019-07-25 16:43:48 +09:00
Hidetake Iwata
9685507d7d Refactor: use OIDCConfig in input of authentication use-case (#117) 2019-07-25 13:49:09 +09:00
dependabot-preview[bot]
1c66099496 Bump github.com/go-test/deep from 1.0.1 to 1.0.2 (#113)
Bumps [github.com/go-test/deep](https://github.com/go-test/deep) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/go-test/deep/releases)
- [Changelog](https://github.com/go-test/deep/blob/master/CHANGES.md)
- [Commits](https://github.com/go-test/deep/compare/v1.0.1...v1.0.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-16 13:20:42 +09:00
Hidetake Iwata
681faa07ca Update README.md 2019-07-11 11:06:58 +09:00
76 changed files with 2687 additions and 1954 deletions

View File

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

View File

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

View File

@@ -3,13 +3,11 @@ TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
.PHONY: check run diagram release clean
all: $(TARGET)
.PHONY: check
check:
golangci-lint run
$(MAKE) -C e2e_test/keys/testdata
go test -v -race -cover -coverprofile=coverage.out ./...
$(TARGET): $(wildcard *.go)
@@ -18,6 +16,7 @@ $(TARGET): $(wildcard *.go)
$(TARGET_PLUGIN): $(TARGET)
ln -sf $(TARGET) $@
.PHONY: run
run: $(TARGET_PLUGIN)
-PATH=.:$(PATH) kubectl oidc-login --help
@@ -29,11 +28,16 @@ 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
.PHONY: release
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
.PHONY: clean
clean:
-rm $(TARGET)
-rm $(TARGET_PLUGIN)

306
README.md
View File

@@ -2,8 +2,9 @@
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.
This is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
When you run `kubectl`, kubelogin opens the browser and you can log in to the provider.
Then kubelogin gets a token from the provider and kubectl calls the Kubernetes APIs with the token.
## Getting Started
@@ -19,22 +20,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.1/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 dex and GitHub](docs/dex.md)
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
You can run kubelogin as the following methods:
- Credential plugin mode
- Standalone mode
### Login by the command
### Credential plugin mode
Just run the command:
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 directory (default `~/.kube/cache/oidc-login`).
Kubelogin will perform authentication if the token cache file does not exist.
### Standalone mode
You can run kubelogin as a standalone command.
In this method, you need to manually run the command before running kubectl.
Configure the kubeconfig like:
```yaml
- name: keycloak
user:
auth-provider:
config:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: https://issuer.example.com
name: oidc
```
Run kubelogin:
```sh
kubelogin
@@ -47,7 +122,7 @@ It automatically opens the browser and you can log in to the provider.
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
After authentication, 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
@@ -56,7 +131,7 @@ You got a valid token until 2019-05-18 10:28:51 +0900 JST
Updated ~/.kubeconfig
```
Now you can access to the cluster.
Now you can access the cluster.
```
% kubectl get pods
@@ -64,6 +139,22 @@ NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
Your kubeconfig looks like:
```yaml
users:
- name: keycloak
user:
auth-provider:
config:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: https://issuer.example.com
id-token: ey... # kubelogin will add or update the ID token here
refresh-token: ey... # kubelogin will add or update the refresh token here
name: oidc
```
If the ID token is valid, kubelogin does nothing.
```
@@ -75,69 +166,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).
### Credential plugin mode
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-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
-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
```
### Standalone mode
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 +258,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 +273,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 +333,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 +361,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.

View File

@@ -1,180 +0,0 @@
package cmd
import (
"context"
"fmt"
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
// Set provides an implementation and interface for Cmd.
var Set = wire.NewSet(
wire.Struct(new(Cmd), "*"),
wire.Bind(new(adaptors.Cmd), new(*Cmd)),
)
const examples = ` # Login to the provider using authorization code grant.
%[1]s
# Login to the provider using resource owner password credentials grant.
%[1]s --username USERNAME --password PASSWORD
# Wrap kubectl and login transparently
alias kubectl='%[1]s exec -- kubectl'`
var defaultListenPort = []int{8000, 18000}
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
Login usecases.Login
LoginAndExec usecases.LoginAndExec
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)
versionCmd := cobra.Command{
Use: "version",
Short: "Print the version information",
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
cmd.Logger.Printf("%s version %s", executable, version)
},
}
rootCmd.AddCommand(&versionCmd)
rootCmd.SetArgs(args[1:])
if err := rootCmd.Execute(); err != nil {
cmd.Logger.Debugf(1, "error while parsing the arguments: %s", err)
return 1
}
return exitCode
}
// kubectlOptions represents kubectl specific options.
type kubectlOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
Verbose int
}
func (o *kubectlOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
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")
}
// kubeloginOptions represents application specific options.
type kubeloginOptions struct {
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
func (o *kubeloginOptions) 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")
}

View File

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

View File

@@ -1,46 +0,0 @@
//+build wireinject
// Package di provides dependency injection.
package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/cmd"
"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/usecases"
"github.com/int128/kubelogin/usecases/auth"
"github.com/int128/kubelogin/usecases/login"
)
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() adaptors.Cmd {
wire.Build(
auth.Set,
auth.ExtraSet,
login.Set,
cmd.Set,
env.Set,
kubeconfig.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 {
wire.Build(
auth.Set,
login.Set,
cmd.Set,
env.Set,
kubeconfig.Set,
oidc.Set,
)
return nil
}

View File

@@ -1,86 +0,0 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package di
import (
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/cmd"
"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/usecases"
"github.com/int128/kubelogin/usecases/auth"
"github.com/int128/kubelogin/usecases/login"
)
// Injectors from di.go:
func NewCmd() adaptors.Cmd {
adaptorsLogger := logger.New()
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
envEnv := &env.Env{}
showLocalServerURL := &auth.ShowLocalServerURL{
Logger: adaptorsLogger,
}
authentication := &auth.Authentication{
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
loginLogin := &login.Login{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: adaptorsLogger,
}
exec := &login.Exec{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Env: envEnv,
Logger: adaptorsLogger,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
LoginAndExec: exec,
Logger: adaptorsLogger,
}
return cmdCmd
}
func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL) adaptors.Cmd {
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
envEnv := &env.Env{}
authentication := &auth.Authentication{
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
loginLogin := &login.Login{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: adaptorsLogger,
}
exec := &login.Exec{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Env: envEnv,
Logger: adaptorsLogger,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
LoginAndExec: exec,
Logger: adaptorsLogger,
}
return cmdCmd
}

141
docs/dex.md Normal file
View File

@@ -0,0 +1,141 @@
# Getting Started with dex and GitHub
## Prerequisite
- You have a GitHub account.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed.
## 1. Setup GitHub OAuth
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
- Application name: (any)
- Homepage URL: `https://dex.example.com`
- Authorization callback URL: `https://dex.example.com/callback`
## 2. Setup dex
Configure the dex with the following config:
```yaml
issuer: https://dex.example.com
connectors:
- type: github
id: github
name: GitHub
config:
clientID: YOUR_GITHUB_CLIENT_ID
clientSecret: YOUR_GITHUB_CLIENT_SECRET
redirectURI: https://dex.example.com/callback
staticClients:
- id: kubernetes
name: Kubernetes
redirectURIs:
- http://localhost:8000
- http://localhost:18000
secret: YOUR_DEX_CLIENT_SECRET
```
Now test authentication with the dex.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://dex.example.com \
--oidc-client-id=kubernetes \
--oidc-client-secret=YOUR_DEX_CLIENT_SECRET
```
You should get claims like:
```
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://dex.example.com
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
17:21:32.052683 get_token.go:57: ID token has the claim: aud=kubernetes
```
## 3. Setup Kubernetes API server
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://dex.example.com
--oidc-client-id=kubernetes
```
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://dex.example.com
oidcClientID: kubernetes
```
## 4. Create a role binding
Here assign the `cluster-admin` role to your subject.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: YOUR_SUBJECT
```
You can create a custom role and assign it as well.
## 5. Setup kubeconfig
Configure the kubeconfig like:
```yaml
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: dex
name: dex@example.k8s.local
current-context: dex@example.k8s.local
kind: Config
preferences: {}
users:
- name: dex
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://dex.example.com
- --oidc-client-id=kubernetes
- --oidc-client-secret=YOUR_DEX_CLIENT_SECRET
```
You can share the kubeconfig to your team members for on-boarding.
## 6. Run kubectl
Make sure you can access 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
```

View File

@@ -13,10 +13,32 @@ Open [Google APIs Console](https://console.developers.google.com/apis/credential
- Application Type: Other
Now test authentication with Google Identity Platform.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://accounts.google.com \
--oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
You should get claims like:
```
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://accounts.google.com
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
17:21:32.052683 get_token.go:57: ID token has the claim: aud=YOUR_CLIENT_ID.apps.googleusercontent.com
```
## 2. Setup Kubernetes API server
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
@@ -28,7 +50,7 @@ spec:
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to you.
Here assign the `cluster-admin` role to your subject.
```yaml
kind: ClusterRoleBinding
@@ -41,54 +63,53 @@ roleRef:
name: cluster-admin
subjects:
- kind: User
name: https://accounts.google.com#1234567890
name: YOUR_SUBJECT
```
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
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: google
name: google@example.k8s.local
current-context: google@example.k8s.local
kind: Config
preferences: {}
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
```
Make sure you can access to the Kubernetes cluster.
You can share the kubeconfig to your team members for on-boarding.
## 5. Run kubectl
Make sure you can access 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

View File

@@ -28,10 +28,34 @@ You can associate client roles by adding the following mapper:
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
Now test authentication with the Keycloak.
```sh
kubectl oidc-login get-token -v1 \
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--oidc-client-id=kubernetes \
--oidc-client-secret=YOUR_CLIENT_SECRET
```
You should get claims like:
```
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://keycloak.example.com/auth/realms/YOUR_REALM
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
17:21:32.052683 get_token.go:57: ID token has the claim: aud=kubernetes
17:21:32.052694 get_token.go:57: ID token has the claim: groups=[kubernetes:admin]
```
## 2. Setup Kubernetes API server
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 +86,48 @@ 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
apiVersion: v1
clusters:
- cluster:
server: https://api.example.com
name: example.k8s.local
contexts:
- context:
cluster: example.k8s.local
user: keycloak
name: keycloak@example.k8s.local
current-context: keycloak@example.k8s.local
kind: Config
preferences: {}
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
```
Make sure you can access to the Kubernetes cluster.
You can share the kubeconfig to your team members for on-boarding.
## 5. Run kubectl
Make sure you can access 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

View File

@@ -1,40 +0,0 @@
# Team Operation
## kops
Export the kubeconfig.
```sh
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
```
Remove the `admin` access from the kubeconfig.
It should look as like:
```yaml
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS...
server: https://api.hello.k8s.example.com
name: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
current-context: hello.k8s.local
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
```
You can share the kubeconfig to your team members for easy onboarding.

View File

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

View File

@@ -0,0 +1,85 @@
package e2e_test
import (
"context"
"io/ioutil"
"os"
"testing"
"time"
"github.com/golang/mock/gomock"
"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/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/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
cacheDir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a cache dir: %s", err)
}
defer func() {
if err := os.RemoveAll(cacheDir); err != nil {
t.Errorf("could not clean up the cache dir: %s", err)
}
}()
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-dir", cacheDir,
"--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)
}
}

View File

@@ -1,4 +1 @@
/CA
*.key
*.csr
*.crt

View File

@@ -1,13 +1,13 @@
all: ca.key ca.crt server.key server.crt jws.key
.PHONY: clean
all: server.crt ca.crt jws.key
clean:
rm -v ca.* server.*
-rm -v ca.* server.* jws.*
ca.key:
openssl genrsa -out $@ 1024
.INTERMEDIATE: ca.csr
ca.csr: openssl.cnf ca.key
openssl req -config openssl.cnf \
-new \
@@ -26,6 +26,7 @@ ca.crt: ca.csr ca.key
server.key:
openssl genrsa -out $@ 1024
.INTERMEDIATE: server.csr
server.csr: openssl.cnf server.key
openssl req -config openssl.cnf \
-new \

11
e2e_test/keys/testdata/ca.crt vendored Normal file
View File

@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBnTCCAQYCCQCuPrhkr+BvGzANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
ZWxsbyBDQTAeFw0xOTA4MTgwNjAwMDZaFw0xOTA5MTcwNjAwMDZaMBMxETAPBgNV
BAMMCEhlbGxvIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnSTDsRx4U
JmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ4Pc51itOycWN1nclNVb489i9
J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3Lu4mysxyOtYZNsdST8GBCP0m4
2tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
ACfgNePlOLnLz1zJrWN6RZ6q0a+SSK8HdgSiKSF66SBIRILFoQmapBLXRY9YyATt
cdgg7pOd1WGCMqlOnhL56c8X5n+j/LGM5hc9PaEJA5vru7EBrnbxCkg0n8yp4Swc
8KFV5IiZ5D8t03AHjrXLQg8/HRzTuFRJJ1nJmc+FbnjT
-----END CERTIFICATE-----

15
e2e_test/keys/testdata/ca.key vendored Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDnSTDsRx4UJmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ
4Pc51itOycWN1nclNVb489i9J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3L
u4mysxyOtYZNsdST8GBCP0m42tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQAB
AoGAaYmTYm29QvKW4et9oPxDjpYG0bqlz7P0xFRR9kKtKTATAMHjWeu2xFR/JI+b
rvJLIdZqHmWe5AmMb3NxZgfLonEB71ohaKQha1L8Vc7aoedRheJvqqaNr+ZxoCMO
8xcjsaMYxLEVt0tg6XyKyEhi1/hOufFZ4BSng4oQbrpaNIkCQQD6hPEzzPZtMEEe
eRdwTVUIStKFMQbRdwZ5Oc7pyDk2U+SFRJiqkBkqnmFekcf2UgbBQxem+GMhWNgE
LItKy/wVAkEA7FiHxbzn2msaE3hZCWudtnXqmJNuPO0zJ5icXe2svwmwPfLA/rm9
iazCuyzyK67J8IG9QgIjQFYXtQbMr2chGwJBAMm3dghBx0LQEf8Zfdf9TLSqmqyI
d3b+IgZGl+cCQ58NGfp863ibIsiAUuK0+4/JKItBHLBjXF6jjPx/aYFGkqkCQH4w
GnXCEYx1qJuCow87jR4xQQsrlC0lfC2E9t/TmWr6UkYRCWg3ZXJPcj0bl0Upcppd
ut22ZHniPZAizEBOcMcCQQDnMEOxufxhMsx2NC8yON/noewLINqKcbkMsl6DjvJl
+wLbQmzJ0j+uIBgdpsj4rWnEr7GxoL2eWG44QDUBco79
-----END RSA PRIVATE KEY-----

15
e2e_test/keys/testdata/jws.key vendored Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQCZukkN1GxMlNXkpOZxCnvCF874/rn1sNKzO98fOwmBRPG2+m/c
yqBY7t2L2nihqz3+GZTiHmzSrBzMAGPVW1qGmk9KYg3m7akz9SiCxdoUkgM9MCCp
X/s8IhgtXkyoKFPcGdwHblDl/3aJG02b6TAQD8vTNQAKoKw7L0FST+pvRwIDAQAB
AoGBAJT1fXR5MbfDQL+dSe6fSex5RYTgzzDTdldW3I1Wl487Tz0OzvYTIe0LCIJL
4DhHxnpCL5IsCSbav8ytVA+ZxczHpEW6UxbalXt5UfgFu0joTrdoGxDcVWgUCW3J
Olbln0lOP55wViKh509gt45Za3VxJrNul3khVfVj7qGG9cKBAkEAxwtT8LxwqTYF
nqoeZvPp15JAqlgdk38ttJa4KEqvpBTSxNIXkL9T5gJ+irKZAzxlz/U7bhn5mw6E
3xFiljOXpQJBAMW3XRFOjgNBXNjbt81wREF5LdZl9EI8cRMSH6xljt2uwSqw4EG3
76gFvccUd+WnfspFQZVypSSD4pWzsAqh13sCQQCA9BLW5Y7r4ab0a2y08JNwaT1h
3yKSO5QF6pu25uQyHpeKkj5YNcyKONV40EqXsRqZB10QcN2omlh1GJNRkm1NAkEA
qV3lr4mnRUqcinfM/4MINT3k8h/sGUFFa5y+3SMyOtwURMm3kRRLi5c/dmYmPug4
SHUDNU48AQeo9awzRShWOQJBAJdw+cfRgi4fo3HY33uZdFa1T9G+qTA2ijhco6O3
8tOc0yOFEtPNXM87MsHGIQP3ZCLfIY1gs2O3WCTFbPxR4rc=
-----END RSA PRIVATE KEY-----

View File

@@ -10,7 +10,7 @@ new_certs_dir = $dir
default_md = sha256
policy = policy_match
serial = $dir/serial
default_days = 365
default_days = 3650
[ policy_match ]
countryName = optional

52
e2e_test/keys/testdata/server.crt vendored Normal file
View File

@@ -0,0 +1,52 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 0 (0x0)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Hello CA
Validity
Not Before: Aug 18 06:00:06 2019 GMT
Not After : Aug 15 06:00:06 2029 GMT
Subject: CN=localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (1024 bit)
Modulus:
00:d6:4e:eb:3a:cb:25:f9:7e:92:22:f2:63:99:da:
08:05:8b:a3:e7:d3:fd:71:3e:bd:da:c5:d5:63:b7:
d3:7b:f8:cd:1a:2e:5c:a2:4f:48:98:c2:b4:da:e8:
1e:d3:d7:8f:d8:ee:a9:70:d0:9d:4f:f4:8d:95:e5:
8e:9a:71:b6:80:aa:0b:cb:28:1d:f6:0d:7e:aa:78:
bf:30:e6:58:d7:6b:92:8f:19:1c:7d:95:f8:d5:2f:
8c:58:49:98:88:05:50:88:80:a9:77:c4:16:b4:c1:
00:45:1e:d3:d0:ed:98:4d:f7:a3:5d:f1:82:cb:a5:
4d:19:64:4d:43:db:13:d4:17
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
X509v3 Subject Alternative Name:
DNS:localhost
Signature Algorithm: sha256WithRSAEncryption
5a:5c:5e:8b:de:82:86:f4:98:40:0e:cf:c5:51:fe:89:46:49:
f0:26:d2:a5:06:e3:91:43:c1:f8:b2:ad:b7:a1:23:13:1a:80:
45:00:51:70:b6:06:63:c6:a8:c8:22:5d:1b:00:e0:4a:8c:2e:
ce:b4:da:b1:89:8a:d2:d0:e3:eb:0f:16:34:45:a1:bd:64:5c:
48:41:8c:0a:bf:66:be:1c:a8:35:47:ce:b0:dc:c8:4f:5e:c1:
ec:ef:21:fb:45:55:95:e3:99:40:46:0b:6c:8a:b3:d5:f0:bf:
39:a4:ba:c4:d7:58:88:58:08:07:98:59:6e:ca:9c:08:e4:c4:
4f:db
-----BEGIN CERTIFICATE-----
MIIBzTCCATagAwIBAgIBADANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhIZWxs
byBDQTAeFw0xOTA4MTgwNjAwMDZaFw0yOTA4MTUwNjAwMDZaMBQxEjAQBgNVBAMM
CWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1k7rOssl+X6S
IvJjmdoIBYuj59P9cT692sXVY7fTe/jNGi5cok9ImMK02uge09eP2O6pcNCdT/SN
leWOmnG2gKoLyygd9g1+qni/MOZY12uSjxkcfZX41S+MWEmYiAVQiICpd8QWtMEA
RR7T0O2YTfejXfGCy6VNGWRNQ9sT1BcCAwEAAaMwMC4wCQYDVR0TBAIwADALBgNV
HQ8EBAMCBeAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4GB
AFpcXovegob0mEAOz8VR/olGSfAm0qUG45FDwfiyrbehIxMagEUAUXC2BmPGqMgi
XRsA4EqMLs602rGJitLQ4+sPFjRFob1kXEhBjAq/Zr4cqDVHzrDcyE9ewezvIftF
VZXjmUBGC2yKs9XwvzmkusTXWIhYCAeYWW7KnAjkxE/b
-----END CERTIFICATE-----

15
e2e_test/keys/testdata/server.key vendored Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDWTus6yyX5fpIi8mOZ2ggFi6Pn0/1xPr3axdVjt9N7+M0aLlyi
T0iYwrTa6B7T14/Y7qlw0J1P9I2V5Y6acbaAqgvLKB32DX6qeL8w5ljXa5KPGRx9
lfjVL4xYSZiIBVCIgKl3xBa0wQBFHtPQ7ZhN96Nd8YLLpU0ZZE1D2xPUFwIDAQAB
AoGBAJhNR7Dl1JwFzndViWE6aP7/6UEFEBWeADDs7aTLbFmrTJ+xmRWkgLRHk14L
HnVwuYLywaoyJ8o9wy1nEbxC2e4zWZ94d351MQf3/komCXDBzEsktfAcNsAFnMmS
HZuGXfhi0FYWoftpIGxUmEBmQRcq0ctycbLves6TY3y+oajpAkEA8UHmSr/zsM3E
XQXPp2BCAvRrTH/njk4R0jwB29Bi89gt/XDD4uvfWbHw7TZxnZuCpWisnxpMPIwa
1rjqIQmhEwJBAONncQUOxwYCIuvraIhV0QtkIUa+YpTvAxP8ZNXx+agtHmHG2TTf
kGv2YddvjxXZItN/FZOzUGm9OptaeLRTpW0CQHO8CEzNnoqve0agtgf2LlSaiiqt
pRhoLTZsYPvhEMcnapCNGvtt6bxul0REfOZ9poPRHhZJGE9naqydEnv80Y8CQQC3
pxLfws95SsBpR/VkJepuCK/XMmrrXRxfR7coEgROjiG7VZyV1vgMOS9Ljg1A19wI
cto6LtcCjpCGZsqU1/kBAkEAv2tXBts3vuIjguZNMz7KLWmu3zG2SQRaqdEZwL+R
DQmD5tbI6gEtd5OmgmSiW8A4mpfgFYvG7Um2fwi7TTXtSA==
-----END RSA PRIVATE KEY-----

View File

@@ -1,7 +1,7 @@
package logger
import (
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/logger"
)
func New(t testingLogger) *logger.Logger {

386
e2e_test/login_test.go Normal file
View File

@@ -0,0 +1,386 @@
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/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/pkg/di"
"github.com/int128/kubelogin/pkg/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)
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)
}
}

31
go.mod
View File

@@ -1,32 +1,23 @@
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/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
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
View File

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

View File

@@ -4,7 +4,7 @@ import (
"context"
"os"
"github.com/int128/kubelogin/di"
"github.com/int128/kubelogin/pkg/di"
)
var version = "HEAD"

View File

@@ -3,26 +3,27 @@ kind: Plugin
metadata:
name: oidc-login
spec:
shortDescription: Login for OpenID Connect authentication
homepage: https://github.com/int128/kubelogin
shortDescription: Log in to the OpenID Connect provider
description: |
This plugin gets a token from the OIDC provider and writes it to the kubeconfig.
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
Just run:
% kubectl oidc-login
## Credential plugin mode
kubectl executes oidc-login before calling the Kubernetes APIs.
oidc-login automatically opens the browser and you can log in to the provider.
After authentication, kubectl gets the token from oidc-login and you can access the cluster.
See https://github.com/int128/kubelogin#credential-plugin-mode for more.
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.
## Standalone mode
Run `kubectl oidc-login`.
It automatically opens the browser and you can log in to the provider.
After authentication, it writes the token to the kubeconfig and you can access the cluster.
See https://github.com/int128/kubelogin#standalone-mode for more.
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

211
pkg/adaptors/cmd/cmd.go Normal file
View File

@@ -0,0 +1,211 @@
package cmd
import (
"context"
"fmt"
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"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.
var Set = wire.NewSet(
wire.Struct(new(Cmd), "*"),
wire.Bind(new(adaptors.Cmd), new(*Cmd)),
)
const examples = ` # Login to the provider using the authorization code flow.
%[1]s
# Login to the provider using the resource owner password credentials flow.
%[1]s --username USERNAME --password PASSWORD
# Run as a credential plugin.
%[1]s get-token --oidc-issuer-url=https://issuer.example.com`
var defaultListenPort = []int{8000, 18000}
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
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 {
executable := filepath.Base(args[0])
rootCmd := newRootCmd(ctx, executable, cmd)
rootCmd.Version = version
rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
getTokenCmd := newGetTokenCmd(ctx, cmd)
rootCmd.AddCommand(getTokenCmd)
versionCmd := &cobra.Command{
Use: "version",
Short: "Print the version information",
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
cmd.Logger.Printf("%s version %s", executable, version)
},
}
rootCmd.AddCommand(versionCmd)
rootCmd.SetArgs(args[1:])
if err := rootCmd.Execute(); err != nil {
cmd.Logger.Printf("error: %s", err)
cmd.Logger.Debugf(1, "stacktrace: %+v", err)
return 1
}
return 0
}
// kubectlOptions represents kubectl specific options.
type kubectlOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
Verbose int
}
func (o *kubectlOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
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")
}
// loginOptions represents the options for Login use-case.
type loginOptions struct {
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
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
TokenCacheDir 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.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
}
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,
TokenCacheDir: o.TokenCacheDir,
}
if err := cmd.GetToken.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)
}
return nil
},
}
o.register(c.Flags())
return c
}

View File

@@ -0,0 +1,206 @@
package cmd
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/mock_usecases"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("login/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
ListenPort: defaultListenPort,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("login/FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("login/TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Login: mock_usecases.NewMockLogin(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("get-token/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_usecases.NewMockGetToken(ctrl)
getToken.EXPECT().
Do(ctx, usecases.GetTokenIn{
ListenPort: defaultListenPort,
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
GetToken: getToken,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("get-token/FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_usecases.NewMockGetToken(ctrl)
getToken.EXPECT().
Do(ctx, usecases.GetTokenIn{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
GetToken: getToken,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("get-token/MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
GetToken: mock_usecases.NewMockGetToken(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("get-token/TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
GetToken: mock_usecases.NewMockGetToken(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
}

View 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/pkg/adaptors"
"github.com/int128/kubelogin/pkg/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
}

View File

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

View File

@@ -4,10 +4,11 @@ import (
"context"
"time"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/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/pkg/adaptors Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,OIDCDecoder,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 {
FindByKey(dir string, key credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error)
Save(dir string, key credentialplugin.TokenCacheKey, cache credentialplugin.TokenCache) error
}
type CredentialPluginInteraction interface {
Write(out credentialplugin.Output) error
}
type OIDC interface {
New(ctx context.Context, config OIDCClientConfig) (OIDCClient, error)
}
@@ -32,7 +42,6 @@ type OIDCClientConfig struct {
type OIDCClient interface {
AuthenticateByCode(ctx context.Context, in OIDCAuthenticateByCodeIn) (*OIDCAuthenticateOut, error)
AuthenticateByPassword(ctx context.Context, in OIDCAuthenticateByPasswordIn) (*OIDCAuthenticateOut, error)
Verify(ctx context.Context, in OIDCVerifyIn) (*OIDCVerifyOut, error)
Refresh(ctx context.Context, in OIDCRefreshIn) (*OIDCAuthenticateOut, error)
}
@@ -58,26 +67,22 @@ type OIDCAuthenticateOut struct {
IDTokenClaims map[string]string // string representation of claims for logging
}
// OIDCVerifyIn represents an input DTO of OIDCClient.Verify.
type OIDCVerifyIn struct {
IDToken string
RefreshToken string
}
// OIDCVerifyIn represents an output DTO of OIDCClient.Verify.
type OIDCVerifyOut struct {
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
// OIDCRefreshIn represents an input DTO of OIDCClient.Refresh.
type OIDCRefreshIn struct {
RefreshToken string
}
type OIDCDecoder interface {
DecodeIDToken(t string) (*DecodedIDToken, error)
}
type DecodedIDToken struct {
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
type Env interface {
ReadPassword(prompt string) (string, error)
Exec(ctx context.Context, executable string, args []string) (int, error)
}
type Logger interface {

View File

@@ -2,7 +2,7 @@ package kubeconfig
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
// Set provides an implementation and interface for Kubeconfig.

View File

@@ -3,7 +3,7 @@ package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"

View File

@@ -5,7 +5,7 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)

View File

@@ -3,7 +3,7 @@ package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
)

View File

@@ -5,7 +5,7 @@ import (
"os"
"testing"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"os"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
// Set provides an implementation and interface for Logger.

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
type mockDebugLogger struct {

View File

@@ -2,7 +2,7 @@ package mock_adaptors
import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
func NewLogger(t testingLogger, ctrl *gomock.Controller) *Logger {

View File

@@ -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/pkg/adaptors (interfaces: Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,OIDCDecoder,Env,Logger)
// Package mock_adaptors is a generated GoMock package.
package mock_adaptors
@@ -7,8 +7,9 @@ package mock_adaptors
import (
context "context"
gomock "github.com/golang/mock/gomock"
adaptors "github.com/int128/kubelogin/adaptors"
kubeconfig "github.com/int128/kubelogin/models/kubeconfig"
adaptors "github.com/int128/kubelogin/pkg/adaptors"
credentialplugin "github.com/int128/kubelogin/pkg/models/credentialplugin"
kubeconfig "github.com/int128/kubelogin/pkg/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
}
// FindByKey mocks base method
func (m *MockTokenCacheRepository) FindByKey(arg0 string, arg1 credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error) {
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
ret0, _ := ret[0].(*credentialplugin.TokenCache)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindByKey indicates an expected call of FindByKey
func (mr *MockTokenCacheRepositoryMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockTokenCacheRepository)(nil).FindByKey), arg0, arg1)
}
// Save mocks base method
func (m *MockTokenCacheRepository) Save(arg0 string, arg1 credentialplugin.TokenCacheKey, arg2 credentialplugin.TokenCache) error {
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Save indicates an expected call of Save
func (mr *MockTokenCacheRepositoryMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockTokenCacheRepository)(nil).Save), arg0, arg1, arg2)
}
// 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
@@ -158,17 +242,40 @@ func (mr *MockOIDCClientMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockOIDCClient)(nil).Refresh), arg0, arg1)
}
// Verify mocks base method
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCVerifyOut)
// MockOIDCDecoder is a mock of OIDCDecoder interface
type MockOIDCDecoder struct {
ctrl *gomock.Controller
recorder *MockOIDCDecoderMockRecorder
}
// MockOIDCDecoderMockRecorder is the mock recorder for MockOIDCDecoder
type MockOIDCDecoderMockRecorder struct {
mock *MockOIDCDecoder
}
// NewMockOIDCDecoder creates a new mock instance
func NewMockOIDCDecoder(ctrl *gomock.Controller) *MockOIDCDecoder {
mock := &MockOIDCDecoder{ctrl: ctrl}
mock.recorder = &MockOIDCDecoderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDCDecoder) EXPECT() *MockOIDCDecoderMockRecorder {
return m.recorder
}
// DecodeIDToken mocks base method
func (m *MockOIDCDecoder) DecodeIDToken(arg0 string) (*adaptors.DecodedIDToken, error) {
ret := m.ctrl.Call(m, "DecodeIDToken", arg0)
ret0, _ := ret[0].(*adaptors.DecodedIDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Verify indicates an expected call of Verify
func (mr *MockOIDCClientMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDCClient)(nil).Verify), arg0, arg1)
// DecodeIDToken indicates an expected call of DecodeIDToken
func (mr *MockOIDCDecoderMockRecorder) DecodeIDToken(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeIDToken", reflect.TypeOf((*MockOIDCDecoder)(nil).DecodeIDToken), arg0)
}
// MockEnv is a mock of Env interface
@@ -194,19 +301,6 @@ func (m *MockEnv) EXPECT() *MockEnvMockRecorder {
return m.recorder
}
// Exec mocks base method
func (m *MockEnv) Exec(arg0 context.Context, arg1 string, arg2 []string) (int, error) {
ret := m.ctrl.Call(m, "Exec", arg0, arg1, arg2)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exec indicates an expected call of Exec
func (mr *MockEnvMockRecorder) Exec(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockEnv)(nil).Exec), arg0, arg1, arg2)
}
// ReadPassword mocks base method
func (m *MockEnv) ReadPassword(arg0 string) (string, error) {
ret := m.ctrl.Call(m, "ReadPassword", arg0)

View File

@@ -0,0 +1,53 @@
package oidc
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/int128/kubelogin/pkg/adaptors"
"golang.org/x/xerrors"
)
type Decoder struct{}
// DecodeIDToken returns the claims of the ID token.
// Note that this method does not verify the signature and always trust it.
func (d *Decoder) DecodeIDToken(t string) (*adaptors.DecodedIDToken, error) {
parts := strings.Split(t, ".")
if len(parts) != 3 {
return nil, xerrors.Errorf("token contains an invalid number of segments")
}
b, err := jwt.DecodeSegment(parts[1])
if err != nil {
return nil, xerrors.Errorf("could not decode the token: %w", err)
}
var claims jwt.StandardClaims
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&claims); err != nil {
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
}
var rawClaims map[string]interface{}
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&rawClaims); err != nil {
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
}
return &adaptors.DecodedIDToken{
IDTokenExpiry: time.Unix(claims.ExpiresAt, 0),
IDTokenClaims: dumpRawClaims(rawClaims),
}, nil
}
func dumpRawClaims(rawClaims map[string]interface{}) map[string]string {
claims := make(map[string]string)
for k, v := range rawClaims {
switch v.(type) {
case float64:
claims[k] = fmt.Sprintf("%.f", v.(float64))
default:
claims[k] = fmt.Sprintf("%v", v)
}
}
return claims
}

View File

@@ -0,0 +1,87 @@
package oidc
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
)
func TestDecoder_DecodeIDToken(t *testing.T) {
var decoder Decoder
t.Run("ValidToken", func(t *testing.T) {
expiry := time.Now().Round(time.Second)
idToken := newIDToken(t, "https://issuer.example.com", expiry)
decodedToken, err := decoder.DecodeIDToken(idToken)
if err != nil {
t.Fatalf("DecodeIDToken error: %s", err)
}
if decodedToken.IDTokenExpiry != expiry {
t.Errorf("IDTokenExpiry wants %s but %s", expiry, decodedToken.IDTokenExpiry)
}
t.Logf("IDTokenClaims=%+v", decodedToken.IDTokenClaims)
})
t.Run("InvalidToken", func(t *testing.T) {
decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE")
if err == nil {
t.Errorf("error wants non-nil but nil")
} else {
t.Logf("expected error: %+v", err)
}
if decodedToken != nil {
t.Errorf("decodedToken wants nil but %+v", decodedToken)
}
})
}
func newIDToken(t *testing.T, issuer string, expiry time.Time) string {
t.Helper()
claims := struct {
jwt.StandardClaims
Nonce string `json:"nonce"`
Groups []string `json:"groups"`
EmailVerified bool `json:"email_verified"`
}{
StandardClaims: jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
ExpiresAt: expiry.Unix(),
},
Nonce: "NONCE",
Groups: []string{"admin", "users"},
EmailVerified: false,
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(readPrivateKey(t, "testdata/jws.key"))
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func readPrivateKey(t *testing.T, name string) *rsa.PrivateKey {
t.Helper()
b, err := ioutil.ReadFile(name)
if err != nil {
t.Fatalf("could not read the file: %s", err)
}
block, rest := pem.Decode(b)
if block == nil {
t.Fatalf("could not decode PEM")
}
if len(rest) > 0 {
t.Fatalf("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatalf("could not parse the key: %s", err)
}
return k
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
)
const (
@@ -24,7 +24,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the request: %s", err)
t.Logger.Debugf(logLevelDumpHeaders, "could not dump the request: %s", err)
return t.Base.RoundTrip(req)
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
@@ -34,7 +34,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
}
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the response: %s", err)
t.Logger.Debugf(logLevelDumpHeaders, "could not dump the response: %s", err)
return resp, err
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))

View File

@@ -8,8 +8,8 @@ import (
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
)
type mockTransport struct {

View File

@@ -6,22 +6,33 @@ import (
"encoding/binary"
"fmt"
"net/http"
"os"
"time"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/oidc/logging"
"github.com/int128/kubelogin/adaptors/oidc/tls"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/oidc/logging"
"github.com/int128/kubelogin/pkg/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), "*"),
wire.Bind(new(adaptors.OIDC), new(*Factory)),
wire.Struct(new(Decoder)),
wire.Bind(new(adaptors.OIDCDecoder), new(*Decoder)),
)
type Factory struct {
@@ -71,11 +82,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 +141,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)
@@ -153,32 +167,9 @@ func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAut
}, nil
}
// 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)
}
verifier := c.provider.Verifier(&oidc.Config{
ClientID: c.oauth2Config.ClientID,
SkipExpiryCheck: true,
})
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
}
return &adaptors.OIDCVerifyOut{
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
// 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,
@@ -212,17 +203,5 @@ func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adapt
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
claims := make(map[string]string)
for k, v := range rawClaims {
switch v.(type) {
case float64:
claims[k] = fmt.Sprintf("%f", v.(float64))
default:
claims[k] = fmt.Sprintf("%s", v)
}
}
if err != nil {
return claims, xerrors.Errorf("error while decoding the ID token: %w", err)
}
return claims, nil
return dumpRawClaims(rawClaims), err
}

8
pkg/adaptors/oidc/testdata/Makefile vendored Normal file
View File

@@ -0,0 +1,8 @@
all: jws.key
jws.key:
openssl genrsa -out $@ 1024
.PHONY: clean
clean:
-rm -v jws.key

15
pkg/adaptors/oidc/testdata/jws.key vendored Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCrH34yA/f/sBOUlkYnRtd2jgDZ3WivhidqvoQaa73xqTazbkn6
GZ9r7jx0CGLRV2bmErj2WoyT54yrhezrKh0YXAHlrwLdsmV4dwiV0lOfUJd9P/vF
e2hiAWv4CcO9ZuNkTsrxM5W8Wdj2tjqOvsIn4We+HWPkpknT7VtT5RrumwIDAQAB
AoGAFqy5oA7+kZbXQV0YNqQgcMkoO7Ym5Ps1xeMwxf94z8jIQsZebxFuGnMa95UU
4wBd1ias85fUANUxwpigaBjQee5Hk+dnfUe1snUWYNm9H6tKrXEF8ajer3a2knEv
GfK0CSEumFougfW2xG88ChGTS60wc+MIRfXERCvWpGm/5EECQQDdv5IBSi89g/R1
5AGZKFCoqr6Zw5bWEKPzCCYJZzncR1ER9vP2AnMExM8Io/87WYvmpZIUrXJvQYm8
hkfVOcBZAkEAxY4VcqmRWru3zmnbj21MwcwtgESaONkWsHeYs1C/Y/3zt7TuelYz
ZJ9aUuUsaiJLEs9Y26nMt0L0snWGr2noEwJBANaDp1PWFyMUTt3pB17JcFXqb15C
pt1I1cGapWk9Uez1lMijNNhNAEWhuoKqW5Nnif5DN7EHJYfZR8x3vm/YYWkCQHAA
0iAkCwjKDLe2RIjYiwAE5ncmbdl1GuwJokVnrlrei+LHbb1mSdTuk6MT006JCs8r
R1GivzHXgCv9fdLN1IkCQHxRvv9RPND80eEkdMv4qu0s22OLRhLQ/pb+YeT5Cjjv
pJYWKrvXdRZcuNde9JiiTgK2UW1wM8KeD/EGvK2yF6M=
-----END RSA PRIVATE KEY-----

View File

@@ -1,9 +1,8 @@
.PHONY: clean
all: ca1.crt ca1.crt.base64 ca2.crt ca2.crt.base64 ca3.crt ca3.crt.base64
.PHONY: clean
clean:
rm -v *.key *.csr *.crt *.base64
-rm -v *.key *.csr *.crt *.base64
%.key:
openssl genrsa -out $@ 1024

View File

@@ -6,7 +6,7 @@ import (
"encoding/base64"
"io/ioutil"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/pkg/adaptors"
"golang.org/x/xerrors"
)

View File

@@ -4,9 +4,9 @@ import (
"io/ioutil"
"testing"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/e2e_test/logger"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
)
func TestNewConfig(t *testing.T) {

View File

@@ -0,0 +1,64 @@
package tokencache
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/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)),
)
// Repository provides access to the token cache on the local filesystem.
// Filename of a token cache is sha256 digest of the issuer, zero-character and client ID.
type Repository struct{}
func (r *Repository) FindByKey(dir string, key credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error) {
filename := filepath.Join(dir, computeFilename(key))
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 (r *Repository) Save(dir string, key credentialplugin.TokenCacheKey, cache credentialplugin.TokenCache) error {
if err := os.MkdirAll(dir, 0700); err != nil {
return xerrors.Errorf("could not create directory %s: %w", dir, err)
}
filename := filepath.Join(dir, computeFilename(key))
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(&cache); err != nil {
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
}
return nil
}
func computeFilename(key credentialplugin.TokenCacheKey) string {
s := sha256.New()
_, _ = s.Write([]byte(key.IssuerURL))
_, _ = s.Write([]byte{0x00})
_, _ = s.Write([]byte(key.ClientID))
return hex.EncodeToString(s.Sum(nil))
}

View File

@@ -0,0 +1,81 @@
package tokencache
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/go-test/deep"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
)
func TestRepository_FindByKey(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)
}
}()
key := credentialplugin.TokenCacheKey{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
}
json := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}`
filename := filepath.Join(dir, computeFilename(key))
if err := ioutil.WriteFile(filename, []byte(json), 0600); err != nil {
t.Fatalf("could not write to the temp file: %s", err)
}
tokenCache, err := r.FindByKey(dir, key)
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_Save(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)
}
}()
key := credentialplugin.TokenCacheKey{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
}
tokenCache := credentialplugin.TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Save(dir, key, tokenCache); err != nil {
t.Errorf("err wants nil but %+v", err)
}
filename := filepath.Join(dir, computeFilename(key))
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)
}
})
}

57
pkg/di/di.go Normal file
View File

@@ -0,0 +1,57 @@
//+build wireinject
// Package di provides dependency injection.
package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
credentialPluginAdaptor "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/auth"
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/login"
)
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() adaptors.Cmd {
wire.Build(
auth.Set,
auth.ExtraSet,
login.Set,
credentialPluginUseCase.Set,
cmd.Set,
env.Set,
kubeconfig.Set,
tokencache.Set,
credentialPluginAdaptor.Set,
oidc.Set,
logger.Set,
)
return nil
}
// 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
}

96
pkg/di/wire_gen.go Normal file
View File

@@ -0,0 +1,96 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package di
import (
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/env"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/auth"
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/login"
)
// Injectors from di.go:
func NewCmd() adaptors.Cmd {
adaptorsLogger := logger.New()
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
decoder := &oidc.Decoder{}
envEnv := &env.Env{}
showLocalServerURL := &auth.ShowLocalServerURL{
Logger: adaptorsLogger,
}
authentication := &auth.Authentication{
OIDC: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
loginLogin := &login.Login{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: adaptorsLogger,
}
repository := &tokencache.Repository{}
interaction := &credentialplugin.Interaction{}
getToken := &credentialplugin2.GetToken{
Authentication: authentication,
TokenCacheRepository: repository,
Interaction: interaction,
Logger: adaptorsLogger,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
GetToken: getToken,
Logger: adaptorsLogger,
}
return cmdCmd
}
func NewCmdForHeadless(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL, credentialPluginInteraction adaptors.CredentialPluginInteraction) adaptors.Cmd {
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
decoder := &oidc.Decoder{}
envEnv := &env.Env{}
authentication := &auth.Authentication{
OIDC: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
loginLogin := &login.Login{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: adaptorsLogger,
}
repository := &tokencache.Repository{}
getToken := &credentialplugin2.GetToken{
Authentication: authentication,
TokenCacheRepository: repository,
Interaction: credentialPluginInteraction,
Logger: adaptorsLogger,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
GetToken: getToken,
Logger: adaptorsLogger,
}
return cmdCmd
}

View File

@@ -0,0 +1,22 @@
// Package credentialplugin provides models for the credential plugin.
package credentialplugin
import "time"
// TokenCacheKey represents a key of a token cache.
type TokenCacheKey struct {
IssuerURL string
ClientID string
}
// TokenCache represents a token cache.
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
}

View File

@@ -5,8 +5,8 @@ import (
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/usecases"
"golang.org/x/xerrors"
)
@@ -39,14 +39,38 @@ const passwordPrompt = "Password: "
//
type Authentication struct {
OIDC adaptors.OIDC
OIDCDecoder adaptors.OIDCDecoder
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
}
func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
if in.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "checking expiration of the existing token")
// Skip verification of the token to reduce time of a discovery request.
// Here it trusts the signature and claims and checks only expiration,
// because the token has been verified before caching.
token, err := u.OIDCDecoder.DecodeIDToken(in.OIDCConfig.IDToken)
if err != nil {
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
}
if token.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
u.Logger.Debugf(1, "you already have a valid token until %s", token.IDTokenExpiry)
return &usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: in.OIDCConfig.IDToken,
RefreshToken: in.OIDCConfig.RefreshToken,
IDTokenExpiry: token.IDTokenExpiry,
IDTokenClaims: token.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "you have an expired token at %s", token.IDTokenExpiry)
}
u.Logger.Debugf(1, "initializing an OIDC client")
client, err := u.OIDC.New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
Config: in.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
@@ -54,29 +78,10 @@ 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 err != nil {
return nil, xerrors.Errorf("invalid ID token in the kubeconfig, you need to remove it 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")
return &usecases.AuthenticationOut{
AlreadyHasValidIDToken: true,
IDToken: in.CurrentAuthProvider.OIDCConfig.IDToken,
RefreshToken: in.CurrentAuthProvider.OIDCConfig.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "You have an expired token at %s", out.IDTokenExpiry)
}
if in.CurrentAuthProvider.OIDCConfig.RefreshToken != "" {
u.Logger.Debugf(1, "Refreshing the token")
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{
@@ -86,11 +91,11 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "Could not refresh the token: %s", err)
u.Logger.Debugf(1, "could not refresh the token: %s", err)
}
if in.Username == "" {
u.Logger.Debugf(1, "Performing the authentication code flow")
u.Logger.Debugf(1, "performing the authentication code flow")
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
@@ -107,7 +112,7 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
}, nil
}
u.Logger.Debugf(1, "Performing the resource owner password credentials flow")
u.Logger.Debugf(1, "performing the resource owner password credentials flow")
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {

View File

@@ -7,10 +7,10 @@ import (
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"golang.org/x/xerrors"
)
@@ -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,30 +214,23 @@ 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)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "VALID_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("VALID_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
Config: in.CurrentAuthProvider.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDC: mock_adaptors.NewMockOIDC(ctrl),
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
@@ -267,22 +252,21 @@ 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)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: "VALID_REFRESH_TOKEN",
@@ -296,12 +280,13 @@ 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{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDC: mockOIDC,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {
@@ -324,22 +309,21 @@ 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)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
Return(&adaptors.OIDCVerifyOut{
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: "EXPIRED_REFRESH_TOKEN",
@@ -358,12 +342,13 @@ 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{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDC: mockOIDC,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, in)
if err != nil {

View File

@@ -0,0 +1,77 @@
// 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/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/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")
u.Logger.Debugf(1, "finding a token from cache directory %s", in.TokenCacheDir)
cacheKey := credentialplugin.TokenCacheKey{IssuerURL: in.IssuerURL, ClientID: in.ClientID}
cache, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, cacheKey)
if err != nil {
u.Logger.Debugf(1, "could not find a token cache: %s", err)
cache = &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: cache.IDToken,
RefreshToken: cache.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, "the ID token has the claim: %s=%v", k, v)
}
if !out.AlreadyHasValidIDToken {
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
cache := credentialplugin.TokenCache{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
}
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, cacheKey, cache); err != nil {
return xerrors.Errorf("could not write the token cache: %w", err)
}
}
u.Logger.Debugf(1, "writing the token to client-go")
if err := u.Interaction.Write(credentialplugin.Output{Token: out.IDToken, Expiry: out.IDTokenExpiry}); err != nil {
return xerrors.Errorf("could not write the token to client-go: %w", err)
}
return nil
}

View File

@@ -0,0 +1,181 @@
package credentialplugin
import (
"context"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/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",
TokenCacheDir: "/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().
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
Return(nil, xerrors.New("file not found"))
tokenCacheRepository.EXPECT().
Save("/path/to/token-cache",
credentialplugin.TokenCacheKey{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
},
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",
TokenCacheDir: "/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().
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
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",
TokenCacheDir: "/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().
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
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")
}
})
}

View File

@@ -4,10 +4,10 @@ import (
"context"
"time"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/pkg/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/pkg/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
TokenCacheDir 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.

View File

@@ -4,17 +4,15 @@ import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/usecases"
"golang.org/x/xerrors"
)
// 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?
@@ -44,23 +42,23 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Printf(oidcConfigErrorMessage)
return xerrors.Errorf("could not find the current authentication provider: %w", err)
}
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)
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,
OIDCConfig: authProvider.OIDCConfig,
SkipOpenBrowser: in.SkipOpenBrowser,
ListenPort: in.ListenPort,
Username: in.Username,
Password: in.Password,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return xerrors.Errorf("error while authentication: %w", err)
}
for k, v := range out.IDTokenClaims {
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
u.Logger.Debugf(1, "the 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)
@@ -70,7 +68,7 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
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)
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)
}

View File

@@ -6,10 +6,10 @@ import (
"time"
"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"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/mock_usecases"
"golang.org/x/xerrors"
)
@@ -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",

View File

@@ -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/pkg/usecases (interfaces: Login,GetToken,Authentication)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
@@ -7,7 +7,7 @@ package mock_usecases
import (
context "context"
gomock "github.com/golang/mock/gomock"
usecases "github.com/int128/kubelogin/usecases"
usecases "github.com/int128/kubelogin/pkg/usecases"
reflect "reflect"
)
@@ -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

View File

@@ -1,77 +0,0 @@
package login
import (
"context"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"golang.org/x/xerrors"
)
// Exec provide the use case of transparently executing kubectl.
//
// If the current auth provider is not oidc, just run kubectl.
// If the kubeconfig has a valid token, just run kubectl.
// Otherwise, update the kubeconfig and run kubectl.
//
type Exec struct {
Authentication usecases.Authentication
Kubeconfig adaptors.Kubeconfig
Env adaptors.Env
Logger adaptors.Logger
}
func (u *Exec) Do(ctx context.Context, in usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
if err := u.login(ctx, in.LoginIn); err != nil {
return nil, xerrors.Errorf("could not log in to the provider: %w", err)
}
u.Logger.Debugf(1, "Executing the command %s %s", in.Executable, in.Args)
exitCode, err := u.Env.Exec(ctx, in.Executable, in.Args)
if err != nil {
return nil, xerrors.Errorf("could not execute kubectl: %w", err)
}
u.Logger.Debugf(1, "The command exited with status %d", exitCode)
return &usecases.LoginAndExecOut{ExitCode: exitCode}, nil
}
func (u *Exec) login(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
authProvider, err := u.Kubeconfig.GetCurrentAuthProvider(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
u.Logger.Debugf(1, "The current authentication provider is not oidc: %s", err)
return nil
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", authProvider.UserName)
u.Logger.Debugf(1, "A token will be written to %s", authProvider.LocationOfOrigin)
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
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
}

View File

@@ -1,283 +0,0 @@
package login
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/mock_usecases"
"golang.org/x/xerrors"
)
func TestExec_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginAndExecIn{
Executable: "kubectl",
Args: []string{"foo", "bar"},
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
ListenPort: []int{10000},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
},
}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(currentAuthProvider, nil)
mockKubeconfig.EXPECT().
UpdateAuthProvider(&kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
})
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(123, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{
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)
}
})
}