diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c14d0e2fe00..732db483c87 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -180,4 +180,4 @@ vaccine: stage: vaccine needs: [create-multiarch-lib-injection-image] script: | - .gitlab/scripts/vaccine.sh lloeki/ssi "${CI_COMMIT_SHA}" + .gitlab/scripts/vaccine.sh lloeki/ssi "${CI_COMMIT_SHA}" "glci:${CI_PIPELINE_ID}" diff --git a/.gitlab/scripts/vaccine.sh b/.gitlab/scripts/vaccine.sh index bb9f8728f0c..910f46dc43f 100755 --- a/.gitlab/scripts/vaccine.sh +++ b/.gitlab/scripts/vaccine.sh @@ -1,91 +1,336 @@ -#!/bin/bash - -set -e - -GH_VACCINE_PAT=$(vault kv get -field=vaccine-token kv/k8s/gitlab-runner/dd-trace-rb/github-token) -REPO="Datadog/vaccine" -POLL_INTERVAL=60 # seconds - -REF="${1:-master}" -SHA="${2:-"$(git rev-parse HEAD)"}" - -# Trigger workflow -echo "Triggering workflow..." -TRIGGER_RESPONSE=$(curl -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token $GH_VACCINE_PAT" \ - -w "\n%{http_code}" \ - "https://api.github.com/repos/$REPO/actions/workflows/vaccine.yml/dispatches" \ - -d '{"ref":"'${REF}'", "inputs": {"commit_sha": "'$CI_COMMIT_SHA'"}}' 2>&1) - -HTTP_STATUS=$(echo "$TRIGGER_RESPONSE" | tail -n1) -if [ "$HTTP_STATUS" -ne 204 ]; then - echo "Error: Workflow trigger failed with status $HTTP_STATUS" - echo "Response: $(echo "$TRIGGER_RESPONSE" | sed '$ d')" - exit 1 -fi +#!/usr/bin/env bash -echo "Successfully triggered workflow. Waiting for workflow to start..." -sleep 5 # Give GitHub a moment to create the workflow run +set -euo pipefail -# Get the most recent workflow run -echo "Fetching most recent workflow run..." -RUNS_RESPONSE=$(curl -s \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token $GH_VACCINE_PAT" \ - -w "\n%{http_code}" \ - "https://api.github.com/repos/$REPO/actions/runs?event=workflow_dispatch&per_page=1" 2>&1) +### Secret redaction utilities -HTTP_STATUS=$(echo "$RUNS_RESPONSE" | tail -n1) -RESPONSE_BODY=$(echo "$RUNS_RESPONSE" | sed '$ d') +__SECRETS=() -if [ "$HTTP_STATUS" -ne 200 ]; then - echo "Error: Fetching runs failed with status $HTTP_STATUS" - echo "Response: $RESPONSE_BODY" - exit 1 -fi +# add secret(s) to known secrets +secret_add() { + __SECRETS+=("$@") +} -# Get the most recent run ID -WORKFLOW_ID=$(echo "$RESPONSE_BODY" | jq -r '.workflow_runs[0].id') +# redact all known secrets +# shellcheck disable=SC2120 +secret_redact() { + local redact="${1:-REDACTED}" + local args=() -if [ -z "$WORKFLOW_ID" ] || [ "$WORKFLOW_ID" = "null" ]; then - echo "Error: Could not find recent workflow run" - exit 1 -fi + local redact_escaped + redact_escaped="$(printf '%s\n' "${redact}" | sed -e 's/[\/&]/\\&/g')" + + for secret in "${__SECRETS[@]}"; do + # escape sed pattern characters + local secret_escaped + secret_escaped="$(printf '%s\n' "${secret}" | sed -e 's/[]\/$*.^[]/\\&/g')" + + # build sed arg list + args+=(-e "s^${secret_escaped}^${redact_escaped}^gi") + done + + # perform replacement + sed "${args[@]}" +} + +# redirect whole script output to be redacted +# must be used only after all secrets are known +secret_redir_redact() { + exec > >(secret_redact) 2> >(secret_redact 1>&2) +} + +# log safely +log() { + printf "%s\n" "$*" | secret_redact 1>&2 +} -echo "Found workflow run ID: $WORKFLOW_ID" +# run safely +run() { + "$@" > >(secret_redact) 2> >(secret_redact 1>&2) +} -# Poll workflow status -while true; do - RUN_RESPONSE=$(curl -s \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token $GH_VACCINE_PAT" \ - -w "\n%{http_code}" \ - "https://api.github.com/repos/$REPO/actions/runs/$WORKFLOW_ID" 2>&1) +# run command with safe logging +log_and_run() { + local cmd=( "$@" ) - HTTP_STATUS=$(echo "$RUN_RESPONSE" | tail -n1) - RESPONSE_BODY=$(echo "$RUN_RESPONSE" | sed '$ d') + ( + printf '* Command:' + for e in "${cmd[@]}"; do + printf " " + printf -v quoted "%q" "${e}" + if [[ "${quoted}" == "${e}" ]]; then + printf "%s" "${e}" + else + printf "%s" "'${e//'/\"'\"}'" + fi + done + printf "\n" + ) | secret_redact 1>&2 + + run "${cmd[@]}" +} + +# obtain GitHub vaccine token from vault +get_vaccine_token() { + vault kv get -field=vaccine-token kv/k8s/gitlab-runner/dd-trace-rb/github-token +} + +# get current commit SHA +head_commit_sha() { + git rev-parse HEAD +} + +github_api_get() { + local token="$1" + local repo="$2" + local endpoint="$3" + shift 3 + local params=("$@") + + local cmd=( + curl + -s + -H "Accept: application/vnd.github.v3+json" + -H "Authorization: token ${token}" + -w "\n%{http_code}\n" + ) + + if [[ "${#params[@]}" -gt 0 ]]; then + cmd+=(-G) + fi - if [ "$HTTP_STATUS" -ne 200 ]; then - echo "Error: Fetching run status failed with status $HTTP_STATUS" - echo "Response: $RESPONSE_BODY" - exit 1 - fi + for param in "${params[@]}"; do + cmd+=(-d "${param}") + done - STATUS=$(echo "$RESPONSE_BODY" | jq -r .status) - CONCLUSION=$(echo "$RESPONSE_BODY" | jq -r .conclusion) + cmd+=( + "https://api.github.com/repos/${repo}/${endpoint}" + ) - if [ "$STATUS" = "completed" ]; then - if [ "$CONCLUSION" = "success" ]; then - echo "✅ Workflow completed successfully!" - exit 0 + log_and_run "${cmd[@]}" +} + +github_api_post() { + local token="$1" + local repo="$2" + local endpoint="$3" + local body="$4" + + local cmd=( + curl + -s + -X POST + -H "Accept: application/vnd.github.v3+json" + -H "Authorization: token ${token}" + -w "\n%{http_code}\n" + ) + cmd+=( + -d "${body}" + ) + cmd+=( + + "https://api.github.com/repos/${repo}/${endpoint}" + ) + + log_and_run "${cmd[@]}" +} + +github_workflow_dispatch() { + local token="$1" + local repo="$2" + local ref="$3" + local workflow="$4" + local inputs="$5" + + github_api_post "${token}" "${repo}" \ + "actions/workflows/${workflow}/dispatches" \ + '{"ref":"'"${ref}"'", "inputs": '"${inputs}"'}' +} + +github_workflow_runs() { + local token="$1" + local repo="$2" + local ref="$3" + local workflow="$4" + shift 4 + local params=("$@") + + github_api_get "${token}" "${repo}" \ + "actions/workflows/${workflow}/runs" \ + "${params[@]}" + # cat runs.json +} + +github_workflow_run() { + local token="$1" + local repo="$2" + local id="$3" + shift 3 + local params=("$@") + + github_api_get "${token}" "${repo}" \ + "actions/runs/${id}" \ + "${params[@]}" + # cat run.json +} + +check_status() { + local status="$1" + + # - connect final stdout from fd3 + # - remove last line of stdin and copy to fd3 + # - get last line of stdin + # - match against status code + # - since match is last, its exit is the return code + local body="$(cat)" + local res="$(printf "%s" "${body}" | tail -n1)" + + if [[ "${res}" == "${status}" ]]; then + printf "%s" "${body}" | sed -e '$d' else - echo "❌ Workflow failed with conclusion: $CONCLUSION" - echo "See details: https://github.com/$REPO/actions/runs/$WORKFLOW_ID" - exit 1 + log "*** Error: unexpected status ${res}" + log "*** BODY START" + log "${body}" + log "*** BODY END" fi - fi +} + +dispatch_workflow() { + local vaccine_github_token="$1" + local vaccine_ref="$2" + local ddtrace_commit_sha="$3" + local trigger_id="${4:-}" - echo "Current status: $STATUS (Checking again in ${POLL_INTERVAL}s)" - sleep $POLL_INTERVAL -done + local vaccine_repo='DataDog/vaccine' + local vaccine_workflow='vaccine.yml' + + log "*** Trigger workflow ${vaccine_workflow} at ${vaccine_repo}@${vaccine_ref} for dd-trace-rb@${ddtrace_commit_sha}" + github_workflow_dispatch "${vaccine_github_token}" "${vaccine_repo}" "${vaccine_ref}" "${vaccine_workflow}" '{"dd-lib-ruby-init-tag": "'"${ddtrace_commit_sha}"'", "trigger-id": "'"${trigger_id}"'"}' | check_status 204 +} + +search_workflow_run() { + local vaccine_github_token="$1" + local vaccine_ref="$2" + local ddtrace_commit_sha="$3" + local trigger_id="${4:-}" + + local vaccine_repo='DataDog/vaccine' + local vaccine_workflow='vaccine.yml' + local name="dd-lib-ruby-init:${ddtrace_commit_sha}${trigger_id+" ${trigger_id}"}" + + log "*** Search runs for workflow ${vaccine_workflow} at ${vaccine_repo}@${vaccine_ref} for dd-trace-rb@${ddtrace_commit_sha}${trigger_id+ "triggered by ${trigger_id}"}" + github_workflow_runs "${vaccine_github_token}" "${vaccine_repo}" "${vaccine_ref}" "${vaccine_workflow}" 'event=workflow_dispatch' 'per_page=10' \ + | check_status 200 \ + | workflow_runs \ + | select_by name "${name}" \ + | useful_run_keys \ + | workflow_run_id \ + | head -1 +} + +workflow_runs() { + jq '.workflow_runs[]' +} + +select_by() { + local key="$1" + local value="$2" + + jq 'select(.'"${key}"'=="'"${value}"'")' +} + +useful_run_keys() { + jq '{id: .id, workflow_id: .workflow_id, event: .event, name: .name, display_title: .display_title, run_number: .run_number, run_attempt: .run_attempt, url: .url, html_url: .html_url, created_at: .created_at, status: .status, conclusion: .conclusion}' +} + +workflow_run_id() { + jq -r '.id' +} + +poll_workflow_run() { + local vaccine_github_token="$1" + local vaccine_poll_interval="$2" + local workflow_run_id="$3" + shift 3 + local cmd=("$@") + + local vaccine_repo='DataDog/vaccine' + + github_workflow_run "${vaccine_github_token}" "${vaccine_repo}" "${workflow_run_id}" \ + | check_status 200 \ + | useful_run_keys \ + | "${cmd[@]}" +} + +workflow_completed() { + jq 'select(.status == "completed")' +} + +workflow_successful() { + jq -e 'select(.status == "completed" and .conclusion == "success")' +} + +main() { + local vaccine_repo + local vaccine_ref + local vaccine_workflow + local vaccine_github_token + local vaccine_poll_interval + local ddtrace_commit_sha + local trigger_id + + vaccine_repo='DataDog/vaccine' + vaccine_ref="${1:-master}" + ddtrace_commit_sha="${2:-"${CI_COMMIT_SHA:-"$(head_commit_sha)"}"}" + trigger_id="${3:-}" + vaccine_poll_interval="10" # seconds + + vaccine_github_token="$(get_vaccine_token)" + secret_add "${vaccine_github_token}" + + # secret_redir_redact + + dispatch_workflow "${vaccine_github_token}" "${vaccine_ref}" "${ddtrace_commit_sha}" "${trigger_id}" + + while true; do + log "*** Waiting ${vaccine_poll_interval}s" + sleep ${vaccine_poll_interval} + + local run_id + run_id="$(search_workflow_run "${vaccine_github_token}" "${vaccine_ref}" "${ddtrace_commit_sha}" "${trigger_id}")" + + if [[ -z "${run_id}" ]]; then + continue + fi + + log "*** Found workflow run id ${run_id}" + log "*** See: https://github.com/${vaccine_repo}/actions/runs/${run_id}" + + while true; do + completed="$(poll_workflow_run "${vaccine_github_token}" "${vaccine_poll_interval}" "${run_id}" workflow_completed)" + + if [[ -n "${completed}" ]]; then + if printf "%s\n" "${completed}" | workflow_successful; then + log "*** Workflow run successful" + log "*** See: https://github.com/${vaccine_repo}/actions/runs/${run_id}" + exit 0 + else + log "*** Workflow run failed" + log "*** WORKFLOW RUN START" + log "${completed}" + log "*** WORKFLOW RUN END" + log "*** See: https://github.com/${vaccine_repo}/actions/runs/${run_id}" + exit 1 + fi + fi + + log "*** Waiting ${vaccine_poll_interval}s to poll again" + sleep "${vaccine_poll_interval}" + done + done + +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi