diff --git a/images/stackrox/sandbox-stackrox-ci/settings.json b/images/stackrox/sandbox-stackrox-ci/settings.json index 3433177..e0e4562 100644 --- a/images/stackrox/sandbox-stackrox-ci/settings.json +++ b/images/stackrox/sandbox-stackrox-ci/settings.json @@ -17,7 +17,6 @@ "Bash(timeout *)", "Bash(while *)", "Read", - "Write(artifacts/*)", "Edit(artifacts/*)", "Glob", "Grep", diff --git a/tasks/acs-ci-nightly/README.md b/tasks/acs-ci-nightly/README.md index e6f6e94..e6ec2e3 100644 --- a/tasks/acs-ci-nightly/README.md +++ b/tasks/acs-ci-nightly/README.md @@ -25,9 +25,6 @@ validated. - Output: `/sandbox/acs-triage-agent/artifacts/ci-triage.json`, downloaded to the caller's output directory. The output is optional so partial diagnostics can still be retained when analysis fails. -- Preflight: the trusted caller may run `workflow/preflight.yaml` in the same - workspace before the main task. It verifies read-only GCS and Jira access - from inside the sandbox without exposing response bodies or credentials. - Cleanup: the sandbox and host-side source staging are removed after outputs are downloaded. Downloaded artifacts and any external reads remain with the caller. diff --git a/tasks/acs-ci-nightly/openshell/README.md b/tasks/acs-ci-nightly/openshell/README.md index afe33c4..bf58f07 100644 --- a/tasks/acs-ci-nightly/openshell/README.md +++ b/tasks/acs-ci-nightly/openshell/README.md @@ -18,6 +18,12 @@ points Google Cloud CLI tools at OpenShell's combined CA bundle so the sandbox verifies the proxy certificate without disabling TLS. The image keeps standalone `gsutil` only as a temporary compatibility fallback. +The task payload sets `BASH_ENV` to a runtime-only shell fragment that maps +the provider-injected `GCP_SA_ACCESS_TOKEN` to gcloud's +`CLOUDSDK_AUTH_ACCESS_TOKEN`. The token value is never interpolated into the +workflow or payload, and gcloud therefore uses the OpenShell-managed +short-lived provider token rather than a local account or service-account key. + The `github_git` policy is intentionally unauthenticated and read-only because the StackRox repositories used by this task are public. The Atlassian and Prow GCS provider instances remain gateway-owned; repository source being public diff --git a/tasks/acs-ci-nightly/workflow/CI-NIGHTLY.md b/tasks/acs-ci-nightly/workflow/CI-NIGHTLY.md index eb68b83..85d535f 100644 --- a/tasks/acs-ci-nightly/workflow/CI-NIGHTLY.md +++ b/tasks/acs-ci-nightly/workflow/CI-NIGHTLY.md @@ -10,8 +10,13 @@ Run only the CI failure analysis portion of the ACS triage agent. repositories only when needed for the analysis. Use unauthenticated HTTPS `git clone` or `git fetch` for these public repositories; do not run `gh auth login` or push to them. -4. Find failures from the last 24 hours in the Prow nightly jobs under - `gs://${GCS_BUCKET:-test-platform-results}/logs/` using `gcloud storage`. +4. Follow the repository's existing `ci-coordinator.md` instructions for the + Prow failure lookup. The trusted task provides a bounded `gsutil` compatibility + command backed by the configured bucket; do not replace it with a recursive + bucket or wildcard listing. Treat + `/tmp/openshell-nightly-failures.tsv` as authoritative; every data row is a + verified `FAILURE` and must be analyzed as such. Do not invent job, build, + or GCS path values. 5. Spawn the repository's CI analysis agents as instructed and wait for their results. 6. Write exactly `/sandbox/acs-triage-agent/artifacts/ci-triage.json` using diff --git a/tasks/acs-ci-nightly/workflow/gsutil b/tasks/acs-ci-nightly/workflow/gsutil new file mode 100755 index 0000000..932b05e --- /dev/null +++ b/tasks/acs-ci-nightly/workflow/gsutil @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Compatibility shim for the existing ACS triage instructions. The public +# bucket is read through bounded GCS JSON/object requests because the image's +# gcloud object reads do not work reliably through the OpenShell proxy. +configured_bucket="${GCS_BUCKET:-test-platform-results-public}" + +rewrite_uri() { + local uri=$1 + local lower_uri + lower_uri=$(printf '%s' "$uri" | tr '[:upper:]' '[:lower:]') + case "$uri" in + gs://test-platform-results|gs://test-platform-results/*) + uri="gs://${configured_bucket}${uri#gs://test-platform-results}" + ;; + "gs://${configured_bucket}"|"gs://${configured_bucket}"/*) ;; + *) + echo "gsutil compatibility wrapper: unsupported bucket in $uri" >&2 + return 2 + ;; + esac + + case "$lower_uri" in + */../*|*/./*|*/..|*/.|*%2e*) + echo "gsutil compatibility wrapper: unsupported path in $uri" >&2 + return 2 + ;; + esac + printf '%s\n' "$uri" +} + +list_nightly_jobs() { + local api="https://storage.googleapis.com/storage/v1/b/${configured_bucket}/o" + local page_token="" + + while :; do + local response + local -a query_args=( + --data-urlencode "prefix=logs/branch-ci-stackrox-stackrox-nightlies-" + --data-urlencode "delimiter=/" + --data-urlencode "maxResults=1000" + ) + if [[ -n "$page_token" ]]; then + query_args+=(--data-urlencode "pageToken=$page_token") + fi + + local -a curl_args=( + --silent --show-error --fail + --connect-timeout 10 --max-time 60 + --get + "${query_args[@]}" + "$api" + ) + response=$(timeout 90s curl "${curl_args[@]}") + + printf '%s' "$response" \ + | jq -r --arg bucket "$configured_bucket" \ + '.prefixes[]? | "gs://" + $bucket + "/" + .' + + page_token=$(printf '%s' "$response" | jq -r '.nextPageToken // empty') + [[ -z "$page_token" ]] && break + done +} + +list_builds() { + local job=$1 + local api="https://storage.googleapis.com/storage/v1/b/${configured_bucket}/o" + local object_prefix="${job#gs://${configured_bucket}/}" + local page_token="" + + while :; do + local response + local -a query_args=( + --data-urlencode "prefix=$object_prefix" + --data-urlencode "delimiter=/" + --data-urlencode "maxResults=1000" + ) + if [[ -n "$page_token" ]]; then + query_args+=(--data-urlencode "pageToken=$page_token") + fi + + local -a curl_args=( + --silent --show-error --fail + --connect-timeout 10 --max-time 60 + --get + "${query_args[@]}" + "$api" + ) + response=$(timeout 90s curl "${curl_args[@]}") + printf '%s' "$response" \ + | jq -r '.prefixes[]? | rtrimstr("/") | split("/") | .[-1] | select(test("^[0-9]+$"))' + + page_token=$(printf '%s' "$response" | jq -r '.nextPageToken // empty') + [[ -z "$page_token" ]] && break + done +} + +list_objects() { + local uri=$1 + local api="https://storage.googleapis.com/storage/v1/b/${configured_bucket}/o" + local object_prefix="${uri#gs://${configured_bucket}/}" + local -a curl_args=( + --silent --show-error --fail + --connect-timeout 10 --max-time 60 + --get + --data-urlencode "prefix=$object_prefix" + --data-urlencode "delimiter=/" + --data-urlencode "maxResults=1000" + "$api" + ) + local response + response=$(timeout 90s curl "${curl_args[@]}") + printf '%s' "$response" \ + | jq -r --arg bucket "$configured_bucket" \ + '(.items[]?.name, .prefixes[]?) | "gs://" + $bucket + "/" + .' +} + +read_object() { + local uri=$1 + local object_path="${uri#gs://${configured_bucket}/}" + timeout 30s curl --silent --show-error --fail \ + --connect-timeout 10 --max-time 15 \ + "https://storage.googleapis.com/${configured_bucket}/${object_path}" +} + +read_finished() { + local gcs_path=$1 + read_object "${gcs_path%/}/finished.json" +} + +collect_failures() { + local cutoff + local jobs + local job + local job_prefix + local finished + local build + local gcs_path + local timestamp + local result + local candidates + cutoff=$(date -u -d '24 hours ago' +%s) + jobs=$(list_nightly_jobs) + printf '# Every data row below is a verified CI failure.\n' + printf '# job\tbuild\tgcs_path\ttimestamp\tresult\n' + + while IFS= read -r job; do + [[ -z "$job" ]] && continue + job_prefix="${job%/}/" + finished="" + build="" + if ! candidates=$(list_builds "$job"); then + echo "gsutil compatibility wrapper: failed to list builds for $job" >&2 + return 1 + fi + while IFS= read -r candidate; do + [[ -z "$candidate" ]] && continue + candidate_path="${job_prefix}${candidate}" + if candidate_finished=$(read_finished "$candidate_path" 2>/dev/null); then + build="$candidate" + gcs_path="$candidate_path" + finished="$candidate_finished" + break + fi + done < <(printf '%s\n' "$candidates" | sort -nr) + [[ -n "$build" && -n "$finished" ]] || continue + timestamp=$(printf '%s' "$finished" | jq -r '.timestamp // 0') + result=$(printf '%s' "$finished" | jq -r '.result // ""') + if [[ "$timestamp" -ge "$cutoff" && "$result" == FAILURE ]]; then + printf '%s\t%s\t%s\t%s\tFAILURE\n' \ + "${job#gs://${configured_bucket}/}" "$build" "$gcs_path" "$timestamp" + fi + done <<<"$jobs" +} + +command=${1:-} +shift || true + +case "$command" in + collect) + collect_failures + ;; + ls) + if [[ $# -ne 1 ]]; then + echo "gsutil compatibility wrapper supports: gsutil ls URI" >&2 + exit 2 + fi + uri=$(rewrite_uri "$1") + if [[ "$uri" == "gs://${configured_bucket}/logs/" ]]; then + list_nightly_jobs + else + list_objects "$uri" + fi + ;; + cat) + if [[ $# -ne 1 ]]; then + echo "gsutil compatibility wrapper supports: gsutil cat URI" >&2 + exit 2 + fi + uri=$(rewrite_uri "$1") + if [[ "$uri" == gs://*/logs/*/latest-build.txt ]]; then + job_prefix="${uri%/latest-build.txt}/" + build=$(list_builds "$job_prefix" | sort -n | tail -n 1) + if [[ -z "$build" ]]; then + echo "gsutil compatibility wrapper: no numeric build for $job_prefix" >&2 + exit 1 + fi + printf '%s\n' "$build" + exit 0 + fi + read_object "$uri" + ;; + *) + echo "gsutil compatibility wrapper supports only ls and cat" >&2 + exit 2 + ;; +esac diff --git a/tasks/acs-ci-nightly/workflow/harness.yaml b/tasks/acs-ci-nightly/workflow/harness.yaml index 4c08206..d95cdd8 100644 --- a/tasks/acs-ci-nightly/workflow/harness.yaml +++ b/tasks/acs-ci-nightly/workflow/harness.yaml @@ -22,6 +22,7 @@ sandbox: READ_ONLY_MODE: "true" TRIAGE_RUN_URL: ${TRIAGE_RUN_URL} GCS_BUCKET: test-platform-results-public + BASH_ENV: /sandbox/.bash_env CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE: /etc/openshell-tls/ca-bundle.pem CURL_CA_BUNDLE: /etc/openshell-tls/ca-bundle.pem @@ -33,6 +34,26 @@ source: payloads: - source: CI-NIGHTLY.md destination: /sandbox/CI-NIGHTLY.md + - source: gsutil + destination: /sandbox/tools/gsutil + - content: | + # OpenShell injects the short-lived provider token; gcloud needs this + # runtime-only compatibility mapping. + export PATH="/sandbox/tools:$PATH" + gsutil() { /bin/bash /sandbox/tools/gsutil "$@"; } + export -f gsutil + if [ -n "$GCP_SA_ACCESS_TOKEN" ]; then + export CLOUDSDK_AUTH_ACCESS_TOKEN="$GCP_SA_ACCESS_TOKEN" + fi + if ! printenv OPENSHELL_GCS_MANIFEST_STARTED >/dev/null 2>&1; then + export OPENSHELL_GCS_MANIFEST_STARTED=1 + /bin/bash /sandbox/tools/gsutil collect \ + >/tmp/openshell-nightly-failures.tsv \ + 2>/tmp/openshell-nightly-failures.err || { + echo "collection failed; see /tmp/openshell-nightly-failures.err" >&2 + } + fi + destination: /sandbox/.bash_env agent: type: claude diff --git a/tasks/acs-ci-nightly/workflow/preflight.yaml b/tasks/acs-ci-nightly/workflow/preflight.yaml deleted file mode 100644 index ea07b6a..0000000 --- a/tasks/acs-ci-nightly/workflow/preflight.yaml +++ /dev/null @@ -1,70 +0,0 @@ -version: 1 -name: acs-preflight - -sandbox: - image: ${ACS_TRIAGE_IMAGE} - policy: - file: ../openshell/policy.yaml - providers: - - atlassian-triage-read - - prow-gcs-read - env: - JIRA_URL: https://redhat.atlassian.net - GCS_BUCKET: test-platform-results-public - CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE: /etc/openshell-tls/ca-bundle.pem - CURL_CA_BUNDLE: /etc/openshell-tls/ca-bundle.pem - -agent: - type: /bin/sh - args: - - -c - - | - set -u - mkdir -p /sandbox/artifacts - log=/sandbox/artifacts/preflight.log - gcs_error=/tmp/preflight-gcs-error - jira_error=/tmp/preflight-jira-error - status=0 - - { - echo "OpenShell ACS preflight" - echo "GCS bucket: $GCS_BUCKET" - echo "Jira URL: $JIRA_URL" - echo - - echo "Jira: GET $JIRA_URL/rest/api/2/myself" - jira_status=$(curl --silent --show-error --output /dev/null \ - --write-out '%{http_code}' --connect-timeout 10 --max-time 30 \ - --user "$JIRA_EMAIL:$JIRA_API_TOKEN" \ - "$JIRA_URL/rest/api/2/myself" 2>"$jira_error") - jira_exit=$? - if [ "$jira_exit" -ne 0 ]; then - echo "Jira: FAIL (curl exit $jira_exit)" - sed -n '1,20p' "$jira_error" - status=1 - elif [ "$jira_status" = 200 ]; then - echo "Jira: PASS (HTTP 200)" - else - echo "Jira: FAIL (HTTP $jira_status)" - status=1 - fi - echo - - echo "GCS: /opt/google-cloud-sdk/bin/gcloud storage ls gs://$GCS_BUCKET" - if timeout 30s /opt/google-cloud-sdk/bin/gcloud storage ls "gs://$GCS_BUCKET" \ - >/dev/null 2>"$gcs_error"; then - echo "GCS: PASS" - else - gcs_status=$? - echo "GCS: FAIL (exit $gcs_status)" - sed -n '1,20p' "$gcs_error" - status=1 - fi - } >"$log" 2>&1 - - cat "$log" - exit "$status" - -outputs: - - source: /sandbox/artifacts/preflight.log - destination: preflight.log