Compare commits

...

88 Commits

Author SHA1 Message Date
Hidetake Iwata
86681b82c5 Refactor homebrew formula (#468) 2021-02-11 20:27:52 +09:00
Hidetake Iwata
cc231f7f81 Create question.md 2021-02-11 17:55:57 +09:00
renovate[bot]
44ffd69cbf Update module coreos/go-oidc to v3 (#463)
* Update module coreos/go-oidc to v3

* Update import path to github.com/coreos/go-oidc/v3/oidc

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Hidetake Iwata <int128@gmail.com>
2021-02-11 17:23:22 +09:00
Hidetake Iwata
c3f636300e Update to golangci-lint v1.36.0 (#460)
* Update golangci-lint.yaml

* Use golang.org/x/term

SA1019: package golang.org/x/crypto/ssh/terminal is deprecated: this package moved to golang.org/x/term.  (staticcheck)

* Add workflow name
2021-02-11 17:14:32 +09:00
renovate[bot]
f1a2539262 Update module spf13/cobra to v1.1.3 (#466)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-02-10 22:00:51 +00:00
renovate[bot]
5e7cb2aff1 Update golang.org/x/oauth2 commit hash to 6667018 (#465)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-02-10 21:23:31 +00:00
renovate[bot]
e47054ccdb Update module spf13/cobra to v1.1.2 (#464)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-02-10 03:26:03 +00:00
renovate[bot]
51d5af57cc Update cimg/go Docker tag to v1.15.8 (#462)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-02-05 00:42:53 +00:00
renovate[bot]
f29ea3a1c7 Update golang.org/x/oauth2 commit hash to 0101308 (#461)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-02-01 18:00:27 +00:00
renovate[bot]
1bb8fb2dc9 Update module k8s.io/klog/v2 to v2.5.0 (#459)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-27 19:33:47 +00:00
renovate[bot]
3e5c3e5918 Update golang.org/x/oauth2 commit hash to f9ce19e (#458)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-26 20:48:53 +00:00
renovate[bot]
cf85625b56 Update module google/wire to v0.5.0 (#457)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-26 03:37:13 +00:00
renovate[bot]
6987910fe6 Update golang.org/x/oauth2 commit hash to af13f52 (#456)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-25 21:04:32 +00:00
renovate[bot]
d89c2dc961 Update module coreos/go-oidc to v3 (#442)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-23 15:19:17 +09:00
renovate[bot]
4e2a99ba8e Update module chromedp/chromedp to v0.6.5 (#455)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-22 14:56:36 +00:00
renovate[bot]
3e915d0811 Update cimg/go Docker tag to v1.15.7 (#454)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-20 17:15:53 +00:00
renovate[bot]
1ca272f61a Update golang.org/x/net commit hash to 5f4716e (#453)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-19 21:31:20 +00:00
renovate[bot]
5b1cc1c994 Update module chromedp/chromedp to v0.6.4 (#452)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-17 02:49:01 +00:00
renovate[bot]
fa416f2910 Update github.com/pkg/browser commit hash to ce105d0 (#450)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-15 05:13:23 +00:00
renovate[bot]
ad65baa624 Update alpine Docker tag to v3.13 (#449)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-15 03:19:59 +00:00
renovate[bot]
25b8d29a44 Update module k8s.io/client-go to v0.20.2 (#448)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-14 03:06:18 +00:00
renovate[bot]
92b09e3e6f Update module k8s.io/apimachinery to v0.20.2 (#447)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-14 00:44:36 +00:00
renovate[bot]
10f904adbb Update golang.org/x/oauth2 commit hash to d3ed898 (#446)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-13 22:33:17 +00:00
renovate[bot]
55d2498dae Update golang.org/x/oauth2 commit hash to 8b1d76f (#445)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-13 17:31:48 +00:00
renovate[bot]
eaf5658d45 Update module chromedp/chromedp to v0.6.1 (#444)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-13 06:34:34 +00:00
renovate[bot]
5608c76764 Update golang.org/x/oauth2 commit hash to 01de73c (#443)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-12 21:18:59 +00:00
Hidetake Iwata
4915ce165f Delete CODEOWNERS 2021-01-11 11:55:00 +09:00
renovate[bot]
bd186d6cfc Update golang.org/x/sync commit hash to 09787c9 (#390)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-10 18:45:47 +00:00
renovate[bot]
411b42b6af Update golang.org/x/oauth2 commit hash to 08078c5 (#415)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-10 17:28:10 +00:00
renovate[bot]
671ee8ecf1 Update golang.org/x/net commit hash to 6772e93 (#407)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-10 16:36:05 +00:00
renovate[bot]
e5b179dfec Update golang.org/x/crypto commit hash to eec23a3 (#391)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-10 15:42:21 +00:00
renovate[bot]
435cdf4a78 Update github.com/pkg/browser commit hash to 0426ae3 (#416)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-10 14:12:42 +00:00
Hidetake Iwata
f94ff640d6 Update renovate.json 2021-01-10 22:49:33 +09:00
renovate[bot]
9be8740f45 Update cimg/go Docker tag to v1.15.6 (#434)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-10 13:16:22 +00:00
Hidetake Iwata
637d091b40 Enable renovate-approve 2021-01-10 21:36:03 +09:00
Hidetake Iwata
637fc746fd Enable automerge 2021-01-10 21:32:24 +09:00
renovate[bot]
7e063b5dda Update module yaml to v2.4.0 (#433)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-10 17:45:49 +09:00
renovate[bot]
202a8616c6 Update module chromedp/chromedp to v0.6.0 (#435)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-09 21:44:56 +09:00
renovate[bot]
21b88cf037 Update module k8s.io/klog/v2 to v2.4.0 (#441)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-01-09 20:17:26 +09:00
Yuri V
38772898fc Switch klog to v2 (#439)
Co-authored-by: Hidetake Iwata <int128@gmail.com>
2021-01-09 18:51:04 +09:00
Hidetake Iwata
58d1839f3e Create CODEOWNERS 2021-01-09 18:13:47 +09:00
renovate[bot]
c7606e8151 Update module google/go-cmp to v0.5.4 (#432)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-12-19 22:26:34 +09:00
renovate[bot]
eb0f2009e2 Update module k8s.io/client-go to v0.20.1 (#438)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-12-19 21:14:06 +09:00
Hidetake Iwata
cffb00f386 Refactor: extract tests into authentication_test.go (#431) 2020-11-23 18:20:47 +09:00
Hidetake Iwata
8e1a63b1a2 Change mutex scope to bind address port (#430) 2020-11-23 17:41:07 +09:00
Hidetake Iwata
ebf81debe1 Refactor: credentialplugin/get_token_test.go (#429)
* Refactor: extract const vars

* Refactor: extract ROPC test case
2020-11-22 20:01:45 +09:00
Hidetake Iwata
2f271b5870 Refactor: replace Input fields with oidc.Provider (#428) 2020-11-21 12:35:23 +09:00
Hidetake Iwata
b1d8e8f7e1 Refactor: rewrite with Go errors package (#427) 2020-11-21 12:10:42 +09:00
Hidetake Iwata
5a3227409c Refactor: rename to infrastructure package (#426) 2020-11-21 07:56:52 +09:00
Hidetake Iwata
13d232ec21 Refactor: move oidc/client package (#425) 2020-11-21 07:27:34 +09:00
Hidetake Iwata
9bab6b2ccd Refactor: extract tokencache and repository package (#424) 2020-11-20 07:01:16 +09:00
Hidetake Iwata
93fc548544 Refactor: extract kubeconfig package (#423)
* Refactor: extract kubeconfig package

* Refactor: use pass by value instead of reference
2020-11-18 10:36:42 +09:00
Hidetake Iwata
4773b67abd Refactor: extract credentialplugin package (#422) 2020-11-18 10:00:39 +09:00
renovate[bot]
009d03cb69 Update module google/go-cmp to v0.5.3 (#417)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-11-14 21:24:05 +09:00
renovate[bot]
417c556e8f Update module k8s.io/client-go to v0.19.4 (#420)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-11-14 20:51:43 +09:00
renovate[bot]
2542d6456c Update cimg/go Docker tag to v1.15.5 (#418)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-11-14 17:54:11 +09:00
renovate[bot]
e3af16ca8f Update cimg/go Docker tag to v1.15.4 (#410)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-11-10 21:30:26 +09:00
Hidetake Iwata
8926e8940a Fix TLS error if CA certificate is not set (#412) 2020-11-08 15:56:41 +09:00
Hidetake Iwata
ce7784b8a0 Add TLS renegotiation flags (#411) 2020-11-07 13:08:29 +09:00
Hidetake Iwata
34762216c1 Refactor: extract tlsclientconfig.Config (#409) 2020-11-03 14:37:24 +09:00
Eric Poitras
878847f937 feat(389): Prevent concurrent authentication using a lockfile. (#397)
* feat(389): Prevent concurrent authentication using a lockfile to protect the local port allocation.

* Fix test

* Refactor: inline values

Co-authored-by: Hidetake Iwata <int128@gmail.com>
2020-10-25 14:32:53 +09:00
Hidetake Iwata
8a392ba25a Refactor: use gotestsum for CircleCI (#377) 2020-10-25 12:47:32 +09:00
Hidetake Iwata
b701a6f0aa Refactor: aggregate test cases to lease and full options (#406) 2020-10-25 12:24:35 +09:00
Hidetake Iwata
10091a3238 go mod tidy 2020-10-25 11:35:58 +09:00
Christoph Stäbler
d1b89e3d38 Add username in token cache key (#404) 2020-10-24 20:44:29 +09:00
renovate[bot]
e862ac7eac Update module spf13/cobra to v1.1.1 (#400)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-10-24 09:26:08 +09:00
renovate[bot]
d051d80435 Update module k8s.io/client-go to v0.19.3 (#403)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-10-24 09:25:29 +09:00
Hidetake Iwata
14e58ac4c2 Update README.md 2020-10-18 08:51:09 +09:00
renovate[bot]
748eb12fc0 Update module spf13/cobra to v1.1.0 (#398)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Hidetake Iwata <int128@gmail.com>
2020-10-18 08:43:36 +09:00
renovate[bot]
8b232eeb3e Update cimg/go Docker tag to v1.15.3 (#399)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-10-18 08:37:40 +09:00
Hidetake Iwata
0694a1cd0b Update system-test.yaml 2020-10-18 08:37:17 +09:00
Hidetake Iwata
9ddeb33d27 Update golangci-lint.yaml 2020-10-18 08:35:13 +09:00
Hidetake Iwata
64bfc5a465 Refactor authentication use-cases (#395) 2020-10-03 20:01:26 +09:00
Hidetake Iwata
5b2c82fc33 Refactor: replace DTO with oidc.TokenSet type (#394)
* Refactor: remove IDTokenClaims from TokenSet and decode in use-cases

* Refactor: use oidc.TokenSet for cache repository
2020-10-03 17:49:21 +09:00
Hidetake Iwata
1dee4a354e Refactor: extract oidc.Provider (#393) 2020-10-03 08:35:35 +09:00
Hidetake Iwata
336f2b83d5 go mod tidy 2020-10-03 08:06:47 +09:00
Hidetake Iwata
6071dd83a3 Update feature_request.md 2020-09-28 09:38:53 +09:00
Hidetake Iwata
784378cbe6 Update bug_report.md 2020-09-28 09:36:42 +09:00
renovate[bot]
fe54383df9 Update module k8s.io/client-go to v0.19.2 (#380)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-09-28 09:29:26 +09:00
Hidetake Iwata
257c05dbf3 Change authentication timeout to 180 sec (#388) 2020-09-28 09:28:44 +09:00
Hidetake Iwata
e543a7bbe0 Update usage.md 2020-09-27 22:04:31 +09:00
Hidetake Iwata
ebdfcfb1c8 Add --authentication-timeout-sec flag (#387) 2020-09-27 21:55:55 +09:00
Hidetake Iwata
7bc76a5e79 Refactor: system test (#386) 2020-09-26 16:36:05 +09:00
Hidetake Iwata
ed0a5318ec Fix system test for kind v0.9.0 (#385) 2020-09-26 12:19:34 +09:00
Hidetake Iwata
881786a820 Add integration test for HTTPS local server (#383) 2020-09-25 10:14:17 +09:00
Hidetake Iwata
5ab2f9e01e Refactor: replace temporary dirs with t.TempDir() (#382) 2020-09-25 10:10:11 +09:00
TJ Miller
56169d1673 Add support for HTTPS redirect URI (#381)
* Add local server certificate option

* fix trailing slash from step 5 kubectl config set-credentials

* Add local https documentation

* Change flags to --local-server-cert and --local-server-key

* Add tests for flags

Co-authored-by: TJ Miller <millert@us.ibm.com>
Co-authored-by: Hidetake Iwata <int128@gmail.com>
2020-09-25 09:44:00 +09:00
renovate[bot]
592f2722fd Update cimg/go Docker tag to v1.15.2 (#373)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2020-09-11 21:03:11 +09:00
113 changed files with 2730 additions and 2126 deletions

View File

@@ -3,22 +3,22 @@ version: 2.1
jobs:
test:
docker:
- image: cimg/go:1.14.7
- image: cimg/go:1.15.8
steps:
- checkout
- restore_cache:
keys:
- go-sum-{{ checksum "go.sum" }}
- run: make -C .circleci install-test-deps
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- run: make dist
- run: mkdir -p /tmp/gotest
- run: gotestsum --junitfile /tmp/gotest/gotest.xml -- -v -race -cover -coverprofile=coverage.out ./...
- store_test_results:
path: /tmp/gotest
- save_cache:
key: go-sum-{{ checksum "go.sum" }}
paths:
- ~/go/pkg
- store_artifacts:
path: gotest.log
- run: bash <(curl -s https://codecov.io/bash)
release:
macos:

View File

@@ -7,17 +7,14 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
## Describe the issue
A clear and concise description of what the issue is.
**To Reproduce**
Steps to reproduce the behavior.
## To reproduce
A console log or steps to reproduce the issue.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment**
- OS: [e.g. macOS, Linux]
- kubelogin version: [e.g. 1.19.3]
- kubectl version: [e.g. 1.19]
- OpenID Connect provider: [e.g. Google, Okta]
## Your environment
- OS: e.g. macOS
- kubelogin version: e.g. v1.19
- kubectl version: e.g. v1.19
- OpenID Connect provider: e.g. Google

View File

@@ -7,11 +7,9 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
## Purpose of the feature (why)
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
## Your idea (how)
A clear and concise description of any alternative solutions or features you've considered.

20
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
name: Question
about: Feel free to ask a question
title: ''
labels: question
assignees: ''
---
## Describe the question
A clear and concise description of what the issue is.
## To reproduce
A console log or steps to reproduce the issue.
## Your environment
- OS: e.g. macOS
- kubelogin version: e.g. v1.19
- kubectl version: e.g. v1.19
- OpenID Connect provider: e.g. Google

View File

@@ -2,6 +2,12 @@
"extends": [
"config:base"
],
"packageRules": [
{
"updateTypes": ["minor", "patch", "digest"],
"automerge": true
}
],
"postUpdateOptions": [
"gomodTidy"
]

View File

@@ -1,8 +1,11 @@
name: golangci-lint
on:
push:
pull_request:
paths:
- .github/workflows/golangci-lint.yaml
- '**.go'
jobs:
golangci:
name: lint
@@ -11,4 +14,4 @@ jobs:
- uses: actions/checkout@v2
- uses: golangci/golangci-lint-action@v2
with:
version: v1.30
version: v1.36

View File

@@ -1,10 +1,14 @@
on:
push:
pull_request:
paths:
- .github/workflows/system-test.yaml
- system_test/**
- pkg/**
- go.*
push:
# for go mod tidy
branches: [master]
paths: [go.*]
jobs:
system-test:
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts
@@ -12,7 +16,7 @@ jobs:
steps:
- uses: actions/setup-go@v2
with:
go-version: 1.14
go-version: 1.15
id: go
- uses: actions/checkout@v2
- uses: actions/cache@v2
@@ -24,6 +28,6 @@ jobs:
# https://packages.ubuntu.com/xenial/libnss3-tools
- run: sudo apt update
- run: sudo apt install -y libnss3-tools
- run: mkdir -p ~/.pki/nssdb
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
- run: make -C system_test -j3 setup
- run: make -C system_test test
- run: make -C system_test -j3

2
.gitignore vendored
View File

@@ -1,11 +1,9 @@
/.idea
/system_test/output/
/acceptance_test/output/
/dist/output
/coverage.out
/gotest.log
/kubelogin
/kubectl-oidc_login

View File

@@ -13,10 +13,6 @@ all: $(TARGET)
$(TARGET): $(wildcard **/*.go)
go build -o $@ -ldflags "$(LDFLAGS)"
.PHONY: check
check:
go test -v -race -cover -coverprofile=coverage.out ./... > gotest.log
.PHONY: dist
dist: dist/output
dist/output:

View File

@@ -61,7 +61,7 @@ kubectl get pods
```
Kubectl executes kubelogin before calling the Kubernetes APIs.
Kubelogin automatically opens the browser and you can log in to the provider.
Kubelogin automatically opens the browser, and you can log in to the provider.
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
@@ -101,6 +101,21 @@ You got a token with the following claims:
}
```
You can increase the log level by `-v1` option.
```yaml
users:
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- -v1
```
You can verify kubelogin works with your provider using [acceptance test](acceptance_test).
@@ -123,19 +138,18 @@ You can access the Kubernetes Dashboard using kubelogin and [kauthproxy](https:/
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests for improving code and documents.
Your pull request will be merged into master with squash.
### Development
Go 1.13 or later is required.
Go 1.15+ is required.
```sh
# Run lint and tests
make check
# Compile and run the command
make
./kubelogin
```
### System test
See also:
See [system test](system_test) for more.
- [system test](system_test)
- [acceptance_test](acceptance_test)

2
dist/Dockerfile vendored
View File

@@ -1,4 +1,4 @@
FROM alpine:3.12
FROM alpine:3.13
ARG KUBELOGIN_VERSION="{{ env "VERSION" }}"
ARG KUBELOGIN_SHA256="{{ sha256 .linux_amd64_archive }}"

21
dist/kubelogin.rb vendored
View File

@@ -1,27 +1,24 @@
class Kubelogin < Formula
desc "A kubectl plugin for Kubernetes OpenID Connect authentication"
homepage "https://github.com/int128/kubelogin"
baseurl = "https://github.com/int128/kubelogin/releases/download"
version "{{ env "VERSION" }}"
if OS.mac?
kernel = "darwin"
on_macos do
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip"
sha256 "{{ sha256 .darwin_amd64_archive }}"
elsif OS.linux?
kernel = "linux"
end
on_linux do
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip"
sha256 "{{ sha256 .linux_amd64_archive }}"
end
url baseurl + "/#{version}/kubelogin_#{kernel}_amd64.zip"
end
def install
bin.install "kubelogin" => "kubelogin"
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"
end
test do
system "#{bin}/kubelogin -h"
system "#{bin}/kubectl-oidc_login -h"
end
end

View File

@@ -12,12 +12,17 @@ Flags:
--oidc-client-secret string Client secret of the provider
--oidc-extra-scope strings Scopes to request to the provider
--token-cache-dir string Path to a directory for token cache (default "~/.kube/cache/oidc-login")
--certificate-authority string Path to a cert file for the certificate authority
--certificate-authority-data string Base64 encoded cert for the certificate authority
--certificate-authority stringArray Path to a cert file for the certificate authority
--certificate-authority-data stringArray Base64 encoded cert for the certificate authority
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--tls-renegotiation-once If set, allow a remote server to request renegotiation once per connection
--tls-renegotiation-freely If set, allow a remote server to repeatedly request renegotiation
--grant-type string Authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-address strings [authcode] Address to bind to the local server. If multiple addresses are set, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--skip-open-browser [authcode] Do not open the browser automatically
--authentication-timeout-sec int [authcode] Timeout of authentication in seconds (default 180)
--local-server-cert string [authcode] Certificate path for the local server
--local-server-key string [authcode] Certificate key path for the local server
--open-url-after-authentication string [authcode] If set, open the URL in the browser after authentication
--oidc-redirect-url-hostname string [authcode] Hostname of the redirect URL (default "localhost")
--oidc-auth-request-extra-params stringToString [authcode, authcode-keyboard] Extra query parameters to send with an authentication request (default [])
@@ -43,6 +48,18 @@ Global Flags:
## Options
### Authentication timeout
By default, you need to log in to your provider in the browser within 3 minutes.
This prevents a process from remaining forever.
You can change the timeout by the following flag:
```yaml
- --authentication-timeout-sec=60
```
For now this timeout works only for the authorization code flow.
### Extra scopes
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
@@ -52,7 +69,7 @@ You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
- --oidc-extra-scope=profile
```
### CA Certificate
### CA certificate
You can use your self-signed certificate for the provider.
@@ -61,12 +78,11 @@ You can use your self-signed certificate for the provider.
- --certificate-authority-data=LS0t...
```
### HTTP Proxy
### HTTP proxy
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
## Authentication flows
Kubelogin support the following flows:
@@ -92,6 +108,13 @@ You can change the listening address.
- --listen-address=127.0.0.1:23456
```
You can specify a certificate for the local webserver if HTTPS is required by your identity provider.
```yaml
- --local-server-cert=localhost.crt
- --local-server-key=localhost.key
```
You can change the hostname of redirect URI from the default value `localhost`.
```yaml

35
go.mod
View File

@@ -1,27 +1,26 @@
module github.com/int128/kubelogin
go 1.12
go 1.13
require (
github.com/chromedp/chromedp v0.5.3
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/alexflint/go-filemutex v1.1.0
github.com/chromedp/chromedp v0.6.5
github.com/coreos/go-oidc/v3 v3.0.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.2
github.com/google/wire v0.4.0
github.com/google/go-cmp v0.5.4
github.com/google/wire v0.5.0
github.com/int128/oauth2cli v1.13.0
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/cobra v1.0.0
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4
github.com/spf13/cobra v1.1.3
github.com/spf13/pflag v1.0.5
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.3.0
k8s.io/apimachinery v0.19.0
k8s.io/client-go v0.19.0
k8s.io/klog v1.0.0
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
golang.org/x/oauth2 v0.0.0-20210210192628-66670185b0cd
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
gopkg.in/yaml.v2 v2.4.0
k8s.io/apimachinery v0.20.2
k8s.io/client-go v0.20.2
k8s.io/klog/v2 v2.5.0
)

257
go.sum
View File

@@ -6,7 +6,6 @@ cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxK
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
@@ -22,6 +21,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -32,44 +32,51 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/alexflint/go-filemutex v1.1.0 h1:IAWuUuRYL2hETx5b8vCgwnD+xSdlsTQY6s2JjBsqLdg=
github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac h1:T7V5BXqnYd55Hj/g5uhDYumg9Fp3rMTS6bykYtTIFX4=
github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
github.com/chromedp/chromedp v0.5.3 h1:F9LafxmYpsQhWQBdCs+6Sret1zzeeFyHS5LkRF//Ffg=
github.com/chromedp/chromedp v0.5.3/go.mod h1:YLdPtndaHQ4rCpSpBG+IPpy9JvX0VD+7aaLxYgYj28w=
github.com/chromedp/cdproto v0.0.0-20210122124816-7a656c010d57 h1:htpyTFarq7OHx9SpkQ+7x20thTQA6JAsgnuMGoPbH4E=
github.com/chromedp/cdproto v0.0.0-20210122124816-7a656c010d57/go.mod h1:55pim6Ht4LJKdVLlyFJV/g++HsEA1hQxPbB5JyNdZC0=
github.com/chromedp/chromedp v0.6.5 h1:hPaDYBpvD2WFicln0ByzV+XRhSOtLgAgsu39O455iWY=
github.com/chromedp/chromedp v0.6.5/go.mod h1:/Q6h52DkrFuvOgmCuR6O3xT5g0bZYoPqjANKBEvQGEY=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ=
github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -88,6 +95,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -101,17 +110,22 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs=
github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
@@ -145,6 +159,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -157,6 +173,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
@@ -171,21 +189,41 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
@@ -197,17 +235,18 @@ github.com/int128/listener v1.1.0/go.mod h1:68WkmTN8PQtLzc9DucIaagAKeGVyMnyyKIkW
github.com/int128/oauth2cli v1.13.0 h1:3wR48gSHdOPFgHmtnXzs4wEFA6p24prPqLXu6QUOujw=
github.com/int128/oauth2cli v1.13.0/go.mod h1:EUpEzcUumoVjLPHD7y2KGxwfXYqxDVqwDfqQ+u7qAwM=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -215,14 +254,25 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
@@ -239,16 +289,19 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -262,33 +315,40 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/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.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -302,16 +362,19 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -341,11 +404,14 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -354,11 +420,12 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -367,6 +434,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -375,6 +443,10 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgN
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -382,8 +454,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210210192628-66670185b0cd h1:2arJsLyTCJGek+eeptQ3z49Rqndm0f+zvvpwNIXWNIA=
golang.org/x/oauth2 v0.0.0-20210210192628-66670185b0cd/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
@@ -396,9 +468,13 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03i
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -407,15 +483,15 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -427,10 +503,20 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8 h1:de2yTH1xuxjmGB7i6Z5o2z3RCHVa0XlpSZzjd8Fe6bE=
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -438,30 +524,36 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
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/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -486,6 +578,7 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -549,7 +642,6 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -570,6 +662,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
@@ -582,19 +675,23 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -602,26 +699,26 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc=
k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw=
k8s.io/apimachinery v0.19.0 h1:gjKnAda/HZp5k4xQYjL0K/Yb66IvNqjthCb03QlKpaQ=
k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
k8s.io/client-go v0.19.0 h1:1+0E0zfWFIWeyRhQYWzimJOyAk2UT7TiARaLNwJCf7k=
k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU=
k8s.io/api v0.20.2 h1:y/HR22XDZY3pniu9hIFDLpUCPq2w5eQ6aV/VFQ7uJMw=
k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8=
k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg=
k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
k8s.io/client-go v0.20.2 h1:uuf+iIAbfnCSw8IGAv/Rg0giM+2bOzHLOsbbrwrdhNQ=
k8s.io/client-go v0.20.2/go.mod h1:kH5brqWqp7HDxUFKoEgiI4v8G1xzbe9giaCenUWJzgE=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg=
k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.5.0 h1:8mOnjf1RmUPW6KRqQCfYSZq/K20Unmp3IhuZUhxl8KI=
k8s.io/klog/v2 v2.5.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA=
sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=

View File

@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"io"
"io/ioutil"
"os"
"testing"
"time"
@@ -14,8 +13,8 @@ import (
"github.com/int128/kubelogin/integration_test/httpdriver"
"github.com/int128/kubelogin/integration_test/keypair"
"github.com/int128/kubelogin/integration_test/oidcserver"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/infrastructure/browser"
"github.com/int128/kubelogin/pkg/testing/clock"
"github.com/int128/kubelogin/pkg/testing/logger"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -30,17 +29,9 @@ import (
// 4. Verify the output.
//
func TestCredentialPlugin(t *testing.T) {
timeout := 3 * time.Second
timeout := 10 * time.Second
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
tokenCacheDir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a cache dir: %s", err)
}
defer func() {
if err := os.RemoveAll(tokenCacheDir); err != nil {
t.Errorf("could not clean up the cache dir: %s", err)
}
}()
tokenCacheDir := t.TempDir()
for name, tc := range map[string]struct {
keyPair keypair.KeyPair
@@ -340,6 +331,38 @@ func TestCredentialPlugin(t *testing.T) {
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("RedirectURLHTTPS", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "https://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{
TLSConfig: keypair.Server.TLSConfig,
BodyContains: "Authenticated",
}),
now: now,
stdout: &stdout,
args: []string{
"--local-server-cert", keypair.Server.CertPath,
"--local-server-key", keypair.Server.KeyPath,
},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("ExtraParams", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)

View File

@@ -2,8 +2,8 @@ package kubeconfig
import (
"html/template"
"io/ioutil"
"os"
"path/filepath"
"testing"
"gopkg.in/yaml.v2"
@@ -22,7 +22,7 @@ type Values struct {
// Create creates a kubeconfig file and returns path to it.
func Create(t *testing.T, v *Values) string {
t.Helper()
f, err := ioutil.TempFile("", "kubeconfig")
f, err := os.Create(filepath.Join(t.TempDir(), "kubeconfig"))
if err != nil {
t.Fatal(err)
}

View File

@@ -3,11 +3,10 @@ package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"golang.org/x/xerrors"
)
func New(t *testing.T, provider Provider) *Handler {
@@ -29,7 +28,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.t.Logf("%d %s %s", wr.statusCode, r.Method, r.RequestURI)
return
}
if errResp := new(ErrorResponse); xerrors.As(err, &errResp) {
if errResp := new(ErrorResponse); errors.As(err, &errResp) {
h.t.Logf("400 %s %s: %s", r.Method, r.RequestURI, err)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(400)
@@ -62,14 +61,14 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(discoveryResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
return fmt.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/certs":
certificatesResponse := h.provider.GetCertificates()
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(certificatesResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
return fmt.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/auth":
q := r.URL.Query()
@@ -84,13 +83,13 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
RawQuery: q,
})
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
return fmt.Errorf("authentication error: %w", err)
}
to := fmt.Sprintf("%s?state=%s&code=%s", redirectURI, state, code)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/token":
if err := r.ParseForm(); err != nil {
return xerrors.Errorf("could not parse the form: %w", err)
return fmt.Errorf("could not parse the form: %w", err)
}
grantType := r.Form.Get("grant_type")
switch grantType {
@@ -100,12 +99,12 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
CodeVerifier: r.Form.Get("code_verifier"),
})
if err != nil {
return xerrors.Errorf("token request error: %w", err)
return fmt.Errorf("token request error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
return fmt.Errorf("could not render json: %w", err)
}
case "password":
// 4.3. Resource Owner Password Credentials Grant
@@ -113,12 +112,12 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
username, password, scope := r.Form.Get("username"), r.Form.Get("password"), r.Form.Get("scope")
tokenResponse, err := h.provider.AuthenticatePassword(username, password, scope)
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
return fmt.Errorf("authentication error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
return fmt.Errorf("could not render json: %w", err)
}
case "refresh_token":
// 12.1. Refresh Request
@@ -126,12 +125,12 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
refreshToken := r.Form.Get("refresh_token")
tokenResponse, err := h.provider.Refresh(refreshToken)
if err != nil {
return xerrors.Errorf("token refresh error: %w", err)
return fmt.Errorf("token refresh error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
return fmt.Errorf("could not render json: %w", err)
}
default:
// 5.2. Error Response

View File

@@ -4,6 +4,7 @@ package oidcserver
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"math/big"
"strings"
"testing"
@@ -13,7 +14,6 @@ import (
"github.com/int128/kubelogin/integration_test/oidcserver/handler"
"github.com/int128/kubelogin/integration_test/oidcserver/http"
"github.com/int128/kubelogin/pkg/testing/jwt"
"golang.org/x/xerrors"
)
type Server interface {
@@ -133,7 +133,7 @@ func (sv *server) AuthenticateCode(req handler.AuthenticationRequest) (code stri
func (sv *server) Exchange(req handler.TokenRequest) (*handler.TokenResponse, error) {
if req.Code != "YOUR_AUTH_CODE" {
return nil, xerrors.Errorf("code wants %s but was %s", "YOUR_AUTH_CODE", req.Code)
return nil, fmt.Errorf("code wants %s but was %s", "YOUR_AUTH_CODE", req.Code)
}
if sv.lastAuthenticationRequest.CodeChallengeMethod == "S256" {
// https://tools.ietf.org/html/rfc7636#section-4.6

View File

@@ -10,8 +10,8 @@ import (
"github.com/int128/kubelogin/integration_test/keypair"
"github.com/int128/kubelogin/integration_test/kubeconfig"
"github.com/int128/kubelogin/integration_test/oidcserver"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/infrastructure/browser"
"github.com/int128/kubelogin/pkg/testing/clock"
"github.com/int128/kubelogin/pkg/testing/logger"
)

View File

@@ -1,71 +0,0 @@
// Package certpool provides loading certificates from files or base64 encoded string.
package certpool
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"github.com/google/wire"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_certpool/mock_certpool.go github.com/int128/kubelogin/pkg/adaptors/certpool Interface
// Set provides an implementation and interface.
var Set = wire.NewSet(
wire.Value(NewFunc(New)),
wire.Struct(new(CertPool), "*"),
wire.Bind(new(Interface), new(*CertPool)),
)
type NewFunc func() Interface
// New returns an instance which implements the Interface.
func New() Interface {
return &CertPool{pool: x509.NewCertPool()}
}
type Interface interface {
AddFile(filename string) error
AddBase64Encoded(s string) error
SetRootCAs(cfg *tls.Config)
}
// CertPool represents a pool of certificates.
type CertPool struct {
pool *x509.CertPool
}
// SetRootCAs sets cfg.RootCAs if it has any certificate.
// Otherwise do nothing.
func (p *CertPool) SetRootCAs(cfg *tls.Config) {
if len(p.pool.Subjects()) > 0 {
cfg.RootCAs = p.pool
}
}
// AddFile loads the certificate from the file.
func (p *CertPool) AddFile(filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !p.pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
// AddBase64Encoded loads the certificate from the base64 encoded string.
func (p *CertPool) AddBase64Encoded(s string) error {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return xerrors.Errorf("could not decode base64: %w", err)
}
if !p.pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -1,58 +0,0 @@
package certpool
import (
"crypto/tls"
"io/ioutil"
"testing"
)
func TestCertPool_AddFile(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
p := New()
if err := p.AddFile("testdata/ca1.crt"); err != nil {
t.Errorf("AddFile error: %s", err)
}
var cfg tls.Config
p.SetRootCAs(&cfg)
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
})
t.Run("Invalid", func(t *testing.T) {
p := New()
err := p.AddFile("testdata/Makefile")
if err == nil {
t.Errorf("AddFile wants an error but was nil")
}
})
}
func TestCertPool_AddBase64Encoded(t *testing.T) {
p := New()
if err := p.AddBase64Encoded(readFile(t, "testdata/ca2.crt.base64")); err != nil {
t.Errorf("AddBase64Encoded error: %s", err)
}
var cfg tls.Config
p.SetRootCAs(&cfg)
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
}
func TestCertPool_SetRootCAs(t *testing.T) {
p := New()
var cfg tls.Config
p.SetRootCAs(&cfg)
if cfg.RootCAs != nil {
t.Errorf("cfg.RootCAs wants nil but was %+v", cfg.RootCAs)
}
}
func readFile(t *testing.T, filename string) string {
t.Helper()
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("ReadFile error: %s", err)
}
return string(b)
}

View File

@@ -1,74 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/certpool (interfaces: Interface)
// Package mock_certpool is a generated GoMock package.
package mock_certpool
import (
tls "crypto/tls"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// AddBase64Encoded mocks base method.
func (m *MockInterface) AddBase64Encoded(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddBase64Encoded", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddBase64Encoded indicates an expected call of AddBase64Encoded.
func (mr *MockInterfaceMockRecorder) AddBase64Encoded(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBase64Encoded", reflect.TypeOf((*MockInterface)(nil).AddBase64Encoded), arg0)
}
// AddFile mocks base method.
func (m *MockInterface) AddFile(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddFile", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddFile indicates an expected call of AddFile.
func (mr *MockInterfaceMockRecorder) AddFile(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFile", reflect.TypeOf((*MockInterface)(nil).AddFile), arg0)
}
// SetRootCAs mocks base method.
func (m *MockInterface) SetRootCAs(arg0 *tls.Config) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetRootCAs", arg0)
}
// SetRootCAs indicates an expected call of SetRootCAs.
func (mr *MockInterfaceMockRecorder) SetRootCAs(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRootCAs", reflect.TypeOf((*MockInterface)(nil).SetRootCAs), arg0)
}

View File

@@ -1,382 +0,0 @@
package cmd
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/int128/kubelogin/pkg/usecases/standalone/mock_standalone"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("root", func(t *testing.T) {
tests := map[string]struct {
args []string
in standalone.Input
}{
"Defaults": {
args: []string{executable},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
RedirectURLHostname: "localhost",
},
},
},
},
"when --listen-port is set, it should convert the port to address": {
args: []string{
executable,
"--listen-port", "10080",
"--listen-port", "20080",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
RedirectURLHostname: "localhost",
},
},
},
},
"when --listen-port is set, it should ignore --listen-address flags": {
args: []string{
executable,
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:30080",
"--listen-address", "127.0.0.1:40080",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
RedirectURLHostname: "localhost",
},
},
},
},
"FullOptions": {
args: []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--certificate-authority-data", "BASE64ENCODED",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--open-url-after-authentication", "https://example.com/success.html",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
OpenURLAfterAuthentication: "https://example.com/success.html",
RedirectURLHostname: "localhost",
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"--grant-type", "authcode-keyboard",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authcode.KeyboardOption{},
},
},
},
"GrantType=password": {
args: []string{executable,
"--grant-type", "password",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
},
},
"GrantType=auto": {
args: []string{executable,
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
t.Run("get-token", func(t *testing.T) {
tests := map[string]struct {
args []string
in credentialplugin.Input
}{
"Defaults": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
RedirectURLHostname: "localhost",
},
},
},
},
"FullOptions": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--certificate-authority-data", "BASE64ENCODED",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--open-url-after-authentication", "https://example.com/success.html",
"--oidc-auth-request-extra-params", "ttl=86400",
"--oidc-auth-request-extra-params", "reauth=true",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
OpenURLAfterAuthentication: "https://example.com/success.html",
RedirectURLHostname: "localhost",
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "authcode-keyboard",
"--oidc-auth-request-extra-params", "ttl=86400",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authcode.KeyboardOption{
AuthRequestExtraParams: map[string]string{"ttl": "86400"},
},
},
},
},
"GrantType=password": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "password",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
},
},
"GrantType=auto": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
t.Run("MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
}

View File

@@ -1,15 +0,0 @@
package cmd
import "github.com/spf13/pflag"
type tlsOptions struct {
CACertFilename string
CACertData string
SkipTLSVerify bool
}
func (o *tlsOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded cert for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
}

View File

@@ -1,93 +0,0 @@
// Package oidcclient provides a client of OpenID Connect.
package oidcclient
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/logging"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
)
type FactoryInterface interface {
New(ctx context.Context, config Config) (Interface, error)
}
// Config represents a configuration of OpenID Connect client.
type Config struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
}
type Factory struct {
Clock clock.Interface
Logger logger.Interface
}
// New returns an instance of adaptors.Interface with the given configuration.
func (f *Factory) New(ctx context.Context, config Config) (Interface, error) {
var tlsConfig tls.Config
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
config.CertPool.SetRootCAs(&tlsConfig)
baseTransport := &http.Transport{
TLSClientConfig: &tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.IssuerURL)
if err != nil {
return nil, xerrors.Errorf("oidc discovery error: %w", err)
}
supportedPKCEMethods, err := extractSupportedPKCEMethods(provider)
if err != nil {
return nil, xerrors.Errorf("could not determine supported PKCE methods: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Scopes: append(config.ExtraScopes, oidc.ScopeOpenID),
},
clock: f.Clock,
logger: f.Logger,
supportedPKCEMethods: supportedPKCEMethods,
}, nil
}
func extractSupportedPKCEMethods(provider *oidc.Provider) ([]string, error) {
var d struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
if err := provider.Claims(&d); err != nil {
return nil, fmt.Errorf("invalid discovery document: %w", err)
}
return d.CodeChallengeMethodsSupported, nil
}

View File

@@ -1,97 +0,0 @@
package tokencache
import (
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"github.com/google/wire"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_tokencache/mock_tokencache.go github.com/int128/kubelogin/pkg/adaptors/tokencache Interface
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Repository), "*"),
wire.Bind(new(Interface), new(*Repository)),
)
type Interface interface {
FindByKey(dir string, key Key) (*Value, error)
Save(dir string, key Key, value Value) error
}
// Key represents a key of a token cache.
type Key struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CACertFilename string
CACertData string
SkipTLSVerify bool
}
// Value represents a value of a token cache.
type Value struct {
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// Repository provides access to the token cache on the local filesystem.
// Filename of a token cache is sha256 digest of the issuer, zero-character and client ID.
type Repository struct{}
func (r *Repository) FindByKey(dir string, key Key) (*Value, error) {
filename, err := computeFilename(key)
if err != nil {
return nil, xerrors.Errorf("could not compute the key: %w", err)
}
p := filepath.Join(dir, filename)
f, err := os.Open(p)
if err != nil {
return nil, xerrors.Errorf("could not open file %s: %w", p, err)
}
defer f.Close()
d := json.NewDecoder(f)
var c Value
if err := d.Decode(&c); err != nil {
return nil, xerrors.Errorf("invalid json file %s: %w", p, err)
}
return &c, nil
}
func (r *Repository) Save(dir string, key Key, value Value) error {
if err := os.MkdirAll(dir, 0700); err != nil {
return xerrors.Errorf("could not create directory %s: %w", dir, err)
}
filename, err := computeFilename(key)
if err != nil {
return xerrors.Errorf("could not compute the key: %w", err)
}
p := filepath.Join(dir, filename)
f, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return xerrors.Errorf("could not create file %s: %w", p, err)
}
defer f.Close()
e := json.NewEncoder(f)
if err := e.Encode(&value); err != nil {
return xerrors.Errorf("json encode error: %w", err)
}
return nil
}
func computeFilename(key Key) (string, error) {
s := sha256.New()
e := gob.NewEncoder(s)
if err := e.Encode(&key); err != nil {
return "", xerrors.Errorf("could not encode the key: %w", err)
}
h := hex.EncodeToString(s.Sum(nil))
return h, nil
}

View File

@@ -3,19 +3,22 @@ package cmd
import (
"fmt"
"strings"
"time"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
type authenticationOptions struct {
GrantType string
ListenAddress []string
ListenPort []int // deprecated
AuthenticationTimeoutSec int
SkipOpenBrowser bool
LocalServerCertFile string
LocalServerKeyFile string
OpenURLAfterAuthentication string
RedirectURLHostname string
AuthRequestExtraParams map[string]string
@@ -54,6 +57,9 @@ func (o *authenticationOptions) addFlags(f *pflag.FlagSet) {
panic(err)
}
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "[authcode] Do not open the browser automatically")
f.IntVar(&o.AuthenticationTimeoutSec, "authentication-timeout-sec", defaultAuthenticationTimeoutSec, "[authcode] Timeout of authentication in seconds")
f.StringVar(&o.LocalServerCertFile, "local-server-cert", "", "[authcode] Certificate path for the local server")
f.StringVar(&o.LocalServerKeyFile, "local-server-key", "", "[authcode] Certificate key path for the local server")
f.StringVar(&o.OpenURLAfterAuthentication, "open-url-after-authentication", "", "[authcode] If set, open the URL in the browser after authentication")
f.StringVar(&o.RedirectURLHostname, "oidc-redirect-url-hostname", "localhost", "[authcode] Hostname of the redirect URL")
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "[authcode, authcode-keyboard] Extra query parameters to send with an authentication request")
@@ -67,6 +73,9 @@ func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSe
s.AuthCodeBrowserOption = &authcode.BrowserOption{
BindAddress: o.determineListenAddress(),
SkipOpenBrowser: o.SkipOpenBrowser,
AuthenticationTimeout: time.Duration(o.AuthenticationTimeoutSec) * time.Second,
LocalServerCertFile: o.LocalServerCertFile,
LocalServerKeyFile: o.LocalServerKeyFile,
OpenURLAfterAuthentication: o.OpenURLAfterAuthentication,
RedirectURLHostname: o.RedirectURLHostname,
AuthRequestExtraParams: o.AuthRequestExtraParams,
@@ -81,7 +90,7 @@ func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSe
Password: o.Password,
}
default:
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
err = fmt.Errorf("grant-type must be one of (%s)", allGrantType)
}
return
}

View File

@@ -0,0 +1,141 @@
package cmd
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/spf13/pflag"
)
func Test_authenticationOptions_grantOptionSet(t *testing.T) {
tests := map[string]struct {
args []string
want authentication.GrantOptionSet
}{
"NoFlag": {
want: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
"FullOptions": {
args: []string{
"--grant-type", "authcode",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--authentication-timeout-sec", "10",
"--local-server-cert", "/path/to/local-server-cert",
"--local-server-key", "/path/to/local-server-key",
"--open-url-after-authentication", "https://example.com/success.html",
"--oidc-redirect-url-hostname", "example",
"--oidc-auth-request-extra-params", "ttl=86400",
"--oidc-auth-request-extra-params", "reauth=true",
"--username", "USER",
"--password", "PASS",
},
want: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
AuthenticationTimeout: 10 * time.Second,
LocalServerCertFile: "/path/to/local-server-cert",
LocalServerKeyFile: "/path/to/local-server-key",
OpenURLAfterAuthentication: "https://example.com/success.html",
RedirectURLHostname: "example",
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
},
},
},
"when --listen-port is set, it should convert the port to address": {
args: []string{
"--listen-port", "10080",
"--listen-port", "20080",
},
want: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
"when --listen-port is set, it should ignore --listen-address flags": {
args: []string{
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:30080",
"--listen-address", "127.0.0.1:40080",
},
want: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
"GrantType=authcode-keyboard": {
args: []string{
"--grant-type", "authcode-keyboard",
},
want: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authcode.KeyboardOption{},
},
},
"GrantType=password": {
args: []string{
"--grant-type", "password",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
want: authentication.GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
},
"GrantType=auto": {
args: []string{
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
want: authentication.GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
var o authenticationOptions
f := pflag.NewFlagSet("", pflag.ContinueOnError)
o.addFlags(f)
if err := f.Parse(c.args); err != nil {
t.Fatalf("Parse error: %s", err)
}
got, err := o.grantOptionSet()
if err != nil {
t.Fatalf("grantOptionSet error: %s", err)
}
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -5,7 +5,7 @@ import (
"runtime"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/spf13/cobra"
"k8s.io/client-go/util/homedir"
)
@@ -26,6 +26,8 @@ type Interface interface {
var defaultListenAddress = []string{"127.0.0.1:8000", "127.0.0.1:18000"}
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
const defaultAuthenticationTimeoutSec = 180
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {
Root *Root

219
pkg/cmd/cmd_test.go Normal file
View File

@@ -0,0 +1,219 @@
package cmd
import (
"context"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/int128/kubelogin/pkg/usecases/standalone/mock_standalone"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("root", func(t *testing.T) {
tests := map[string]struct {
args []string
in standalone.Input
}{
"Defaults": {
args: []string{executable},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
},
"FullOptions": {
args: []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"-v1",
},
in: standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
t.Run("get-token", func(t *testing.T) {
tests := map[string]struct {
args []string
in credentialplugin.Input
}{
"Defaults": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
Provider: oidc.Provider{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
},
"FullOptions": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"-v1",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
Provider: oidc.Provider{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
t.Run("MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
}

View File

@@ -1,11 +1,14 @@
package cmd
import (
"github.com/int128/kubelogin/pkg/adaptors/logger"
"errors"
"fmt"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
// getTokenOptions represents the options for get-token command.
@@ -44,31 +47,31 @@ func (cmd *GetToken) New() *cobra.Command {
return err
}
if o.IssuerURL == "" {
return xerrors.New("--oidc-issuer-url is missing")
return errors.New("--oidc-issuer-url is missing")
}
if o.ClientID == "" {
return xerrors.New("--oidc-client-id is missing")
return errors.New("--oidc-client-id is missing")
}
return nil
},
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("get-token: %w", err)
return fmt.Errorf("get-token: %w", err)
}
in := credentialplugin.Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
TokenCacheDir: o.TokenCacheDir,
GrantOptionSet: grantOptionSet,
Provider: oidc.Provider{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
},
TokenCacheDir: o.TokenCacheDir,
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
}
if err := cmd.GetToken.Do(c.Context(), in); err != nil {
return xerrors.Errorf("get-token: %w", err)
return fmt.Errorf("get-token: %w", err)
}
return nil
},

View File

@@ -1,12 +1,12 @@
package cmd
import (
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"fmt"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
const rootDescription = `Log in to the OpenID Connect provider.
@@ -51,19 +51,17 @@ func (cmd *Root) New() *cobra.Command {
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("invalid option: %w", err)
return fmt.Errorf("invalid option: %w", err)
}
in := standalone.Input{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
}
if err := cmd.Standalone.Do(c.Context(), in); err != nil {
return xerrors.Errorf("login: %w", err)
return fmt.Errorf("login: %w", err)
}
return nil
},

View File

@@ -1,10 +1,10 @@
package cmd
import (
"fmt"
"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.
@@ -39,17 +39,15 @@ func (cmd *Setup) New() *cobra.Command {
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("setup: %w", err)
return fmt.Errorf("setup: %w", err)
}
in := setup.Stage2Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
GrantOptionSet: grantOptionSet,
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
}
if c.Flags().Lookup("listen-address").Changed {
in.ListenAddressArgs = o.authenticationOptions.ListenAddress
@@ -59,7 +57,7 @@ func (cmd *Setup) New() *cobra.Command {
return nil
}
if err := cmd.Setup.DoStage2(c.Context(), in); err != nil {
return xerrors.Errorf("setup: %w", err)
return fmt.Errorf("setup: %w", err)
}
return nil
},

43
pkg/cmd/tls.go Normal file
View File

@@ -0,0 +1,43 @@
package cmd
import (
"crypto/tls"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/spf13/pflag"
)
type tlsOptions struct {
CACertFilename []string
CACertData []string
SkipTLSVerify bool
RenegotiateOnceAsClient bool
RenegotiateFreelyAsClient bool
}
func (o *tlsOptions) addFlags(f *pflag.FlagSet) {
f.StringArrayVar(&o.CACertFilename, "certificate-authority", nil, "Path to a cert file for the certificate authority")
f.StringArrayVar(&o.CACertData, "certificate-authority-data", nil, "Base64 encoded cert for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.BoolVar(&o.RenegotiateOnceAsClient, "tls-renegotiation-once", false, "If set, allow a remote server to request renegotiation once per connection")
f.BoolVar(&o.RenegotiateFreelyAsClient, "tls-renegotiation-freely", false, "If set, allow a remote server to repeatedly request renegotiation")
}
func (o tlsOptions) tlsClientConfig() tlsclientconfig.Config {
return tlsclientconfig.Config{
CACertFilename: o.CACertFilename,
CACertData: o.CACertData,
SkipTLSVerify: o.SkipTLSVerify,
Renegotiation: o.renegotiationSupport(),
}
}
func (o tlsOptions) renegotiationSupport() tls.RenegotiationSupport {
if o.RenegotiateOnceAsClient {
return tls.RenegotiateOnceAsClient
}
if o.RenegotiateFreelyAsClient {
return tls.RenegotiateFreelyAsClient
}
return tls.RenegotiateNever
}

92
pkg/cmd/tls_test.go Normal file
View File

@@ -0,0 +1,92 @@
package cmd
import (
"crypto/tls"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/spf13/pflag"
)
func Test_tlsOptions_tlsClientConfig(t *testing.T) {
tests := map[string]struct {
args []string
want tlsclientconfig.Config
}{
"NoFlag": {},
"SkipTLSVerify": {
args: []string{
"--insecure-skip-tls-verify",
},
want: tlsclientconfig.Config{
SkipTLSVerify: true,
},
},
"CACertFilename1": {
args: []string{
"--certificate-authority", "/path/to/cert1",
},
want: tlsclientconfig.Config{
CACertFilename: []string{"/path/to/cert1"},
},
},
"CACertFilename2": {
args: []string{
"--certificate-authority", "/path/to/cert1",
"--certificate-authority", "/path/to/cert2",
},
want: tlsclientconfig.Config{
CACertFilename: []string{"/path/to/cert1", "/path/to/cert2"},
},
},
"CACertData1": {
args: []string{
"--certificate-authority-data", "base64encoded1",
},
want: tlsclientconfig.Config{
CACertData: []string{"base64encoded1"},
},
},
"CACertData2": {
args: []string{
"--certificate-authority-data", "base64encoded1",
"--certificate-authority-data", "base64encoded2",
},
want: tlsclientconfig.Config{
CACertData: []string{"base64encoded1", "base64encoded2"},
},
},
"RenegotiateOnceAsClient": {
args: []string{
"--tls-renegotiation-once",
},
want: tlsclientconfig.Config{
Renegotiation: tls.RenegotiateOnceAsClient,
},
},
"RenegotiateFreelyAsClient": {
args: []string{
"--tls-renegotiation-freely",
},
want: tlsclientconfig.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
var o tlsOptions
f := pflag.NewFlagSet("", pflag.ContinueOnError)
o.addFlags(f)
if err := f.Parse(c.args); err != nil {
t.Fatalf("Parse error: %s", err)
}
got := o.tlsClientConfig()
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -0,0 +1,10 @@
// Package credentialplugin provides the types for client-go credential plugins.
package credentialplugin
import "time"
// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
}

View File

@@ -1,18 +1,18 @@
// Package credentialpluginwriter provides a writer for a credential plugin.
package credentialpluginwriter
// Package writer provides a writer for a credential plugin.
package writer
import (
"encoding/json"
"time"
"fmt"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"golang.org/x/xerrors"
"github.com/int128/kubelogin/pkg/credentialplugin"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
)
//go:generate mockgen -destination mock_credentialpluginwriter/mock_credentialpluginwriter.go github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter Interface
//go:generate mockgen -destination mock_writer/mock_writer.go github.com/int128/kubelogin/pkg/credentialplugin/writer Interface
var Set = wire.NewSet(
wire.Struct(new(Writer), "*"),
@@ -20,13 +20,7 @@ var Set = wire.NewSet(
)
type Interface interface {
Write(out Output) error
}
// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
Write(out credentialplugin.Output) error
}
type Writer struct {
@@ -34,7 +28,7 @@ type Writer struct {
}
// Write writes the ExecCredential to standard output for kubectl.
func (w *Writer) Write(out Output) error {
func (w *Writer) Write(out credentialplugin.Output) error {
ec := &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
@@ -47,7 +41,7 @@ func (w *Writer) Write(out Output) error {
}
e := json.NewEncoder(w.Stdout)
if err := e.Encode(ec); err != nil {
return xerrors.Errorf("could not write the ExecCredential: %w", err)
return fmt.Errorf("could not write the ExecCredential: %w", err)
}
return nil
}

View File

@@ -1,47 +1,47 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/credentialplugin/writer (interfaces: Interface)
// Package mock_credentialpluginwriter is a generated GoMock package.
package mock_credentialpluginwriter
// Package mock_writer is a generated GoMock package.
package mock_writer
import (
gomock "github.com/golang/mock/gomock"
credentialpluginwriter "github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
credentialplugin "github.com/int128/kubelogin/pkg/credentialplugin"
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Write mocks base method.
func (m *MockInterface) Write(arg0 credentialpluginwriter.Output) error {
// Write mocks base method
func (m *MockInterface) Write(arg0 credentialplugin.Output) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Write indicates an expected call of Write.
// Write indicates an expected call of Write
func (mr *MockInterfaceMockRecorder) Write(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockInterface)(nil).Write), arg0)

View File

@@ -5,24 +5,26 @@ package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/cmd"
"github.com/int128/kubelogin/pkg/credentialplugin/writer"
"github.com/int128/kubelogin/pkg/infrastructure/browser"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/infrastructure/mutex"
"github.com/int128/kubelogin/pkg/infrastructure/reader"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
kubeconfigLoader "github.com/int128/kubelogin/pkg/kubeconfig/loader"
kubeconfigWriter "github.com/int128/kubelogin/pkg/kubeconfig/writer"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/tlsclientconfig/loader"
"github.com/int128/kubelogin/pkg/tokencache/repository"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
)
// NewCmd returns an instance of adaptors.Cmd.
// NewCmd returns an instance of infrastructure.Cmd.
func NewCmd() cmd.Interface {
wire.Build(
NewCmdForHeadless,
@@ -36,7 +38,7 @@ func NewCmd() cmd.Interface {
return nil
}
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
// NewCmdForHeadless returns an instance of infrastructure.Cmd for headless testing.
func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interface, browser.Interface) cmd.Interface {
wire.Build(
// use-cases
@@ -45,14 +47,16 @@ func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interf
credentialplugin.Set,
setup.Set,
// adaptors
// infrastructure
cmd.Set,
reader.Set,
kubeconfig.Set,
tokencache.Set,
oidcclient.Set,
certpool.Set,
credentialpluginwriter.Set,
kubeconfigLoader.Set,
kubeconfigWriter.Set,
repository.Set,
client.Set,
loader.Set,
writer.Set,
mutex.Set,
)
return nil
}

View File

@@ -6,17 +6,19 @@
package di
import (
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/cmd"
writer2 "github.com/int128/kubelogin/pkg/credentialplugin/writer"
"github.com/int128/kubelogin/pkg/infrastructure/browser"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/infrastructure/mutex"
"github.com/int128/kubelogin/pkg/infrastructure/reader"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
loader2 "github.com/int128/kubelogin/pkg/kubeconfig/loader"
"github.com/int128/kubelogin/pkg/kubeconfig/writer"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/tlsclientconfig/loader"
"github.com/int128/kubelogin/pkg/tokencache/repository"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
@@ -28,7 +30,6 @@ import (
// Injectors from di.go:
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() cmd.Interface {
clockReal := &clock.Real{}
stdin := _wireFileValue
@@ -44,9 +45,10 @@ var (
_wireOsFileValue = os.Stdout
)
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout stdio.Stdout, loggerInterface logger.Interface, browserInterface browser.Interface) cmd.Interface {
factory := &oidcclient.Factory{
loaderLoader := loader.Loader{}
factory := &client.Factory{
Loader: loaderLoader,
Clock: clockInterface,
Logger: loggerInterface,
}
@@ -66,36 +68,37 @@ func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout
Logger: loggerInterface,
}
authenticationAuthentication := &authentication.Authentication{
OIDCClient: factory,
ClientFactory: factory,
Logger: loggerInterface,
Clock: clockInterface,
AuthCodeBrowser: authcodeBrowser,
AuthCodeKeyboard: keyboard,
ROPC: ropcROPC,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
Logger: loggerInterface,
}
newFunc := _wireNewFuncValue
loader3 := &loader2.Loader{}
writerWriter := &writer.Writer{}
standaloneStandalone := &standalone.Standalone{
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
NewCertPool: newFunc,
Logger: loggerInterface,
Authentication: authenticationAuthentication,
KubeconfigLoader: loader3,
KubeconfigWriter: writerWriter,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
Logger: loggerInterface,
}
repository := &tokencache.Repository{}
writer := &credentialpluginwriter.Writer{
repositoryRepository := &repository.Repository{}
writer3 := &writer2.Writer{
Stdout: stdout,
}
mutexMutex := &mutex.Mutex{
Logger: loggerInterface,
}
getToken := &credentialplugin.GetToken{
Authentication: authenticationAuthentication,
TokenCacheRepository: repository,
NewCertPool: newFunc,
Writer: writer,
TokenCacheRepository: repositoryRepository,
Writer: writer3,
Mutex: mutexMutex,
Logger: loggerInterface,
}
cmdGetToken := &cmd.GetToken{
@@ -104,7 +107,6 @@ func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout
}
setupSetup := &setup.Setup{
Authentication: authenticationAuthentication,
NewCertPool: newFunc,
Logger: loggerInterface,
}
cmdSetup := &cmd.Setup{
@@ -118,7 +120,3 @@ func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout
}
return cmdCmd
}
var (
_wireNewFuncValue = certpool.NewFunc(certpool.New)
)

View File

@@ -7,7 +7,7 @@ import (
"github.com/pkg/browser"
)
//go:generate mockgen -destination mock_browser/mock_browser.go github.com/int128/kubelogin/pkg/adaptors/browser Interface
//go:generate mockgen -destination mock_browser/mock_browser.go github.com/int128/kubelogin/pkg/infrastructure/browser Interface
func init() {
// In credential plugin mode, some browser launcher writes a message to stdout

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/browser (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/infrastructure/browser (interfaces: Interface)
// Package mock_browser is a generated GoMock package.
package mock_browser
@@ -9,30 +9,30 @@ import (
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Open mocks base method.
// Open mocks base method
func (m *MockInterface) Open(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Open", arg0)
@@ -40,7 +40,7 @@ func (m *MockInterface) Open(arg0 string) error {
return ret0
}
// Open indicates an expected call of Open.
// Open indicates an expected call of Open
func (mr *MockInterfaceMockRecorder) Open(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockInterface)(nil).Open), arg0)

View File

@@ -7,7 +7,7 @@ import (
"github.com/google/wire"
"github.com/spf13/pflag"
"k8s.io/klog"
"k8s.io/klog/v2"
)
// Set provides an implementation and interface for Logger.
@@ -56,5 +56,5 @@ func (*Logger) V(level int) Verbose {
// IsEnabled returns true if the level is enabled.
func (*Logger) IsEnabled(level int) bool {
return bool(klog.V(klog.Level(level)))
return klog.V(klog.Level(level)).Enabled()
}

View File

@@ -0,0 +1,64 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/infrastructure/mutex (interfaces: Interface)
// Package mock_mutex is a generated GoMock package.
package mock_mutex
import (
context "context"
gomock "github.com/golang/mock/gomock"
mutex "github.com/int128/kubelogin/pkg/infrastructure/mutex"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Acquire mocks base method
func (m *MockInterface) Acquire(arg0 context.Context, arg1 string) (*mutex.Lock, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Acquire", arg0, arg1)
ret0, _ := ret[0].(*mutex.Lock)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Acquire indicates an expected call of Acquire
func (mr *MockInterfaceMockRecorder) Acquire(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Acquire", reflect.TypeOf((*MockInterface)(nil).Acquire), arg0, arg1)
}
// Release mocks base method
func (m *MockInterface) Release(arg0 *mutex.Lock) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Release", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Release indicates an expected call of Release
func (mr *MockInterfaceMockRecorder) Release(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockInterface)(nil).Release), arg0)
}

View File

@@ -0,0 +1,88 @@
package mutex
import (
"context"
"fmt"
"github.com/alexflint/go-filemutex"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"os"
"path"
)
//go:generate mockgen -destination mock_mutex/mock_mutex.go github.com/int128/kubelogin/pkg/infrastructure/mutex Interface
var Set = wire.NewSet(
wire.Struct(new(Mutex), "*"),
wire.Bind(new(Interface), new(*Mutex)),
)
type Interface interface {
Acquire(ctx context.Context, name string) (*Lock, error)
Release(lock *Lock) error
}
// Lock holds the lock data.
type Lock struct {
Data interface{}
Name string
}
type Mutex struct {
Logger logger.Interface
}
// internalAcquire wait for acquisition of the lock
func internalAcquire(fm *filemutex.FileMutex) chan error {
result := make(chan error)
go func() {
if err := fm.Lock(); err != nil {
result <- err
}
close(result)
}()
return result
}
// internalRelease disposes of resources associated with a lock
func internalRelease(fm *filemutex.FileMutex, lfn string, log logger.Interface) error {
err := fm.Close()
if err != nil {
log.V(1).Infof("Error closing lock file %s: %s", lfn, err)
}
return err
}
// LockFileName get the lock file name from the lock name.
func LockFileName(name string) string {
return path.Join(os.TempDir(), fmt.Sprintf(".kubelogin.%s.lock", name))
}
// Acquire acquire a lock for the specified name. The context could be used to set a timeout.
func (m *Mutex) Acquire(ctx context.Context, name string) (*Lock, error) {
lfn := LockFileName(name)
fm, err := filemutex.New(lfn)
if err != nil {
return nil, fmt.Errorf("error creating mutex file %s: %w", lfn, err)
}
lockChan := internalAcquire(fm)
select {
case <-ctx.Done():
_ = internalRelease(fm, lfn, m.Logger)
return nil, ctx.Err()
case err := <-lockChan:
if err != nil {
_ = internalRelease(fm, lfn, m.Logger)
return nil, fmt.Errorf("error acquiring lock on file %s: %w", lfn, err)
}
return &Lock{Data: fm, Name: name}, nil
}
}
// Release release the specified lock
func (m *Mutex) Release(lock *Lock) error {
fm := lock.Data.(*filemutex.FileMutex)
lfn := LockFileName(lock.Name)
return internalRelease(fm, lfn, m.Logger)
}

View File

@@ -0,0 +1,64 @@
package mutex
import (
"fmt"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"golang.org/x/net/context"
"math/rand"
"sync"
"testing"
"time"
)
func TestMutex(t *testing.T) {
t.Run("Test successful parallel acquisition with no reentry allowed", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
nbConcurrency := 20
wg := sync.WaitGroup{}
events := make(chan int, nbConcurrency*2)
errors := make(chan error, nbConcurrency)
doLockUnlock := func() {
defer wg.Done()
m := Mutex{
Logger: logger.New(),
}
if mutex, err := m.Acquire(ctx, "test"); err == nil {
events <- 1
var dur = time.Duration(rand.Intn(5000))
time.Sleep(dur * time.Microsecond)
events <- -1
if err := m.Release(mutex); err != nil {
errors <- fmt.Errorf("Release error: %w", err)
}
} else {
errors <- fmt.Errorf("Acquire error: %w", err)
}
}
for i := 0; i < nbConcurrency; i++ {
wg.Add(1)
go doLockUnlock()
}
wg.Wait()
close(events)
close(errors)
countConcurrent := 0
for delta := range events {
countConcurrent += delta
if countConcurrent > 1 {
t.Errorf("The mutex did not prevented reentry: %d", countConcurrent)
}
}
for anError := range errors {
t.Errorf("The gorouting returned an error: %s", anError)
}
})
}

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/reader (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/infrastructure/reader (interfaces: Interface)
// Package mock_reader is a generated GoMock package.
package mock_reader
@@ -9,30 +9,30 @@ import (
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// ReadPassword mocks base method.
// ReadPassword mocks base method
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadPassword", arg0)
@@ -41,13 +41,13 @@ func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
return ret0, ret1
}
// ReadPassword indicates an expected call of ReadPassword.
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
}
// ReadString mocks base method.
// ReadString mocks base method
func (m *MockInterface) ReadString(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadString", arg0)
@@ -56,7 +56,7 @@ func (m *MockInterface) ReadString(arg0 string) (string, error) {
return ret0, ret1
}
// ReadString indicates an expected call of ReadString.
// ReadString indicates an expected call of ReadString
func (mr *MockInterfaceMockRecorder) ReadString(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadString", reflect.TypeOf((*MockInterface)(nil).ReadString), arg0)

View File

@@ -9,12 +9,11 @@ import (
"syscall"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
"golang.org/x/term"
)
//go:generate mockgen -destination mock_reader/mock_reader.go github.com/int128/kubelogin/pkg/adaptors/reader Interface
//go:generate mockgen -destination mock_reader/mock_reader.go github.com/int128/kubelogin/pkg/infrastructure/reader Interface
// Set provides an implementation and interface for Reader.
var Set = wire.NewSet(
@@ -34,12 +33,12 @@ type Reader struct {
// ReadString reads a string from the stdin.
func (x *Reader) ReadString(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", xerrors.Errorf("write error: %w", err)
return "", fmt.Errorf("write error: %w", err)
}
r := bufio.NewReader(x.Stdin)
s, err := r.ReadString('\n')
if err != nil {
return "", xerrors.Errorf("read error: %w", err)
return "", fmt.Errorf("read error: %w", err)
}
s = strings.TrimRight(s, "\r\n")
return s, nil
@@ -48,14 +47,14 @@ func (x *Reader) ReadString(prompt string) (string, error) {
// ReadPassword reads a password from the stdin without echo back.
func (*Reader) ReadPassword(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", xerrors.Errorf("write error: %w", err)
return "", fmt.Errorf("write error: %w", err)
}
b, err := terminal.ReadPassword(int(syscall.Stdin))
b, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", xerrors.Errorf("read error: %w", err)
return "", fmt.Errorf("read error: %w", err)
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", xerrors.Errorf("write error: %w", err)
return "", fmt.Errorf("write error: %w", err)
}
return string(b), nil
}

View File

@@ -6,10 +6,9 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"golang.org/x/xerrors"
)
// DecodeWithoutVerify decodes the JWT string and returns the claims.
@@ -17,18 +16,18 @@ import (
func DecodeWithoutVerify(s string) (*Claims, error) {
payload, err := DecodePayloadAsRawJSON(s)
if err != nil {
return nil, xerrors.Errorf("could not decode the payload: %w", err)
return nil, fmt.Errorf("could not decode the payload: %w", err)
}
var claims struct {
Subject string `json:"sub,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
}
if err := json.NewDecoder(bytes.NewReader(payload)).Decode(&claims); err != nil {
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
return nil, fmt.Errorf("could not decode the json of token: %w", err)
}
var prettyJson bytes.Buffer
if err := json.Indent(&prettyJson, payload, "", " "); err != nil {
return nil, xerrors.Errorf("could not indent the json of token: %w", err)
return nil, fmt.Errorf("could not indent the json of token: %w", err)
}
return &Claims{
Subject: claims.Subject,
@@ -41,11 +40,11 @@ func DecodeWithoutVerify(s string) (*Claims, error) {
func DecodePayloadAsPrettyJSON(s string) (string, error) {
payload, err := DecodePayloadAsRawJSON(s)
if err != nil {
return "", xerrors.Errorf("could not decode the payload: %w", err)
return "", fmt.Errorf("could not decode the payload: %w", err)
}
var prettyJson bytes.Buffer
if err := json.Indent(&prettyJson, payload, "", " "); err != nil {
return "", xerrors.Errorf("could not indent the json of token: %w", err)
return "", fmt.Errorf("could not indent the json of token: %w", err)
}
return prettyJson.String(), nil
}
@@ -54,11 +53,11 @@ func DecodePayloadAsPrettyJSON(s string) (string, error) {
func DecodePayloadAsRawJSON(s string) ([]byte, error) {
parts := strings.SplitN(s, ".", 3)
if len(parts) != 3 {
return nil, xerrors.Errorf("wants %d segments but got %d segments", 3, len(parts))
return nil, fmt.Errorf("wants %d segments but got %d segments", 3, len(parts))
}
payloadJSON, err := decodePayload(parts[1])
if err != nil {
return nil, xerrors.Errorf("could not decode the payload: %w", err)
return nil, fmt.Errorf("could not decode the payload: %w", err)
}
return payloadJSON, nil
}
@@ -66,7 +65,7 @@ func DecodePayloadAsRawJSON(s string) ([]byte, error) {
func decodePayload(payload string) ([]byte, error) {
b, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(payload)
if err != nil {
return nil, xerrors.Errorf("invalid base64: %w", err)
return nil, fmt.Errorf("invalid base64: %w", err)
}
return b, nil
}

View File

@@ -1,21 +1,37 @@
package kubeconfig
package loader
import (
"errors"
"fmt"
"strings"
"golang.org/x/xerrors"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/kubeconfig"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func (*Kubeconfig) GetCurrentAuthProvider(explicitFilename string, contextName ContextName, userName UserName) (*AuthProvider, error) {
//go:generate mockgen -destination mock_loader/mock_loader.go github.com/int128/kubelogin/pkg/kubeconfig/loader Interface
var Set = wire.NewSet(
wire.Struct(new(Loader), "*"),
wire.Bind(new(Interface), new(*Loader)),
)
type Interface interface {
GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error)
}
type Loader struct{}
func (Loader) GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
config, err := loadByDefaultRules(explicitFilename)
if err != nil {
return nil, xerrors.Errorf("could not load the kubeconfig: %w", err)
return nil, fmt.Errorf("could not load the kubeconfig: %w", err)
}
auth, err := findCurrentAuthProvider(config, contextName, userName)
if err != nil {
return nil, xerrors.Errorf("could not find the current auth provider: %w", err)
return nil, fmt.Errorf("could not find the current auth provider: %w", err)
}
return auth, nil
}
@@ -25,7 +41,7 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
rules.ExplicitPath = explicitFilename
config, err := rules.Load()
if err != nil {
return nil, xerrors.Errorf("load error: %w", err)
return nil, fmt.Errorf("load error: %w", err)
}
return config, err
}
@@ -34,29 +50,29 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
// If contextName is given, this returns the user of the context.
// If userName is given, this ignores the context and returns the user.
// If any context or user is not found, this returns an error.
func findCurrentAuthProvider(config *api.Config, contextName ContextName, userName UserName) (*AuthProvider, error) {
func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
if userName == "" {
if contextName == "" {
contextName = ContextName(config.CurrentContext)
contextName = kubeconfig.ContextName(config.CurrentContext)
}
contextNode, ok := config.Contexts[string(contextName)]
if !ok {
return nil, xerrors.Errorf("context %s does not exist", contextName)
return nil, fmt.Errorf("context %s does not exist", contextName)
}
userName = UserName(contextNode.AuthInfo)
userName = kubeconfig.UserName(contextNode.AuthInfo)
}
userNode, ok := config.AuthInfos[string(userName)]
if !ok {
return nil, xerrors.Errorf("user %s does not exist", userName)
return nil, fmt.Errorf("user %s does not exist", userName)
}
if userNode.AuthProvider == nil {
return nil, xerrors.New("auth-provider is missing")
return nil, errors.New("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return nil, xerrors.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
return nil, fmt.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
}
if userNode.AuthProvider.Config == nil {
return nil, xerrors.New("auth-provider.config is missing")
return nil, errors.New("auth-provider.config is missing")
}
m := userNode.AuthProvider.Config
@@ -64,7 +80,7 @@ func findCurrentAuthProvider(config *api.Config, contextName ContextName, userNa
if m["extra-scopes"] != "" {
extraScopes = strings.Split(m["extra-scopes"], ",")
}
return &AuthProvider{
return &kubeconfig.AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,

View File

@@ -1,10 +1,11 @@
package kubeconfig
package loader
import (
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)
@@ -105,7 +106,7 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &AuthProvider{
want := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
@@ -145,7 +146,7 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &AuthProvider{
want := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
@@ -173,7 +174,7 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &AuthProvider{
want := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
IDPIssuerURL: "https://accounts.google.com",

View File

@@ -1,39 +1,39 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/kubeconfig (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/kubeconfig/loader (interfaces: Interface)
// Package mock_kubeconfig is a generated GoMock package.
package mock_kubeconfig
// Package mock_loader is a generated GoMock package.
package mock_loader
import (
gomock "github.com/golang/mock/gomock"
kubeconfig "github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
kubeconfig "github.com/int128/kubelogin/pkg/kubeconfig"
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// GetCurrentAuthProvider mocks base method.
// GetCurrentAuthProvider mocks base method
func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
@@ -42,22 +42,8 @@ func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.Cont
return ret0, ret1
}
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider.
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
func (mr *MockInterfaceMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockInterface)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
}
// UpdateAuthProvider mocks base method.
func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider.
func (mr *MockInterfaceMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockInterface)(nil).UpdateAuthProvider), arg0)
}

View File

@@ -1,23 +1,5 @@
package kubeconfig
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/logger"
)
//go:generate mockgen -destination mock_kubeconfig/mock_kubeconfig.go github.com/int128/kubelogin/pkg/adaptors/kubeconfig Interface
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Kubeconfig), "*"),
wire.Bind(new(Interface), new(*Kubeconfig)),
)
type Interface interface {
GetCurrentAuthProvider(explicitFilename string, contextName ContextName, userName UserName) (*AuthProvider, error)
UpdateAuthProvider(auth *AuthProvider) error
}
// ContextName represents name of a context.
type ContextName string
@@ -39,7 +21,3 @@ type AuthProvider struct {
IDToken string // (optional) id-token
RefreshToken string // (optional) refresh-token
}
type Kubeconfig struct {
Logger logger.Interface
}

View File

@@ -0,0 +1,48 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/kubeconfig/writer (interfaces: Interface)
// Package mock_writer is a generated GoMock package.
package mock_writer
import (
gomock "github.com/golang/mock/gomock"
kubeconfig "github.com/int128/kubelogin/pkg/kubeconfig"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// UpdateAuthProvider mocks base method
func (m *MockInterface) UpdateAuthProvider(arg0 kubeconfig.AuthProvider) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
func (mr *MockInterfaceMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockInterface)(nil).UpdateAuthProvider), arg0)
}

View File

@@ -1,35 +1,50 @@
package kubeconfig
package writer
import (
"fmt"
"strings"
"golang.org/x/xerrors"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/kubeconfig"
"k8s.io/client-go/tools/clientcmd"
)
func (*Kubeconfig) UpdateAuthProvider(p *AuthProvider) error {
//go:generate mockgen -destination mock_writer/mock_writer.go github.com/int128/kubelogin/pkg/kubeconfig/writer Interface
var Set = wire.NewSet(
wire.Struct(new(Writer), "*"),
wire.Bind(new(Interface), new(*Writer)),
)
type Interface interface {
UpdateAuthProvider(p kubeconfig.AuthProvider) error
}
type Writer struct{}
func (Writer) UpdateAuthProvider(p kubeconfig.AuthProvider) error {
config, err := clientcmd.LoadFromFile(p.LocationOfOrigin)
if err != nil {
return xerrors.Errorf("could not load %s: %w", p.LocationOfOrigin, err)
return fmt.Errorf("could not load %s: %w", p.LocationOfOrigin, err)
}
userNode, ok := config.AuthInfos[string(p.UserName)]
if !ok {
return xerrors.Errorf("user %s does not exist", p.UserName)
return fmt.Errorf("user %s does not exist", p.UserName)
}
if userNode.AuthProvider == nil {
return xerrors.Errorf("auth-provider is missing")
return fmt.Errorf("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return xerrors.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
return fmt.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
}
copyAuthProviderConfig(p, userNode.AuthProvider.Config)
if err := clientcmd.WriteToFile(*config, p.LocationOfOrigin); err != nil {
return xerrors.Errorf("could not update %s: %w", p.LocationOfOrigin, err)
return fmt.Errorf("could not update %s: %w", p.LocationOfOrigin, err)
}
return nil
}
func copyAuthProviderConfig(p *AuthProvider, m map[string]string) {
func copyAuthProviderConfig(p kubeconfig.AuthProvider, m map[string]string) {
setOrDeleteKey(m, "idp-issuer-url", p.IDPIssuerURL)
setOrDeleteKey(m, "client-id", p.ClientID)
setOrDeleteKey(m, "client-secret", p.ClientSecret)

View File

@@ -1,4 +1,4 @@
package kubeconfig
package writer
import (
"io/ioutil"
@@ -6,10 +6,11 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/kubeconfig"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {
var k Kubeconfig
var w Writer
t.Run("MinimumKeys", func(t *testing.T) {
f := newKubeconfigFile(t)
@@ -18,7 +19,7 @@ func TestKubeconfig_UpdateAuth(t *testing.T) {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuthProvider(&AuthProvider{
if err := w.UpdateAuthProvider(kubeconfig.AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
IDPIssuerURL: "https://accounts.google.com",
@@ -65,7 +66,7 @@ users:
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuthProvider(&AuthProvider{
if err := w.UpdateAuthProvider(kubeconfig.AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
IDPIssuerURL: "https://accounts.google.com",

View File

@@ -1,22 +1,21 @@
package oidcclient
package client
import (
"context"
"fmt"
"net/http"
"time"
gooidc "github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/jwt"
gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/pkce"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_oidcclient/mock_oidcclient.go github.com/int128/kubelogin/pkg/adaptors/oidcclient Interface
//go:generate mockgen -destination mock_client/mock_client.go github.com/int128/kubelogin/pkg/oidc/client Interface
type Interface interface {
GetAuthCodeURL(in AuthCodeURLInput) string
@@ -50,6 +49,8 @@ type GetTokenByAuthCodeInput struct {
RedirectURLHostname string
AuthRequestExtraParams map[string]string
LocalServerSuccessHTML string
LocalServerCertFile string
LocalServerKeyFile string
}
type client struct {
@@ -80,11 +81,13 @@ func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeIn
LocalServerReadyChan: localServerReadyChan,
RedirectURLHostname: in.RedirectURLHostname,
LocalServerSuccessHTML: in.LocalServerSuccessHTML,
LocalServerCertFile: in.LocalServerCertFile,
LocalServerKeyFile: in.LocalServerKeyFile,
Logf: c.logger.V(1).Infof,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, xerrors.Errorf("oauth2 error: %w", err)
return nil, fmt.Errorf("oauth2 error: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
@@ -105,7 +108,7 @@ func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput)
opts := tokenRequestOptions(in.PKCEParams)
token, err := cfg.Exchange(ctx, in.Code, opts...)
if err != nil {
return nil, xerrors.Errorf("exchange error: %w", err)
return nil, fmt.Errorf("exchange error: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
@@ -145,7 +148,7 @@ func (c *client) GetTokenByROPC(ctx context.Context, username, password string)
ctx = c.wrapContext(ctx)
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, username, password)
if err != nil {
return nil, xerrors.Errorf("resource owner password credentials flow error: %w", err)
return nil, fmt.Errorf("resource owner password credentials flow error: %w", err)
}
return c.verifyToken(ctx, token, "")
}
@@ -160,7 +163,7 @@ func (c *client) Refresh(ctx context.Context, refreshToken string) (*oidc.TokenS
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, xerrors.Errorf("could not refresh the token: %w", err)
return nil, fmt.Errorf("could not refresh the token: %w", err)
}
return c.verifyToken(ctx, token, "")
}
@@ -170,27 +173,18 @@ func (c *client) Refresh(ctx context.Context, refreshToken string) (*oidc.TokenS
func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce string) (*oidc.TokenSet, error) {
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
return nil, fmt.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&gooidc.Config{ClientID: c.oauth2Config.ClientID, Now: c.clock.Now})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the ID token: %w", err)
return nil, fmt.Errorf("could not verify the ID token: %w", err)
}
if nonce != "" && nonce != verifiedIDToken.Nonce {
return nil, xerrors.Errorf("nonce did not match (wants %s but got %s)", nonce, verifiedIDToken.Nonce)
}
pretty, err := jwt.DecodePayloadAsPrettyJSON(idToken)
if err != nil {
return nil, xerrors.Errorf("could not decode the token: %w", err)
return nil, fmt.Errorf("nonce did not match (wants %s but got %s)", nonce, verifiedIDToken.Nonce)
}
return &oidc.TokenSet{
IDToken: idToken,
IDTokenClaims: jwt.Claims{
Subject: verifiedIDToken.Subject,
Expiry: verifiedIDToken.Expiry,
Pretty: pretty,
},
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}

View File

@@ -0,0 +1,87 @@
// Package client provides a client of OpenID Connect.
package client
import (
"context"
"fmt"
"net/http"
gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/oidc/client/logging"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/tlsclientconfig/loader"
"golang.org/x/oauth2"
)
//go:generate mockgen -destination mock_client/mock_factory.go github.com/int128/kubelogin/pkg/oidc/client FactoryInterface
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
)
type FactoryInterface interface {
New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error)
}
type Factory struct {
Loader loader.Loader
Clock clock.Interface
Logger logger.Interface
}
// New returns an instance of infrastructure.Interface with the given configuration.
func (f *Factory) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error) {
rawTLSClientConfig, err := f.Loader.Load(tlsClientConfig)
if err != nil {
return nil, fmt.Errorf("could not load the TLS client config: %w", err)
}
baseTransport := &http.Transport{
TLSClientConfig: rawTLSClientConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := gooidc.NewProvider(ctx, p.IssuerURL)
if err != nil {
return nil, fmt.Errorf("oidc discovery error: %w", err)
}
supportedPKCEMethods, err := extractSupportedPKCEMethods(provider)
if err != nil {
return nil, fmt.Errorf("could not determine supported PKCE methods: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Scopes: append(p.ExtraScopes, gooidc.ScopeOpenID),
},
clock: f.Clock,
logger: f.Logger,
supportedPKCEMethods: supportedPKCEMethods,
}, nil
}
func extractSupportedPKCEMethods(provider *gooidc.Provider) ([]string, error) {
var d struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
if err := provider.Claims(&d); err != nil {
return nil, fmt.Errorf("invalid discovery document: %w", err)
}
return d.CodeChallengeMethodsSupported, nil
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
)
const (

View File

@@ -1,42 +1,42 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/oidcclient (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/oidc/client (interfaces: Interface)
// Package mock_oidcclient is a generated GoMock package.
package mock_oidcclient
// Package mock_client is a generated GoMock package.
package mock_client
import (
context "context"
gomock "github.com/golang/mock/gomock"
oidcclient "github.com/int128/kubelogin/pkg/adaptors/oidcclient"
oidc "github.com/int128/kubelogin/pkg/oidc"
client "github.com/int128/kubelogin/pkg/oidc/client"
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// ExchangeAuthCode mocks base method.
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.ExchangeAuthCodeInput) (*oidc.TokenSet, error) {
// ExchangeAuthCode mocks base method
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 client.ExchangeAuthCodeInput) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExchangeAuthCode", arg0, arg1)
ret0, _ := ret[0].(*oidc.TokenSet)
@@ -44,28 +44,28 @@ func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.E
return ret0, ret1
}
// ExchangeAuthCode indicates an expected call of ExchangeAuthCode.
// ExchangeAuthCode indicates an expected call of ExchangeAuthCode
func (mr *MockInterfaceMockRecorder) ExchangeAuthCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthCode", reflect.TypeOf((*MockInterface)(nil).ExchangeAuthCode), arg0, arg1)
}
// GetAuthCodeURL mocks base method.
func (m *MockInterface) GetAuthCodeURL(arg0 oidcclient.AuthCodeURLInput) string {
// GetAuthCodeURL mocks base method
func (m *MockInterface) GetAuthCodeURL(arg0 client.AuthCodeURLInput) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthCodeURL", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL.
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL
func (mr *MockInterfaceMockRecorder) GetAuthCodeURL(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthCodeURL", reflect.TypeOf((*MockInterface)(nil).GetAuthCodeURL), arg0)
}
// GetTokenByAuthCode mocks base method.
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidc.TokenSet, error) {
// GetTokenByAuthCode mocks base method
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 client.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByAuthCode", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.TokenSet)
@@ -73,13 +73,13 @@ func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient
return ret0, ret1
}
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode.
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode
func (mr *MockInterfaceMockRecorder) GetTokenByAuthCode(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByAuthCode", reflect.TypeOf((*MockInterface)(nil).GetTokenByAuthCode), arg0, arg1, arg2)
}
// GetTokenByROPC mocks base method.
// GetTokenByROPC mocks base method
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByROPC", arg0, arg1, arg2)
@@ -88,13 +88,13 @@ func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string)
return ret0, ret1
}
// GetTokenByROPC indicates an expected call of GetTokenByROPC.
// GetTokenByROPC indicates an expected call of GetTokenByROPC
func (mr *MockInterfaceMockRecorder) GetTokenByROPC(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByROPC", reflect.TypeOf((*MockInterface)(nil).GetTokenByROPC), arg0, arg1, arg2)
}
// Refresh mocks base method.
// Refresh mocks base method
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
@@ -103,13 +103,13 @@ func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidc.TokenS
return ret0, ret1
}
// Refresh indicates an expected call of Refresh.
// Refresh indicates an expected call of Refresh
func (mr *MockInterfaceMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockInterface)(nil).Refresh), arg0, arg1)
}
// SupportedPKCEMethods mocks base method.
// SupportedPKCEMethods mocks base method
func (m *MockInterface) SupportedPKCEMethods() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SupportedPKCEMethods")
@@ -117,7 +117,7 @@ func (m *MockInterface) SupportedPKCEMethods() []string {
return ret0
}
// SupportedPKCEMethods indicates an expected call of SupportedPKCEMethods.
// SupportedPKCEMethods indicates an expected call of SupportedPKCEMethods
func (mr *MockInterfaceMockRecorder) SupportedPKCEMethods() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedPKCEMethods", reflect.TypeOf((*MockInterface)(nil).SupportedPKCEMethods))

View File

@@ -0,0 +1,52 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/oidc/client (interfaces: FactoryInterface)
// Package mock_client is a generated GoMock package.
package mock_client
import (
context "context"
gomock "github.com/golang/mock/gomock"
oidc "github.com/int128/kubelogin/pkg/oidc"
client "github.com/int128/kubelogin/pkg/oidc/client"
tlsclientconfig "github.com/int128/kubelogin/pkg/tlsclientconfig"
reflect "reflect"
)
// MockFactoryInterface is a mock of FactoryInterface interface
type MockFactoryInterface struct {
ctrl *gomock.Controller
recorder *MockFactoryInterfaceMockRecorder
}
// MockFactoryInterfaceMockRecorder is the mock recorder for MockFactoryInterface
type MockFactoryInterfaceMockRecorder struct {
mock *MockFactoryInterface
}
// NewMockFactoryInterface creates a new mock instance
func NewMockFactoryInterface(ctrl *gomock.Controller) *MockFactoryInterface {
mock := &MockFactoryInterface{ctrl: ctrl}
mock.recorder = &MockFactoryInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFactoryInterface) EXPECT() *MockFactoryInterfaceMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockFactoryInterface) New(arg0 context.Context, arg1 oidc.Provider, arg2 tlsclientconfig.Config) (client.Interface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "New", arg0, arg1, arg2)
ret0, _ := ret[0].(client.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockFactoryInterfaceMockRecorder) New(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockFactoryInterface)(nil).New), arg0, arg1, arg2)
}

View File

@@ -4,23 +4,33 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"fmt"
"github.com/int128/kubelogin/pkg/jwt"
"golang.org/x/xerrors"
)
// TokenSet represents an output DTO of
// Interface.GetTokenByAuthCode, Interface.GetTokenByROPC and Interface.Refresh.
// Provider represents an OIDC provider.
type Provider struct {
IssuerURL string
ClientID string
ClientSecret string // optional
ExtraScopes []string // optional
}
// TokenSet represents a set of ID token and refresh token.
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenClaims jwt.Claims
IDToken string
RefreshToken string
}
func (ts TokenSet) DecodeWithoutVerify() (*jwt.Claims, error) {
return jwt.DecodeWithoutVerify(ts.IDToken)
}
func NewState() (string, error) {
b, err := random32()
if err != nil {
return "", xerrors.Errorf("could not generate a random: %w", err)
return "", fmt.Errorf("could not generate a random: %w", err)
}
return base64URLEncode(b), nil
}
@@ -28,7 +38,7 @@ func NewState() (string, error) {
func NewNonce() (string, error) {
b, err := random32()
if err != nil {
return "", xerrors.Errorf("could not generate a random: %w", err)
return "", fmt.Errorf("could not generate a random: %w", err)
}
return base64URLEncode(b), nil
}
@@ -36,7 +46,7 @@ func NewNonce() (string, error) {
func random32() ([]byte, error) {
b := make([]byte, 32)
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
return nil, xerrors.Errorf("read error: %w", err)
return nil, fmt.Errorf("read error: %w", err)
}
return b, nil
}

View File

@@ -7,8 +7,7 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"golang.org/x/xerrors"
"fmt"
)
var Plain Params
@@ -45,7 +44,7 @@ func New(methods []string) (Params, error) {
func NewS256() (Params, error) {
b, err := random32()
if err != nil {
return Plain, xerrors.Errorf("could not generate a random: %w", err)
return Plain, fmt.Errorf("could not generate a random: %w", err)
}
return computeS256(b), nil
}
@@ -53,7 +52,7 @@ func NewS256() (Params, error) {
func random32() ([]byte, error) {
b := make([]byte, 32)
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
return nil, xerrors.Errorf("read error: %w", err)
return nil, fmt.Errorf("read error: %w", err)
}
return b, nil
}

View File

@@ -3,7 +3,7 @@ package logger
import (
"fmt"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/spf13/pflag"
)

View File

@@ -0,0 +1,11 @@
package tlsclientconfig
import "crypto/tls"
// Config represents a config for TLS client.
type Config struct {
CACertFilename []string
CACertData []string
SkipTLSVerify bool
Renegotiation tls.RenegotiationSupport
}

View File

@@ -0,0 +1,72 @@
// Package loader provides loading certificates from files or base64 encoded string.
package loader
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
)
// Set provides an implementation and interface.
var Set = wire.NewSet(
wire.Struct(new(Loader), "*"),
wire.Bind(new(Interface), new(*Loader)),
)
type Interface interface {
Load(config tlsclientconfig.Config) (*tls.Config, error)
}
// Loader represents a pool of certificates.
type Loader struct{}
func (l *Loader) Load(config tlsclientconfig.Config) (*tls.Config, error) {
rootCAs := x509.NewCertPool()
for _, f := range config.CACertFilename {
if err := addFile(rootCAs, f); err != nil {
return nil, fmt.Errorf("could not load the certificate from %s: %w", f, err)
}
}
for _, d := range config.CACertData {
if err := addBase64Encoded(rootCAs, d); err != nil {
return nil, fmt.Errorf("could not load the certificate: %w", err)
}
}
if len(rootCAs.Subjects()) == 0 {
// use the host's root CA set
rootCAs = nil
}
return &tls.Config{
RootCAs: rootCAs,
InsecureSkipVerify: config.SkipTLSVerify,
Renegotiation: config.Renegotiation,
}, nil
}
func addFile(p *x509.CertPool, filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("could not read: %w", err)
}
if !p.AppendCertsFromPEM(b) {
return errors.New("invalid certificate")
}
return nil
}
func addBase64Encoded(p *x509.CertPool, s string) error {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return fmt.Errorf("could not decode base64: %w", err)
}
if !p.AppendCertsFromPEM(b) {
return errors.New("invalid certificate")
}
return nil
}

View File

@@ -0,0 +1,60 @@
package loader
import (
"io/ioutil"
"testing"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
)
func TestLoader_Load(t *testing.T) {
var loader Loader
t.Run("Zero", func(t *testing.T) {
cfg, err := loader.Load(tlsclientconfig.Config{})
if err != nil {
t.Errorf("Load error: %s", err)
}
if cfg.RootCAs != nil {
t.Errorf("RootCAs wants nil but was %+v", cfg.RootCAs)
}
})
t.Run("ValidFile", func(t *testing.T) {
cfg, err := loader.Load(tlsclientconfig.Config{
CACertFilename: []string{"testdata/ca1.crt"},
})
if err != nil {
t.Errorf("Load error: %s", err)
}
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
})
t.Run("InvalidFile", func(t *testing.T) {
_, err := loader.Load(tlsclientconfig.Config{
CACertFilename: []string{"testdata/Makefile"},
})
if err == nil {
t.Errorf("AddFile wants an error but was nil")
}
})
t.Run("ValidBase64", func(t *testing.T) {
cfg, err := loader.Load(tlsclientconfig.Config{
CACertData: []string{readFile(t, "testdata/ca2.crt.base64")},
})
if err != nil {
t.Errorf("Load error: %s", err)
}
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
})
}
func readFile(t *testing.T, filename string) string {
t.Helper()
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("ReadFile error: %s", err)
}
return string(b)
}

View File

@@ -1,62 +1,63 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/tokencache (interfaces: Interface)
// Source: github.com/int128/kubelogin/pkg/tokencache/repository (interfaces: Interface)
// Package mock_tokencache is a generated GoMock package.
package mock_tokencache
// Package mock_repository is a generated GoMock package.
package mock_repository
import (
gomock "github.com/golang/mock/gomock"
tokencache "github.com/int128/kubelogin/pkg/adaptors/tokencache"
oidc "github.com/int128/kubelogin/pkg/oidc"
tokencache "github.com/int128/kubelogin/pkg/tokencache"
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// FindByKey mocks base method.
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache.Value, error) {
// FindByKey mocks base method
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*oidc.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
ret0, _ := ret[0].(*tokencache.Value)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindByKey indicates an expected call of FindByKey.
// FindByKey indicates an expected call of FindByKey
func (mr *MockInterfaceMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockInterface)(nil).FindByKey), arg0, arg1)
}
// Save mocks base method.
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.Value) error {
// Save mocks base method
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 oidc.TokenSet) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Save indicates an expected call of Save.
// Save indicates an expected call of Save
func (mr *MockInterfaceMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockInterface)(nil).Save), arg0, arg1, arg2)

View File

@@ -0,0 +1,93 @@
package repository
import (
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/tokencache"
)
//go:generate mockgen -destination mock_repository/mock_repository.go github.com/int128/kubelogin/pkg/tokencache/repository Interface
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Repository), "*"),
wire.Bind(new(Interface), new(*Repository)),
)
type Interface interface {
FindByKey(dir string, key tokencache.Key) (*oidc.TokenSet, error)
Save(dir string, key tokencache.Key, tokenSet oidc.TokenSet) error
}
type entity struct {
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// Repository provides access to the token cache on the local filesystem.
// Filename of a token cache is sha256 digest of the issuer, zero-character and client ID.
type Repository struct{}
func (r *Repository) FindByKey(dir string, key tokencache.Key) (*oidc.TokenSet, error) {
filename, err := computeFilename(key)
if err != nil {
return nil, fmt.Errorf("could not compute the key: %w", err)
}
p := filepath.Join(dir, filename)
f, err := os.Open(p)
if err != nil {
return nil, fmt.Errorf("could not open file %s: %w", p, err)
}
defer f.Close()
d := json.NewDecoder(f)
var e entity
if err := d.Decode(&e); err != nil {
return nil, fmt.Errorf("invalid json file %s: %w", p, err)
}
return &oidc.TokenSet{
IDToken: e.IDToken,
RefreshToken: e.RefreshToken,
}, nil
}
func (r *Repository) Save(dir string, key tokencache.Key, tokenSet oidc.TokenSet) error {
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("could not create directory %s: %w", dir, err)
}
filename, err := computeFilename(key)
if err != nil {
return fmt.Errorf("could not compute the key: %w", err)
}
p := filepath.Join(dir, filename)
f, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("could not create file %s: %w", p, err)
}
defer f.Close()
e := entity{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
}
if err := json.NewEncoder(f).Encode(&e); err != nil {
return fmt.Errorf("json encode error: %w", err)
}
return nil
}
func computeFilename(key tokencache.Key) (string, error) {
s := sha256.New()
e := gob.NewEncoder(s)
if err := e.Encode(&key); err != nil {
return "", fmt.Errorf("could not encode the key: %w", err)
}
h := hex.EncodeToString(s.Sum(nil))
return h, nil
}

View File

@@ -1,28 +1,21 @@
package tokencache
package repository
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/tokencache"
)
func TestRepository_FindByKey(t *testing.T) {
var r Repository
t.Run("Success", func(t *testing.T) {
dir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a temp dir: %s", err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("could not clean up the temp dir: %s", err)
}
}()
key := Key{
dir := t.TempDir()
key := tokencache.Key{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
@@ -40,12 +33,12 @@ func TestRepository_FindByKey(t *testing.T) {
t.Fatalf("could not write to the temp file: %s", err)
}
value, err := r.FindByKey(dir, key)
got, err := r.FindByKey(dir, key)
if err != nil {
t.Errorf("err wants nil but %+v", err)
}
want := &Value{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if diff := cmp.Diff(want, value); diff != "" {
want := &oidc.TokenSet{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
@@ -55,17 +48,8 @@ func TestRepository_Save(t *testing.T) {
var r Repository
t.Run("Success", func(t *testing.T) {
dir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a temp dir: %s", err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("could not clean up the temp dir: %s", err)
}
}()
key := Key{
dir := t.TempDir()
key := tokencache.Key{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
@@ -73,8 +57,8 @@ func TestRepository_Save(t *testing.T) {
CACertFilename: "/path/to/cert",
SkipTLSVerify: false,
}
value := Value{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Save(dir, key, value); err != nil {
tokenSet := oidc.TokenSet{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Save(dir, key, tokenSet); err != nil {
t.Errorf("err wants nil but %+v", err)
}

13
pkg/tokencache/types.go Normal file
View File

@@ -0,0 +1,13 @@
package tokencache
// Key represents a key of a token cache.
type Key struct {
IssuerURL string
ClientID string
ClientSecret string
Username string
ExtraScopes []string
CACertFilename string
CACertData string
SkipTLSVerify bool
}

View File

@@ -2,22 +2,26 @@ package authcode
import (
"context"
"fmt"
"time"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/infrastructure/browser"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/pkce"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
)
type BrowserOption struct {
SkipOpenBrowser bool
BindAddress []string
AuthenticationTimeout time.Duration
OpenURLAfterAuthentication string
RedirectURLHostname string
AuthRequestExtraParams map[string]string
LocalServerCertFile string
LocalServerKeyFile string
}
// Browser provides the authentication code flow using the browser.
@@ -26,25 +30,25 @@ type Browser struct {
Logger logger.Interface
}
func (u *Browser) Do(ctx context.Context, o *BrowserOption, client oidcclient.Interface) (*oidc.TokenSet, error) {
func (u *Browser) Do(ctx context.Context, o *BrowserOption, oidcClient client.Interface) (*oidc.TokenSet, error) {
u.Logger.V(1).Infof("starting the authentication code flow using the browser")
state, err := oidc.NewState()
if err != nil {
return nil, xerrors.Errorf("could not generate a state: %w", err)
return nil, fmt.Errorf("could not generate a state: %w", err)
}
nonce, err := oidc.NewNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
return nil, fmt.Errorf("could not generate a nonce: %w", err)
}
p, err := pkce.New(client.SupportedPKCEMethods())
p, err := pkce.New(oidcClient.SupportedPKCEMethods())
if err != nil {
return nil, xerrors.Errorf("could not generate PKCE parameters: %w", err)
return nil, fmt.Errorf("could not generate PKCE parameters: %w", err)
}
successHTML := BrowserSuccessHTML
if o.OpenURLAfterAuthentication != "" {
successHTML = BrowserRedirectHTML(o.OpenURLAfterAuthentication)
}
in := oidcclient.GetTokenByAuthCodeInput{
in := client.GetTokenByAuthCodeInput{
BindAddress: o.BindAddress,
State: state,
Nonce: nonce,
@@ -52,7 +56,12 @@ func (u *Browser) Do(ctx context.Context, o *BrowserOption, client oidcclient.In
RedirectURLHostname: o.RedirectURLHostname,
AuthRequestExtraParams: o.AuthRequestExtraParams,
LocalServerSuccessHTML: successHTML,
LocalServerCertFile: o.LocalServerCertFile,
LocalServerKeyFile: o.LocalServerKeyFile,
}
ctx, cancel := context.WithTimeout(ctx, o.AuthenticationTimeout)
defer cancel()
readyChan := make(chan string, 1)
var out *oidc.TokenSet
var eg errgroup.Group
@@ -75,21 +84,21 @@ Please visit the following URL in your browser manually: %s`, err, url)
}
return nil
case <-ctx.Done():
return xerrors.Errorf("context cancelled while waiting for the local server: %w", ctx.Err())
return fmt.Errorf("context cancelled while waiting for the local server: %w", ctx.Err())
}
})
eg.Go(func() error {
defer close(readyChan)
tokenSet, err := client.GetTokenByAuthCode(ctx, in, readyChan)
tokenSet, err := oidcClient.GetTokenByAuthCode(ctx, in, readyChan)
if err != nil {
return xerrors.Errorf("authorization code flow error: %w", err)
return fmt.Errorf("authorization code flow error: %w", err)
}
out = tokenSet
u.Logger.V(1).Infof("got a token set by the authorization code flow")
return nil
})
if err := eg.Wait(); err != nil {
return nil, xerrors.Errorf("authentication error: %w", err)
return nil, fmt.Errorf("authentication error: %w", err)
}
u.Logger.V(1).Infof("finished the authorization code flow via the browser")
return out, nil

View File

@@ -7,20 +7,14 @@ import (
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/infrastructure/browser/mock_browser"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/oidc/client/mock_client"
"github.com/int128/kubelogin/pkg/testing/logger"
)
func TestBrowser_Do(t *testing.T) {
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: "PRETTY_JSON",
}
timeout := 5 * time.Second
t.Run("Success", func(t *testing.T) {
@@ -31,15 +25,18 @@ func TestBrowser_Do(t *testing.T) {
o := &BrowserOption{
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
AuthenticationTimeout: 10 * time.Second,
LocalServerCertFile: "/path/to/local-server-cert",
LocalServerKeyFile: "/path/to/local-server-key",
OpenURLAfterAuthentication: "https://example.com/success.html",
RedirectURLHostname: "localhost",
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().SupportedPKCEMethods()
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().SupportedPKCEMethods()
mockClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, in oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
Do(func(_ context.Context, in client.GetTokenByAuthCodeInput, readyChan chan<- string) {
if diff := cmp.Diff(o.BindAddress, in.BindAddress); diff != "" {
t.Errorf("BindAddress mismatch (-want +got):\n%s", diff)
}
@@ -52,24 +49,28 @@ func TestBrowser_Do(t *testing.T) {
if diff := cmp.Diff(o.AuthRequestExtraParams, in.AuthRequestExtraParams); diff != "" {
t.Errorf("AuthRequestExtraParams mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(o.LocalServerKeyFile, in.LocalServerKeyFile); diff != "" {
t.Errorf("LocalServerKeyFile mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(o.LocalServerCertFile, in.LocalServerCertFile); diff != "" {
t.Errorf("LocalServerCertFile mismatch (-want +got):\n%s", diff)
}
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := Browser{
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
got, err := u.Do(ctx, o, mockClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
@@ -82,19 +83,19 @@ func TestBrowser_Do(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &BrowserOption{
BindAddress: []string{"127.0.0.1:8000"},
BindAddress: []string{"127.0.0.1:8000"},
AuthenticationTimeout: 10 * time.Second,
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().SupportedPKCEMethods()
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().SupportedPKCEMethods()
mockClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
Do(func(_ context.Context, _ client.GetTokenByAuthCodeInput, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
mockBrowser := mock_browser.NewMockInterface(ctrl)
mockBrowser.EXPECT().
@@ -103,14 +104,13 @@ func TestBrowser_Do(t *testing.T) {
Logger: logger.New(t),
Browser: mockBrowser,
}
got, err := u.Do(ctx, o, mockOIDCClient)
got, err := u.Do(ctx, o, mockClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)

View File

@@ -2,13 +2,13 @@ package authcode
import (
"context"
"fmt"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/infrastructure/reader"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/pkce"
"golang.org/x/xerrors"
)
const keyboardPrompt = "Enter code: "
@@ -24,21 +24,21 @@ type Keyboard struct {
Logger logger.Interface
}
func (u *Keyboard) Do(ctx context.Context, o *KeyboardOption, client oidcclient.Interface) (*oidc.TokenSet, error) {
func (u *Keyboard) Do(ctx context.Context, o *KeyboardOption, oidcClient client.Interface) (*oidc.TokenSet, error) {
u.Logger.V(1).Infof("starting the authorization code flow with keyboard interactive")
state, err := oidc.NewState()
if err != nil {
return nil, xerrors.Errorf("could not generate a state: %w", err)
return nil, fmt.Errorf("could not generate a state: %w", err)
}
nonce, err := oidc.NewNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce: %w", err)
return nil, fmt.Errorf("could not generate a nonce: %w", err)
}
p, err := pkce.New(client.SupportedPKCEMethods())
p, err := pkce.New(oidcClient.SupportedPKCEMethods())
if err != nil {
return nil, xerrors.Errorf("could not generate PKCE parameters: %w", err)
return nil, fmt.Errorf("could not generate PKCE parameters: %w", err)
}
authCodeURL := client.GetAuthCodeURL(oidcclient.AuthCodeURLInput{
authCodeURL := oidcClient.GetAuthCodeURL(client.AuthCodeURLInput{
State: state,
Nonce: nonce,
PKCEParams: p,
@@ -48,18 +48,18 @@ func (u *Keyboard) Do(ctx context.Context, o *KeyboardOption, client oidcclient.
u.Logger.Printf("Please visit the following URL in your browser: %s", authCodeURL)
code, err := u.Reader.ReadString(keyboardPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read an authorization code: %w", err)
return nil, fmt.Errorf("could not read an authorization code: %w", err)
}
u.Logger.V(1).Infof("exchanging the code and token")
tokenSet, err := client.ExchangeAuthCode(ctx, oidcclient.ExchangeAuthCodeInput{
tokenSet, err := oidcClient.ExchangeAuthCode(ctx, client.ExchangeAuthCodeInput{
Code: code,
PKCEParams: p,
Nonce: nonce,
RedirectURI: oobRedirectURI,
})
if err != nil {
return nil, xerrors.Errorf("could not exchange the authorization code: %w", err)
return nil, fmt.Errorf("could not exchange the authorization code: %w", err)
}
u.Logger.V(1).Infof("finished the authorization code flow with keyboard interactive")
return tokenSet, nil

View File

@@ -7,22 +7,16 @@ import (
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader/mock_reader"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/infrastructure/reader/mock_reader"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/oidc/client/mock_client"
"github.com/int128/kubelogin/pkg/testing/logger"
)
var nonNil = gomock.Not(gomock.Nil())
func TestKeyboard_Do(t *testing.T) {
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: "PRETTY_JSON",
}
timeout := 5 * time.Second
t.Run("Success", func(t *testing.T) {
@@ -33,27 +27,26 @@ func TestKeyboard_Do(t *testing.T) {
o := &KeyboardOption{
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().SupportedPKCEMethods()
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().SupportedPKCEMethods()
mockClient.EXPECT().
GetAuthCodeURL(nonNil).
Do(func(in oidcclient.AuthCodeURLInput) {
Do(func(in client.AuthCodeURLInput) {
if diff := cmp.Diff(o.AuthRequestExtraParams, in.AuthRequestExtraParams); diff != "" {
t.Errorf("AuthRequestExtraParams mismatch (-want +got):\n%s", diff)
}
}).
Return("https://issuer.example.com/auth")
mockOIDCClient.EXPECT().
mockClient.EXPECT().
ExchangeAuthCode(nonNil, nonNil).
Do(func(_ context.Context, in oidcclient.ExchangeAuthCodeInput) {
Do(func(_ context.Context, in client.ExchangeAuthCodeInput) {
if in.Code != "YOUR_AUTH_CODE" {
t.Errorf("Code wants YOUR_AUTH_CODE but was %s", in.Code)
}
}).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
IDTokenClaims: dummyTokenClaims,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
mockReader := mock_reader.NewMockInterface(ctrl)
mockReader.EXPECT().
@@ -63,14 +56,13 @@ func TestKeyboard_Do(t *testing.T) {
Reader: mockReader,
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
got, err := u.Do(ctx, o, mockClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
IDTokenClaims: dummyTokenClaims,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)

View File

@@ -2,17 +2,16 @@ package authentication
import (
"context"
"fmt"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_authentication/mock_authentication.go github.com/int128/kubelogin/pkg/usecases/authentication Interface
@@ -32,15 +31,10 @@ type Interface interface {
// Input represents an input DTO of the Authentication use-case.
type Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
IDToken string // optional, from the token cache
RefreshToken string // optional, from the token cache
GrantOptionSet GrantOptionSet
Provider oidc.Provider
GrantOptionSet GrantOptionSet
CachedTokenSet *oidc.TokenSet // optional
TLSClientConfig tlsclientconfig.Config
}
type GrantOptionSet struct {
@@ -69,7 +63,7 @@ type Output struct {
// If the Password is not set, it asks a password by the prompt.
//
type Authentication struct {
OIDCClient oidcclient.FactoryInterface
ClientFactory client.FactoryInterface
Logger logger.Interface
Clock clock.Interface
AuthCodeBrowser *authcode.Browser
@@ -78,77 +72,60 @@ type Authentication struct {
}
func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
if in.IDToken != "" {
if in.CachedTokenSet != nil {
u.Logger.V(1).Infof("checking expiration of the existing token")
// Skip verification of the token to reduce time of a discovery request.
// Here it trusts the signature and claims and checks only expiration,
// because the token has been verified before caching.
claims, err := jwt.DecodeWithoutVerify(in.IDToken)
claims, err := in.CachedTokenSet.DecodeWithoutVerify()
if err != nil {
return nil, xerrors.Errorf("invalid token cache (you may need to remove): %w", err)
return nil, fmt.Errorf("invalid token cache (you may need to remove): %w", err)
}
if !claims.IsExpired(u.Clock) {
u.Logger.V(1).Infof("you already have a valid token until %s", claims.Expiry)
return &Output{
AlreadyHasValidIDToken: true,
TokenSet: oidc.TokenSet{
IDToken: in.IDToken,
RefreshToken: in.RefreshToken,
IDTokenClaims: *claims,
},
TokenSet: *in.CachedTokenSet,
}, nil
}
u.Logger.V(1).Infof("you have an expired token at %s", claims.Expiry)
}
u.Logger.V(1).Infof("initializing an OpenID Connect client")
client, err := u.OIDCClient.New(ctx, oidcclient.Config{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: in.CertPool,
SkipTLSVerify: in.SkipTLSVerify,
})
oidcClient, err := u.ClientFactory.New(ctx, in.Provider, in.TLSClientConfig)
if err != nil {
return nil, xerrors.Errorf("oidc error: %w", err)
return nil, fmt.Errorf("oidc error: %w", err)
}
if in.RefreshToken != "" {
if in.CachedTokenSet != nil && in.CachedTokenSet.RefreshToken != "" {
u.Logger.V(1).Infof("refreshing the token")
out, err := client.Refresh(ctx, in.RefreshToken)
tokenSet, err := oidcClient.Refresh(ctx, in.CachedTokenSet.RefreshToken)
if err == nil {
return &Output{
TokenSet: oidc.TokenSet{
IDToken: out.IDToken,
IDTokenClaims: out.IDTokenClaims,
RefreshToken: out.RefreshToken,
},
}, nil
return &Output{TokenSet: *tokenSet}, nil
}
u.Logger.V(1).Infof("could not refresh the token: %s", err)
}
if in.GrantOptionSet.AuthCodeBrowserOption != nil {
tokenSet, err := u.AuthCodeBrowser.Do(ctx, in.GrantOptionSet.AuthCodeBrowserOption, client)
tokenSet, err := u.AuthCodeBrowser.Do(ctx, in.GrantOptionSet.AuthCodeBrowserOption, oidcClient)
if err != nil {
return nil, xerrors.Errorf("authcode-browser error: %w", err)
return nil, fmt.Errorf("authcode-browser error: %w", err)
}
return &Output{TokenSet: *tokenSet}, nil
}
if in.GrantOptionSet.AuthCodeKeyboardOption != nil {
tokenSet, err := u.AuthCodeKeyboard.Do(ctx, in.GrantOptionSet.AuthCodeKeyboardOption, client)
tokenSet, err := u.AuthCodeKeyboard.Do(ctx, in.GrantOptionSet.AuthCodeKeyboardOption, oidcClient)
if err != nil {
return nil, xerrors.Errorf("authcode-keyboard error: %w", err)
return nil, fmt.Errorf("authcode-keyboard error: %w", err)
}
return &Output{TokenSet: *tokenSet}, nil
}
if in.GrantOptionSet.ROPCOption != nil {
tokenSet, err := u.ROPC.Do(ctx, in.GrantOptionSet.ROPCOption, client)
tokenSet, err := u.ROPC.Do(ctx, in.GrantOptionSet.ROPCOption, oidcClient)
if err != nil {
return nil, xerrors.Errorf("ropc error: %w", err)
return nil, fmt.Errorf("ropc error: %w", err)
}
return &Output{TokenSet: *tokenSet}, nil
}
return nil, xerrors.Errorf("any authorization grant must be set")
return nil, fmt.Errorf("any authorization grant must be set")
}

View File

@@ -2,36 +2,39 @@ package authentication
import (
"context"
"errors"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/oidc/client/mock_client"
"github.com/int128/kubelogin/pkg/testing/clock"
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
testingLogger "github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"golang.org/x/xerrors"
)
func TestAuthentication_Do(t *testing.T) {
timeout := 5 * time.Second
expiryTime := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
cachedIDToken := testingJWT.EncodeF(t, func(claims *testingJWT.Claims) {
claims.Issuer = "https://issuer.example.com"
claims.Subject = "SUBJECT"
dummyProvider := oidc.Provider{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
dummyTLSClientConfig := tlsclientconfig.Config{
CACertFilename: []string{"/path/to/cert"},
}
issuedIDToken := testingJWT.EncodeF(t, func(claims *testingJWT.Claims) {
claims.Issuer = "https://accounts.google.com"
claims.Subject = "YOUR_SUBJECT"
claims.ExpiresAt = expiryTime.Unix()
})
dummyClaims := jwt.Claims{
Subject: "SUBJECT",
Expiry: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
Pretty: "PRETTY_JSON",
}
t.Run("HasValidIDToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
@@ -39,10 +42,11 @@ func TestAuthentication_Do(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: cachedIDToken,
Provider: dummyProvider,
TLSClientConfig: dummyTLSClientConfig,
CachedTokenSet: &oidc.TokenSet{
IDToken: issuedIDToken,
},
}
u := Authentication{
Logger: testingLogger.New(t),
@@ -55,16 +59,7 @@ func TestAuthentication_Do(t *testing.T) {
want := &Output{
AlreadyHasValidIDToken: true,
TokenSet: oidc.TokenSet{
IDToken: cachedIDToken,
IDTokenClaims: jwt.Claims{
Subject: "SUBJECT",
Expiry: expiryTime,
Pretty: `{
"exp": 1577934245,
"iss": "https://issuer.example.com",
"sub": "SUBJECT"
}`,
},
IDToken: issuedIDToken,
},
}
if diff := cmp.Diff(want, got); diff != "" {
@@ -78,32 +73,28 @@ func TestAuthentication_Do(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: cachedIDToken,
RefreshToken: "VALID_REFRESH_TOKEN",
Provider: dummyProvider,
TLSClientConfig: dummyTLSClientConfig,
CachedTokenSet: &oidc.TokenSet{
IDToken: issuedIDToken,
RefreshToken: "VALID_REFRESH_TOKEN",
},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().
Refresh(ctx, "VALID_REFRESH_TOKEN").
Return(&oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyClaims,
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
}, nil)
mockClientFactory := mock_client.NewMockFactoryInterface(ctrl)
mockClientFactory.EXPECT().
New(ctx, dummyProvider, dummyTLSClientConfig).
Return(mockClient, nil)
u := Authentication{
OIDCClient: &oidcclientFactory{
t: t,
client: mockOIDCClient,
want: oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
},
Logger: testingLogger.New(t),
Clock: clock.Fake(expiryTime.Add(+time.Hour)),
ClientFactory: mockClientFactory,
Logger: testingLogger.New(t),
Clock: clock.Fake(expiryTime.Add(+time.Hour)),
}
got, err := u.Do(ctx, in)
if err != nil {
@@ -111,9 +102,8 @@ func TestAuthentication_Do(t *testing.T) {
}
want := &Output{
TokenSet: oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyClaims,
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
},
}
if diff := cmp.Diff(want, got); diff != "" {
@@ -127,45 +117,42 @@ func TestAuthentication_Do(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
Provider: dummyProvider,
TLSClientConfig: dummyTLSClientConfig,
GrantOptionSet: GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
BindAddress: []string{"127.0.0.1:8000"},
SkipOpenBrowser: true,
AuthenticationTimeout: 10 * time.Second,
},
},
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: cachedIDToken,
RefreshToken: "EXPIRED_REFRESH_TOKEN",
CachedTokenSet: &oidc.TokenSet{
IDToken: issuedIDToken,
RefreshToken: "EXPIRED_REFRESH_TOKEN",
},
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().SupportedPKCEMethods()
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().SupportedPKCEMethods()
mockClient.EXPECT().
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
Return(nil, xerrors.New("token has expired"))
mockOIDCClient.EXPECT().
Return(nil, errors.New("token has expired"))
mockClient.EXPECT().
GetTokenByAuthCode(gomock.Any(), gomock.Any(), gomock.Any()).
Do(func(_ context.Context, _ oidcclient.GetTokenByAuthCodeInput, readyChan chan<- string) {
Do(func(_ context.Context, _ client.GetTokenByAuthCodeInput, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyClaims,
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
}, nil)
mockClientFactory := mock_client.NewMockFactoryInterface(ctrl)
mockClientFactory.EXPECT().
New(ctx, dummyProvider, dummyTLSClientConfig).
Return(mockClient, nil)
u := Authentication{
OIDCClient: &oidcclientFactory{
t: t,
client: mockOIDCClient,
want: oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
},
Logger: testingLogger.New(t),
Clock: clock.Fake(expiryTime.Add(+time.Hour)),
ClientFactory: mockClientFactory,
Logger: testingLogger.New(t),
Clock: clock.Fake(expiryTime.Add(+time.Hour)),
AuthCodeBrowser: &authcode.Browser{
Logger: testingLogger.New(t),
},
@@ -176,9 +163,8 @@ func TestAuthentication_Do(t *testing.T) {
}
want := &Output{
TokenSet: oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenClaims: dummyClaims,
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
},
}
if diff := cmp.Diff(want, got); diff != "" {
@@ -192,35 +178,29 @@ func TestAuthentication_Do(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
in := Input{
Provider: dummyProvider,
TLSClientConfig: dummyTLSClientConfig,
GrantOptionSet: GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
mockClientFactory := mock_client.NewMockFactoryInterface(ctrl)
mockClientFactory.EXPECT().
New(ctx, dummyProvider, dummyTLSClientConfig).
Return(mockClient, nil)
u := Authentication{
OIDCClient: &oidcclientFactory{
t: t,
client: mockOIDCClient,
want: oidcclient.Config{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
},
Logger: testingLogger.New(t),
ClientFactory: mockClientFactory,
Logger: testingLogger.New(t),
ROPC: &ropc.ROPC{
Logger: testingLogger.New(t),
},
@@ -231,9 +211,8 @@ func TestAuthentication_Do(t *testing.T) {
}
want := &Output{
TokenSet: oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}
if diff := cmp.Diff(want, got); diff != "" {
@@ -241,16 +220,3 @@ func TestAuthentication_Do(t *testing.T) {
}
})
}
type oidcclientFactory struct {
t *testing.T
client oidcclient.Interface
want oidcclient.Config
}
func (f *oidcclientFactory) New(_ context.Context, got oidcclient.Config) (oidcclient.Interface, error) {
if diff := cmp.Diff(f.want, got); diff != "" {
f.t.Errorf("mismatch (-want +got):\n%s", diff)
}
return f.client, nil
}

View File

@@ -11,30 +11,30 @@ import (
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Do mocks base method.
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 authentication.Input) (*authentication.Output, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
@@ -43,7 +43,7 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 authentication.Input) (*au
return ret0, ret1
}
// Do indicates an expected call of Do.
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)

View File

@@ -2,12 +2,12 @@ package ropc
import (
"context"
"fmt"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/infrastructure/reader"
"github.com/int128/kubelogin/pkg/oidc"
"golang.org/x/xerrors"
"github.com/int128/kubelogin/pkg/oidc/client"
)
const usernamePrompt = "Username: "
@@ -24,25 +24,25 @@ type ROPC struct {
Logger logger.Interface
}
func (u *ROPC) Do(ctx context.Context, in *Option, client oidcclient.Interface) (*oidc.TokenSet, error) {
func (u *ROPC) Do(ctx context.Context, in *Option, oidcClient client.Interface) (*oidc.TokenSet, error) {
u.Logger.V(1).Infof("starting the resource owner password credentials flow")
if in.Username == "" {
var err error
in.Username, err = u.Reader.ReadString(usernamePrompt)
if err != nil {
return nil, xerrors.Errorf("could not read a username: %w", err)
return nil, fmt.Errorf("could not read a username: %w", err)
}
}
if in.Password == "" {
var err error
in.Password, err = u.Reader.ReadPassword(passwordPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read a password: %w", err)
return nil, fmt.Errorf("could not read a password: %w", err)
}
}
tokenSet, err := client.GetTokenByROPC(ctx, in.Username, in.Password)
tokenSet, err := oidcClient.GetTokenByROPC(ctx, in.Username, in.Password)
if err != nil {
return nil, xerrors.Errorf("resource owner password credentials flow error: %w", err)
return nil, fmt.Errorf("resource owner password credentials flow error: %w", err)
}
u.Logger.V(1).Infof("finished the resource owner password credentials flow")
return tokenSet, nil

View File

@@ -2,25 +2,19 @@ package ropc
import (
"context"
"errors"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/mock_oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader/mock_reader"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/infrastructure/reader/mock_reader"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/oidc/client/mock_client"
"github.com/int128/kubelogin/pkg/testing/logger"
"golang.org/x/xerrors"
)
func TestROPC_Do(t *testing.T) {
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: "PRETTY_JSON",
}
timeout := 5 * time.Second
t.Run("AskUsernameAndPassword", func(t *testing.T) {
@@ -29,13 +23,12 @@ func TestROPC_Do(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
o := &Option{}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
IDTokenClaims: dummyTokenClaims,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
mockReader := mock_reader.NewMockInterface(ctrl)
mockReader.EXPECT().ReadString(usernamePrompt).Return("USER", nil)
@@ -44,14 +37,13 @@ func TestROPC_Do(t *testing.T) {
Reader: mockReader,
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
got, err := u.Do(ctx, o, mockClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
@@ -67,25 +59,23 @@ func TestROPC_Do(t *testing.T) {
Username: "USER",
Password: "PASS",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
u := ROPC{
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
got, err := u.Do(ctx, o, mockClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
@@ -100,13 +90,12 @@ func TestROPC_Do(t *testing.T) {
o := &Option{
Username: "USER",
}
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
mockClient := mock_client.NewMockInterface(ctrl)
mockClient.EXPECT().
GetTokenByROPC(gomock.Any(), "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
mockEnv := mock_reader.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
@@ -114,14 +103,13 @@ func TestROPC_Do(t *testing.T) {
Reader: mockEnv,
Logger: logger.New(t),
}
got, err := u.Do(ctx, o, mockOIDCClient)
got, err := u.Do(ctx, o, mockClient)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
@@ -137,12 +125,12 @@ func TestROPC_Do(t *testing.T) {
Username: "USER",
}
mockEnv := mock_reader.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", xerrors.New("error"))
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", errors.New("error"))
u := ROPC{
Reader: mockEnv,
Logger: logger.New(t),
}
out, err := u.Do(ctx, o, mock_oidcclient.NewMockInterface(ctrl))
out, err := u.Do(ctx, o, mock_client.NewMockInterface(ctrl))
if err == nil {
t.Errorf("err wants non-nil but nil")
}

View File

@@ -5,14 +5,21 @@ package credentialplugin
import (
"context"
"fmt"
"net"
"strings"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/credentialplugin"
"github.com/int128/kubelogin/pkg/credentialplugin/writer"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/infrastructure/mutex"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/tokencache"
"github.com/int128/kubelogin/pkg/tokencache/repository"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"golang.org/x/xerrors"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
)
//go:generate mockgen -destination mock_credentialplugin/mock_credentialplugin.go github.com/int128/kubelogin/pkg/usecases/credentialplugin Interface
@@ -28,92 +35,107 @@ type Interface interface {
// Input represents an input DTO of the GetToken use-case.
type Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CACertFilename string // optional
CACertData string // optional
SkipTLSVerify bool
TokenCacheDir string
GrantOptionSet authentication.GrantOptionSet
Provider oidc.Provider
TokenCacheDir string
GrantOptionSet authentication.GrantOptionSet
TLSClientConfig tlsclientconfig.Config
}
type GetToken struct {
Authentication authentication.Interface
TokenCacheRepository tokencache.Interface
NewCertPool certpool.NewFunc
Writer credentialpluginwriter.Interface
TokenCacheRepository repository.Interface
Writer writer.Interface
Mutex mutex.Interface
Logger logger.Interface
}
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)
// Prevent multiple concurrent port binding using a file mutex.
// See https://github.com/int128/kubelogin/issues/389
bindPorts := extractBindAddressPorts(in.GrantOptionSet.AuthCodeBrowserOption)
if bindPorts != nil {
key := fmt.Sprintf("get-token-%s", strings.Join(bindPorts, "-"))
u.Logger.V(1).Infof("acquiring a lock %s", key)
lock, err := u.Mutex.Acquire(ctx, key)
if err != nil {
return fmt.Errorf("could not acquire a lock: %w", err)
}
defer func() {
if err := u.Mutex.Release(lock); err != nil {
u.Logger.V(1).Infof("could not release the lock: %s", err)
}
}()
}
u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
tokenCacheKey := tokencache.Key{
IssuerURL: in.Provider.IssuerURL,
ClientID: in.Provider.ClientID,
ClientSecret: in.Provider.ClientSecret,
ExtraScopes: in.Provider.ExtraScopes,
CACertFilename: strings.Join(in.TLSClientConfig.CACertFilename, ","),
CACertData: strings.Join(in.TLSClientConfig.CACertData, ","),
SkipTLSVerify: in.TLSClientConfig.SkipTLSVerify,
}
if in.GrantOptionSet.ROPCOption != nil {
tokenCacheKey.Username = in.GrantOptionSet.ROPCOption.Username
}
cachedTokenSet, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, tokenCacheKey)
if err != nil {
return xerrors.Errorf("could not get a token: %w", err)
u.Logger.V(1).Infof("could not find a token cache: %s", err)
}
authenticationInput := authentication.Input{
Provider: in.Provider,
GrantOptionSet: in.GrantOptionSet,
CachedTokenSet: cachedTokenSet,
TLSClientConfig: in.TLSClientConfig,
}
authenticationOutput, err := u.Authentication.Do(ctx, authenticationInput)
if err != nil {
return fmt.Errorf("authentication error: %w", err)
}
idTokenClaims, err := authenticationOutput.TokenSet.DecodeWithoutVerify()
if err != nil {
return fmt.Errorf("you got an invalid token: %w", err)
}
u.Logger.V(1).Infof("you got a token: %s", idTokenClaims.Pretty)
if authenticationOutput.AlreadyHasValidIDToken {
u.Logger.V(1).Infof("you already have a valid token until %s", idTokenClaims.Expiry)
} else {
u.Logger.V(1).Infof("you got a valid token until %s", idTokenClaims.Expiry)
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, tokenCacheKey, authenticationOutput.TokenSet); err != nil {
return fmt.Errorf("could not write the token cache: %w", err)
}
}
u.Logger.V(1).Infof("writing the token to client-go")
if err := u.Writer.Write(credentialpluginwriter.Output{Token: out.TokenSet.IDToken, Expiry: out.TokenSet.IDTokenClaims.Expiry}); err != nil {
return xerrors.Errorf("could not write the token to client-go: %w", err)
out := credentialplugin.Output{
Token: authenticationOutput.TokenSet.IDToken,
Expiry: idTokenClaims.Expiry,
}
if err := u.Writer.Write(out); err != nil {
return fmt.Errorf("could not write the token to client-go: %w", err)
}
return nil
}
func (u *GetToken) getTokenFromCacheOrProvider(ctx context.Context, in Input) (*authentication.Output, error) {
u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
tokenCacheKey := tokencache.Key{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CACertFilename: in.CACertFilename,
CACertData: in.CACertData,
SkipTLSVerify: in.SkipTLSVerify,
func extractBindAddressPorts(o *authcode.BrowserOption) []string {
if o == nil {
return nil
}
tokenCacheValue, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, tokenCacheKey)
if err != nil {
u.Logger.V(1).Infof("could not find a token cache: %s", err)
tokenCacheValue = &tokencache.Value{}
}
certPool := u.NewCertPool()
if in.CACertFilename != "" {
if err := certPool.AddFile(in.CACertFilename); err != nil {
return nil, xerrors.Errorf("could not load the certificate file: %w", err)
var ports []string
for _, addr := range o.BindAddress {
_, port, err := net.SplitHostPort(addr)
if err != nil {
return nil // invalid address
}
}
if in.CACertData != "" {
if err := certPool.AddBase64Encoded(in.CACertData); err != nil {
return nil, xerrors.Errorf("could not load the certificate data: %w", err)
if port == "0" {
return nil // any port
}
ports = append(ports, port)
}
out, err := u.Authentication.Do(ctx, authentication.Input{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: certPool,
SkipTLSVerify: in.SkipTLSVerify,
IDToken: tokenCacheValue.IDToken,
RefreshToken: tokenCacheValue.RefreshToken,
GrantOptionSet: in.GrantOptionSet,
})
if err != nil {
return nil, xerrors.Errorf("authentication error: %w", err)
}
u.Logger.V(1).Infof("you got a token: %s", out.TokenSet.IDTokenClaims.Pretty)
if out.AlreadyHasValidIDToken {
u.Logger.V(1).Infof("you already have a valid token until %s", out.TokenSet.IDTokenClaims.Expiry)
return out, nil
}
u.Logger.V(1).Infof("you got a valid token until %s", out.TokenSet.IDTokenClaims.Expiry)
newTokenCacheValue := tokencache.Value{
IDToken: out.TokenSet.IDToken,
RefreshToken: out.TokenSet.RefreshToken,
}
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, tokenCacheKey, newTokenCacheValue); err != nil {
return nil, xerrors.Errorf("could not write the token cache: %w", err)
}
return out, nil
return ports
}

View File

@@ -2,105 +2,186 @@ package credentialplugin
import (
"context"
"errors"
"testing"
"time"
"github.com/int128/kubelogin/pkg/credentialplugin"
"github.com/int128/kubelogin/pkg/infrastructure/mutex"
"github.com/int128/kubelogin/pkg/infrastructure/mutex/mock_mutex"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter/mock_credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/adaptors/tokencache/mock_tokencache"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/credentialplugin/writer/mock_writer"
"github.com/int128/kubelogin/pkg/oidc"
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/tokencache"
"github.com/int128/kubelogin/pkg/tokencache/repository/mock_repository"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
"golang.org/x/xerrors"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
)
func TestGetToken_Do(t *testing.T) {
dummyTokenClaims := jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: "PRETTY_JSON",
dummyProvider := oidc.Provider{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
issuedIDTokenExpiration := time.Now().Add(1 * time.Hour).Round(time.Second)
issuedIDToken := testingJWT.EncodeF(t, func(claims *testingJWT.Claims) {
claims.Issuer = "https://accounts.google.com"
claims.Subject = "YOUR_SUBJECT"
claims.ExpiresAt = issuedIDTokenExpiration.Unix()
})
issuedTokenSet := oidc.TokenSet{
IDToken: issuedIDToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
}
issuedOutput := credentialplugin.Output{
Token: issuedIDToken,
Expiry: issuedIDTokenExpiration,
}
grantOptionSet := authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:0"},
},
}
t.Run("FullOptions", func(t *testing.T) {
var grantOptionSet authentication.GrantOptionSet
t.Run("NoTokenCache", func(t *testing.T) {
tokenCacheKey := tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Provider: dummyProvider,
TokenCacheDir: "/path/to/token-cache",
CACertFilename: "/path/to/cert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPool.EXPECT().
AddFile("/path/to/cert")
mockCertPool.EXPECT().
AddBase64Encoded("BASE64ENCODED")
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
SkipTLSVerify: true,
Provider: dummyProvider,
GrantOptionSet: grantOptionSet,
}).
Return(&authentication.Output{
TokenSet: oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: dummyTokenClaims,
},
}, nil)
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
FindByKey("/path/to/token-cache",
tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CACertFilename: "/path/to/cert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
}).
Return(nil, xerrors.New("file not found"))
tokenCacheRepository.EXPECT().
Save("/path/to/token-cache",
tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CACertFilename: "/path/to/cert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
},
tokencache.Value{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
})
credentialPluginWriter := mock_credentialpluginwriter.NewMockInterface(ctrl)
credentialPluginWriter.EXPECT().
Write(credentialpluginwriter.Output{
Token: "YOUR_ID_TOKEN",
Expiry: dummyTokenClaims.Expiry,
})
Return(&authentication.Output{TokenSet: issuedTokenSet}, nil)
mockRepository := mock_repository.NewMockInterface(ctrl)
mockRepository.EXPECT().
FindByKey("/path/to/token-cache", tokenCacheKey).
Return(nil, errors.New("file not found"))
mockRepository.EXPECT().
Save("/path/to/token-cache", tokenCacheKey, issuedTokenSet)
mockWriter := mock_writer.NewMockInterface(ctrl)
mockWriter.EXPECT().Write(issuedOutput)
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Writer: credentialPluginWriter,
TokenCacheRepository: mockRepository,
Writer: mockWriter,
Mutex: mock_mutex.NewMockInterface(ctrl),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("NeedBindPortMutex", func(t *testing.T) {
grantOptionSet := authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:8080"},
},
}
tokenCacheKey := tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
Provider: dummyProvider,
TokenCacheDir: "/path/to/token-cache",
GrantOptionSet: grantOptionSet,
}
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
Provider: dummyProvider,
GrantOptionSet: grantOptionSet,
}).
Return(&authentication.Output{TokenSet: issuedTokenSet}, nil)
mockRepository := mock_repository.NewMockInterface(ctrl)
mockRepository.EXPECT().
FindByKey("/path/to/token-cache", tokenCacheKey).
Return(nil, errors.New("file not found"))
mockRepository.EXPECT().
Save("/path/to/token-cache", tokenCacheKey, issuedTokenSet)
mockWriter := mock_writer.NewMockInterface(ctrl)
mockWriter.EXPECT().Write(issuedOutput)
mockMutex := mock_mutex.NewMockInterface(ctrl)
mockMutex.EXPECT().
Acquire(ctx, "get-token-8080").
Return(&mutex.Lock{Data: "testData"}, nil)
mockMutex.EXPECT().
Release(&mutex.Lock{Data: "testData"})
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: mockRepository,
Writer: mockWriter,
Mutex: mockMutex,
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("ROPC", func(t *testing.T) {
grantOptionSet := authentication.GrantOptionSet{
ROPCOption: &ropc.Option{Username: "YOUR_USERNAME"},
}
tokenCacheKey := tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Username: "YOUR_USERNAME",
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
Provider: dummyProvider,
TokenCacheDir: "/path/to/token-cache",
GrantOptionSet: grantOptionSet,
}
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
Provider: dummyProvider,
GrantOptionSet: grantOptionSet,
}).
Return(&authentication.Output{TokenSet: issuedTokenSet}, nil)
mockRepository := mock_repository.NewMockInterface(ctrl)
mockRepository.EXPECT().
FindByKey("/path/to/token-cache", tokenCacheKey).
Return(nil, errors.New("file not found"))
mockRepository.EXPECT().
Save("/path/to/token-cache", tokenCacheKey, issuedTokenSet)
mockWriter := mock_writer.NewMockInterface(ctrl)
mockWriter.EXPECT().Write(issuedOutput)
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: mockRepository,
Writer: mockWriter,
Mutex: mock_mutex.NewMockInterface(ctrl),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
@@ -113,49 +194,36 @@ func TestGetToken_Do(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
Provider: dummyProvider,
TokenCacheDir: "/path/to/token-cache",
GrantOptionSet: grantOptionSet,
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
CertPool: mockCertPool,
Provider: dummyProvider,
CachedTokenSet: &issuedTokenSet,
GrantOptionSet: grantOptionSet,
}).
Return(&authentication.Output{
AlreadyHasValidIDToken: true,
TokenSet: oidc.TokenSet{
IDToken: "VALID_ID_TOKEN",
IDTokenClaims: dummyTokenClaims,
},
TokenSet: issuedTokenSet,
}, nil)
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
mockRepository := mock_repository.NewMockInterface(ctrl)
mockRepository.EXPECT().
FindByKey("/path/to/token-cache", tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}).
Return(&tokencache.Value{
IDToken: "VALID_ID_TOKEN",
}, nil)
credentialPluginWriter := mock_credentialpluginwriter.NewMockInterface(ctrl)
credentialPluginWriter.EXPECT().
Write(credentialpluginwriter.Output{
Token: "VALID_ID_TOKEN",
Expiry: dummyTokenClaims.Expiry,
})
Return(&issuedTokenSet, nil)
mockWriter := mock_writer.NewMockInterface(ctrl)
mockWriter.EXPECT().Write(issuedOutput)
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Writer: credentialPluginWriter,
TokenCacheRepository: mockRepository,
Writer: mockWriter,
Mutex: mock_mutex.NewMockInterface(ctrl),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
@@ -168,34 +236,30 @@ func TestGetToken_Do(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
Provider: dummyProvider,
TokenCacheDir: "/path/to/token-cache",
GrantOptionSet: grantOptionSet,
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
CertPool: mockCertPool,
Provider: dummyProvider,
GrantOptionSet: grantOptionSet,
}).
Return(nil, xerrors.New("authentication error"))
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
Return(nil, errors.New("authentication error"))
mockRepository := mock_repository.NewMockInterface(ctrl)
mockRepository.EXPECT().
FindByKey("/path/to/token-cache", tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
}).
Return(nil, xerrors.New("file not found"))
Return(nil, errors.New("file not found"))
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
NewCertPool: func() certpool.Interface { return mockCertPool },
Writer: mock_credentialpluginwriter.NewMockInterface(ctrl),
TokenCacheRepository: mockRepository,
Writer: mock_writer.NewMockInterface(ctrl),
Mutex: mock_mutex.NewMockInterface(ctrl),
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err == nil {

View File

@@ -11,30 +11,30 @@ import (
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Do mocks base method.
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
@@ -42,7 +42,7 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) er
return ret0
}
// Do indicates an expected call of Do.
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)

View File

@@ -5,8 +5,7 @@ import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
@@ -22,6 +21,5 @@ type Interface interface {
type Setup struct {
Authentication authentication.Interface
NewCertPool certpool.NewFunc
Logger logger.Interface
}

View File

@@ -2,11 +2,14 @@ package setup
import (
"context"
"fmt"
"path/filepath"
"strings"
"text/template"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"golang.org/x/xerrors"
)
var stage2Tpl = template.Must(template.New("").Parse(`
@@ -38,8 +41,9 @@ Run the following command:
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
{{- range .Args }}
--exec-arg={{ . }} \
{{- range $index, $arg := .Args }}
{{- if $index}} \{{end}}
--exec-arg={{ $arg }}
{{- end }}
## 6. Verify cluster access
@@ -69,49 +73,41 @@ type Stage2Input struct {
ClientID string
ClientSecret string
ExtraScopes []string // optional
CACertFilename string // optional
CACertData string // optional
SkipTLSVerify bool
ListenAddressArgs []string // non-nil if set by the command arg
GrantOptionSet authentication.GrantOptionSet
TLSClientConfig tlsclientconfig.Config
}
func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
u.Logger.Printf("authentication in progress...")
certPool := u.NewCertPool()
if in.CACertFilename != "" {
if err := certPool.AddFile(in.CACertFilename); err != nil {
return xerrors.Errorf("could not load the certificate file: %w", err)
}
}
if in.CACertData != "" {
if err := certPool.AddBase64Encoded(in.CACertData); err != nil {
return xerrors.Errorf("could not load the certificate data: %w", err)
}
}
out, err := u.Authentication.Do(ctx, authentication.Input{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
CertPool: certPool,
SkipTLSVerify: in.SkipTLSVerify,
GrantOptionSet: in.GrantOptionSet,
Provider: oidc.Provider{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
},
GrantOptionSet: in.GrantOptionSet,
TLSClientConfig: in.TLSClientConfig,
})
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
return fmt.Errorf("authentication error: %w", err)
}
idTokenClaims, err := out.TokenSet.DecodeWithoutVerify()
if err != nil {
return fmt.Errorf("you got an invalid token: %w", err)
}
v := stage2Vars{
IDTokenPrettyJSON: out.TokenSet.IDTokenClaims.Pretty,
IDTokenPrettyJSON: idTokenClaims.Pretty,
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
Args: makeCredentialPluginArgs(in),
Subject: out.TokenSet.IDTokenClaims.Subject,
Subject: idTokenClaims.Subject,
}
var b strings.Builder
if err := stage2Tpl.Execute(&b, &v); err != nil {
return xerrors.Errorf("could not render the template: %w", err)
return fmt.Errorf("could not render the template: %w", err)
}
u.Logger.Printf(b.String())
return nil
@@ -127,13 +123,13 @@ func makeCredentialPluginArgs(in Stage2Input) []string {
for _, extraScope := range in.ExtraScopes {
args = append(args, "--oidc-extra-scope="+extraScope)
}
if in.CACertFilename != "" {
args = append(args, "--certificate-authority="+in.CACertFilename)
for _, f := range in.TLSClientConfig.CACertFilename {
args = append(args, "--certificate-authority="+f)
}
if in.CACertData != "" {
args = append(args, "--certificate-authority-data="+in.CACertData)
for _, d := range in.TLSClientConfig.CACertData {
args = append(args, "--certificate-authority-data="+d)
}
if in.SkipTLSVerify {
if in.TLSClientConfig.SkipTLSVerify {
args = append(args, "--insecure-skip-tls-verify")
}
@@ -141,6 +137,20 @@ func makeCredentialPluginArgs(in Stage2Input) []string {
if in.GrantOptionSet.AuthCodeBrowserOption.SkipOpenBrowser {
args = append(args, "--skip-open-browser")
}
if in.GrantOptionSet.AuthCodeBrowserOption.LocalServerCertFile != "" {
// Resolve the absolute path for the cert files so the user doesn't have to know
// to use one when running setup.
certpath, err := filepath.Abs(in.GrantOptionSet.AuthCodeBrowserOption.LocalServerCertFile)
if err != nil {
panic(err)
}
keypath, err := filepath.Abs(in.GrantOptionSet.AuthCodeBrowserOption.LocalServerKeyFile)
if err != nil {
panic(err)
}
args = append(args, "--local-server-cert="+certpath)
args = append(args, "--local-server-key="+keypath)
}
}
args = append(args, in.ListenAddressArgs...)
if in.GrantOptionSet.ROPCOption != nil {

View File

@@ -6,59 +6,56 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/certpool/mock_certpool"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/oidc"
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/mock_authentication"
)
func TestSetup_DoStage2(t *testing.T) {
issuedIDToken := testingJWT.EncodeF(t, func(claims *testingJWT.Claims) {
claims.Issuer = "https://issuer.example.com"
claims.Subject = "YOUR_SUBJECT"
claims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
})
dummyTLSClientConfig := tlsclientconfig.Config{
CACertFilename: []string{"/path/to/cert"},
}
var grantOptionSet authentication.GrantOptionSet
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"},
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
GrantOptionSet: grantOptionSet,
TLSClientConfig: dummyTLSClientConfig,
}
mockCertPool := mock_certpool.NewMockInterface(ctrl)
mockCertPool.EXPECT().
AddFile("/path/to/cert")
mockAuthentication := mock_authentication.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
CertPool: mockCertPool,
SkipTLSVerify: true,
GrantOptionSet: grantOptionSet,
Provider: oidc.Provider{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
},
GrantOptionSet: grantOptionSet,
TLSClientConfig: dummyTLSClientConfig,
}).
Return(&authentication.Output{
TokenSet: oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
IDToken: issuedIDToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenClaims: jwt.Claims{
Subject: "YOUR_SUBJECT",
Expiry: time.Date(2019, 1, 2, 3, 4, 5, 0, time.UTC),
Pretty: "PRETTY_JSON",
},
},
}, nil)
u := Setup{
Authentication: mockAuthentication,
NewCertPool: func() certpool.Interface { return mockCertPool },
Logger: logger.New(t),
}
if err := u.DoStage2(ctx, in); err != nil {

View File

@@ -11,30 +11,30 @@ import (
reflect "reflect"
)
// MockInterface is a mock of Interface interface.
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Do mocks base method.
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0, arg1)
@@ -42,7 +42,7 @@ func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
return ret0
}
// Do indicates an expected call of Do.
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)

Some files were not shown because too many files have changed in this diff Show More