diff --git a/.github/workflows/acceptance-test.yaml b/.github/workflows/acceptance-test.yaml
index 4363561..d4e7ea5 100644
--- a/.github/workflows/acceptance-test.yaml
+++ b/.github/workflows/acceptance-test.yaml
@@ -26,4 +26,5 @@ jobs:
# https://packages.ubuntu.com/xenial/libnss3-tools
- run: sudo apt install -y libnss3-tools
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
- - run: make -C acceptance_test -j3
+ - run: make -C acceptance_test -j3 setup
+ - run: make -C acceptance_test test
diff --git a/acceptance_test/Makefile b/acceptance_test/Makefile
index 6555549..ebe4bfc 100644
--- a/acceptance_test/Makefile
+++ b/acceptance_test/Makefile
@@ -5,21 +5,26 @@ KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
export KUBECONFIG
.PHONY: test
-test: build cluster add-dex-ca-cert-for-chrome
- PATH=$(PATH):$(OUTPUT_DIR)/bin acceptance_test -test.v
+test: build
+ PATH=$(PATH):$(OUTPUT_DIR)/bin BROWSER=chromelogin KUBECONFIG=$(KUBECONFIG):kubeconfig_oidc.yaml \
+ kubectl --user=oidc cluster-info
-.PHONY: add-dex-ca-cert-for-chrome
-add-dex-ca-cert-for-chrome: $(OUTPUT_DIR)/ca.crt
+.PHONY: setup
+setup: build dex cluster setup-chrome
+
+.PHONY: setup-chrome
+setup-chrome: $(OUTPUT_DIR)/ca.crt
+ # add the dex server certificate to the trust store
mkdir -p ~/.pki/nssdb
cd ~/.pki/nssdb && certutil -A -d sql:. -n dex -i $(OUTPUT_DIR)/ca.crt -t "TC,,"
# build binaries
.PHONY: build
-build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/acceptance_test
+build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/chromelogin
$(OUTPUT_DIR)/bin/kubectl-oidc_login:
go build -o $@ ..
-$(OUTPUT_DIR)/bin/acceptance_test: acceptance_test.go
- go test -c -o $@ .
+$(OUTPUT_DIR)/bin/chromelogin: chromelogin/main.go
+ go build -o $@ ./chromelogin
# create a Dex server
.PHONY: dex
diff --git a/acceptance_test/README.md b/acceptance_test/README.md
index c1e2673..7aa6098 100644
--- a/acceptance_test/README.md
+++ b/acceptance_test/README.md
@@ -19,8 +19,8 @@ It performs the test by the following steps:
1. Run kubectl.
1. kubectl automatically runs kubelogin.
-1. Open the browser and navigate to `http://localhost:8000`.
-1. Enter the username and password on the browser.
+1. kubelogin automatically runs [chromelogin](chromelogin).
+1. chromelogin opens the browser, navigates to `http://localhost:8000` and enter the username and password.
1. kubelogin gets an authorization code from the browser.
1. kubelogin gets a token.
1. kubectl accesses an API with the token.
@@ -66,12 +66,7 @@ As a result,
- Set the issuer URL to kubectl. See [`kubeconfig_oidc.yaml`](kubeconfig_oidc.yaml).
- Set the issuer URL to kube-apiserver. See [`cluster.yaml`](cluster.yaml).
-
-### Test scenario
-
-- Run `kubectl` and open the browser concurrently.
-- It need to wait until `http://localhost:8000` is available. It prevents the browser error.
-- It need to kill sub-processes finally, i.e. kubectl and kubelogin.
+- Set `BROWSER` environment variable to run [`chromelogin`](chromelogin) by `xdg-open`.
## Run locally
diff --git a/acceptance_test/acceptance_test.go b/acceptance_test/acceptance_test.go
deleted file mode 100644
index 1c30e4c..0000000
--- a/acceptance_test/acceptance_test.go
+++ /dev/null
@@ -1,140 +0,0 @@
-package acceptance_test
-
-import (
- "context"
- "fmt"
- "log"
- "os"
- "os/exec"
- "strings"
- "syscall"
- "testing"
- "time"
-
- "github.com/chromedp/chromedp"
- "golang.org/x/sync/errgroup"
-)
-
-const (
- tokenCacheDir = "output/token-cache"
- kubeconfigEnv = "KUBECONFIG=output/kubeconfig.yaml:kubeconfig_oidc.yaml"
-)
-
-func init() {
- log.SetFlags(log.Lmicroseconds | log.Lshortfile)
-}
-
-func Test(t *testing.T) {
- if _, err := os.Stat("output/kubeconfig.yaml"); err != nil {
- t.Skipf("skip the test: %s", err)
- }
- if err := os.RemoveAll(tokenCacheDir); err != nil {
- t.Fatalf("could not remove the token cache: %s", err)
- }
- ctx := context.TODO()
- eg, ctx := errgroup.WithContext(ctx)
- eg.Go(func() error { return runKubectl(ctx, t, eg) })
- eg.Go(func() error { return runBrowser(ctx) })
- if err := eg.Wait(); err != nil {
- t.Errorf("error: %s", err)
- }
-}
-
-func runKubectl(ctx context.Context, t *testing.T, eg *errgroup.Group) error {
- ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
- defer cancel()
- cmd := exec.Command("kubectl", "--user=oidc", "--namespace=dex", "get", "deploy")
- cmd.Env = append(os.Environ(), kubeconfigEnv)
- cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
-
- eg.Go(func() error {
- <-ctx.Done()
- if cmd.Process == nil {
- log.Printf("process not started")
- return nil
- }
- if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
- log.Printf("process terminated with exit code %d", cmd.ProcessState.ExitCode())
- return nil
- }
- log.Printf("sending SIGTERM to pid %d", cmd.Process.Pid)
- // kill the child processes
- // https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773
- if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM); err != nil {
- t.Errorf("could not send a signal: %s", err)
- }
- return nil
- })
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("could not run a command: %w", err)
- }
- return nil
-}
-
-func runBrowser(ctx context.Context) error {
- execOpts := chromedp.DefaultExecAllocatorOptions[:]
- execOpts = append(execOpts, chromedp.NoSandbox)
- ctx, cancel := chromedp.NewExecAllocator(ctx, execOpts...)
- defer cancel()
- ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
- defer cancel()
- ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
- defer cancel()
- if err := openKubeloginAndLogInToDex(ctx); err != nil {
- return fmt.Errorf("could not run the browser: %w", err)
- }
- return nil
-}
-
-func openKubeloginAndLogInToDex(ctx context.Context) error {
- for {
- var location string
- err := chromedp.Run(ctx,
- chromedp.Navigate(`http://localhost:8000`),
- chromedp.Location(&location),
- )
- if err != nil {
- return err
- }
- log.Printf("location: %s", location)
- if strings.HasPrefix(location, `http://`) || strings.HasPrefix(location, `https://`) {
- break
- }
- time.Sleep(2 * time.Second)
- }
-
- err := chromedp.Run(ctx,
- // https://dex-server:10443/dex/auth/local
- chromedp.WaitVisible(`#login`),
- logPageMetadata(),
- chromedp.SendKeys(`#login`, `admin@example.com`),
- chromedp.SendKeys(`#password`, `password`),
- chromedp.Submit(`#submit-login`),
- // https://dex-server:10443/dex/approval
- chromedp.WaitVisible(`.dex-btn.theme-btn--success`),
- logPageMetadata(),
- chromedp.Submit(`.dex-btn.theme-btn--success`),
- // http://localhost:8000
- chromedp.WaitReady(`body`),
- logPageMetadata(),
- )
- if err != nil {
- return err
- }
- return nil
-}
-
-func logPageMetadata() chromedp.Action {
- var location string
- var title string
- return chromedp.Tasks{
- chromedp.Location(&location),
- chromedp.Title(&title),
- chromedp.ActionFunc(func(ctx context.Context) error {
- log.Printf("location: %s [%s]", location, title)
- return nil
- }),
- }
-}
diff --git a/acceptance_test/chromelogin/main.go b/acceptance_test/chromelogin/main.go
new file mode 100644
index 0000000..1f4e317
--- /dev/null
+++ b/acceptance_test/chromelogin/main.go
@@ -0,0 +1,93 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/chromedp/chromedp"
+)
+
+func init() {
+ log.SetFlags(log.Lmicroseconds | log.Lshortfile)
+}
+
+func main() {
+ if len(os.Args) != 2 {
+ log.Fatalf("usage: %s URL", os.Args[0])
+ return
+ }
+ url := os.Args[1]
+ if err := runBrowser(context.Background(), url); err != nil {
+ log.Fatalf("error: %s", err)
+ }
+}
+
+func runBrowser(ctx context.Context, url string) error {
+ execOpts := chromedp.DefaultExecAllocatorOptions[:]
+ execOpts = append(execOpts, chromedp.NoSandbox)
+ ctx, cancel := chromedp.NewExecAllocator(ctx, execOpts...)
+ defer cancel()
+ ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
+ defer cancel()
+ ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+ if err := logInToDex(ctx, url); err != nil {
+ return fmt.Errorf("could not run the browser: %w", err)
+ }
+ return nil
+}
+
+func logInToDex(ctx context.Context, url string) error {
+ for {
+ var location string
+ err := chromedp.Run(ctx,
+ chromedp.Navigate(url),
+ chromedp.Location(&location),
+ )
+ if err != nil {
+ return err
+ }
+ log.Printf("location: %s", location)
+ if strings.HasPrefix(location, `http://`) || strings.HasPrefix(location, `https://`) {
+ break
+ }
+ time.Sleep(1 * time.Second)
+ }
+
+ err := chromedp.Run(ctx,
+ // https://dex-server:10443/dex/auth/local
+ chromedp.WaitVisible(`#login`),
+ logPageMetadata(),
+ chromedp.SendKeys(`#login`, `admin@example.com`),
+ chromedp.SendKeys(`#password`, `password`),
+ chromedp.Submit(`#submit-login`),
+ // https://dex-server:10443/dex/approval
+ chromedp.WaitVisible(`.dex-btn.theme-btn--success`),
+ logPageMetadata(),
+ chromedp.Submit(`.dex-btn.theme-btn--success`),
+ // http://localhost:8000
+ chromedp.WaitReady(`body`),
+ logPageMetadata(),
+ )
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func logPageMetadata() chromedp.Action {
+ var location string
+ var title string
+ return chromedp.Tasks{
+ chromedp.Location(&location),
+ chromedp.Title(&title),
+ chromedp.ActionFunc(func(ctx context.Context) error {
+ log.Printf("location: %s [%s]", location, title)
+ return nil
+ }),
+ }
+}
diff --git a/acceptance_test/kubeconfig_oidc.yaml b/acceptance_test/kubeconfig_oidc.yaml
index 2a83b75..d8822a9 100644
--- a/acceptance_test/kubeconfig_oidc.yaml
+++ b/acceptance_test/kubeconfig_oidc.yaml
@@ -15,7 +15,6 @@ users:
- --certificate-authority=output/ca.crt
- --token-cache-dir=output/token-cache
- --listen-address=127.0.0.1:8000
- - --skip-open-browser
- -v1
command: kubectl
contexts:
diff --git a/docs/acceptance-test-diagram.svg b/docs/acceptance-test-diagram.svg
index bb1f85c..798cb55 100644
--- a/docs/acceptance-test-diagram.svg
+++ b/docs/acceptance-test-diagram.svg
@@ -1,3 +1,3 @@
-
\ No newline at end of file
+
\ No newline at end of file