diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index efa4e4125..9729ebb08 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -1,8 +1,7 @@ $schema: "http://json.schemastore.org/buildkite" env: - # GCP VM image for the Vagrant smoke test. Build it in sourcegraph/infrastructure by running `make e2e` in `gcp/images` + # GCP VM image for the smoke test. Build it in sourcegraph/infrastructure by running `make e2e` in `gcp/images` IMAGE: "e2e-db8a40720-2024-08-26t08-44-10z" - VAGRANT_RUN_ENV: "CI" steps: - label: ":docker:" command: .buildkite/validate-docker-compose.sh @@ -12,10 +11,27 @@ steps: command: .buildkite/prettier-check.sh agents: { queue: "vagrant" } - - label: ":rice: docker-compose-test" - command: .buildkite/vagrant-run.sh docker-test + - label: ":bash:" + command: git ls-files -z '*.sh' .buildkite/hooks | xargs -0 shellcheck + agents: { queue: "vagrant" } + + # Builds on main always run the smoke test. if_changed compares against origin/main, + # so it finds no changes in a commit which is already on main, unless it is the newest + - &smoke_test + label: ":rice: docker-compose-test" + command: .buildkite/smoke-test-vm.sh artifact_paths: ./*.log agents: { queue: "vagrant" } + # smoke-test-vm.sh deletes VMs and disks older than an hour, so no job may run that long + timeout_in_minutes: 30 + if: build.branch == "main" + + # Builds on other branches skip the smoke test if they only change docs + - <<: *smoke_test + if: build.branch != "main" + if_changed: + include: "**" + exclude: "**.md" # Please keep in mind that the release manifest uses specific branch names when creating releases. # Therefore, if you update them, or if you decide to change how we detect what kind of build we're dealing diff --git a/.buildkite/smoke-test-vm.sh b/.buildkite/smoke-test-vm.sh new file mode 100755 index 000000000..0f0884e97 --- /dev/null +++ b/.buildkite/smoke-test-vm.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash + +# Runs test/smoke-test.sh and test/executor-smoke-test.sh on a new GCP VM booted from $IMAGE, +# then deletes the VM + +cd "$(dirname "${BASH_SOURCE[0]}")/.." +set -euo pipefail + +project="sourcegraph-ci" +zone="us-central1-f" + +# GCP creates a disk, then a VM, both with the same name. Naming them after the job +# lets the cleanup find them if the job is cancelled before the VM exists +instance_name_prefix="docker-test-job-" +job_instance_name_prefix="${instance_name_prefix}${BUILDKITE_JOB_ID}-" + +# The builder account key, and an SSH key for this job only +temporary_directory="$(mktemp -d)" + +# Ask GCP to delete the VMs matching a filter, and the matching disks which no VM uses, +# without waiting the 1-2 minutes a deletion takes. GCP deletes a VM's disk with the VM +request_deletion() { + { + gcloud compute instances list --project="$project" --filter="$1" --format="value(selfLink)" && + gcloud compute disks list --project="$project" --filter="($1) AND -users:*" --format="value(selfLink)" + } | tee /dev/stderr | + xargs --no-run-if-empty --max-args=1 curl --fail --silent --show-error --output /dev/null \ + --request DELETE --header "Authorization: Bearer $(gcloud auth print-access-token)" +} + +cleanup() { + request_deletion "name~^${job_instance_name_prefix}" || + echo "Could not delete this job's VMs. GCP deletes each VM an hour after it started" + rm -rf "$temporary_directory" +} +trap cleanup EXIT + +# The VM's guest agent authorizes this key for the buildkite user. +# The keepalive ends the connection to a VM which stopped answering, after a minute +ssh-keygen -q -t ed25519 -N "" -C buildkite -f "$temporary_directory/ssh-key" +ssh_options=(-i "$temporary_directory/ssh-key" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR + -o ConnectTimeout=5 -o ServerAliveInterval=15 -o ServerAliveCountMax=4) + +echo --- ":lock: builder account key" +gcloud secrets versions access latest --secret=e2e-builder-sa-key --quiet --project="$project" >"$temporary_directory/builder-key.json" +DOCKER_PASSWORD="$(gcloud secrets versions access latest --secret=DOCKER_PASSWORD_BUILDKITE_AGENT --quiet --project="$project")" +export CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE="$temporary_directory/builder-key.json" + +echo --- ":wastebasket: deleting VMs and disks left behind by jobs which died before their cleanup" +# The smoke test step times out after 30 minutes, so a VM or disk older than an hour has no job. +# GCP deletes the VMs itself after an hour, but not a disk whose VM was never created +request_deletion "name~^${instance_name_prefix} AND creationTimestamp<-PT1H" || echo "Could not delete stale VMs and disks" + +# The caller checks the exit status of the two functions below, which stops `set -e` +# from applying inside them, so they return on the first failure themselves + +# Sets $remote once the VM exists +start_vm() { + echo "--- :gcloud: starting VM $instance_name" + # `beta`, because --max-run-duration is only there in the agents' gcloud 417. It is in every track from gcloud 483 + ip_address="$(gcloud beta compute instances create "$instance_name" \ + --project="$project" --zone="$zone" \ + --image="$IMAGE" --image-project="$project" \ + --machine-type=custom-16-20480 \ + --boot-disk-size=20GB --boot-disk-type=pd-standard \ + --max-run-duration=1h --instance-termination-action=DELETE \ + --network=default --no-address \ + --service-account="e2e-builder@${project}.iam.gserviceaccount.com" --scopes=compute-rw,storage-ro \ + --labels=team=dev-infra,type=smoke-test \ + --metadata="ssh-keys=buildkite:$(cat "$temporary_directory/ssh-key.pub")" \ + --format="value(networkInterfaces[0].networkIP)")" || return + remote="buildkite@${ip_address}" + + echo --- ":ssh: waiting for the VM to accept SSH" + deadline=$((SECONDS + 300)) + until ssh "${ssh_options[@]}" "$remote" true; do + if ((SECONDS > deadline)); then + echo "The VM did not accept SSH within 5 minutes" + return 1 + fi + sleep 2 + done +} + +run_smoke_tests() { + echo --- ":package: copying the repository to the VM" + tar --create --gzip --exclude=.git --exclude=node_modules . | + ssh "${ssh_options[@]}" "$remote" "sudo mkdir /deploy-sourcegraph-docker && sudo tar --extract --gzip --directory=/deploy-sourcegraph-docker" || + return + + echo --- ":docker: running the smoke tests" + # Runs as root on the VM. The Docker Hub password comes over stdin, so it does not show in the VM's process list + remote_script=' +set -euo pipefail +read -r DOCKER_PASSWORD +export DOCKER_USERNAME=sourcegraph DOCKER_PASSWORD +/deploy-sourcegraph-docker/test/smoke-test.sh +# The Ubuntu 20.04 VM image has no jq. Skip /etc/apt/sources.list.d: that image lacks the key for its Chrome repository, so a full update fails +command -v jq >/dev/null || { apt-get update --quiet --option Dir::Etc::sourceparts=- && apt-get install --quiet --yes jq; } +/deploy-sourcegraph-docker/test/executor-smoke-test.sh +' + # shellcheck disable=SC2029 # remote_script is quoted here, to run on the VM + ssh "${ssh_options[@]}" "$remote" "sudo bash -c $(printf %q "$remote_script")" <<<"$DOCKER_PASSWORD" +} + +for attempt in 1 2; do + instance_name="${job_instance_name_prefix}${attempt}" + remote="" + exit_code=0 + { start_vm && run_smoke_tests; } || exit_code=$? + if [ "$exit_code" = 0 ]; then + break + fi + + # A VM which still answers SSH ran the tests, and they failed. + # Copy back the container logs the smoke test wrote, for the step's artifacts + if [ -n "$remote" ] && ssh -o ConnectTimeout=15 "${ssh_options[@]}" "$remote" true; then + ssh "${ssh_options[@]}" "$remote" "cd /deploy-sourcegraph-docker && sudo tar --create --gzip -- *.log" | tar --extract --gzip || true + break + fi + + # A VM which does not answer SSH did not fail the tests, so run them again on a new VM + echo "VM $instance_name does not answer SSH, in attempt $attempt of 2" + request_deletion "name=${instance_name}" || true +done + +# Not a plain `exit`, after which shellcheck reports that cleanup is never invoked (SC2329) +[ "$exit_code" = 0 ] || exit "$exit_code" diff --git a/.buildkite/vagrant-run.sh b/.buildkite/vagrant-run.sh deleted file mode 100755 index a54aeb97d..000000000 --- a/.buildkite/vagrant-run.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "${BASH_SOURCE[0]}")/.." -set -euo pipefail - -box="$1" -exit_code=0 - -pushd "test" - -cleanup() { - vagrant destroy -f "$box" -} - -echo --- ":vagrant: installing plugins" -vagrant --version -vagrant plugin install vagrant-google --plugin-version '2.7.0' -vagrant plugin install vagrant-env -vagrant plugin install vagrant-scp - -trap cleanup EXIT - -echo --- ":lock: builder account key" -KEY_PATH="/tmp/e2e-builder.json" -if [ ! -f ${KEY_PATH} ]; then - gcloud secrets versions access latest --secret=e2e-builder-sa-key --quiet --project=sourcegraph-ci > "${KEY_PATH}" -fi -export GOOGLE_JSON_KEY_LOCATION="${KEY_PATH}" -export DOCKER_USERNAME=sourcegraph -export DOCKER_PASSWORD="$(gcloud secrets versions access latest --secret=DOCKER_PASSWORD_BUILDKITE_AGENT --quiet --project=sourcegraph-ci)" - -echo --- ":vagrant: starting box $box" -vagrant up "$box" --provider=google || exit_code=$? - -if [ "$exit_code" != 0 ]; then - vagrant scp "${box}:/deploy-sourcegraph-docker/*.log" ../ - exit $exit_code -fi diff --git a/.buildkite/validate-docker-compose.sh b/.buildkite/validate-docker-compose.sh index 3c7bac418..8a716a3d7 100755 --- a/.buildkite/validate-docker-compose.sh +++ b/.buildkite/validate-docker-compose.sh @@ -1,7 +1,19 @@ #!/usr/bin/env bash -cd "$(dirname "${BASH_SOURCE[0]}")/.." +cd "$(dirname "${BASH_SOURCE[0]}")/../docker-compose" set -euxo pipefail -cd docker-compose -docker-compose -f docker-compose.yaml config -q +docker compose version + +# Every Compose file, combined the way its docs say to run it +docker compose --file docker-compose.yaml config --quiet +docker compose --file db-only-migrate.docker-compose.yaml config --quiet +docker compose --file docker-compose.yaml --file jaeger/docker-compose.yaml config --quiet +docker compose --file docker-compose.yaml --file jaeger/docker-compose.yaml --file dev/docker-compose.yaml config --quiet +docker compose --file docker-compose.yaml --file executors/executor.docker-compose.yaml config --quiet +docker compose --file executors/executor.docker-compose.yaml config --quiet +# The combination the smoke test deploys +docker compose --file docker-compose.yaml --file jaeger/docker-compose.yaml --file executors/executor.docker-compose.yaml --file ../test/docker-compose.override.yaml config --quiet +for override in examples/*/docker-compose.override.yaml; do + docker compose --file docker-compose.yaml --file "$override" config --quiet +done diff --git a/.gitignore b/.gitignore index 9cf3cdc72..3ecd4b8fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .env .envrc -.vagrant *.log node_modules/ diff --git a/.tool-versions b/.tool-versions index 5a9e0f50f..d90ecf90e 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,4 @@ nodejs 16.20.2 yarn 1.22.4 github-cli 2.103.3 +shellcheck 0.11.0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c08789561 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# AGENTS.md + +This repository is the deployment reference for [Sourcegraph with Docker Compose](https://sourcegraph.com/docs/self-hosted/deploy/docker-compose). Customers run `docker-compose/docker-compose.yaml` at a release tag: they check the tag out in a clone of this repository, or merge it into their own fork. We recommend they keep their changes in a `docker-compose.override.yaml` file. The repository holds configuration and a few shell scripts, no application code. + +## Layout + +- `docker-compose/docker-compose.yaml`: the base file every customer runs. It mounts the service configuration in `caddy/`, `pgsql/`, `codeintel-db/`, `codeinsights-db/`, `grafana/`, `prometheus/`, and `otel-collector/`. +- `docker-compose/db-only-migrate.docker-compose.yaml`: starts only the `pgsql` and `codeintel-db` services, for backup and restore. It `extends` both from the base file, so change them there. +- `docker-compose/examples/`: example override files. `docker-compose/executors/` and `docker-compose/jaeger/` are optional extra compose files. `docker-compose/dev/` is an overlay for developing this repository. +- `test/` and `.buildkite/`: the smoke tests and the CI pipeline. +- `release.yaml`: the release steps that `sg release` runs. + +## Checks + +- `.tool-versions` pins Node, Yarn, the GitHub CLI, and shellcheck; CI installs them with mise. +- `.buildkite/validate-docker-compose.sh` runs `docker compose config` on every compose file, in each combination the docs say to run. Add a line to it for every new compose file or combination. +- Check formatting with `yarn && yarn run prettier-check`; `yarn run prettier` rewrites the files. Prettier covers only JavaScript and JSON files. `.prettierignore` excludes Markdown and `.github/`, and no formatter runs on YAML, so match the surrounding YAML style by hand. +- CI runs shellcheck on every shell script: `git ls-files -z '*.sh' .buildkite/hooks | xargs -0 shellcheck`. +- The `docker-compose-test` step runs `.buildkite/smoke-test-vm.sh`, which boots a GCP VM from the image named in `IMAGE` in `.buildkite/pipeline.yaml`, runs the two tests below on it, and deletes it. The step reads CI secrets and creates a billed VM, so take its result from the pull request's Buildkite build; run the script yourself only when a person asks. Branches which only change Markdown files skip the step. + - `test/smoke-test.sh` starts the base file, `jaeger/`, `executors/`, and `test/docker-compose.override.yaml`, then checks that every service has a running container, no container restarted, the health checks pass, and the frontend and Jaeger respond. It counts the services itself, so a new service in those files needs no change to the test. Add a new optional compose file to its `compose_files`. + - `test/executor-smoke-test.sh` checks that the executor registers with the frontend. +- If the VM stops answering SSH, the script runs the tests once more on a new VM. A test failure is not retried, and the step uploads every container's logs as artifacts. +- GCP deletes each VM an hour after it starts, and the script deletes the VMs and disks which older jobs left behind. Keep the step's `timeout_in_minutes` under an hour. +- The VM image is built from `gcp/images/e2e` in sourcegraph/infrastructure. The release branches name their image too, so leave old images in place. +- The CI agents' queue is still named `vagrant`. Release branches up to `8.1.x` run the smoke test with Vagrant (`.buildkite/vagrant-run.sh`), so a CI change on `main` does not backport cleanly. + +## Conventions + +- Customer override files refer to the base file's service names, volume names, published ports, and environment variable names. Describe every rename or removal of one in the pull request description. +- Every `image:` has the form `index.docker.io/sourcegraph/:@sha256:`. The release process sets the versions and digests (`sg ops update-images` in `release.yaml`) and brings them to `main` in "Update main: build v…" pull requests. Leave them unchanged; Renovate is turned off for these images for the same reason. +- `.github/PULL_REQUEST_TEMPLATE.md` requires a "Test plan" section in every pull request description. + +## Branches and releases + +- `main` tracks development. Each release is a tag such as `v8.1.0`. +- To copy a pull request to a release branch such as `8.1.x`, add the label `backport 8.1.x` to it. `.github/workflows/backport.yml` opens the backport pull request once the original merges. +- The release tooling owns the branches named `internal/release-*`, `promote/release-*`, and `release-v*`, and the `v*` tags. Do not create, push to, or delete them: CI runs the release steps on a branch whose name starts with `internal/release-` or `promote/release-`, and each `release-v*` branch holds the commit of a tag that customers deploy. diff --git a/batch-change/release.yaml b/batch-change/release.yaml index 2534f0a07..66cbb53ac 100644 --- a/batch-change/release.yaml +++ b/batch-change/release.yaml @@ -138,7 +138,16 @@ promoteToPublic: git checkout "${promote_branch}" git switch -c "${release_branch}" - git tag {{version}} + # A retried finalize finds its own tag; only a tag elsewhere is an error + if tagged=$(git rev-parse -q --verify "refs/tags/{{version}}^{commit}"); then + if [ "${tagged}" != "$(git rev-parse HEAD)" ]; then + echo "Tag {{version}} already exists at ${tagged}, not at ${promote_branch} ($(git rev-parse HEAD))" >&2 + exit 1 + fi + echo "Tag {{version}} already points at ${promote_branch}, not recreating it" + else + git tag {{version}} + fi git push origin ${release_branch} --tags # Web URL to the tag diff --git a/docker-compose/db-only-migrate.docker-compose.yaml b/docker-compose/db-only-migrate.docker-compose.yaml index 152de093d..6302de6e2 100644 --- a/docker-compose/db-only-migrate.docker-compose.yaml +++ b/docker-compose/db-only-migrate.docker-compose.yaml @@ -1,54 +1,20 @@ -# This file contains the pgsql service definition (the primary Sourcegraph database deployment), -# as well as the codeintel-db service definition (a separate postgres deployment). Both run postgres. -# This file is used for database-only operations such as backup and restore. +# This file starts only the pgsql service (the primary Sourcegraph database), +# and the codeintel-db service (a separate Postgres deployment). +# It is used for database-only operations such as backup and restore. # See https://sourcegraph.com/docs/self-hosted/deploy/docker-compose/operations # -# 🚨This file MUST be kept in sync with the pgsql definition in docker-compose/docker-compose.yaml +# Both services are defined in docker-compose.yaml, so this file cannot drift from it version: '2.4' services: - # Description: PostgreSQL database for various data. - # - # Disk: 128GB / persistent SSD - # Ports exposed to other Sourcegraph services: 5432/TCP 9187/TCP - # Ports exposed to the public internet: none - # pgsql: - container_name: pgsql - image: 'index.docker.io/sourcegraph/postgresql-16:8.1.4690@sha256:1bd9b7314bdb83d12162c20e1c8da580ca27c43a4f391404a30339faeefa033b' - cpus: 4 - mem_limit: '2g' - security_opt: - - no-new-privileges:true - healthcheck: - test: '/liveness.sh' - interval: 10s - timeout: 1s - retries: 3 - start_period: 15s - volumes: - - 'pgsql:/data/' - networks: - - sourcegraph - restart: always + extends: + file: docker-compose.yaml + service: pgsql codeintel-db: - container_name: codeintel-db - image: 'index.docker.io/sourcegraph/postgresql-16:8.1.4690@sha256:1bd9b7314bdb83d12162c20e1c8da580ca27c43a4f391404a30339faeefa033b' - cpus: 4 - mem_limit: '2g' - security_opt: - - no-new-privileges:true - healthcheck: - test: '/liveness.sh' - interval: 10s - timeout: 1s - retries: 3 - start_period: 15s - volumes: - - 'codeintel-db:/data/' - networks: - - sourcegraph - restart: always + extends: + file: docker-compose.yaml + service: codeintel-db volumes: pgsql: diff --git a/docker-compose/dev/run.sh b/docker-compose/dev/run.sh index f5c3881b4..e7b85ff19 100755 --- a/docker-compose/dev/run.sh +++ b/docker-compose/dev/run.sh @@ -3,7 +3,7 @@ cd "$(dirname "${BASH_SOURCE[0]}")/../.." set -euxo pipefail -docker-compose \ +docker compose \ -f docker-compose/docker-compose.yaml \ -f docker-compose/jaeger/docker-compose.yaml \ -f docker-compose/dev/docker-compose.yaml \ diff --git a/docker-compose/executors/executor.docker-compose.yaml b/docker-compose/executors/executor.docker-compose.yaml index cf5dadeba..87e43dcc3 100644 --- a/docker-compose/executors/executor.docker-compose.yaml +++ b/docker-compose/executors/executor.docker-compose.yaml @@ -31,3 +31,6 @@ services: networks: - sourcegraph restart: always + +networks: + sourcegraph: diff --git a/test/Vagrantfile b/test/Vagrantfile deleted file mode 100644 index 09bd66030..000000000 --- a/test/Vagrantfile +++ /dev/null @@ -1,74 +0,0 @@ -# -*- mode: ruby -*- -# # vi: set ft=ruby : -# Specify minimum Vagrant version and Vagrant API version -Vagrant.require_version '>= 1.6.0' -VAGRANTFILE_API_VERSION = '2'.freeze -# Require YAML module -require 'yaml' - -# Read YAML file with box details -servers = YAML.load_file('servers.yaml') - -# Create boxes -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - # Iterate through entries in YAML file - servers.each do |server| - config.env.enable - config.vm.define server['name'] do |srv| - srv.vm.box = server['box'] - srv.vm.synced_folder '../', '/deploy-sourcegraph-docker' - srv.vm.boot_timeout = 600 - - # Setup for CI or Local - case ENV['VAGRANT_RUN_ENV'] - when 'CI' - project_id = server['project_id'] - external_ip = server['external_ip'] - use_private_ip = server['use_private_ip'] - username = server['username'] - ssh_key_path = server['ssh_key_path'] - else - project_id = ENV['VAGRANT_GCP_PROJECT_ID'] || 'sourcegraph-server' - external_ip = nil - use_private_ip = false - username = ENV['VAGRANT_SSH_USER'] || ENV['USER'] - ssh_key_path = ENV['VAGRANT_SSH_KEY'] || '~/.ssh/id_rsa' - end - - srv.vm.provider :google do |g, o| - g.machine_type = server['machine_type'] - g.image = ENV['IMAGE'] - g.image_project_id = 'sourcegraph-ci' - g.google_project_id = project_id - g.name = "#{server['name']}-"+"#{Time.now.to_i}"+"#{rand(100000)}" - g.network = server['network'] - g.external_ip = external_ip - g.use_private_ip = use_private_ip - g.service_account = server['service_account'] - g.labels = { - "team" => "dev-infra", - "type" => "vagrant" - } - g.scopes = [ - "compute-rw", - "storage-ro" - ] - o.ssh.username = username - o.ssh.private_key_path = ssh_key_path - end - - srv.vm.provision 'shell', inline: <<-SHELL - #!/usr/bin/env bash - cat << EOF >> /root/.profile -export GIT_BRANCH=#{ENV['BUILDKITE_BRANCH']} -export DOCKER_USERNAME=#{ENV['DOCKER_USERNAME']} -export DOCKER_PASSWORD=#{ENV['DOCKER_PASSWORD']} -EOF - SHELL - - server['shell_commands'].each do |sh| - srv.vm.provision 'shell', inline: sh - end - end - end -end diff --git a/test/docker-compose.override.yaml b/test/docker-compose.override.yaml new file mode 100644 index 000000000..2cc86799c --- /dev/null +++ b/test/docker-compose.override.yaml @@ -0,0 +1,8 @@ +version: '2.4' +services: + # The executor exits unless both are set. + # executor-smoke-test.sh sets this password as `executors.accessToken` in the site config + executor: + environment: + - EXECUTOR_FRONTEND_PASSWORD=executor-smoke-test-token + - EXECUTOR_QUEUE_NAME=batches diff --git a/test/executor-smoke-test.sh b/test/executor-smoke-test.sh index f92e1b474..052c829d1 100755 --- a/test/executor-smoke-test.sh +++ b/test/executor-smoke-test.sh @@ -3,18 +3,18 @@ set -euo pipefail # Executor smoke test for deploy-sourcegraph-docker # -# Requires: a running Sourcegraph docker-compose stack (from smoke-test.sh or manual start). -# This script creates a site admin account, configures executor access, starts an executor -# container, and verifies it registers with the Sourcegraph instance. +# Requires: a running Sourcegraph docker-compose stack with an executor container +# (from smoke-test.sh or manual start). +# This script creates a site admin account, configures executor access, and verifies +# the executor registers with the Sourcegraph instance. # -# Dependencies: curl, jq, docker compose +# Dependencies: curl, jq, docker FRONTEND_URL="${FRONTEND_URL:-http://localhost:80}" -EXECUTOR_TOKEN="executor-smoke-test-token" +EXECUTOR_TOKEN=$(docker exec executor printenv EXECUTOR_FRONTEND_PASSWORD) ADMIN_EMAIL="smoke-test@sourcegraph.com" ADMIN_USERNAME="smoke-admin" ADMIN_PASSWORD="smoke-test-password-123!" -COMPOSE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../docker-compose" && pwd)" echo "=== Executor Smoke Test ===" @@ -74,19 +74,8 @@ curl -s -o /dev/null -X POST "${FRONTEND_URL}/.api/graphql" \ --data-raw "{\"query\":\"mutation UpdateSiteConfig(\$input: String!) { updateSiteConfiguration(lastID: $LAST_ID, input: \$input) }\", \"variables\":{\"input\":$NEW_CONFIG}}" echo " Site config updated" -# Step 5: Start executor container -echo "Step 5: Starting executor container..." -EXECUTOR_CONTAINER=$(docker compose \ - -f "${COMPOSE_DIR}/docker-compose.yaml" \ - -f "${COMPOSE_DIR}/executors/executor.docker-compose.yaml" \ - run -d \ - -e "EXECUTOR_FRONTEND_PASSWORD=${EXECUTOR_TOKEN}" \ - -e EXECUTOR_QUEUE_NAME=batches \ - executor 2>&1 | tail -1) -echo " Executor container started: ${EXECUTOR_CONTAINER:0:12}" - -# Step 6: Wait for executor to register -echo "Step 6: Waiting for executor to register..." +# Step 5: Wait for executor to register +echo "Step 5: Waiting for executor to register..." MAX_ATTEMPTS=24 for i in $(seq 1 $MAX_ATTEMPTS); do COUNT=$(curl -s -X POST "${FRONTEND_URL}/.api/graphql" \ @@ -110,8 +99,8 @@ echo "=== EXECUTOR SMOKE TEST FAILED ===" echo "Executor did not register within $((MAX_ATTEMPTS * 5))s" echo "" echo "Container status:" -docker inspect "$EXECUTOR_CONTAINER" --format '{{.State.Status}} (exit code: {{.State.ExitCode}})' 2>/dev/null || echo " Container not found" +docker inspect executor --format '{{.State.Status}} (exit code: {{.State.ExitCode}})' 2>/dev/null || echo " Container not found" echo "" echo "Container logs:" -docker logs "$EXECUTOR_CONTAINER" 2>&1 | tail -20 || echo " No logs available" +docker logs executor 2>&1 | tail -20 || echo " No logs available" exit 1 diff --git a/test/servers.yaml b/test/servers.yaml deleted file mode 100644 index 36780dbe7..000000000 --- a/test/servers.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -- name: docker-test - box: google/gce - machine_type: "custom-16-20480" - project_id: sourcegraph-ci - external_ip: false - use_private_ip: true - network: default - username: buildkite - ssh_key_path: "~/.ssh/id_rsa" - service_account: e2e-builder@sourcegraph-ci.iam.gserviceaccount.com - shell_commands: - - "/deploy-sourcegraph-docker/test/smoke-test.sh" - diff --git a/test/smoke-test.sh b/test/smoke-test.sh index 5aac87dca..2c8255cdf 100755 --- a/test/smoke-test.sh +++ b/test/smoke-test.sh @@ -3,7 +3,7 @@ set -euxfo pipefail configure_docker() { set +x - if [ -n "${DOCKER_USERNAME}" ] && [ -n "${DOCKER_PASSWORD}" ]; then + if [ -n "${DOCKER_USERNAME:-}" ] && [ -n "${DOCKER_PASSWORD:-}" ]; then docker login -u "${DOCKER_USERNAME}" --password-stdin <<<"$DOCKER_PASSWORD" fi set -x @@ -11,64 +11,86 @@ configure_docker() { gcloud auth configure-docker us-central1-docker.pkg.dev,us-docker.pkg.dev } -expect_containers="25" +# Every service a customer can deploy, started and checked by the same commands +compose_files=( + --file docker-compose/docker-compose.yaml + --file docker-compose/jaeger/docker-compose.yaml + --file docker-compose/executors/executor.docker-compose.yaml + --file test/docker-compose.override.yaml +) deploy_sourcegraph() { - cd $(dirname "${BASH_SOURCE[0]}")/.. - docker-compose --file docker-compose/docker-compose.yaml up -d -t 600 + cd "$(dirname "${BASH_SOURCE[0]}")/.." + docker compose version + # `up` pulls images one at a time, `pull` pulls them in parallel. + # Docker Hub times out now and then; `up` pulls whatever is still missing after three attempts + for _ in 1 2 3; do + docker compose "${compose_files[@]}" pull --quiet && break + sleep 10 + done + docker compose "${compose_files[@]}" up -d -t 600 +} - echo "Giving containers 90s to start..." - sleep 90 +wait_until_healthy() { + echo "Waiting up to 5 minutes for the frontend to respond, and container health checks to pass..." + for _ in {1..60}; do + if curl --fail --silent --output /dev/null http://localhost:80/healthz && + [[ -z "$(docker ps --quiet --filter health=starting --filter health=unhealthy)" ]]; then + return + fi + sleep 5 + done + docker ps --format '{{.Names}}: {{.Status}}' + echo "TEST FAILURE: containers were not healthy after 5 minutes" + exit 1 } test_count() { - echo "TEST: Number of expected containers created" - containers_count=$(docker ps --format '{{.Names}}' | wc -l) + echo "TEST: Every service has a running container" + # migrator exits when it is done, every other service keeps running + expect_containers=$(docker compose "${compose_files[@]}" config --services | grep --count --invert-match '^migrator$') + containers_count=$(docker ps --filter status=running --format '{{.Names}}' | wc -l) if [[ "$containers_count" -ne "$expect_containers" ]]; then - docker ps --format '{{.Names}}' + docker ps --all --format '{{.Names}}: {{.Status}}' echo - echo "TEST FAILURE: expected $expect_containers containers, found $containers_count" + echo "TEST FAILURE: expected $expect_containers running containers, found $containers_count" exit 1 fi } test_containers() { - echo "TEST: Checking every 10s that containers are running for 5 minutes..." - for i in {0..1}; do - containers=$(docker ps --format '{{.Names}}' | xargs -I{} -n1 sh -c "printf '{}: ' && docker inspect --format '{{.State.Status}}' {}") - containers_running=$(echo "$containers" | grep -c "running") - if [[ "$containers_running" -ne "$expect_containers" ]]; then - containers_failing=$(docker ps --format '{{.Names}}:{{.Status}}' | grep -v Up | cut -f 1 -d :) - echo "TEST FAILURE: expected $expect_containers containers running, found $containers_running. The following containers are failing: $containers_failing" - exit 1 - fi - echo "Containers running OK.. waiting 10s" - sleep 1 - done + echo "TEST: No container has restarted" + restarted_containers=$(docker ps --quiet | xargs docker inspect --format '{{.Name}} restarted {{.RestartCount}} times' | grep -v ' restarted 0 times' || true) + if [[ -n "$restarted_containers" ]]; then + echo "TEST FAILURE: $restarted_containers" + exit 1 + fi echo "TEST: Checking frontend is accessible" curl -f http://localhost:80 curl -f http://localhost:80/healthz + echo "TEST: Checking Jaeger is accessible" + curl --fail --silent --show-error --location --output /dev/null http://localhost:16686/-/debug/jaeger/ + echo "ALL TESTS PASSED" } -catch_errors() { - count=$(docker ps --format '{{.Names}}:{{.Status}}' | grep -c -v Up) || true - if [[ $count -ne 0 ]]; then - containers_failing=$(docker ps --format '{{.Names}}:{{.Status}}' | grep -v Up | cut -f 1 -d :) - echo - for cf in $containers_failing; do - echo "$cf is failing. Review the log files uploaded as artefacts to see errors." - docker logs -t "$cf" >"$cf".log 2>&1 +# On failure, save every container's logs, which the CI step uploads as artifacts +save_logs_on_failure() { + exit_code=$? + if [[ $exit_code -ne 0 ]]; then + docker ps --all --format '{{.Names}}: {{.Status}}' + for container in $(docker ps --all --format '{{.Names}}'); do + docker logs --timestamps "$container" >"$container.log" 2>&1 || true done - exit 1 fi } -trap catch_errors EXIT +trap save_logs_on_failure EXIT configure_docker deploy_sourcegraph +wait_until_healthy test_count test_containers