diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7982d62..8d873a0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,7 +53,9 @@ jobs: vault: image: hashicorp/vault:${{ matrix.vault_version }} env: - SKIP_SETCAP: true + SKIP_SETCAP: "true" + VAULT_ADDR: http://127.0.0.1:8200 + VAULT_TOKEN: 227e1cce-6bf7-30bb-2d2a-acc854318caf VAULT_DEV_ROOT_TOKEN_ID: 227e1cce-6bf7-30bb-2d2a-acc854318caf ports: - 8200:8200 @@ -215,3 +217,52 @@ jobs: - name: Dependency Review uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 + + e2e-test: + name: E2E test + runs-on: ubuntu-latest + strategy: + matrix: + vault_version: ["1.11.12", "1.12.8", "1.13.4", "1.14.1"] + + services: + vault: + image: hashicorp/vault:${{ matrix.vault_version }} + env: + SKIP_SETCAP: "true" + VAULT_ADDR: http://127.0.0.1:8200 + VAULT_TOKEN: 227e1cce-6bf7-30bb-2d2a-acc854318caf + VAULT_DEV_ROOT_TOKEN_ID: 227e1cce-6bf7-30bb-2d2a-acc854318caf + ports: + - 8200:8200 + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up Nix + uses: cachix/install-nix-action@7ac1ec25491415c381d9b62f0657c7a028df52a7 # v24 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + + - name: Set up magic Nix cache + uses: DeterminateSystems/magic-nix-cache-action@8a218f9e264e9c3803c9a1ee1c30d8e4ab55be63 # v2 + + - name: Set up Go cache + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ github.job }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ github.job }}-${{ runner.os }}-go- + + - name: Prepare Nix shell + run: nix develop --impure .#ci + + - name: Test + run: nix develop --impure .#ci -c make test-e2e + env: + VAULT_VERSION: ${{ matrix.vault_version }} diff --git a/Makefile b/Makefile index cbba064..eddf1e9 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,21 @@ export PATH := $(abspath bin/):${PATH} # Dependency versions GOLANGCI_VERSION = 1.53.3 LICENSEI_VERSION = 0.8.0 -GORELEASER_VERSION = 1.18.2 +COSIGN_VERSION = 2.2.2 +GORELEASER_VERSION = 1.23.0 +BATS_VERSION = 1.2.1 + +##@ General + +# Targets commented with ## will be visible in "make help" info. +# Comments marked with ##@ will be used as categories for a group of targets. + +.PHONY: help +.DEFAULT_GOAL := help +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development .PHONY: up up: ## Start development environment @@ -19,6 +33,8 @@ stop: ## Stop development environment down: ## Destroy development environment docker compose down -v +##@ Build + .PHONY: build build: ## Build binary @mkdir -p build @@ -34,7 +50,9 @@ container-image: ## Build container image .PHONY: binary-snapshot binary-snapshot: ## Build binary snapshot - goreleaser release --rm-dist --skip-publish --snapshot + VERSION=v${GORELEASER_VERSION} goreleaser release --clean --skip=publish --snapshot + +##@ Checks .PHONY: check check: test lint ## Run checks (tests and linters) @@ -43,6 +61,11 @@ check: test lint ## Run checks (tests and linters) test: ## Run tests go test -race -v ./... +.PHONY: test-e2e +test-e2e: ## Run e2e tests + @export BATS_LIB_PATH=${PWD}/bin/bats-core/libexec/bats-core/lib && \ + bats e2e + .PHONY: lint lint: lint-go lint-docker lint-yaml lint: ## Run linters @@ -68,7 +91,9 @@ license-check: ## Run license check licensei check licensei header -deps: bin/golangci-lint bin/licensei bin/goreleaser +##@ Dependencies + +deps: bin/golangci-lint bin/licensei bin/cosign bin/goreleaser bin/bats deps: ## Install dependencies bin/golangci-lint: @@ -79,14 +104,34 @@ bin/licensei: @mkdir -p bin curl -sfL https://raw.githubusercontent.com/goph/licensei/master/install.sh | bash -s -- v${LICENSEI_VERSION} -bin/goreleaser: +bin/cosign: @mkdir -p bin - @mkdir -p tmpgoreleaser - curl -sfL https://goreleaser.com/static/run | VERSION=v${GORELEASER_VERSION} TMPDIR=${PWD}/tmpgoreleaser bash -s -- --version - mv tmpgoreleaser/goreleaser bin/ - @rm -rf tmpgoreleaser + @OS=$$(uname -s); \ + case $$OS in \ + "Linux") \ + curl -sSfL https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64 -o bin/cosign; \ + ;; \ + "Darwin") \ + curl -sSfL https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-darwin-arm64 -o bin/cosign; \ + ;; \ + *) \ + echo "Unsupported OS: $$OS"; \ + exit 1; \ + ;; \ + esac + @chmod +x bin/cosign -.PHONY: help -.DEFAULT_GOAL := help -help: - @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-10s\033[0m %s\n", $$1, $$2}' +bin/goreleaser: + @mkdir -p bin + curl -sfL https://goreleaser.com/static/run -o bin/goreleaser + @chmod +x bin/goreleaser + +bin/bats: + @mkdir -p bin/bats-core + @mkdir -p tmpbats + git clone https://github.com/bats-core/bats-core.git tmpbats + bash tmpbats/install.sh bin/bats-core + @ln -sF ${PWD}/bin/bats-core/bin/bats ${PWD}/bin + @rm -rf tmpbats + git clone https://github.com/bats-core/bats-support.git bin/bats-core/libexec/bats-core/lib/bats-support + git clone https://github.com/bats-core/bats-assert.git bin/bats-core/libexec/bats-core/lib/bats-assert diff --git a/docker-compose.yaml b/docker-compose.yaml index 5421c6e..b4a83d1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,9 +2,12 @@ version: "3.9" services: vault: + container_name: vault image: hashicorp/vault:1.14.1 ports: - 127.0.0.1:8200:8200 environment: - SKIP_SETCAP: true + SKIP_SETCAP: "true" + VAULT_ADDR: http://127.0.0.1:8200 + VAULT_TOKEN: 227e1cce-6bf7-30bb-2d2a-acc854318caf VAULT_DEV_ROOT_TOKEN_ID: 227e1cce-6bf7-30bb-2d2a-acc854318caf diff --git a/e2e/file-provider.bats b/e2e/file-provider.bats new file mode 100644 index 0000000..bb0e4f1 --- /dev/null +++ b/e2e/file-provider.bats @@ -0,0 +1,31 @@ +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + setup_pod + + run go build + assert_success +} + +setup_pod() { + TMPFILE=$(mktemp) + printf "secret-value" > "$TMPFILE" + + export SECRET_INIT_PROVIDER="file" + export FILE_MOUNT_PATH="/" + export Secret="file:$TMPFILE" +} + +teardown() { + rm -f "$TMPFILE" + rm -f secret-init +} + +@test "secret successfully loaded" { + run_output=$(./secret-init env | grep Secret) + assert_success + expected_output="Secret=secret-value" + + assert_equal "$run_output" "$expected_output" +} diff --git a/e2e/vault-provider.bats b/e2e/vault-provider.bats new file mode 100644 index 0000000..38925d4 --- /dev/null +++ b/e2e/vault-provider.bats @@ -0,0 +1,148 @@ +vault_container_name="vault" + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + start_vault + + setup_pod + + run go build + assert_success +} + +start_vault() { + docker compose up -d + + # wait for Vault to be ready + max_attempts=${MAX_ATTEMPTS:-10} + + for ((attempts = 0; attempts < max_attempts; attempts++)); do + if docker compose exec -T "$vault_container_name" vault status > /dev/null 2>&1; then + break + fi + sleep 1 + done +} + +setup_pod() { + TMPFILE=$(mktemp) + printf "227e1cce-6bf7-30bb-2d2a-acc854318caf" > "$TMPFILE" + + export SECRET_INIT_PROVIDER="vault" + export VAULT_ADDR="http://127.0.0.1:8200" + export VAULT_TOKEN_FILE="$TMPFILE" + + export MYSQL_PASSWORD=vault:secret/data/test/mysql#MYSQL_PASSWORD + export AWS_SECRET_ACCESS_KEY=vault:secret/data/test/aws#AWS_SECRET_ACCESS_KEY + export AWS_ACCESS_KEY_ID=vault:secret/data/test/aws#AWS_ACCESS_KEY_ID +} + +set_vault_token() { + local token=$1 + export VAULT_TOKEN="$token" +} + +set_daemon_mode() { + export SECRET_INIT_DAEMON="true" +} + +add_secrets_to_vault() { + docker exec "$vault_container_name" vault kv put secret/test/mysql MYSQL_PASSWORD=3xtr3ms3cr3t + docker exec "$vault_container_name" vault kv put secret/test/aws AWS_ACCESS_KEY_ID=secretId AWS_SECRET_ACCESS_KEY=s3cr3t +} + +teardown() { + stop_vault + + rm -f "$TMPFILE" + rm -f secret-init +} + +stop_vault() { + remove_secrets_from_vault + docker compose down +} + +remove_secrets_from_vault() { + docker exec "$vault_container_name" vault kv delete secret/test/mysql + docker exec "$vault_container_name" vault kv delete secret/test/aws +} + +assert_output_contains() { + local expected=$1 + local output=$2 + + echo "$output" | grep -qF "$expected" || fail "Expected line not found: $expected" +} + +check_process_status() { + local process_name="$1" + + if pgrep -f "$process_name" > /dev/null; then + echo "Process is running" + else + echo "Process is not running" + fi +} + + +@test "secrets successfully loaded from vault" { + set_vault_token 227e1cce-6bf7-30bb-2d2a-acc854318caf + add_secrets_to_vault + + run_output=$(./secret-init env | grep 'MYSQL_PASSWORD\|AWS_SECRET_ACCESS_KEY\|AWS_ACCESS_KEY_ID') + assert_success + + assert_output_contains "MYSQL_PASSWORD=3xtr3ms3cr3t" "$run_output" + assert_output_contains "AWS_SECRET_ACCESS_KEY=s3cr3t" "$run_output" + assert_output_contains "AWS_ACCESS_KEY_ID=secretId" "$run_output" +} + +@test "secrets successfully loaded from vault using vault:login as token" { + set_vault_token "vault:login" + add_secrets_to_vault + + run_output=$(./secret-init env | grep 'MYSQL_PASSWORD\|AWS_SECRET_ACCESS_KEY\|AWS_ACCESS_KEY_ID') + assert_success + + assert_output_contains "MYSQL_PASSWORD=3xtr3ms3cr3t" "$run_output" + assert_output_contains "AWS_SECRET_ACCESS_KEY=s3cr3t" "$run_output" + assert_output_contains "AWS_ACCESS_KEY_ID=secretId" "$run_output" +} + +@test "secrets successfully loaded from vault using vault:login as token and daemon mode enabled" { + set_vault_token "vault:login" + set_daemon_mode + add_secrets_to_vault + + run_output=$(./secret-init env | grep 'MYSQL_PASSWORD\|AWS_SECRET_ACCESS_KEY\|AWS_ACCESS_KEY_ID') + assert_success + + assert_output_contains "MYSQL_PASSWORD=3xtr3ms3cr3t" "$run_output" + assert_output_contains "AWS_SECRET_ACCESS_KEY=s3cr3t" "$run_output" + assert_output_contains "AWS_ACCESS_KEY_ID=secretId" "$run_output" + + # Check if the process is still running in the background + check_process_status "secret-init env" + assert_success +} + +@test "secrets successfully loaded from vault using VAULT_FROM_PATH" { + # unset env vars to ensure secret-init will utilize VAULT_FROM_PATH + unset MYSQL_PASSWORD + unset AWS_SECRET_ACCESS_KEY + unset AWS_ACCESS_KEY_ID + + set_vault_token 227e1cce-6bf7-30bb-2d2a-acc854318caf + add_secrets_to_vault + export VAULT_FROM_PATH="secret/data/test/mysql,secret/data/test/aws" + + run_output=$(./secret-init env | grep 'MYSQL_PASSWORD\|AWS_SECRET_ACCESS_KEY\|AWS_ACCESS_KEY_ID') + assert_success + + assert_output_contains "MYSQL_PASSWORD=3xtr3ms3cr3t" "$run_output" + assert_output_contains "AWS_SECRET_ACCESS_KEY=s3cr3t" "$run_output" + assert_output_contains "AWS_ACCESS_KEY_ID=secretId" "$run_output" +} diff --git a/flake.nix b/flake.nix index f72a041..22f9c00 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,7 @@ gnumake # golangci-lint + (bats.withLibraries (p: [ p.bats-support p.bats-assert ])) goreleaser # TODO: remove once https://github.com/NixOS/nixpkgs/pull/254878 hits unstable diff --git a/provider/vault/vault_test.go b/provider/vault/vault_test.go index 0503fd0..51459ef 100644 --- a/provider/vault/vault_test.go +++ b/provider/vault/vault_test.go @@ -91,9 +91,8 @@ func TestNewProvider(t *testing.T) { func setupTestLogger() { originalLogger = slog.Default() - // Redirect logs to avoid polluting the test output - handler := slog.NewTextHandler(io.Discard, nil) - testLogger := slog.New(handler) + // Discard logs to avoid polluting the test output + testLogger := slog.New(slog.NewTextHandler(io.Discard, nil)) slog.SetDefault(testLogger) }