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 @@ -
CI machine
CI machine
Container
Container
Kubernetes cluster
Kubernetes cluster
(8) verify token
(8) verify token
kube-apiserver
kube-apiserver
(5) callback
(5) callback
Chrome
Chrome
(7) access with token
(7) access with token
(2) run
(2) run
kubectl
kubectl
(6) get token
(6) get token
kubectl-oidc_login
kubectl-oidc_login
(3) run
(3) run
(1) run
(1) run
acceptance_test
acceptance_test
(4) enter user/pass
(4) enter user/pass
CA
certificate
CA...
Container
Container
Dex
Dex
server
certificate
serve...
CA
certificate
CA...
CA
certificate
CA...
Viewer does not support full SVG 1.1
\ No newline at end of file +
CI machine
CI machine
Container
Container
Cluster
Cluster
(8) verify token
(8) verify token
kube-apiserver
kube-apiserver
(5) callback
(5) callback
Chrome
Chrome
(7) access with token
(7) access with token
(2) run
(2) run
kubectl
kubectl
(6) get token
(6) get token
(3) run
(3) run
kubectl-oidc_login
kubectl-oidc_login
(4) login
(4) login
Container
Container
Dex
Dex
(4) run
(4) run
chromelogin
chromelogin
Viewer does not support full SVG 1.1
\ No newline at end of file