mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-19 19:09:50 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b08a49a51 | ||
|
|
9c74f3748b | ||
|
|
17e03f2abc | ||
|
|
ebef81f9d7 | ||
|
|
8d0d82fb71 | ||
|
|
b600e54a12 | ||
|
|
75317f88a1 | ||
|
|
5a794e8ceb | ||
|
|
1fe1ec4c20 | ||
|
|
7676ffbfab | ||
|
|
7e1e6a096b | ||
|
|
4d3d1c3b78 | ||
|
|
1ebdfc0e4f | ||
|
|
9c67c52b34 | ||
|
|
550396e1dd | ||
|
|
34f0578b59 | ||
|
|
604d118b68 | ||
|
|
91959e8a56 |
@@ -2,24 +2,26 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/int128/kubelogin
|
||||
- image: circleci/golang:1.11.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d ./...
|
||||
- run: go get github.com/golang/lint/golint
|
||||
- run: go get -u golang.org/x/lint/golint
|
||||
- run: golint
|
||||
# Workaround for https://github.com/golang/go/issues/27925
|
||||
- run: go get -v k8s.io/client-go
|
||||
- run: go vet
|
||||
- run: go build -v
|
||||
- run: make -C e2e/authserver/testdata
|
||||
- run: go test -v ./...
|
||||
- run: make -C cli_test/authserver/testdata
|
||||
- run: go test -v -race ./...
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/int128/kubelogin
|
||||
- image: circleci/golang:1.11.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d ./...
|
||||
# Workaround for https://github.com/golang/go/issues/27925
|
||||
- run: go get -v k8s.io/client-go
|
||||
- run: go vet
|
||||
- run: curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
workflows:
|
||||
|
||||
258
README.md
258
README.md
@@ -3,193 +3,56 @@
|
||||
This is a command for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
It gets a token from the OIDC provider and writes it to the kubeconfig.
|
||||
|
||||
This may work with various OIDC providers such as Keycloak, Google Identity Platform and Azure AD.
|
||||
|
||||
|
||||
## TL;DR
|
||||
|
||||
You need to setup the OIDC provider and [Kubernetes OIDC authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
You need to setup the following components:
|
||||
|
||||
After initial setup or when the token has been expired, just run `kubelogin`:
|
||||
- OIDC provider
|
||||
- Kubernetes API server
|
||||
- Role for your group or user
|
||||
- kubectl authentication
|
||||
|
||||
You can install this by brew tap or from the [releases](https://github.com/int128/kubelogin/releases).
|
||||
|
||||
```sh
|
||||
brew tap int128/kubelogin
|
||||
brew install kubelogin
|
||||
```
|
||||
|
||||
After initial setup or when the token has been expired, just run `kubelogin`.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
2018/08/27 15:03:06 Reading /home/user/.kube/config
|
||||
2018/08/27 15:03:06 Using current context: hello.k8s.local
|
||||
2018/08/27 15:03:07 Open http://localhost:8000 for authorization
|
||||
```
|
||||
|
||||
It opens the browser and you can log in to the provider.
|
||||
After you logged in to the provider, it closes the browser automatically.
|
||||
|
||||
Then it writes the ID token and refresh token to the kubeconfig.
|
||||
|
||||
```
|
||||
2018/08/27 15:03:07 GET /
|
||||
2018/08/27 15:03:08 GET /?state=a51081925f20c043&session_state=5637cbdf-ffdc-4fab-9fc7-68a3e6f2e73f&code=ey...
|
||||
2018/08/27 15:03:09 Got token for subject=cf228a73-47fe-4986-a2a8-b2ced80a884b
|
||||
2018/08/27 15:03:09 Updated /home/user/.kube/config
|
||||
```
|
||||
|
||||
Please see the later section for details.
|
||||
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.
|
||||
|
||||
For more, see the following documents:
|
||||
|
||||
## Getting Started with Google Account
|
||||
|
||||
### 1. Setup Google API
|
||||
|
||||
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client as follows:
|
||||
|
||||
- Application Type: Web application
|
||||
- Redirect URL: `http://localhost:8000/`
|
||||
|
||||
### 2. Setup Kubernetes cluster
|
||||
|
||||
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://accounts.google.com
|
||||
oidcClientID: YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
```
|
||||
|
||||
Here assign the `cluster-admin` role to your user.
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: oidc-admin-group
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: https://accounts.google.com#1234567890
|
||||
```
|
||||
|
||||
### 3. Setup kubectl and kubelogin
|
||||
|
||||
Setup `kubectl` to authenticate with your identity provider.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials NAME \
|
||||
--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
|
||||
```
|
||||
|
||||
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
|
||||
|
||||
Run `kubelogin` and open http://localhost:8000 in your browser.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
2018/08/10 10:36:38 Reading .kubeconfig
|
||||
2018/08/10 10:36:38 Using current context: hello.k8s.local
|
||||
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
|
||||
2018/08/10 10:36:45 GET /
|
||||
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
|
||||
2018/08/10 10:37:08 Updated .kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
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
|
||||
```
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
## Getting Started with Keycloak
|
||||
|
||||
### 1. Setup Keycloak
|
||||
|
||||
Create an OIDC client as follows:
|
||||
|
||||
- Redirect URL: `http://localhost:8000/`
|
||||
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
|
||||
- Client ID: `kubernetes`
|
||||
- Groups claim: `groups`
|
||||
|
||||
Then create a group `kubernetes:admin` and join to it.
|
||||
|
||||
### 2. Setup Kubernetes cluster
|
||||
|
||||
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
oidcClientID: kubernetes
|
||||
oidcGroupsClaim: groups
|
||||
```
|
||||
|
||||
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
|
||||
|
||||
```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: Group
|
||||
name: /kubernetes:admin
|
||||
```
|
||||
|
||||
### 3. Setup kubectl and kubelogin
|
||||
|
||||
Setup `kubectl` to authenticate with your identity provider.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials NAME \
|
||||
--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
|
||||
```
|
||||
|
||||
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it.
|
||||
|
||||
Run `kubelogin` and make sure you can access to the cluster.
|
||||
See the previous section for details.
|
||||
- [Getting Started with Keycloak](docs/keycloak.md)
|
||||
- [Getting Started with Google Identity Platform](docs/google.md)
|
||||
- [Team Operation](docs/team_ops.md)
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
This supports the following options.
|
||||
|
||||
```
|
||||
kubelogin [OPTIONS]
|
||||
|
||||
Application Options:
|
||||
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
|
||||
--listen-port= Port used by kubelogin to bind its webserver (default: 8000) [$KUBELOGIN_LISTEN_PORT]
|
||||
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
|
||||
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
|
||||
@@ -198,8 +61,8 @@ Help Options:
|
||||
-h, --help Show this help message
|
||||
```
|
||||
|
||||
This supports the following keys of `auth-provider` in kubeconfig.
|
||||
See also [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
|
||||
This also supports the following keys of `auth-provider` in kubeconfig.
|
||||
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
|
||||
|
||||
Key | Direction | Value
|
||||
----|-----------|------
|
||||
@@ -222,62 +85,37 @@ Default to `~/.kube/config`.
|
||||
export KUBECONFIG="$PWD/.kubeconfig"
|
||||
```
|
||||
|
||||
### Team onboarding
|
||||
|
||||
You can share the kubeconfig to your team members for easy setup.
|
||||
### Extra scopes
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
If you are using kops, export the kubeconfig and edit it.
|
||||
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
|
||||
|
||||
```sh
|
||||
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
|
||||
vim .kubeconfig
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
|
||||
```
|
||||
|
||||
Note that kubectl does not accept multiple scopes and 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 set your self-signed certificates for the OIDC provider (not Kubernetes API server) by `idp-certificate-authority` and `idp-certificate-authority-data` in the kubeconfig.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak \
|
||||
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
|
||||
```
|
||||
|
||||
If kubelogin could not parse the certificate, it shows a warning and skips it.
|
||||
|
||||
|
||||
## Contributions
|
||||
|
||||
This is an open source software licensed under Apache License 2.0.
|
||||
Feel free to open issues and pull requests.
|
||||
|
||||
### Build and Test
|
||||
|
||||
```sh
|
||||
go get github.com/int128/kubelogin
|
||||
```
|
||||
|
||||
```sh
|
||||
cd $GOPATH/src/github.com/int128/kubelogin
|
||||
make -C e2e/authserver/testdata
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
### Release
|
||||
|
||||
CircleCI publishes the build to GitHub.
|
||||
See [.circleci/config.yml](.circleci/config.yml).
|
||||
Feel free to open issues and pull requests for improving code and documents.
|
||||
|
||||
110
auth/authcode.go
110
auth/authcode.go
@@ -1,110 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type authCodeFlow struct {
|
||||
Config *oauth2.Config
|
||||
AuthCodeOptions []oauth2.AuthCodeOption
|
||||
ServerPort int // HTTP server port
|
||||
SkipOpenBrowser bool // skip opening browser if true
|
||||
}
|
||||
|
||||
func (f *authCodeFlow) getToken(ctx context.Context) (*oauth2.Token, error) {
|
||||
code, err := f.getAuthCode(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not get an auth code: %s", err)
|
||||
}
|
||||
token, err := f.Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not exchange token: %s", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (f *authCodeFlow) getAuthCode(ctx context.Context) (string, error) {
|
||||
state, err := generateState()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not generate state parameter: %s", err)
|
||||
}
|
||||
codeCh := make(chan string)
|
||||
defer close(codeCh)
|
||||
errCh := make(chan error)
|
||||
defer close(errCh)
|
||||
server := http.Server{
|
||||
Addr: fmt.Sprintf("localhost:%d", f.ServerPort),
|
||||
Handler: &authCodeHandler{
|
||||
authCodeURL: f.Config.AuthCodeURL(state, f.AuthCodeOptions...),
|
||||
gotCode: func(code string, gotState string) {
|
||||
if gotState == state {
|
||||
codeCh <- code
|
||||
} else {
|
||||
errCh <- fmt.Errorf("State does not match, wants %s but %s", state, gotState)
|
||||
}
|
||||
},
|
||||
gotError: func(err error) {
|
||||
errCh <- err
|
||||
},
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
log.Printf("Open http://localhost:%d for authorization", f.ServerPort)
|
||||
if !f.SkipOpenBrowser {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
browser.OpenURL(fmt.Sprintf("http://localhost:%d/", f.ServerPort))
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
server.Shutdown(ctx)
|
||||
return "", err
|
||||
case code := <-codeCh:
|
||||
server.Shutdown(ctx)
|
||||
return code, nil
|
||||
case <-ctx.Done():
|
||||
server.Shutdown(ctx)
|
||||
return "", ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
type authCodeHandler struct {
|
||||
authCodeURL string
|
||||
gotCode func(code string, state string)
|
||||
gotError func(err error)
|
||||
}
|
||||
|
||||
func (h *authCodeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s", r.Method, r.RequestURI)
|
||||
m := r.Method
|
||||
p := r.URL.Path
|
||||
q := r.URL.Query()
|
||||
switch {
|
||||
case m == "GET" && p == "/" && q.Get("error") != "":
|
||||
h.gotError(fmt.Errorf("OAuth Error: %s %s", q.Get("error"), q.Get("error_description")))
|
||||
http.Error(w, "OAuth Error", 500)
|
||||
|
||||
case m == "GET" && p == "/" && q.Get("code") != "":
|
||||
h.gotCode(q.Get("code"), q.Get("state"))
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<html><body>OK<script>window.close()</script></body></html>`)
|
||||
|
||||
case m == "GET" && p == "/":
|
||||
http.Redirect(w, r, h.authCodeURL, 302)
|
||||
|
||||
default:
|
||||
http.Error(w, "Not Found", 404)
|
||||
}
|
||||
}
|
||||
23
auth/oidc.go
23
auth/oidc.go
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"github.com/int128/oauth2cli"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -23,7 +24,7 @@ type Config struct {
|
||||
ClientSecret string
|
||||
ExtraScopes []string // Additional scopes
|
||||
Client *http.Client // HTTP client for oidc and oauth2
|
||||
ServerPort int // HTTP server port
|
||||
LocalServerPort int // HTTP server port
|
||||
SkipOpenBrowser bool // skip opening browser if true
|
||||
}
|
||||
|
||||
@@ -36,20 +37,18 @@ func (c *Config) GetTokenSet(ctx context.Context) (*TokenSet, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not discovery the OIDC issuer: %s", err)
|
||||
}
|
||||
oauth2Config := &oauth2.Config{
|
||||
Endpoint: provider.Endpoint(),
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
Scopes: append(c.ExtraScopes, oidc.ScopeOpenID),
|
||||
RedirectURL: fmt.Sprintf("http://localhost:%d/", c.ServerPort),
|
||||
}
|
||||
flow := &authCodeFlow{
|
||||
ServerPort: c.ServerPort,
|
||||
flow := oauth2cli.AuthCodeFlow{
|
||||
Config: oauth2.Config{
|
||||
Endpoint: provider.Endpoint(),
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
Scopes: append(c.ExtraScopes, oidc.ScopeOpenID),
|
||||
},
|
||||
LocalServerPort: c.LocalServerPort,
|
||||
SkipOpenBrowser: c.SkipOpenBrowser,
|
||||
Config: oauth2Config,
|
||||
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
|
||||
}
|
||||
token, err := flow.getToken(ctx)
|
||||
token, err := flow.GetToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not get a token: %s", err)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateState() (string, error) {
|
||||
var n uint64
|
||||
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", n), nil
|
||||
}
|
||||
@@ -32,6 +32,7 @@ func Parse(osArgs []string, version string) (*CLI, error) {
|
||||
// CLI represents an interface of this command.
|
||||
type CLI struct {
|
||||
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
|
||||
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
|
||||
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
|
||||
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
|
||||
}
|
||||
@@ -68,17 +69,14 @@ func (c *CLI) Run(ctx context.Context) error {
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`,
|
||||
err, cfg.CurrentContext)
|
||||
}
|
||||
tlsConfig, err := c.tlsConfig(authProvider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not configure TLS: %s", err)
|
||||
}
|
||||
tlsConfig := c.tlsConfig(authProvider)
|
||||
authConfig := &auth.Config{
|
||||
Issuer: authProvider.IDPIssuerURL(),
|
||||
ClientID: authProvider.ClientID(),
|
||||
ClientSecret: authProvider.ClientSecret(),
|
||||
ExtraScopes: authProvider.ExtraScopes(),
|
||||
Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}},
|
||||
ServerPort: 8000,
|
||||
LocalServerPort: c.ListenPort,
|
||||
SkipOpenBrowser: c.SkipOpenBrowser,
|
||||
}
|
||||
token, err := authConfig.GetTokenSet(ctx)
|
||||
|
||||
49
cli/tls.go
49
cli/tls.go
@@ -11,32 +11,47 @@ import (
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
)
|
||||
|
||||
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProvider) (*tls.Config, error) {
|
||||
func (c *CLI) tlsConfig(authProvider *kubeconfig.OIDCAuthProvider) *tls.Config {
|
||||
p := x509.NewCertPool()
|
||||
if ca := authProvider.IDPCertificateAuthority(); ca != "" {
|
||||
b, err := ioutil.ReadFile(ca)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read %s: %s", ca, err)
|
||||
if err := appendCertFile(p, ca); err != nil {
|
||||
log.Printf("Skip CA certificate of idp-certificate-authority: %s", err)
|
||||
} else {
|
||||
log.Printf("Using CA certificate: %s", ca)
|
||||
}
|
||||
if p.AppendCertsFromPEM(b) != true {
|
||||
return nil, fmt.Errorf("Could not append CA certificate from %s", ca)
|
||||
}
|
||||
log.Printf("Using CA certificate: %s", ca)
|
||||
}
|
||||
if ca := authProvider.IDPCertificateAuthorityData(); ca != "" {
|
||||
b, err := base64.StdEncoding.DecodeString(ca)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not decode idp-certificate-authority-data: %s", err)
|
||||
if err := appendCertData(p, ca); err != nil {
|
||||
log.Printf("Skip CA certificate of idp-certificate-authority-data: %s", err)
|
||||
} else {
|
||||
log.Printf("Using CA certificate of idp-certificate-authority-data")
|
||||
}
|
||||
if p.AppendCertsFromPEM(b) != true {
|
||||
return nil, fmt.Errorf("Could not append CA certificate from idp-certificate-authority-data")
|
||||
}
|
||||
log.Printf("Using CA certificate: idp-certificate-authority-data")
|
||||
}
|
||||
|
||||
cfg := &tls.Config{InsecureSkipVerify: c.SkipTLSVerify}
|
||||
if len(p.Subjects()) > 0 {
|
||||
cfg.RootCAs = p
|
||||
}
|
||||
return cfg, nil
|
||||
return cfg
|
||||
}
|
||||
|
||||
func appendCertFile(p *x509.CertPool, name string) error {
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not read %s: %s", name, err)
|
||||
}
|
||||
if p.AppendCertsFromPEM(b) != true {
|
||||
return fmt.Errorf("Could not append certificate from %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendCertData(p *x509.CertPool, data string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not decode base64: %s", err)
|
||||
}
|
||||
if p.AppendCertsFromPEM(b) != true {
|
||||
return fmt.Errorf("Could not append certificate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package e2e
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/cli"
|
||||
"github.com/int128/kubelogin/e2e/authserver"
|
||||
"github.com/int128/kubelogin/cli_test/authserver"
|
||||
"github.com/int128/kubelogin/cli_test/kubeconfig"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@@ -26,19 +27,19 @@ import (
|
||||
// 5. Shutdown the auth server.
|
||||
func TestE2E(t *testing.T) {
|
||||
data := map[string]struct {
|
||||
kubeconfigValues kubeconfigValues
|
||||
kubeconfigValues kubeconfig.Values
|
||||
cli cli.CLI
|
||||
serverConfig authserver.Config
|
||||
clientTLS *tls.Config
|
||||
}{
|
||||
"NoTLS": {
|
||||
kubeconfigValues{Issuer: "http://localhost:9000"},
|
||||
kubeconfig.Values{Issuer: "http://localhost:9000"},
|
||||
cli.CLI{},
|
||||
authserver.Config{Issuer: "http://localhost:9000"},
|
||||
&tls.Config{},
|
||||
},
|
||||
"ExtraScope": {
|
||||
kubeconfigValues{
|
||||
kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
ExtraScopes: "profile groups",
|
||||
},
|
||||
@@ -50,7 +51,7 @@ func TestE2E(t *testing.T) {
|
||||
&tls.Config{},
|
||||
},
|
||||
"SkipTLSVerify": {
|
||||
kubeconfigValues{Issuer: "https://localhost:9000"},
|
||||
kubeconfig.Values{Issuer: "https://localhost:9000"},
|
||||
cli.CLI{SkipTLSVerify: true},
|
||||
authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
@@ -60,7 +61,7 @@ func TestE2E(t *testing.T) {
|
||||
&tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
"CACert": {
|
||||
kubeconfigValues{
|
||||
kubeconfig.Values{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthority: authserver.CACert,
|
||||
},
|
||||
@@ -73,7 +74,7 @@ func TestE2E(t *testing.T) {
|
||||
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
|
||||
},
|
||||
"CACertData": {
|
||||
kubeconfigValues{
|
||||
kubeconfig.Values{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, authserver.CACert)),
|
||||
},
|
||||
@@ -85,6 +86,24 @@ func TestE2E(t *testing.T) {
|
||||
},
|
||||
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
|
||||
},
|
||||
"InvalidCACertShouldBeSkipped": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDPCertificateAuthority: "e2e_test.go",
|
||||
},
|
||||
cli.CLI{},
|
||||
authserver.Config{Issuer: "http://localhost:9000"},
|
||||
&tls.Config{},
|
||||
},
|
||||
"InvalidCACertDataShouldBeSkipped": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("foo")),
|
||||
},
|
||||
cli.CLI{},
|
||||
authserver.Config{Issuer: "http://localhost:9000"},
|
||||
&tls.Config{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, c := range data {
|
||||
@@ -93,10 +112,11 @@ func TestE2E(t *testing.T) {
|
||||
defer cancel()
|
||||
server := c.serverConfig.Start(t)
|
||||
defer server.Shutdown(ctx)
|
||||
kubeconfig := createKubeconfig(t, &c.kubeconfigValues)
|
||||
defer os.Remove(kubeconfig)
|
||||
c.cli.KubeConfig = kubeconfig
|
||||
kcfg := kubeconfig.Create(t, &c.kubeconfigValues)
|
||||
defer os.Remove(kcfg)
|
||||
c.cli.KubeConfig = kcfg
|
||||
c.cli.SkipOpenBrowser = true
|
||||
c.cli.ListenPort = 8000
|
||||
|
||||
var eg errgroup.Group
|
||||
eg.Go(func() error {
|
||||
@@ -109,7 +129,7 @@ func TestE2E(t *testing.T) {
|
||||
if err := eg.Wait(); err != nil {
|
||||
t.Fatalf("CLI returned error: %s", err)
|
||||
}
|
||||
verifyKubeconfig(t, kubeconfig)
|
||||
kubeconfig.Verify(t, kcfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package e2e
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
@@ -7,21 +7,23 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type kubeconfigValues struct {
|
||||
// Values represents values in .kubeconfig template.
|
||||
type Values struct {
|
||||
Issuer string
|
||||
ExtraScopes string
|
||||
IDPCertificateAuthority string
|
||||
IDPCertificateAuthorityData string
|
||||
}
|
||||
|
||||
func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
|
||||
// Create creates a kubeconfig file and returns path to it.
|
||||
func Create(t *testing.T, v *Values) string {
|
||||
t.Helper()
|
||||
f, err := ioutil.TempFile("", "kubeconfig")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
tpl, err := template.ParseFiles("testdata/kubeconfig.yaml")
|
||||
tpl, err := template.ParseFiles("kubeconfig/testdata/kubeconfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -31,7 +33,8 @@ func createKubeconfig(t *testing.T, v *kubeconfigValues) string {
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func verifyKubeconfig(t *testing.T, kubeconfig string) {
|
||||
// Verify returns true if the kubeconfig has valid values.
|
||||
func Verify(t *testing.T, kubeconfig string) {
|
||||
b, err := ioutil.ReadFile(kubeconfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
100
docs/google.md
Normal file
100
docs/google.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Getting Started with Google Identity Platform
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- You have a Google account.
|
||||
- You have the Cluster Admin role of the Kubernetes cluster.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed to your computer.
|
||||
|
||||
## 1. Setup Google API
|
||||
|
||||
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
|
||||
|
||||
- Application Type: Other
|
||||
|
||||
## 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).
|
||||
|
||||
### kops
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://accounts.google.com
|
||||
oidcClientID: YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
```
|
||||
|
||||
## 3. Setup Kubernetes cluster
|
||||
|
||||
Here assign the `cluster-admin` role to you.
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: oidc-admin-group
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: https://accounts.google.com#1234567890
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
|
||||
## 4. Setup kubectl
|
||||
|
||||
Configure `kubectl` for the OIDC authentication.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials NAME \
|
||||
--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
|
||||
2018/08/10 10:36:38 Reading .kubeconfig
|
||||
2018/08/10 10:36:38 Using current context: hello.k8s.local
|
||||
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
|
||||
2018/08/10 10:36:45 GET /
|
||||
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
|
||||
2018/08/10 10:37:08 Updated .kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
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
|
||||
```
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
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
|
||||
```
|
||||
107
docs/keycloak.md
Normal file
107
docs/keycloak.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Getting Started with Keycloak
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- You have administrator access to the Keycloak.
|
||||
- You have the Cluster Admin role of the Kubernetes cluster.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed to your computer.
|
||||
|
||||
## 1. Setup Keycloak
|
||||
|
||||
Open the Keycloak and create an OIDC client as follows:
|
||||
|
||||
- Redirect URL: `http://localhost:8000/`
|
||||
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
|
||||
- Client ID: `kubernetes`
|
||||
- Groups claim: `groups`
|
||||
|
||||
Create a group `kubernetes:admin` and join to it.
|
||||
This is used for group based access control.
|
||||
|
||||
## 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).
|
||||
|
||||
### kops
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
oidcClientID: kubernetes
|
||||
oidcGroupsClaim: groups
|
||||
```
|
||||
|
||||
## 3. Setup Kubernetes cluster
|
||||
|
||||
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
|
||||
|
||||
```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: Group
|
||||
name: /kubernetes:admin
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
|
||||
## 4. Setup kubectl
|
||||
|
||||
Configure `kubectl` for the OIDC authentication.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials NAME \
|
||||
--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
|
||||
2018/08/10 10:36:38 Reading .kubeconfig
|
||||
2018/08/10 10:36:38 Using current context: hello.k8s.local
|
||||
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
|
||||
2018/08/10 10:36:45 GET /
|
||||
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
|
||||
2018/08/10 10:37:08 Updated .kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
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
|
||||
```
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
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
|
||||
```
|
||||
40
docs/team_ops.md
Normal file
40
docs/team_ops.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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.
|
||||
30
go.mod
Normal file
30
go.mod
Normal file
@@ -0,0 +1,30 @@
|
||||
module github.com/int128/kubelogin
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.1.1 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/int128/oauth2cli v1.0.0
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/json-iterator/go v1.1.5 // indirect
|
||||
github.com/mitchellh/go-homedir v1.0.0
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/spf13/pflag v1.0.3 // indirect
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
|
||||
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0 // indirect
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.1.9 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.1 // indirect
|
||||
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd // indirect
|
||||
k8s.io/client-go v9.0.0+incompatible
|
||||
)
|
||||
58
go.sum
Normal file
58
go.sum
Normal file
@@ -0,0 +1,58 @@
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
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/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
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/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/int128/oauth2cli v1.0.0 h1:bQcKSgS7lBVIhEJ1IE689oGW6AmTPrh/mQkxqGxX2Z0=
|
||||
github.com/int128/oauth2cli v1.0.0/go.mod h1:ClkmeKFkDlkVtqncv98+V4Gny/luvARCIoK/+3KlKb8=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
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/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/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/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816 h1:mVFkLpejdFLXVUv9E42f3XJVfMdqd0IVLVIVLjZWn5o=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107 h1:63fpDttzclb8owmRoxSaFNbnT1CG25L0Yvnhh9lU1SE=
|
||||
golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0 h1:biUuj9O+0+XckRUCDzjoOGm6yFV5c0IHbm1ODP3e4Zw=
|
||||
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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.1.9 h1:YCFbL5T2gbmC2sMG12s1x2PAlTK5TZNte3hjZEIcCAg=
|
||||
gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd h1:rzRDbWmTXaWr6SDV9caWzwAUPSfvgwX6j89LBnq/ycw=
|
||||
k8s.io/apimachinery v0.0.0-20181031012033-2e0dc82819fd/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/client-go v9.0.0+incompatible h1:NXWpDuPFeVB5lYP1fTqJUtwigjtmRXJNtndnN53ldGI=
|
||||
k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
|
||||
Reference in New Issue
Block a user