diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c32106e..0270212 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,8 @@ on: push: branches: - main + schedule: + - cron: '30 10 * * 1-5' jobs: test: @@ -28,5 +30,11 @@ jobs: restore-keys: | ${{ runner.os }}-go- + - name: Install expect + run: sudo apt-get -y install expect + - name: Run tests - run: make test + env: + E2E_PASSBOLT_PASSPHRASE: ${{ secrets.E2E_PASSBOLT_PASSPHRASE }} + E2E_PASSBOLT_PRIVATE_KEY: ${{ secrets.E2E_PASSBOLT_PRIVATE_KEY }} + run: make test test-e2e diff --git a/.goreleaser.yml b/.goreleaser.yml index bcef8a9..95a2acd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -9,6 +9,7 @@ builds: goos: - linux - darwin + - windows goarm: - "8" diff --git a/Makefile b/Makefile index fcbdc90..93b35f7 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,11 @@ test: ## Run tests go test ./... -coverprofile cover.tmp.out cat cover.tmp.out | grep -v "zz_generated.deepcopy.go" > cover.out +.PHONY: test-e2e +test-e2e: build ## Run e2e tests + (cd e2e && ./interactive.tcl) + (cd e2e && ./non-interactive.tcl) + .PHONY: build build: generate fmt vet $(BIN_FILENAME) ## Build manager binary diff --git a/README.md b/README.md index 2900031..af9b37d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,43 @@ # emergency-credentials-receive -Receives cluster emergency credentials from Passbolt (and S3) + +Guided wizard to receive and decrypt cluster emergency credentials from VSHN Passbolt and our emergency credentials buckets. + +## Usage + +```sh +# Interactive mode +emergency-credentials-receive + +# Non-interactive mode (after private key setup) +EMR_PASSPHRASE=$(pass vshn/passbolt) emergency-credentials-receive c-crashy-wreck-1234 +``` + +## Install from binary + +Install the latest release for your arch and OS with the following command: + +```sh +curl -s "https://raw.githubusercontent.com/vshn/emergency-credentials-receive/main/install.sh" | bash + +# Guided setup +emergency-credentials-receive +``` + +## Development + +There are E2E tests in the `e2e` directory. +They simulate user inputs using [expect](https://core.tcl-lang.org/expect/index). + +To run the tests you need your passbolt private key and the passbolt passphrase. +Or you can use the test credentials from [git.vshn.net](https://git.vshn.net/syn/passbolt-pubkey-sync/-/settings/ci_cd). +Note that the test credentials can only access a very limited set of test clusters. +You can set them as environment variables: + +```sh +export E2E_PASSBOLT_PASSPHRASE="..." +export E2E_PASSBOLT_PRIVATE_KEY="$(cat /path/to/private.key)" +``` + +## Resources + +- Architecture documentation https://kb.vshn.ch/oc4/references/architecture/emergency_credentials.html diff --git a/e2e/interactive.tcl b/e2e/interactive.tcl new file mode 100755 index 0000000..1068084 --- /dev/null +++ b/e2e/interactive.tcl @@ -0,0 +1,69 @@ +#!/usr/bin/env expect + +source ./lib/common.tcl + +set timeout 60 + +set cluster_id "c-appuio-lab-cloudscale-rma-0" +set api_endpoint "https://api.lab-cloudscale-rma-0.appuio.cloud:6443" + +set passphrase [getenv_or_die "E2E_PASSBOLT_PASSPHRASE"] +set private_key [getenv_or_die "E2E_PASSBOLT_PRIVATE_KEY"] + +proc expect_prompt {prompt} { + expect -exact "$prompt" + expect -exact "> " + sleep .5 +} + +# The script assumes vi is used to enter the private key +set ::env(EDITOR) "vi" +file delete -force config.yaml +file delete -force "em-$cluster_id" +set ::env(EMR_CONFIG_DIR) [pwd] + +log "Starting tool" +spawn ../emergency-credentials-receive -omit-token-output +expect -exact "Welcome" + +log "Expecting private key prompt in editor" +expect -exact "Paste your Passbolt private key" +sleep .1 +send -- "i" +log "Omitting user private_key input" +log_user 0 +send -- "$private_key" +# Escape key +send -- "\x1b" +send -- ":x\r" +expect "survey*written" +log "private_key entry done" +log_user 1 + +log "Expecting passphrase prompt" +expect_prompt "Passbolt passphrase" +send -- "$passphrase" +send -- "\r" + +log "Expecting cluster ID prompt" +expect_prompt "Enter your cluster ID" +send -- "$cluster_id" +send -- "\r" + +log "Expecting to have valid credentials" +expect -exact "2 buckets with credentials found" +expect -exact "Emergency credentials found" +expect -exact "OMITTED" + +log "Expecting API endpoint prompt" +expect_prompt "Provide API endpoint" +send "$api_endpoint" +send -- "\r" +expect eof + +test_kubeconfig "em-$cluster_id" + +log "Test successful" + +file delete -force config.yaml +file delete -force "em-$cluster_id" diff --git a/e2e/lib/common.tcl b/e2e/lib/common.tcl new file mode 100644 index 0000000..18afe52 --- /dev/null +++ b/e2e/lib/common.tcl @@ -0,0 +1,26 @@ + +proc log {msg} { + send_user "\n\[TEST\]\t$msg\n" +} + +proc test_kubeconfig {kubeconfig} { + log "Testing kubeconfig $kubeconfig" + set ::env(KUBECONFIG) "$kubeconfig" + + log "Testing kubeconfig is allowed to get nodes" + spawn kubectl get nodes + expect -- "master*Ready*master" + expect eof + + log "Testing kubeconfig is allowed to delete nodes" + spawn kubectl auth can-i delete nodes + expect -- "yes" + expect eof +} + +proc getenv_or_die {var} { + if {![info exists ::env($var)]} { + error "Missing environment variable $var" + } + return "$::env($var)" +} diff --git a/e2e/non-interactive.tcl b/e2e/non-interactive.tcl new file mode 100755 index 0000000..37d0dce --- /dev/null +++ b/e2e/non-interactive.tcl @@ -0,0 +1,35 @@ +#!/usr/bin/env expect + +source ./lib/common.tcl + +set timeout 60 + +set cluster_id "c-appuio-lab-cloudscale-rma-0" +set api_endpoint "https://api.lab-cloudscale-rma-0.appuio.cloud:6443" + +set passphrase [getenv_or_die "E2E_PASSBOLT_PASSPHRASE"] +set private_key [getenv_or_die "E2E_PASSBOLT_PRIVATE_KEY"] + +file delete -force config.yaml +file delete -force "em-$cluster_id" +set ::env(EMR_CONFIG_DIR) [pwd] + +log "Starting tool" +set ::env(EMR_PASSPHRASE) "$passphrase" +set ::env(EMR_KUBERNETES_ENDPOINT) "$api_endpoint" +exec -- jq --null-input --arg key "$private_key" {{passbolt_key: $key}} > config.yaml +spawn ../emergency-credentials-receive -omit-token-output "$cluster_id" +expect -exact "Welcome" + +log "Expecting to have valid credentials" +expect -exact "2 buckets with credentials found" +expect -exact "Emergency credentials found" +expect -exact "OMITTED" +expect eof + +test_kubeconfig "em-$cluster_id" + +log "Test successful" + +file delete -force config.yaml +file delete -force "em-$cluster_id" diff --git a/go.mod b/go.mod index c2d85cd..2c6567b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,70 @@ module github.com/vshn/emergency-credentials-receive go 1.21.6 + +require ( + github.com/Masterminds/sprig/v3 v3.2.3 + github.com/charmbracelet/bubbles v0.17.1 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.9.1 + github.com/cli/cli/v2 v2.42.0 + github.com/google/go-jsonnet v0.20.0 + github.com/minio/minio-go/v7 v7.0.66 + github.com/passbolt/go-passbolt v0.7.0 + golang.org/x/term v0.16.0 + gopkg.in/yaml.v3 v3.0.1 + sigs.k8s.io/yaml v1.1.0 +) + +require ( + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect + github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect + github.com/ProtonMail/gopenpgp/v2 v2.7.4 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/cli/safeexec v1.0.1 // indirect + github.com/cloudflare/circl v1.3.6 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.3.1 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a9bb4cc --- /dev/null +++ b/go.sum @@ -0,0 +1,219 @@ +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= +github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= +github.com/ProtonMail/gopenpgp/v2 v2.7.4 h1:Vz/8+HViFFnf2A6XX8JOvZMrA6F5puwNvvF21O1mRlo= +github.com/ProtonMail/gopenpgp/v2 v2.7.4/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4= +github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/cli/cli/v2 v2.42.0 h1:osl2ftWuBmFsgUcBF9Irt02Lhgr/7jGvAvXROwXIQN8= +github.com/cli/cli/v2 v2.42.0/go.mod h1:Jtsn9iQxcsIE6T9Aj88xSMFnaZP35rjkD+Cpr1QnbUg= +github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= +github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= +github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= +github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= +github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/passbolt/go-passbolt v0.7.0 h1:zwwTCwL3vjTTKln1hxwKuzzax4R/yvxGXSZhMh0OY5Y= +github.com/passbolt/go-passbolt v0.7.0/go.mod h1:af3TVSJ+0A4sXeK8KgVzhV8Tej/i25biFIQjhL0FOMk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..4667dd3 --- /dev/null +++ b/install.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash + +# This script is a modified version of the script from the kustomize repo: +# https://github.com/kubernetes-sigs/kustomize/blob/master/hack/install_kustomize.sh +## Copyright 2022 The Kubernetes Authors. +## SPDX-License-Identifier: Apache-2.0 + +# If no argument is given -> Downloads the most recently released +# emergency-credentials-receive binary to your current working directory. +# (e.g. 'install.sh') +# +# If one argument is given -> +# If that argument is in the format of #.#.#, downloads the specified +# version of the emergency-credentials-receive binary to your current working directory. +# If that argument is something else, downloads the most recently released +# emergency-credentials-receive binary to the specified directory. +# (e.g. 'install.sh 0.1.0' or 'install.sh $(go env GOPATH)/bin') +# +# If two arguments are given -> Downloads the specified version of the +# emergency-credentials-receive binary to the specified directory. +# (e.g. 'install.sh 0.1.0 $(go env GOPATH)/bin') +# +# Fails if the file already exists. + +set -e + +# Unset CDPATH to restore default cd behavior. An exported CDPATH can +# cause cd to output the current directory to STDOUT. +unset CDPATH + +where=$PWD + +release_url=https://api.github.com/repos/vshn/emergency-credentials-receive/releases +if [ -n "$1" ]; then + if [[ "$1" =~ ^[0-9]+(\.[0-9]+){2}$ ]]; then + version=v$1 + release_url=${release_url}/tags/$version + elif [ -n "$2" ]; then + echo "The first argument should be the requested version." + exit 1 + else + where="$1" + fi +fi + +if [ -n "$2" ]; then + where="$2" +fi + +if ! test -d "$where"; then + echo "$where does not exist. Create it first." + exit 1 +fi + +# Emulates `readlink -f` behavior, as this is not available by default on MacOS +# See: https://stackoverflow.com/questions/1055671/how-can-i-get-the-behavior-of-gnus-readlink-f-on-a-mac +function readlink_f { + TARGET_FILE=$1 + + cd "$(dirname "$TARGET_FILE")" + TARGET_FILE=$(basename "$TARGET_FILE") + + # Iterate down a (possible) chain of symlinks + while [ -L "$TARGET_FILE" ] + do + TARGET_FILE=$(readlink "$TARGET_FILE") + cd "$(dirname "$TARGET_FILE")" + TARGET_FILE=$(readlink "$TARGET_FILE") + done + + # Compute the canonicalized name by finding the physical path + # for the directory we're in and appending the target file. + PHYS_DIR=$(pwd -P) + RESULT=$PHYS_DIR/$TARGET_FILE + echo "$RESULT" +} + +function find_release_url() { + local releases=$1 + local opsys=$2 + local arch=$3 + + echo "${releases}" |\ + grep "browser_download.*${opsys}_${arch}" |\ + cut -d '"' -f 4 |\ + sort -V | tail -n 1 +} + +where="$(readlink_f "$where")/" + +if [ -f "${where}emergency-credentials-receive" ]; then + echo "${where}emergency-credentials-receive exists. Remove it first." + exit 1 +elif [ -d "${where}emergency-credentials-receive" ]; then + echo "${where}emergency-credentials-receive exists and is a directory. Remove it first." + exit 1 +fi + +tmpDir=$(mktemp -d) +if [[ ! "$tmpDir" || ! -d "$tmpDir" ]]; then + echo "Could not create temp dir." + exit 1 +fi + +function cleanup { + rm -rf "$tmpDir" +} + +trap cleanup EXIT ERR + +pushd "$tmpDir" >& /dev/null + +opsys=windows +if [[ "$OSTYPE" == linux* ]]; then + opsys=linux +elif [[ "$OSTYPE" == darwin* ]]; then + opsys=darwin +fi + +# Supported values of 'arch': amd64, arm64, ppc64le, s390x +case $(uname -m) in +x86_64) + arch=amd64 + ;; +arm64|aarch64) + arch=arm64 + ;; +*) + arch=amd64 + ;; +esac + +# You can authenticate by exporting the GITHUB_TOKEN in the environment +if [[ -z "${GITHUB_TOKEN}" ]]; then + releases=$(curl -s "$release_url") +else + releases=$(curl -s "$release_url" --header "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +if [[ $releases == *"API rate limit exceeded"* ]]; then + echo "Github rate-limiter failed the request. Either authenticate or wait a couple of minutes." + exit 1 +fi + +RELEASE_URL="$(find_release_url "$releases" "$opsys" "$arch")" + +if [[ -z "$RELEASE_URL" ]]; then + echo "Version $version does not exist or is not available for ${opsys}/${arch}." + exit 1 +fi + +curl -sLO "$RELEASE_URL" + +cp "./emergency-credentials-receive_${opsys}_${arch}" "$where/emergency-credentials-receive" +chmod +x "$where/emergency-credentials-receive" + +popd >& /dev/null + +"${where}emergency-credentials-receive" -h + +echo "emergency-credentials-receive installed to ${where}emergency-credentials-receive" diff --git a/kubectl-config-tmpl.jsonnet b/kubectl-config-tmpl.jsonnet new file mode 100644 index 0000000..27a640e --- /dev/null +++ b/kubectl-config-tmpl.jsonnet @@ -0,0 +1,31 @@ +local tokens = std.extVar('tokens'); +local server = std.extVar('server'); + +local clusterName = 'em-cluster'; + +{ + apiVersion: 'v1', + kind: 'Config', + preferences: {}, + + clusters: [ + { + cluster: { + server: server, + }, + name: clusterName, + }, + ], + contexts: std.mapWithIndex(function(i, _) { + context: { + cluster: clusterName, + user: 'token-%s' % i, + }, + name: if i == 0 then clusterName else "%s-%s" % [clusterName, i+1], + }, tokens), + 'current-context': clusterName, + users: std.mapWithIndex(function(i, t) { + name: 'token-%s' % i, + user: { token: t }, + }, tokens), +} diff --git a/main.go b/main.go index 1b2fdfb..9b3f557 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,393 @@ package main -import "fmt" +import ( + "context" + _ "embed" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/fs" + "os" + "os/signal" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/charmbracelet/lipgloss" + "github.com/cli/cli/v2/pkg/surveyext" + "github.com/google/go-jsonnet" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/passbolt/go-passbolt/api" + "golang.org/x/term" + "gopkg.in/yaml.v3" + ky "sigs.k8s.io/yaml" + + "github.com/vshn/emergency-credentials-receive/pkg/config" + "github.com/vshn/emergency-credentials-receive/pkg/inputs" +) + +//go:embed kubectl-config-tmpl.jsonnet +var kubectlTemplate string + +var sampleConfig = ` + passbolt_key: | + -----BEGIN PGP PRIVATE KEY BLOCK----- + Version: OpenPGP.js v4.10.9 + Comment: https://openpgpjs.org + + [...] + + -----END PGP PRIVATE KEY BLOCK----- +` + +const ( + envVarPassphrase = "EMR_PASSPHRASE" + envVarKubernetesEndpoint = "EMR_KUBERNETES_ENDPOINT" + + defaultEndpoint = "https://cloud.passbolt.com/vshn" + defaultKubernetesEndpoint = "https://kubernetes.default.svc:6443" + // defaultEmergencyCredentialsBucketConfigName is the name of the resource in passbolt that contains the bucket configuration. + defaultEmergencyCredentialsBucketConfigName = "emergency-cedentials-buckets" + + clusterOverviewPage = "https://wiki.vshn.net/x/4whJF" + + userAgent = "emergency-credentials-receive/0.0.0" +) + +var ( + tokenOutputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) + boldStyle = lipgloss.NewStyle().Bold(true) + isTerminal = term.IsTerminal(int(os.Stdout.Fd())) + + omitTokenOutput bool +) func main() { - fmt.Println("Yer a wizard, harry!") + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] [cluster_id]\n\n", os.Args[0]) + fmt.Fprintf(flag.CommandLine.Output(), "Available env variables:\n") + fmt.Fprintf(flag.CommandLine.Output(), "\t%s\t\tthe passphrase to unlock the Passbolt key\n", envVarPassphrase) + fmt.Fprintf(flag.CommandLine.Output(), "\t%s\tthe Kubernetes endpoint written to the created kubeconfig file\n", envVarKubernetesEndpoint) + fmt.Fprintf(flag.CommandLine.Output(), "\t%s\t\tthe directory the configuration is stored in\n", config.EnvConfigDir) + + flag.PrintDefaults() + } + flag.BoolVar(&omitTokenOutput, "omit-token-output", false, "omit token output to STDOUT") + flag.Parse() + + clusterId := flag.Arg(0) + + var saveConfig bool + lln("Welcome to the Emergency Credentials Receive tool!") + lf("This tool will help you receive your cluster emergency credentials from Passbolt.\n\n") + + c, err := config.RetrieveConfig() + if err != nil && errors.Is(err, fs.ErrNotExist) { + lf("No config file found at %q.\n", config.ConfigFile()) + lln("File will be created after a successful login.") + } else if err != nil { + lln("Error retrieving config: ", err) + } + + if c.PassboltKey == "" && isTerminal { + k, err := surveyext.Edit("", "", "\n\n# Paste your Passbolt private key from\n# https://cloud.passbolt.com/vshn/app/settings/keys\n", os.Stdin, os.Stdout, os.Stderr) + if err != nil { + lln("Error retrieving passbolt key: ", err) + os.Exit(1) + } + saveConfig = true + c.PassboltKey = k + } + if c.PassboltKey == "" { + lf("Passbolt key cannot be empty. Please provide interactively or create a config file at %q:\n%s", config.ConfigFile(), sampleConfig) + os.Exit(1) + } + + passphrase := os.Getenv("EMR_PASSPHRASE") + if passphrase == "" && isTerminal { + pf, err := inputs.PassphraseInput("Enter your Passbolt passphrase", "") + if err != nil { + lln("Error retrieving passbolt passphrase: ", err) + os.Exit(1) + } + passphrase = pf + } else if passphrase == "" { + lln("Passphrase cannot be empty.") + lln("Provide interactively or set EMR_PASSPHRASE environment variable.") + os.Exit(1) + } else { + lln("Using passphrase from EMR_PASSPHRASE environment variable.") + } + + if clusterId == "" { + cid, err := inputs.LineInput("Enter the ID of the cluster you want to access", "c-crashy-wreck-1234") + if err != nil { + lln("Error retrieving cluster ID: ", err) + os.Exit(1) + } + clusterId = cid + } + if clusterId == "" { + lln("Cluster ID cannot be empty.") + lln("Provide interactively or as argument.") + os.Exit(1) + } + + client, err := api.NewClient(nil, userAgent, defaultEndpoint, c.PassboltKey, passphrase) + if err != nil { + lf("Error creating passbolt client: %v\n", err) + os.Exit(1) + } + + lln("Logging into passbolt...") + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + if err := client.Login(ctx); err != nil { + lf("Error logging into passbolt: %v\n", err) + os.Exit(1) + } + + if saveConfig { + if err := config.SaveConfig(c); err != nil { + lln("Error saving config: ", err) + } + } + + lf("Logged in. Retrieving bucket configuration from %q...\n", defaultEmergencyCredentialsBucketConfigName) + res, err := client.GetResources(ctx, &api.GetResourcesOptions{}) + if err != nil { + lln("Error retrieving resources from passbolt: ", err) + } + + var resource api.Resource + for _, r := range res { + if r.Name == defaultEmergencyCredentialsBucketConfigName { + resource = r + break + } + } + if resource.ID == "" { + lln("Error retrieving bucket configuration from passbolt: ", fmt.Errorf("could not find resource %q", defaultEmergencyCredentialsBucketConfigName)) + os.Exit(1) + } + lln(" Retrieving bucket secret...") + secret, err := client.GetSecret(ctx, resource.ID) + if err != nil { + lln("Error retrieving bucket secret from passbolt: ", err) + os.Exit(1) + } + + lln(" Decrypting bucket secret...") + conf, err := client.DecryptMessage(secret.Data) + if err != nil { + lln("Error decrypting bucket secret in passbolt: ", err) + os.Exit(1) + } + + lln(" Parsing passbolt secret...") + var pbsc api.SecretDataTypePasswordAndDescription + if err := json.Unmarshal([]byte(conf), &pbsc); err != nil { + lln("Error parsing the decrypted passbolt secret: ", err) + os.Exit(1) + } + lln(" Parsing bucket configuration from secret...") + var bc bucketConfig + if err := yaml.Unmarshal([]byte(pbsc.Password), &bc); err != nil { + lln("Error parsing bucket configuration from passbolt secrets password field: ", err) + os.Exit(1) + } + + lf("%d buckets with credentials found\n", len(bc.Buckets)) + + var emcreds []string + for _, b := range bc.Buckets { + lf("Trying %q (bucket %q, region %q, keyId %q)\n", b.Endpoint, b.Bucket, b.Region, b.AccessKeyId) + + mc, err := minio.New(b.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(b.AccessKeyId, b.SecretAccessKey, ""), + Secure: !b.Insecure, + Region: b.Region, + }) + if err != nil { + lln(" Error creating minio client: ", err) + continue + } + + objectName := clusterId + if b.ObjectNameTemplate != "" { + lln(" Constructing object name from template...") + t, err := template.New("fileName").Funcs(sprig.TxtFuncMap()).Parse(b.ObjectNameTemplate) + if err != nil { + lln(" unable to parse file name template:", err) + continue + } + buf := new(strings.Builder) + if err := t.Execute(buf, struct { + ClusterId string + Context map[string]string + }{ + ClusterId: clusterId, + Context: map[string]string{"ClusterId": clusterId}, + }); err != nil { + lln(" unable to execute file name template:", err) + continue + } + objectName = buf.String() + } + lf(" Downloading %q...\n", objectName) + + // fully read object into memory, otherwise the error message can be very confusing + // it says something like "JSON unmarshal error: not found" + buf, err := minioGetReadAll(ctx, mc, b.Bucket, objectName) + if err != nil { + lln(" Error downloading object: ", err) + continue + } + + lln(" Parsing object...") + var et encryptedToken + if err := json.Unmarshal(buf, &et); err != nil { + lln(" Error parsing object: ", err) + continue + } + + lln(" Trying to decrypt object...") + var decrypted string + for _, s := range et.Secrets { + d, err := client.DecryptMessage(s.Data) + if err == nil { + decrypted = d + break + } + } + if decrypted == "" { + lln(" No decryptable secret found") + continue + } + + emcreds = append(emcreds, decrypted) + } + + if len(emcreds) == 0 { + lln("No valid emergency credentials found") + os.Exit(1) + } + + lf("Emergency credentials found\n\n") + for i, c := range emcreds { + fmt.Println("# ", "Token", i) + if omitTokenOutput { + fmt.Println(tokenOutputStyle.Render("*** OMITTED ***")) + } else { + fmt.Println(tokenOutputStyle.Render(c)) + } + } + + kep := os.Getenv("EMR_KUBERNETES_ENDPOINT") + if kep == "" && isTerminal { + ih := fmt.Sprintf("Provide API endpoint to render kubeconfig. See %q for an overview.", clusterOverviewPage) + k, err := inputs.LineInput(ih, defaultKubernetesEndpoint) + if err != nil { + lln("Error retrieving kubernetes endpoint: ", err) + os.Exit(1) + } + kep = k + } + if kep == "" { + lln("Assuming default kubernetes endpoint.") + kep = defaultKubernetesEndpoint + } + kubeconfig, err := renderKubeconfig(kep, emcreds) + if err != nil { + lln("Error rendering kubeconfig: ", err) + lln("The tokens printed above should continue to work, but you will have to create the kubeconfig manually.") + os.Exit(1) + } + + kcFileName := "em-" + clusterId + if err := os.WriteFile(kcFileName, []byte("# Generated by emergency-credentials-receive\n"+kubeconfig), 0600); err != nil { + lln("Error writing kubeconfig: ", err) + lln("The tokens printed above should continue to work, but you will have to create the kubeconfig manually.") + os.Exit(1) + } + lf("Wrote kubeconfig to %q. Use with:\n\n", kcFileName) + lln(boldStyle.Render(fmt.Sprintf("export KUBECONFIG=%q", kcFileName))) + lln(boldStyle.Render("kubectl get nodes")) +} + +// minioGetReadAll is a helper function to fully download a S3 object into memory. +func minioGetReadAll(ctx context.Context, mc *minio.Client, bucket, objectName string) ([]byte, error) { + object, err := mc.GetObject(ctx, bucket, objectName, minio.GetObjectOptions{}) + if err != nil { + return nil, err + } + defer object.Close() + + return io.ReadAll(object) +} + +func lf(format string, a ...any) { + fmt.Fprintf(os.Stderr, format, a...) +} + +func lln(a ...any) { + fmt.Fprintln(os.Stderr, a...) +} + +func renderKubeconfig(server string, tokens []string) (string, error) { + vm := jsonnet.MakeVM() + + tks, err := json.Marshal(tokens) + if err != nil { + return "", err + } + + vm.ExtVar("server", server) + vm.ExtCode("tokens", string(tks)) + + json, err := vm.EvaluateAnonymousSnippet("kubeconfig", kubectlTemplate) + if err != nil { + return "", err + } + + yml, err := ky.JSONToYAML([]byte(json)) + return string(yml), err +} + +type bucketConfig struct { + Buckets []bucket `yaml:"buckets"` +} + +type bucket struct { + // Endpoint is the S3 endpoint to use. + Endpoint string `yaml:"endpoint"` + // Bucket is the S3 bucket to use. + Bucket string `yaml:"bucket"` + + // AccessKeyId and SecretAccessKey are the S3 credentials to use. + AccessKeyId string `yaml:"accessKeyId"` + // SecretAccessKey is the S3 secret access key to use. + SecretAccessKey string `yaml:"secretAccessKey"` + + // Region is the AWS region to use. + Region string `yaml:"region,omitempty"` + // Insecure allows to use an insecure connection to the S3 endpoint. + Insecure bool `yaml:"insecure,omitempty"` + + // ObjectNameTemplate is a template for the object name to use. + ObjectNameTemplate string `yaml:"objectNameTemplate,omitempty"` +} + +// encryptedToken is the JSON structure of an encrypted token. +type encryptedToken struct { + Secrets []encryptedTokenSecret `json:"secrets"` +} + +// encryptedTokenSecret is the JSON structure of an encrypted token secret. +type encryptedTokenSecret struct { + Data string `json:"data"` } diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..e97c29e --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,66 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "sync" + + "gopkg.in/yaml.v3" +) + +const configFile = "config.yaml" + +var configMux = sync.RWMutex{} + +type Config struct { + PassboltKey string `yaml:"passbolt_key" json:"passbolt_key"` +} + +// ConfigFile returns the path to the config file. +// Also see ConfigDir(). +func ConfigFile() string { + return filepath.Join(ConfigDir(), configFile) +} + +func RetrieveConfig() (Config, error) { + configMux.RLock() + defer configMux.RUnlock() + + configFile := ConfigFile() + + yamlFile, err := os.ReadFile(configFile) + if err != nil { + return Config{}, fmt.Errorf("error reading config file %q: %w", configFile, err) + } + + var config Config + yaml.Unmarshal([]byte(yamlFile), &config) + if err != nil { + return Config{}, fmt.Errorf("error parsing config file %q: %w", configFile, err) + } + + return config, nil +} + +func SaveConfig(config Config) error { + configMux.Lock() + defer configMux.Unlock() + + if err := os.MkdirAll(ConfigDir(), 0700); err != nil { + return fmt.Errorf("error creating config dir %q: %w", ConfigDir(), err) + } + + configFile := filepath.Join(ConfigDir(), configFile) + + yamlFile, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("error marshalling config: %w", err) + } + + if err := os.WriteFile(configFile, yamlFile, 0600); err != nil { + return fmt.Errorf("error writing config file %q: %w", configFile, err) + } + + return nil +} diff --git a/pkg/config/dir.go b/pkg/config/dir.go new file mode 100644 index 0000000..61c98e1 --- /dev/null +++ b/pkg/config/dir.go @@ -0,0 +1,33 @@ +package config + +import ( + "os" + "path/filepath" + "runtime" +) + +const ( + EnvConfigDir = "EMR_CONFIG_DIR" + + appData = "AppData" + xdgConfigHome = "XDG_CONFIG_HOME" + + configDirName = "emergency-credentials-receive" +) + +// ConfigDir returns the path to the config directory. +// Config path precedence: EMR_CONFIG_DIR, XDG_CONFIG_HOME, AppData (windows only), HOME. +func ConfigDir() string { + var path string + if a := os.Getenv(EnvConfigDir); a != "" { + path = a + } else if b := os.Getenv(xdgConfigHome); b != "" { + path = filepath.Join(b, configDirName) + } else if c := os.Getenv(appData); runtime.GOOS == "windows" && c != "" { + path = filepath.Join(c, configDirName) + } else { + d, _ := os.UserHomeDir() + path = filepath.Join(d, ".config", configDirName) + } + return path +} diff --git a/pkg/inputs/line.go b/pkg/inputs/line.go new file mode 100644 index 0000000..2fe21a0 --- /dev/null +++ b/pkg/inputs/line.go @@ -0,0 +1,92 @@ +package inputs + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +var ErrInterrupted = fmt.Errorf("interrupted") + +func PassphraseInput(header, placeholder string) (string, error) { + p := tea.NewProgram(newLineModel(header, placeholder, true)) + + m, err := p.Run() + if err != nil { + return "", err + } + + return m.(lineModel).textinput.Value(), m.(lineModel).err +} + +func LineInput(header, placeholder string) (string, error) { + p := tea.NewProgram(newLineModel(header, placeholder, false)) + + m, err := p.Run() + if err != nil { + return "", err + } + + return m.(lineModel).textinput.Value(), m.(lineModel).err +} + +type lineModel struct { + header string + + textinput textinput.Model + err error +} + +func newLineModel(header, placeholder string, pw bool) lineModel { + ti := textinput.New() + if pw { + ti.EchoMode = textinput.EchoPassword + } + ti.Placeholder = placeholder + ti.Focus() + + return lineModel{ + header: header, + textinput: ti, + err: nil, + } +} + +func (m lineModel) Init() tea.Cmd { + return textarea.Blink +} + +func (m lineModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlD, tea.KeyEnter: + return m, tea.Quit + case tea.KeyCtrlC: + m.err = ErrInterrupted + return m, tea.Quit + default: + if !m.textinput.Focused() { + cmds = append(cmds, m.textinput.Focus()) + } + } + } + + var cmd tea.Cmd + m.textinput, cmd = m.textinput.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} + +func (m lineModel) View() string { + return fmt.Sprintf( + "\n%s\n\n%s\n\n%s\n\n", + m.header, + m.textinput.View(), + "(ctrl+c to quit)", + ) +}