mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-14 16:39:51 +00:00
Refactor: Use BROWSER env var to run chrome-login script (#232)
* Use BROWSER env var to run chrome-login script * Update acceptance-test-diagram.svg * Added acceptance-test-diagram.svg
This commit is contained in:
3
.github/workflows/acceptance-test.yaml
vendored
3
.github/workflows/acceptance-test.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
}
|
||||
}
|
||||
93
acceptance_test/chromelogin/main.go
Normal file
93
acceptance_test/chromelogin/main.go
Normal file
@@ -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
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 28 KiB |
Reference in New Issue
Block a user