mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-28 07:53:50 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fccef52a73 | ||
|
|
581284c626 | ||
|
|
b5922f9419 | ||
|
|
7a0ca206d1 | ||
|
|
0bca9ef54b | ||
|
|
2fb551bf1b | ||
|
|
0bc117ddc7 | ||
|
|
8c640f6c73 | ||
|
|
8a5efac337 | ||
|
|
d6e0c761ac | ||
|
|
8925226afe | ||
|
|
89a0f9a79e | ||
|
|
74bb4c62c5 |
@@ -2,7 +2,7 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.3
|
||||
- image: circleci/golang:1.13.3
|
||||
steps:
|
||||
- run: |
|
||||
mkdir -p ~/bin
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
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
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.21.0
|
||||
- run: go get github.com/int128/goxzst
|
||||
- run: go get github.com/tcnksm/ghr
|
||||
- checkout
|
||||
|
||||
46
README.md
46
README.md
@@ -9,7 +9,7 @@ Then kubelogin gets a token from the provider and kubectl access Kubernetes APIs
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Setup
|
||||
### Install
|
||||
|
||||
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
|
||||
|
||||
@@ -26,30 +26,19 @@ unzip kubelogin_linux_amd64.zip
|
||||
ln -s kubelogin kubectl-oidc_login
|
||||
```
|
||||
|
||||
You need to configure the OIDC provider, Kubernetes API server and role binding.
|
||||
### Setup
|
||||
|
||||
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
|
||||
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)
|
||||
- [Getting Started with dex and GitHub](docs/dex.md)
|
||||
- [Getting Started with Keycloak](docs/keycloak.md)
|
||||
|
||||
Configure the kubeconfig to run kubelogin as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
|
||||
It provides transparent login without manually running kubelogin command.
|
||||
For example,
|
||||
Run the following command to show the setup instruction.
|
||||
|
||||
```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
|
||||
```sh
|
||||
kubectl oidc-login setup
|
||||
```
|
||||
|
||||
### Run
|
||||
@@ -84,11 +73,6 @@ 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
|
||||
|
||||
As well as you can update the ID token in the kubeconfig by running the command.
|
||||
See [standalone mode](docs/standalone-mode.md) for more.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -98,21 +82,21 @@ If you are looking for a specific version, see [the release tags](https://github
|
||||
Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
% kubelogin get-token -h
|
||||
% kubectl oidc-login 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
|
||||
--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
|
||||
--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
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
@@ -133,7 +117,7 @@ Global Flags:
|
||||
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
|
||||
```
|
||||
|
||||
See also the options in [standalone mode](docs/standalone-mode.md).
|
||||
See also the options of [standalone mode](docs/standalone-mode.md).
|
||||
|
||||
### Extra scopes
|
||||
|
||||
|
||||
112
docs/dex.md
112
docs/dex.md
@@ -1,12 +1,14 @@
|
||||
# Getting Started with dex and GitHub
|
||||
|
||||
## Prerequisite
|
||||
Prerequisite:
|
||||
|
||||
- You have a GitHub account.
|
||||
- You have an administrator role of the Kubernetes cluster.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed.
|
||||
|
||||
## 1. Setup GitHub OAuth
|
||||
|
||||
## 1. Set up the OpenID Connect Provider
|
||||
|
||||
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
|
||||
|
||||
@@ -14,9 +16,7 @@ Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an a
|
||||
- Homepage URL: `https://dex.example.com`
|
||||
- Authorization callback URL: `https://dex.example.com/callback`
|
||||
|
||||
## 2. Setup dex
|
||||
|
||||
Configure the dex with the following config:
|
||||
Deploy the [dex](https://github.com/dexidp/dex) with the following config:
|
||||
|
||||
```yaml
|
||||
issuer: https://dex.example.com
|
||||
@@ -29,89 +29,79 @@ connectors:
|
||||
clientSecret: YOUR_GITHUB_CLIENT_SECRET
|
||||
redirectURI: https://dex.example.com/callback
|
||||
staticClients:
|
||||
- id: kubernetes
|
||||
- id: YOUR_CLIENT_ID
|
||||
name: Kubernetes
|
||||
redirectURIs:
|
||||
- http://localhost:8000
|
||||
- http://localhost:18000
|
||||
- http://localhost:8000
|
||||
- http://localhost:18000
|
||||
secret: YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Now test authentication with the dex.
|
||||
|
||||
## 2. Verify authentication
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=https://dex.example.com \
|
||||
--oidc-client-id=kubernetes \
|
||||
--oidc-client-secret=YOUR_DEX_CLIENT_SECRET
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
It will open the browser and you can log in to the provider.
|
||||
|
||||
```
|
||||
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=kubernetes
|
||||
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://dex.example.com
|
||||
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=YOUR_SUBJECT
|
||||
```
|
||||
|
||||
## 3. Setup Kubernetes API server
|
||||
## 3. Bind a role
|
||||
|
||||
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.
|
||||
Bind the `cluster-admin` role to you.
|
||||
Apply the following manifest:
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: keycloak-admin-group
|
||||
name: oidc-admin-group
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: YOUR_SUBJECT
|
||||
name: https://dex.example.com#YOUR_SUBJECT
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
As well as you can create a custom role and bind it.
|
||||
|
||||
## 5. Setup kubeconfig
|
||||
|
||||
Configure the kubeconfig like:
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
Add the following options to the kube-apiserver:
|
||||
|
||||
```
|
||||
--oidc-issuer-url=https://dex.example.com
|
||||
--oidc-client-id=YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://dex.example.com
|
||||
oidcClientID: YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
|
||||
## 5. Set up the kubeconfig
|
||||
|
||||
Add the following user to the kubeconfig:
|
||||
|
||||
```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
|
||||
- name: google
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
@@ -120,13 +110,14 @@ users:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://dex.example.com
|
||||
- --oidc-client-id=kubernetes
|
||||
- --oidc-client-secret=YOUR_DEX_CLIENT_SECRET
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 6. Run kubectl
|
||||
|
||||
## 6. Verify cluster access
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
@@ -134,7 +125,6 @@ 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
|
||||
|
||||
@@ -1,56 +1,38 @@
|
||||
# Getting Started with Google Identity Platform
|
||||
|
||||
## Prerequisite
|
||||
Prerequisite:
|
||||
|
||||
- You have a Google account.
|
||||
- You have the Cluster Admin role of the Kubernetes cluster.
|
||||
- You have an administrator role of the Kubernetes cluster.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed to your computer.
|
||||
|
||||
## 1. Setup Google API
|
||||
|
||||
## 1. Set up the OpenID Connect Provider
|
||||
|
||||
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
|
||||
|
||||
- Application Type: Other
|
||||
|
||||
Now test authentication with Google Identity Platform.
|
||||
|
||||
## 2. Verify authentication
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=https://accounts.google.com \
|
||||
--oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
It will open the browser and you can log in to the provider.
|
||||
|
||||
```
|
||||
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://accounts.google.com
|
||||
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=YOUR_SUBJECT
|
||||
```
|
||||
|
||||
## 2. Setup Kubernetes API server
|
||||
## 3. Bind a role
|
||||
|
||||
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
|
||||
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 your subject.
|
||||
Bind the `cluster-admin` role to you.
|
||||
Apply the following manifest:
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
@@ -63,45 +45,56 @@ roleRef:
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: YOUR_SUBJECT
|
||||
name: https://accounts.google.com#YOUR_SUBJECT
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
As well as you can create a custom role and bind it.
|
||||
|
||||
## 4. Setup kubeconfig
|
||||
|
||||
Configure the kubeconfig like:
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
Add the following options to the kube-apiserver:
|
||||
|
||||
```
|
||||
--oidc-issuer-url=https://accounts.google.com
|
||||
--oidc-client-id=YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
## 5. Set up the kubeconfig
|
||||
|
||||
Add the following user to the kubeconfig:
|
||||
|
||||
```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: google
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://accounts.google.com
|
||||
- --oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 5. Run kubectl
|
||||
|
||||
## 6. Verify cluster access
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
@@ -109,7 +102,6 @@ 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
|
||||
|
||||
109
docs/keycloak.md
109
docs/keycloak.md
@@ -1,17 +1,18 @@
|
||||
# Getting Started with Keycloak
|
||||
|
||||
## Prerequisite
|
||||
Prerequisite:
|
||||
|
||||
- You have an administrator role of the Keycloak realm.
|
||||
- You have an administrator role of the Kubernetes cluster.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed.
|
||||
|
||||
## 1. Setup Keycloak
|
||||
|
||||
## 1. Set up the OpenID Connect Provider
|
||||
|
||||
Open the Keycloak and create an OIDC client as follows:
|
||||
|
||||
- Client ID: `kubernetes`
|
||||
- Client ID: `YOUR_CLIENT_ID`
|
||||
- Valid Redirect URLs:
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
@@ -21,105 +22,94 @@ You can associate client roles by adding the following mapper:
|
||||
|
||||
- Name: `groups`
|
||||
- Mapper Type: `User Client Role`
|
||||
- Client ID: `kubernetes`
|
||||
- Client ID: `YOUR_CLIENT_ID`
|
||||
- Client Role prefix: `kubernetes:`
|
||||
- Token Claim Name: `groups`
|
||||
- Add to ID token: on
|
||||
|
||||
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.
|
||||
|
||||
## 2. Verify authentication
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
|
||||
--oidc-client-id=kubernetes \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
It will open the browser and you can log in to the provider.
|
||||
|
||||
```
|
||||
I0827 12:29:03.086476 23722 get_token.go:59] the ID token has the claim: groups=[kubernetes:admin]
|
||||
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=kubernetes
|
||||
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=f08655e2-901f-48e5-8c64-bb9f7784d5df
|
||||
```
|
||||
|
||||
## 2. Setup Kubernetes API server
|
||||
## 3. Bind a role
|
||||
|
||||
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
|
||||
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.
|
||||
Bind the `cluster-admin` role to you.
|
||||
Apply the following manifest:
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: keycloak-admin-group
|
||||
name: oidc-admin-group
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: kubernetes:admin
|
||||
- kind: User
|
||||
name: https://keycloak.example.com/auth/realms/YOUR_REALM#YOUR_SUBJECT
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
As well as you can create a custom role and bind it.
|
||||
|
||||
## 4. Setup kubeconfig
|
||||
|
||||
Configure the kubeconfig like:
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
Add the following options to the kube-apiserver:
|
||||
|
||||
```
|
||||
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
--oidc-client-id=YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
See [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for details.
|
||||
|
||||
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: YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
|
||||
## 5. Set up the kubeconfig
|
||||
|
||||
Add the following user to the kubeconfig:
|
||||
|
||||
```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: keycloak
|
||||
- name: google
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
- --oidc-client-id=kubernetes
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 5. Run kubectl
|
||||
|
||||
## 6. Verify cluster access
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
@@ -127,7 +117,6 @@ 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
|
||||
|
||||
@@ -78,38 +78,36 @@ If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
% kubelogin -h
|
||||
Login to the OpenID Connect provider and update the kubeconfig
|
||||
% kubectl oidc-login -h
|
||||
Login to the OpenID Connect provider.
|
||||
|
||||
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
|
||||
Run the following command to show the setup instruction:
|
||||
|
||||
kubectl oidc-login setup
|
||||
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
Usage:
|
||||
kubelogin [flags]
|
||||
kubelogin [command]
|
||||
|
||||
Examples:
|
||||
# Login to the provider using the authorization code flow.
|
||||
kubelogin
|
||||
|
||||
# Login to the provider using the resource owner password credentials flow.
|
||||
kubelogin --username USERNAME --password PASSWORD
|
||||
|
||||
# Run as a credential plugin.
|
||||
kubelogin get-token --oidc-issuer-url=https://issuer.example.com
|
||||
main [flags]
|
||||
main [command]
|
||||
|
||||
Available Commands:
|
||||
get-token Run as a kubectl credential plugin
|
||||
help Help about any command
|
||||
setup Show the setup instruction
|
||||
version Print the version information
|
||||
|
||||
Flags:
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--context string The name of the kubeconfig context to use
|
||||
--user string The name of the kubeconfig user to use. Prior to --context
|
||||
--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
|
||||
--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
|
||||
--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
|
||||
--add_dir_header If true, adds the file directory to the header
|
||||
--alsologtostderr log to standard error as well as files
|
||||
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
|
||||
@@ -122,8 +120,8 @@ Flags:
|
||||
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
|
||||
-v, --v Level number for the log level verbosity
|
||||
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
|
||||
-h, --help help for kubelogin
|
||||
--version version for kubelogin
|
||||
-h, --help help for main
|
||||
--version version for main
|
||||
```
|
||||
|
||||
### Kubeconfig
|
||||
|
||||
7
e2e_test/keys/testdata/Makefile
vendored
7
e2e_test/keys/testdata/Makefile
vendored
@@ -1,3 +1,5 @@
|
||||
EXPIRY := 3650
|
||||
|
||||
all: ca.key ca.crt server.key server.crt jws.key
|
||||
|
||||
.PHONY: clean
|
||||
@@ -17,7 +19,9 @@ ca.csr: openssl.cnf ca.key
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
ca.crt: ca.csr ca.key
|
||||
openssl x509 -req \
|
||||
openssl x509 \
|
||||
-req \
|
||||
-days $(EXPIRY) \
|
||||
-signkey ca.key \
|
||||
-in ca.csr \
|
||||
-out $@
|
||||
@@ -42,6 +46,7 @@ server.crt: openssl.cnf server.csr ca.key ca.crt
|
||||
touch CA/index.txt.attr
|
||||
echo 00 > CA/serial
|
||||
openssl ca -config openssl.cnf \
|
||||
-days $(EXPIRY) \
|
||||
-extensions v3_req \
|
||||
-batch \
|
||||
-cert ca.crt \
|
||||
|
||||
10
e2e_test/keys/testdata/ca.crt
vendored
10
e2e_test/keys/testdata/ca.crt
vendored
@@ -1,11 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnTCCAQYCCQCuPrhkr+BvGzANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
|
||||
ZWxsbyBDQTAeFw0xOTA4MTgwNjAwMDZaFw0xOTA5MTcwNjAwMDZaMBMxETAPBgNV
|
||||
MIIBnTCCAQYCCQC/aR7GRyndljANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
|
||||
ZWxsbyBDQTAeFw0xOTA5MjUxNDQ0NTFaFw0yOTA5MjIxNDQ0NTFaMBMxETAPBgNV
|
||||
BAMMCEhlbGxvIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnSTDsRx4U
|
||||
JmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ4Pc51itOycWN1nclNVb489i9
|
||||
J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3Lu4mysxyOtYZNsdST8GBCP0m4
|
||||
2tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
|
||||
ACfgNePlOLnLz1zJrWN6RZ6q0a+SSK8HdgSiKSF66SBIRILFoQmapBLXRY9YyATt
|
||||
cdgg7pOd1WGCMqlOnhL56c8X5n+j/LGM5hc9PaEJA5vru7EBrnbxCkg0n8yp4Swc
|
||||
8KFV5IiZ5D8t03AHjrXLQg8/HRzTuFRJJ1nJmc+FbnjT
|
||||
AE/gsgTC4jzYC3icZdhALJTe3JsZ7geN702dE95zSI5LXAzzHJ/j8wGmorQjrMs2
|
||||
iNPjVOdTU6cVWa1Ba29wWakVyVCUqDmDiWHaVhM/Qyyxo6mVlZGFwSnto3zq/h4y
|
||||
KMFJ8lUtFCYMrzo5wqgj2xOjVrN77F6F4XWZbMufh50G
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
1
e2e_test/keys/testdata/openssl.cnf
vendored
1
e2e_test/keys/testdata/openssl.cnf
vendored
@@ -10,7 +10,6 @@ new_certs_dir = $dir
|
||||
default_md = sha256
|
||||
policy = policy_match
|
||||
serial = $dir/serial
|
||||
default_days = 3650
|
||||
|
||||
[ policy_match ]
|
||||
countryName = optional
|
||||
|
||||
@@ -33,7 +33,7 @@ var (
|
||||
// 4. Verify the kubeconfig.
|
||||
//
|
||||
func TestCmd_Run_Standalone(t *testing.T) {
|
||||
timeout := 1 * time.Second
|
||||
timeout := 5 * time.Second
|
||||
|
||||
type testParameter struct {
|
||||
startServer func(t *testing.T, h http.Handler) (string, localserver.Shutdowner)
|
||||
@@ -57,7 +57,7 @@ func TestCmd_Run_Standalone(t *testing.T) {
|
||||
runTest := func(t *testing.T, p testParameter) {
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -85,7 +85,7 @@ func TestCmd_Run_Standalone(t *testing.T) {
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -116,7 +116,7 @@ func TestCmd_Run_Standalone(t *testing.T) {
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -145,7 +145,7 @@ func TestCmd_Run_Standalone(t *testing.T) {
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -178,7 +178,7 @@ func TestCmd_Run_Standalone(t *testing.T) {
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -218,7 +218,7 @@ func TestCmd_Run_Standalone(t *testing.T) {
|
||||
|
||||
t.Run("env:KUBECONFIG", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -245,7 +245,7 @@ func TestCmd_Run_Standalone(t *testing.T) {
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
12
go.mod
12
go.mod
@@ -5,20 +5,20 @@ go 1.12
|
||||
require (
|
||||
github.com/coreos/go-oidc v2.1.0+incompatible
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda
|
||||
github.com/go-test/deep v1.0.3
|
||||
github.com/go-test/deep v1.0.4
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/google/wire v0.3.0
|
||||
github.com/int128/oauth2cli v1.5.0
|
||||
github.com/int128/oauth2cli v1.7.0
|
||||
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/spf13/pflag v1.0.5
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
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
|
||||
gopkg.in/yaml.v2 v2.2.4
|
||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
|
||||
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
|
||||
k8s.io/klog v0.4.0
|
||||
|
||||
33
go.sum
33
go.sum
@@ -4,8 +4,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
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=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
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/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
|
||||
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
@@ -20,18 +18,18 @@ github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
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/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/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 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
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=
|
||||
@@ -47,11 +45,10 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
|
||||
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 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
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/int128/oauth2cli v1.5.0 h1:EOBMCWfroql1hPqPhP+EtDhgO7y6ClFZ/NwJEpBCo1s=
|
||||
github.com/int128/oauth2cli v1.5.0/go.mod h1:ivzuzt+k+bpwLI1Mb1bRq8PiBvwLBsO8L7tX2F9iKKA=
|
||||
github.com/int128/oauth2cli v1.7.0 h1:lguQEIJ4IcSFRTqQ6y7avnfvPqVe0U6dlkW8mC1Epts=
|
||||
github.com/int128/oauth2cli v1.7.0/go.mod h1:bucNn0/es9IhOf0a2MWPvJ5xO5f6JYrCfitQTyjI5lA=
|
||||
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=
|
||||
@@ -70,6 +67,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
||||
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=
|
||||
@@ -83,7 +81,10 @@ 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/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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=
|
||||
@@ -99,14 +100,17 @@ golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6
|
||||
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/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/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -121,11 +125,12 @@ golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
|
||||
@@ -137,12 +142,14 @@ 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=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
k8s.io/api v0.0.0-20190620084959-7cf5895f2711 h1:BblVYz/wE5WtBsD/Gvu54KyBUTJMflolzc5I2DTvh50=
|
||||
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/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
|
||||
|
||||
@@ -16,21 +16,13 @@ var Set = wire.NewSet(
|
||||
wire.Bind(new(Interface), new(*Cmd)),
|
||||
wire.Struct(new(Root), "*"),
|
||||
wire.Struct(new(GetToken), "*"),
|
||||
wire.Struct(new(Setup), "*"),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
@@ -38,6 +30,7 @@ var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
|
||||
type Cmd struct {
|
||||
Root *Root
|
||||
GetToken *GetToken
|
||||
Setup *Setup
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
@@ -54,6 +47,9 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
getTokenCmd := cmd.GetToken.New(ctx)
|
||||
rootCmd.AddCommand(getTokenCmd)
|
||||
|
||||
setupCmd := cmd.Setup.New(ctx)
|
||||
rootCmd.AddCommand(setupCmd)
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
|
||||
@@ -12,24 +12,29 @@ import (
|
||||
|
||||
// getTokenOptions represents the options for get-token command.
|
||||
type getTokenOptions struct {
|
||||
loginOptions
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
ListenPort []int
|
||||
SkipOpenBrowser bool
|
||||
Username string
|
||||
Password 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.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")
|
||||
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.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
|
||||
|
||||
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
@@ -12,38 +11,40 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// kubectlOptions represents kubectl specific options.
|
||||
type kubectlOptions struct {
|
||||
const longDescription = `Login to the OpenID Connect provider.
|
||||
|
||||
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
|
||||
Run the following command to show the setup instruction:
|
||||
|
||||
kubectl oidc-login setup
|
||||
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
`
|
||||
|
||||
// rootOptions represents the options for the root command.
|
||||
type rootOptions struct {
|
||||
Kubeconfig string
|
||||
Context string
|
||||
User string
|
||||
ListenPort []int
|
||||
SkipOpenBrowser bool
|
||||
Username string
|
||||
Password string
|
||||
CertificateAuthority string
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
func (o *kubectlOptions) register(f *pflag.FlagSet) {
|
||||
func (o *rootOptions) 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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
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")
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
@@ -52,15 +53,12 @@ type Root struct {
|
||||
}
|
||||
|
||||
func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
|
||||
var o struct {
|
||||
kubectlOptions
|
||||
loginOptions
|
||||
}
|
||||
var o rootOptions
|
||||
rootCmd := &cobra.Command{
|
||||
Use: executable,
|
||||
Short: "Login to the OpenID Connect provider and update the kubeconfig",
|
||||
Example: fmt.Sprintf(examples, executable),
|
||||
Args: cobra.NoArgs,
|
||||
Use: executable,
|
||||
Short: "Login to the OpenID Connect provider",
|
||||
Long: longDescription,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
in := standalone.Input{
|
||||
KubeconfigFilename: o.Kubeconfig,
|
||||
@@ -79,8 +77,7 @@ func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.kubectlOptions.register(rootCmd.Flags())
|
||||
o.loginOptions.register(rootCmd.Flags())
|
||||
o.register(rootCmd.Flags())
|
||||
cmd.Logger.AddFlags(rootCmd.PersistentFlags())
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
71
pkg/adaptors/cmd/setup.go
Normal file
71
pkg/adaptors/cmd/setup.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// setupOptions represents the options for setup command.
|
||||
type setupOptions struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
ListenPort []int
|
||||
SkipOpenBrowser bool
|
||||
CertificateAuthority string
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
func (o *setupOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider")
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider")
|
||||
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.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.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")
|
||||
}
|
||||
|
||||
type Setup struct {
|
||||
Setup setup.Interface
|
||||
}
|
||||
|
||||
func (cmd *Setup) New(ctx context.Context) *cobra.Command {
|
||||
var o setupOptions
|
||||
c := &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Show the setup instruction",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
in := setup.Stage2Input{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
ListenPort: o.ListenPort,
|
||||
ListenPortIsSet: !reflect.DeepEqual(o.ListenPort, defaultListenPort),
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
}
|
||||
if in.IssuerURL == "" || in.ClientID == "" {
|
||||
cmd.Setup.DoStage1()
|
||||
return nil
|
||||
}
|
||||
if err := cmd.Setup.DoStage2(ctx, in); err != nil {
|
||||
return xerrors.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.register(c.Flags())
|
||||
return c
|
||||
}
|
||||
2
pkg/adaptors/env/env.go
vendored
2
pkg/adaptors/env/env.go
vendored
@@ -36,7 +36,7 @@ type Env struct{}
|
||||
|
||||
// ReadPassword reads a password from the stdin without echo back.
|
||||
func (*Env) ReadPassword(prompt string) (string, error) {
|
||||
if _, err := fmt.Fprint(os.Stderr, "Password: "); err != nil {
|
||||
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
|
||||
return "", xerrors.Errorf("could not write the prompt: %w", err)
|
||||
}
|
||||
b, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
|
||||
@@ -24,10 +24,11 @@ type Interface interface {
|
||||
// TokenSet represents an output DTO of
|
||||
// Interface.AuthenticateByCode, Interface.AuthenticateByPassword and Interface.Refresh.
|
||||
type TokenSet struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
IDTokenSubject string
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
}
|
||||
|
||||
type client struct {
|
||||
|
||||
@@ -16,8 +16,9 @@ type DecoderInterface interface {
|
||||
}
|
||||
|
||||
type DecodedIDToken struct {
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
Subject string
|
||||
Expiry time.Time
|
||||
Claims map[string]string // string representation of claims for logging
|
||||
}
|
||||
|
||||
type Decoder struct{}
|
||||
@@ -42,17 +43,18 @@ func (d *Decoder) DecodeIDToken(t string) (*DecodedIDToken, error) {
|
||||
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
|
||||
}
|
||||
return &DecodedIDToken{
|
||||
IDTokenExpiry: time.Unix(claims.ExpiresAt, 0),
|
||||
IDTokenClaims: dumpRawClaims(rawClaims),
|
||||
Subject: claims.Subject,
|
||||
Expiry: time.Unix(claims.ExpiresAt, 0),
|
||||
Claims: 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) {
|
||||
switch v := v.(type) {
|
||||
case float64:
|
||||
claims[k] = fmt.Sprintf("%.f", v.(float64))
|
||||
claims[k] = fmt.Sprintf("%.f", v)
|
||||
default:
|
||||
claims[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ func TestDecoder_DecodeIDToken(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeIDToken error: %s", err)
|
||||
}
|
||||
if decodedToken.IDTokenExpiry != expiry {
|
||||
t.Errorf("IDTokenExpiry wants %s but %s", expiry, decodedToken.IDTokenExpiry)
|
||||
if decodedToken.Expiry != expiry {
|
||||
t.Errorf("Expiry wants %s but %s", expiry, decodedToken.Expiry)
|
||||
}
|
||||
t.Logf("IDTokenClaims=%+v", decodedToken.IDTokenClaims)
|
||||
t.Logf("Claims=%+v", decodedToken.Claims)
|
||||
})
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE")
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
)
|
||||
|
||||
@@ -25,6 +26,7 @@ func NewCmd() cmd.Interface {
|
||||
wire.Value(auth.DefaultLocalServerReadyFunc),
|
||||
standalone.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
setup.Set,
|
||||
|
||||
// adaptors
|
||||
cmd.Set,
|
||||
@@ -44,6 +46,8 @@ func NewCmdForHeadless(logger.Interface, auth.LocalServerReadyFunc, credentialPl
|
||||
auth.Set,
|
||||
standalone.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
setup.Set,
|
||||
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
)
|
||||
|
||||
@@ -57,9 +58,17 @@ func NewCmd() cmd.Interface {
|
||||
GetToken: getToken,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
setupSetup := &setup.Setup{
|
||||
Authentication: authentication,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdSetup := &cmd.Setup{
|
||||
Setup: setupSetup,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Root: root,
|
||||
GetToken: cmdGetToken,
|
||||
Setup: cmdSetup,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
return cmdCmd
|
||||
@@ -103,9 +112,17 @@ func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc au
|
||||
GetToken: getToken,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
setupSetup := &setup.Setup{
|
||||
Authentication: authentication,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdSetup := &cmd.Setup{
|
||||
Setup: setupSetup,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Root: root,
|
||||
GetToken: cmdGetToken,
|
||||
Setup: cmdSetup,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
return cmdCmd
|
||||
|
||||
@@ -45,6 +45,7 @@ type Input struct {
|
||||
// Output represents an output DTO of the Authentication use-case.
|
||||
type Output struct {
|
||||
AlreadyHasValidIDToken bool
|
||||
IDTokenSubject string
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string
|
||||
IDToken string
|
||||
@@ -84,17 +85,18 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
|
||||
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.V(1).Infof("you already have a valid token until %s", token.IDTokenExpiry)
|
||||
if token.Expiry.After(time.Now()) { //TODO: inject time service
|
||||
u.Logger.V(1).Infof("you already have a valid token until %s", token.Expiry)
|
||||
return &Output{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: in.OIDCConfig.IDToken,
|
||||
RefreshToken: in.OIDCConfig.RefreshToken,
|
||||
IDTokenExpiry: token.IDTokenExpiry,
|
||||
IDTokenClaims: token.IDTokenClaims,
|
||||
IDTokenSubject: token.Subject,
|
||||
IDTokenExpiry: token.Expiry,
|
||||
IDTokenClaims: token.Claims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.V(1).Infof("you have an expired token at %s", token.IDTokenExpiry)
|
||||
u.Logger.V(1).Infof("you have an expired token at %s", token.Expiry)
|
||||
}
|
||||
|
||||
u.Logger.V(1).Infof("initializing an OIDCFactory client")
|
||||
@@ -112,10 +114,11 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
|
||||
out, err := client.Refresh(ctx, in.OIDCConfig.RefreshToken)
|
||||
if err == nil {
|
||||
return &Output{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
IDTokenExpiry: out.IDTokenExpiry,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
IDTokenSubject: out.IDTokenSubject,
|
||||
IDTokenExpiry: out.IDTokenExpiry,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.V(1).Infof("could not refresh the token: %s", err)
|
||||
@@ -130,8 +133,9 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
|
||||
func (u *Authentication) doAuthCodeFlow(ctx context.Context, in Input, client oidc.Interface) (*Output, error) {
|
||||
u.Logger.V(1).Infof("performing the authentication code flow")
|
||||
readyChan := make(chan string, 1)
|
||||
defer close(readyChan)
|
||||
var out Output
|
||||
var eg errgroup.Group
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
select {
|
||||
case url, ok := <-readyChan:
|
||||
@@ -150,20 +154,20 @@ func (u *Authentication) doAuthCodeFlow(ctx context.Context, in Input, client oi
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
return xerrors.Errorf("context cancelled while waiting for the local server: %w", ctx.Err())
|
||||
}
|
||||
})
|
||||
eg.Go(func() error {
|
||||
defer close(readyChan)
|
||||
tokenSet, err := client.AuthenticateByCode(ctx, in.ListenPort, readyChan)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("error while the authorization code flow: %w", err)
|
||||
}
|
||||
out = Output{
|
||||
IDToken: tokenSet.IDToken,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
IDTokenExpiry: tokenSet.IDTokenExpiry,
|
||||
IDTokenClaims: tokenSet.IDTokenClaims,
|
||||
IDToken: tokenSet.IDToken,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
IDTokenSubject: tokenSet.IDTokenSubject,
|
||||
IDTokenExpiry: tokenSet.IDTokenExpiry,
|
||||
IDTokenClaims: tokenSet.IDTokenClaims,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -187,9 +191,10 @@ func (u *Authentication) doPasswordCredentialsFlow(ctx context.Context, in Input
|
||||
return nil, xerrors.Errorf("error while the resource owner password credentials flow: %w", err)
|
||||
}
|
||||
return &Output{
|
||||
IDToken: tokenSet.IDToken,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
IDTokenExpiry: tokenSet.IDTokenExpiry,
|
||||
IDTokenClaims: tokenSet.IDTokenClaims,
|
||||
IDToken: tokenSet.IDToken,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
IDTokenSubject: tokenSet.IDTokenSubject,
|
||||
IDTokenExpiry: tokenSet.IDTokenExpiry,
|
||||
IDTokenClaims: tokenSet.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
|
||||
pastTime := time.Now().Add(-time.Hour) //TODO: inject time service
|
||||
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
|
||||
timeout := 5 * time.Second
|
||||
|
||||
t.Run("AuthorizationCodeFlow", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
@@ -36,12 +38,16 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
}
|
||||
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
|
||||
AuthenticateByCode(gomock.Any(), []int{10000}, gomock.Any()).
|
||||
Do(func(_ context.Context, _ []int, readyChan chan<- string) {
|
||||
readyChan <- "LOCAL_SERVER_URL"
|
||||
}).
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
|
||||
mockOIDCFactory.EXPECT().
|
||||
@@ -60,10 +66,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
@@ -73,7 +80,8 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Run("AuthorizationCodeFlow/OpenBrowser", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
ListenPort: []int{10000},
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
@@ -83,15 +91,16 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
}
|
||||
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
|
||||
AuthenticateByCode(gomock.Any(), []int{10000}, gomock.Any()).
|
||||
Do(func(_ context.Context, _ []int, readyChan chan<- string) {
|
||||
readyChan <- "LOCAL_SERVER_URL"
|
||||
}).
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
|
||||
mockOIDCFactory.EXPECT().
|
||||
@@ -110,10 +119,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
@@ -123,7 +133,8 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Run("ResourceOwnerPasswordCredentialsFlow/UsePassword", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
@@ -136,12 +147,13 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
}
|
||||
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
AuthenticateByPassword(ctx, "USER", "PASS").
|
||||
AuthenticateByPassword(gomock.Any(), "USER", "PASS").
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
|
||||
mockOIDCFactory.EXPECT().
|
||||
@@ -160,10 +172,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
@@ -173,7 +186,8 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Run("ResourceOwnerPasswordCredentialsFlow/AskPassword", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
Username: "USER",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
@@ -183,12 +197,13 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
}
|
||||
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
AuthenticateByPassword(ctx, "USER", "PASS").
|
||||
AuthenticateByPassword(gomock.Any(), "USER", "PASS").
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
|
||||
mockOIDCFactory.EXPECT().
|
||||
@@ -208,10 +223,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
@@ -221,7 +237,8 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Run("ResourceOwnerPasswordCredentialsFlow/AskPasswordError", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
Username: "USER",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
@@ -254,7 +271,8 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Run("HasValidIDToken", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
@@ -266,8 +284,9 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("VALID_ID_TOKEN").
|
||||
Return(&oidc.DecodedIDToken{
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
Subject: "YOUR_SUBJECT",
|
||||
Expiry: futureTime,
|
||||
Claims: dummyTokenClaims,
|
||||
}, nil)
|
||||
u := Authentication{
|
||||
OIDCFactory: mock_oidc.NewMockFactoryInterface(ctrl),
|
||||
@@ -281,6 +300,7 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
want := &Output{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}
|
||||
@@ -292,7 +312,8 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
@@ -305,17 +326,19 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("EXPIRED_ID_TOKEN").
|
||||
Return(&oidc.DecodedIDToken{
|
||||
IDTokenExpiry: pastTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
Subject: "YOUR_SUBJECT",
|
||||
Expiry: pastTime,
|
||||
Claims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Refresh(ctx, "VALID_REFRESH_TOKEN").
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
|
||||
mockOIDCFactory.EXPECT().
|
||||
@@ -333,10 +356,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
@@ -346,9 +370,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
ListenPort: []int{10000},
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
@@ -360,20 +386,25 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("EXPIRED_ID_TOKEN").
|
||||
Return(&oidc.DecodedIDToken{
|
||||
IDTokenExpiry: pastTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
Subject: "YOUR_SUBJECT",
|
||||
Expiry: pastTime,
|
||||
Claims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, xerrors.New("token has expired"))
|
||||
mockOIDCClient.EXPECT().
|
||||
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
|
||||
AuthenticateByCode(gomock.Any(), []int{10000}, gomock.Any()).
|
||||
Do(func(_ context.Context, _ []int, readyChan chan<- string) {
|
||||
readyChan <- "LOCAL_SERVER_URL"
|
||||
}).
|
||||
Return(&oidc.TokenSet{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
|
||||
mockOIDCFactory.EXPECT().
|
||||
@@ -391,10 +422,11 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
IDToken: "NEW_ID_TOKEN",
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
|
||||
@@ -50,7 +50,18 @@ type GetToken struct {
|
||||
|
||||
func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
u.Logger.V(1).Infof("WARNING: log may contain your secrets such as token or password")
|
||||
out, err := u.getTokenFromCacheOrProvider(ctx, in)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not get a token from the cache or provider: %w", err)
|
||||
}
|
||||
u.Logger.V(1).Infof("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
|
||||
}
|
||||
|
||||
func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*auth.Output, error) {
|
||||
u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
|
||||
cacheKey := tokencache.Key{IssuerURL: in.IssuerURL, ClientID: in.ClientID}
|
||||
cache, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, cacheKey)
|
||||
@@ -58,6 +69,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
u.Logger.V(1).Infof("could not find a token cache: %s", err)
|
||||
cache = &tokencache.TokenCache{}
|
||||
}
|
||||
|
||||
out, err := u.Authentication.Do(ctx, auth.Input{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: in.IssuerURL,
|
||||
@@ -75,25 +87,23 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("error while authentication: %w", err)
|
||||
return nil, xerrors.Errorf("error while authentication: %w", err)
|
||||
}
|
||||
for k, v := range out.IDTokenClaims {
|
||||
u.Logger.V(1).Infof("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 := tokencache.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)
|
||||
}
|
||||
if out.AlreadyHasValidIDToken {
|
||||
u.Logger.V(1).Infof("you already have a valid token until %s", out.IDTokenExpiry)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
u.Logger.V(1).Infof("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)
|
||||
u.Logger.V(1).Infof("you got a valid token until %s", out.IDTokenExpiry)
|
||||
newCache := tokencache.TokenCache{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
}
|
||||
return nil
|
||||
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, cacheKey, newCache); err != nil {
|
||||
return nil, xerrors.Errorf("could not write the token cache: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
25
pkg/usecases/setup/setup.go
Normal file
25
pkg/usecases/setup/setup.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Package setup provides the use case of setting up environment.
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
)
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Setup), "*"),
|
||||
wire.Bind(new(Interface), new(*Setup)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
DoStage1()
|
||||
DoStage2(ctx context.Context, in Stage2Input) error
|
||||
}
|
||||
|
||||
type Setup struct {
|
||||
Authentication auth.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
28
pkg/usecases/setup/stage1.go
Normal file
28
pkg/usecases/setup/stage1.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package setup
|
||||
|
||||
const stage1 = `This setup shows the instruction of Kubernetes OpenID Connect authentication.
|
||||
See also https://github.com/int128/kubelogin.
|
||||
|
||||
## 1. Set up the OpenID Connect Provider
|
||||
|
||||
Open the OpenID Connect Provider and create a client.
|
||||
|
||||
For example, Google Identity Platform:
|
||||
Open https://console.developers.google.com/apis/credentials and create an OAuth client of "Other" type.
|
||||
ISSUER is https://accounts.google.com
|
||||
|
||||
## 2. Verify authentication
|
||||
|
||||
Run the following command to proceed.
|
||||
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=ISSUER \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
|
||||
You can set your CA certificate. See also the options by --help.
|
||||
`
|
||||
|
||||
func (u *Setup) DoStage1() {
|
||||
u.Logger.Printf(stage1)
|
||||
}
|
||||
141
pkg/usecases/setup/stage2.go
Normal file
141
pkg/usecases/setup/stage2.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var stage2Tpl = template.Must(template.New("").Parse(`
|
||||
## 3. Bind a role
|
||||
|
||||
Run the following command:
|
||||
|
||||
kubectl apply -f - <<-EOF
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: oidc-cluster-admin
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: {{ .IssuerURL }}#{{ .Subject }}
|
||||
EOF
|
||||
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
Add the following options to the kube-apiserver:
|
||||
|
||||
--oidc-issuer-url={{ .IssuerURL }}
|
||||
--oidc-client-id={{ .ClientID }}
|
||||
|
||||
## 5. Set up the kubeconfig
|
||||
|
||||
Add the following user to the kubeconfig:
|
||||
|
||||
users:
|
||||
- name: google
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
{{- range .Args }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
|
||||
Run kubectl and verify cluster access.
|
||||
`))
|
||||
|
||||
type stage2Vars struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
Args []string
|
||||
Subject string
|
||||
}
|
||||
|
||||
// Stage2Input represents an input DTO of the stage2.
|
||||
type Stage2Input struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string // optional
|
||||
SkipOpenBrowser bool
|
||||
ListenPort []int
|
||||
ListenPortIsSet bool // true if it is set by the command arg
|
||||
CACertFilename string // If set, use the CA cert
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
|
||||
u.Logger.Printf(`## 2. Verify authentication`)
|
||||
out, err := u.Authentication.Do(ctx, auth.Input{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
},
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
ListenPort: in.ListenPort,
|
||||
CACertFilename: in.CACertFilename,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("error while authentication: %w", err)
|
||||
}
|
||||
u.Logger.Printf("You got the following claims in the token:")
|
||||
for k, v := range out.IDTokenClaims {
|
||||
u.Logger.Printf("\t%s=%s", k, v)
|
||||
}
|
||||
|
||||
v := stage2Vars{
|
||||
IssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
Args: makeCredentialPluginArgs(in),
|
||||
Subject: out.IDTokenSubject,
|
||||
}
|
||||
var b strings.Builder
|
||||
if err := stage2Tpl.Execute(&b, &v); err != nil {
|
||||
return xerrors.Errorf("could not render the template: %w", err)
|
||||
}
|
||||
u.Logger.Printf(b.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeCredentialPluginArgs(in Stage2Input) []string {
|
||||
var args []string
|
||||
args = append(args, "--oidc-issuer-url="+in.IssuerURL)
|
||||
args = append(args, "--oidc-client-id="+in.ClientID)
|
||||
if in.ClientSecret != "" {
|
||||
args = append(args, "--oidc-client-secret="+in.ClientSecret)
|
||||
}
|
||||
for _, extraScope := range in.ExtraScopes {
|
||||
args = append(args, "--oidc-extra-scope="+extraScope)
|
||||
}
|
||||
if in.SkipOpenBrowser {
|
||||
args = append(args, "--skip-open-browser")
|
||||
}
|
||||
if in.ListenPortIsSet {
|
||||
for _, port := range in.ListenPort {
|
||||
args = append(args, fmt.Sprintf("--listen-port=%d", port))
|
||||
}
|
||||
}
|
||||
if in.CACertFilename != "" {
|
||||
args = append(args, "--certificate-authority="+in.CACertFilename)
|
||||
}
|
||||
if in.SkipTLSVerify {
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
}
|
||||
return args
|
||||
}
|
||||
59
pkg/usecases/setup/stage2_test.go
Normal file
59
pkg/usecases/setup/stage2_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth/mock_auth"
|
||||
)
|
||||
|
||||
func TestSetup_DoStage2(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.Background()
|
||||
|
||||
in := Stage2Input{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email"},
|
||||
SkipOpenBrowser: true,
|
||||
ListenPort: []int{8000},
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
}
|
||||
|
||||
mockAuthentication := mock_auth.NewMockInterface(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, auth.Input{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email"},
|
||||
},
|
||||
SkipOpenBrowser: true,
|
||||
ListenPort: []int{8000},
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
}).
|
||||
Return(&auth.Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: time.Now().Add(time.Hour),
|
||||
IDTokenClaims: map[string]string{"iss": "https://accounts.google.com"},
|
||||
}, nil)
|
||||
u := Setup{
|
||||
Authentication: mockAuthentication,
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
if err := u.DoStage2(ctx, in); err != nil {
|
||||
t.Errorf("DoStage2 returned error: %+v", err)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package standalone
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
@@ -35,12 +37,9 @@ type Input struct {
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
|
||||
kubectl config set-credentials CONTEXT_NAME \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
|
||||
--auth-provider-arg client-id=YOUR_CLIENT_ID \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
|
||||
const oidcConfigErrorMessage = `You need to set up the kubeconfig for OpenID Connect authentication.
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
`
|
||||
|
||||
// Standalone provides the use case of explicit login.
|
||||
//
|
||||
@@ -62,6 +61,9 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
|
||||
u.Logger.Printf(oidcConfigErrorMessage)
|
||||
return xerrors.Errorf("could not find the current authentication provider: %w", err)
|
||||
}
|
||||
if err := u.showDeprecation(in, authProvider); err != nil {
|
||||
return xerrors.Errorf("could not show deprecation message: %w", err)
|
||||
}
|
||||
u.Logger.V(1).Infof("using the authentication provider of the user %s", authProvider.UserName)
|
||||
u.Logger.V(1).Infof("a token will be written to %s", authProvider.LocationOfOrigin)
|
||||
|
||||
@@ -94,3 +96,65 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var deprecationTpl = template.Must(template.New("").Parse(
|
||||
`IMPORTANT NOTICE:
|
||||
The credential plugin mode is available since v1.14.0.
|
||||
Kubectl will automatically run kubelogin and you do not need to run kubelogin explicitly.
|
||||
|
||||
You can switch to the credential plugin mode by setting the following user to
|
||||
{{ .Kubeconfig }}.
|
||||
---
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
{{- range .Args }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
---
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
`))
|
||||
|
||||
type deprecationVars struct {
|
||||
Kubeconfig string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error {
|
||||
var args []string
|
||||
args = append(args, "--oidc-issuer-url="+p.OIDCConfig.IDPIssuerURL)
|
||||
args = append(args, "--oidc-client-id="+p.OIDCConfig.ClientID)
|
||||
if p.OIDCConfig.ClientSecret != "" {
|
||||
args = append(args, "--oidc-client-secret="+p.OIDCConfig.ClientSecret)
|
||||
}
|
||||
for _, extraScope := range p.OIDCConfig.ExtraScopes {
|
||||
args = append(args, "--oidc-extra-scope="+extraScope)
|
||||
}
|
||||
if p.OIDCConfig.IDPCertificateAuthority != "" {
|
||||
args = append(args, "--certificate-authority="+p.OIDCConfig.IDPCertificateAuthority)
|
||||
}
|
||||
if in.CACertFilename != "" {
|
||||
args = append(args, "--certificate-authority="+in.CACertFilename)
|
||||
}
|
||||
if in.Username != "" {
|
||||
args = append(args, "--username="+in.Username)
|
||||
}
|
||||
|
||||
v := deprecationVars{
|
||||
Kubeconfig: p.LocationOfOrigin,
|
||||
Args: args,
|
||||
}
|
||||
var b strings.Builder
|
||||
if err := deprecationTpl.Execute(&b, &v); err != nil {
|
||||
return xerrors.Errorf("could not render the template: %w", err)
|
||||
}
|
||||
u.Logger.Printf("%s", b.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user