diff --git a/README.md b/README.md index 9a99c32..c2adc23 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ steps: - The time (in seconds) the OIDC token will be valid for before expiry. Must be a non-negative integer. If the flag is omitted or set to 0, the API will choose a default finite lifetime. (default: 0) +### `render-command` (string) + +- An installed binary that when specified, will run twice to process the values of `audience` and `service-account` via stdin. This is intended to be used to render environment variables with an application such as `envsubst`. (default: '') + ## Developing To run testing, shellchecks and plugin linting use use `bk run` with the [Buildkite CLI](https://github.com/buildkite/cli). diff --git a/hooks/pre-command b/hooks/pre-command index 03c7141..11916f5 100755 --- a/hooks/pre-command +++ b/hooks/pre-command @@ -12,6 +12,16 @@ if [[ -z "${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT:-} exit 1 fi +if [[ -n "${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_RENDER_COMMAND:-}" ]]; then + # Test that the given command exists, otherwise fail + command -v "${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_RENDER_COMMAND}" || { + echo "🚨 Render command file '${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_RENDER_COMMAND}' not found" + exit 1 + } + BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE="$(echo "${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE}" | ${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_RENDER_COMMAND})" + BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT="$(echo "${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT}" | ${BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_RENDER_COMMAND})" +fi + # Create a temporary directory with both BSD and GNU mktemp TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'buildkiteXXXX') diff --git a/plugin.yml b/plugin.yml index 32fefc7..ae141e9 100644 --- a/plugin.yml +++ b/plugin.yml @@ -13,6 +13,8 @@ configuration: type: string lifetime: type: number + render-command: + type: string required: - audience - service-account diff --git a/tests/pre-command.bats b/tests/pre-command.bats index 9273394..7a3c1ab 100755 --- a/tests/pre-command.bats +++ b/tests/pre-command.bats @@ -36,6 +36,16 @@ setup() { assert_failure } +@test "fails when render command has non-existent binary" { + export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE="//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite" + export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT="buildkite-example-pipeline@oidc-project.iam.gserviceaccount.com" + export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_RENDER_COMMAND="this-file-purposely-does-not-exist" + + run "$PWD/hooks/pre-command" + + assert_failure +} + @test "succeeds when mktemp fails once" { export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE="//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite" export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT="buildkite-example-pipeline@oidc-project.iam.gserviceaccount.com" @@ -83,3 +93,38 @@ JSON) unstub mktemp unstub buildkite-agent } + +@test "exports credentials with render command using envsubst" { + export GCP_PROJECT_ID=oidc-project + export GCP_PROJECT_NUMBER=123456789 + export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_AUDIENCE="//iam.googleapis.com/projects/\${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite" + export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_SERVICE_ACCOUNT="buildkite-example-pipeline@\${GCP_PROJECT_ID}.iam.gserviceaccount.com" + export BUILDKITE_PLUGIN_GCP_WORKLOAD_IDENTITY_FEDERATION_RENDER_COMMAND="envsubst" + + stub mktemp "-d : echo $BATS_TEST_TMPDIR" + stub buildkite-agent "oidc request-token --audience //iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite --lifetime 0 : echo dummy-jwt" + + run "$PWD/hooks/pre-command" + + assert_success + + assert_output --partial "Requesting OIDC token from Buildkite" + assert_output --partial "Configuring Google Cloud credentials" + + diff $BATS_TEST_TMPDIR/token.json <(echo dummy-jwt) + diff $BATS_TEST_TMPDIR/credentials.json <(cat << JSON +{ + "type": "external_account", + "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/buildkite-example-pipeline/providers/buildkite", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + "token_url": "https://sts.googleapis.com/v1/token", + "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/buildkite-example-pipeline@oidc-project.iam.gserviceaccount.com:generateAccessToken", + "credential_source": { + "file": "$BATS_TEST_TMPDIR/token.json" + } +} +JSON) + + unstub mktemp + unstub buildkite-agent +} diff --git a/tests/setup_suite.bash b/tests/setup_suite.bash new file mode 100644 index 0000000..df372bf --- /dev/null +++ b/tests/setup_suite.bash @@ -0,0 +1,4 @@ +setup_suite() { + echo '# Installing envsubst' >&2 + apk --no-cache add gettext | sed -e 's/^/# /' >&2 +}