~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
190 lines · 7.8 KB Raw History
#!/usr/bin/env bash
set -euo pipefail

image="gitlab/gitlab-ce:19.2.7-ce.0@sha256:a5d347f51bf2d619375d9c075fd40d38effacbf03615cf145810bda151de9c64"
ready_wait_s=300
runner_tag=socialweb-computer-fixture

usage() {
  cat >&2 <<'EOF'
usage: scripts/gitlab-fixture.sh up [--name NAME] [--address ADDR]
       scripts/gitlab-fixture.sh down [--name NAME]
  up    starts a fresh GitLab CE fixture (pinned image, its own docker volumes, plain http on ADDR and a
        port docker picks), sets a generated root password, creates the project root/fixture-probe from
        deploy/gitlab-fixture/template/ and a project runner tagged socialweb-computer-fixture, and prints
        the fixture's env file (URL, project, runner id, paths of the 0600 secret files; never a secret)
  down  removes the container, its volumes and its state directory; refuses nothing, leaves nothing
  --name     the fixture instance (default default); state in ~/.tmp/gitlab-fixture/<name>
  --address  the address the fixture publishes on, reachable by the runner (default: this host's source
             address of the default route)
EOF
  exit 2
}

[ $# -ge 1 ] || usage
cmd="$1"
shift
name=default
address=""
while [ $# -gt 0 ]; do
  case "$1" in
    --name) name="$2"; shift ;;
    --address) address="$2"; shift ;;
    *) usage ;;
  esac
  shift
done
case "$name" in *[!a-z0-9-]* | "") echo "gitlab-fixture: --name must match [a-z0-9-]+" >&2; exit 2 ;; esac

die() { echo "gitlab-fixture: $*" >&2; exit 1; }
say() { echo "gitlab-fixture: $(date -u +%H:%M:%S) $*" >&2; }

for tool in docker jq curl base64; do
  command -v "$tool" >/dev/null 2>&1 || die "$tool is required"
done

root="$(cd "$(dirname "$0")/.." && pwd)"
template="$root/deploy/gitlab-fixture/template"
label="socialweb.gitlab-fixture=$name"
container="socialweb-gitlab-fixture-$name"
state="$HOME/.tmp/gitlab-fixture/$name"
secrets="$state/secrets"
envfile="$state/fixture.env"

down() {
  local ids vols
  ids="$(docker ps -aq --filter "label=$label")"
  [ -z "$ids" ] || docker rm -f $ids >/dev/null
  vols="$(docker volume ls -q --filter "label=$label")"
  [ -z "$vols" ] || docker volume rm $vols >/dev/null
  rm -rf "$state"
  [ -z "$(docker ps -aq --filter "label=$label")" ] || die "a container labeled $label is still there"
  [ -z "$(docker volume ls -q --filter "label=$label")" ] || die "a volume labeled $label is still there"
  say "fixture $name is down; nothing labeled $label is left and $state is gone"
}

if [ "$cmd" = down ]; then
  down
  exit 0
fi
[ "$cmd" = up ] || usage

[ -z "$(docker ps -aq --filter "label=$label")" ] || die "fixture $name exists; run: scripts/gitlab-fixture.sh down --name $name"
if [ -z "$address" ]; then
  address="$(ip -4 route get 1.1.1.1 | sed -n 's/.* src \([0-9.]*\).*/\1/p')"
  [ -n "$address" ] || die "no default route source address; pass --address"
fi

rm -rf "$state"
mkdir -p "$secrets"
chmod 0700 "$state" "$secrets"
random() { head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | cut -c1-"$1"; }
(umask 077; random 32 >"$secrets/root-password"; printf 'glpat-%s' "$(random 26)" >"$secrets/api-token")

for vol in config logs data; do
  docker volume create --label "$label" "$container-$vol" >/dev/null
done

omnibus="$(printf '%s\n' \
  "f = '/etc/gitlab/fixture_external_url'" \
  "100.times { break if File.exist?(f); sleep 0.1 }" \
  "external_url File.read(f).strip" \
  "nginx['listen_port'] = 80" \
  "nginx['listen_https'] = false" \
  "puma['worker_processes'] = 0" \
  "sidekiq['concurrency'] = 5" \
  "prometheus_monitoring['enable'] = false" \
  "gitlab_kas['enable'] = false" \
  "registry['enable'] = false" \
  "gitlab_rails['usage_ping_enabled'] = false")"

say "starting $container from $image"
docker run -d --name "$container" --label "$label" --hostname "gitlab-fixture-$name" \
  --shm-size 256m --memory 8g \
  -p "$address::80" \
  -v "$container-config:/etc/gitlab" -v "$container-logs:/var/log/gitlab" -v "$container-data:/var/opt/gitlab" \
  -e GITLAB_OMNIBUS_CONFIG="$omnibus" \
  "$image" >/dev/null
port="$(docker port "$container" 80/tcp | awk 'NR==1' | sed 's/.*://')"
[ -n "$port" ] || { down; die "docker published no port for 80/tcp"; }
url="http://$address:$port"
printf '%s\n' "$url" | docker exec -i "$container" sh -c 'cat >/etc/gitlab/fixture_external_url'
say "$container publishes $url; waiting for GitLab (at most ${ready_wait_s}s)"

started="$(date +%s)"
until curl -fsS -o /dev/null --max-time 5 "$url/users/sign_in" 2>/dev/null; do
  if [ "$(docker inspect -f '{{.State.Running}}' "$container")" != true ]; then
    docker logs --tail 40 "$container" >&2 || true
    down
    die "the GitLab container stopped during its first start"
  fi
  if [ $(( $(date +%s) - started )) -ge "$ready_wait_s" ]; then
    docker logs --tail 40 "$container" >&2 || true
    down
    die "GitLab did not answer $url/users/sign_in within ${ready_wait_s}s"
  fi
  sleep 3
done
say "GitLab answers after $(( $(date +%s) - started ))s"

rails='lines = STDIN.read.split("\n")
u = User.find_by_username("root")
u.password = lines[0]
u.password_confirmation = lines[0]
u.password_automatically_set = false
u.save!
t = u.personal_access_tokens.create!(scopes: ["api"], name: "gitlab-fixture", expires_at: 2.days.from_now)
t.set_token(lines[1])
t.save!'
{ cat "$secrets/root-password"; echo; cat "$secrets/api-token"; echo; } |
  timeout -k 5 240 docker exec -i "$container" gitlab-rails runner "$rails" >&2 ||
  { down; die "setting the root password and the API token failed"; }
say "root password and API token set (files in $secrets, mode 0600)"

api_token="$(cat "$secrets/api-token")"
api() {
  local method="$1" path="$2"
  shift 2
  printf 'header = "PRIVATE-TOKEN: %s"\n' "$api_token" |
    curl -sS --max-time 30 --fail-with-body -K - -X "$method" "$url/api/v4$path" "$@"
}

project_id="$(api POST /projects --data-urlencode name=fixture-probe --data-urlencode path=fixture-probe \
  --data-urlencode visibility=private | jq -er .id)"
actions='[]'
while IFS= read -r rel; do
  actions="$(printf '%s' "$actions" | jq -c --arg p "$rel" --rawfile c "$template/$rel" '. + [{action: "create", file_path: $p, content: $c}]')"
done < <(cd "$template" && find . -type f -printf '%P\n' | sort)
commit_body="$(jq -nc --argjson a "$actions" '{branch: "main", commit_message: "fixture template from socialweb-computer-kcp deploy/gitlab-fixture/template", actions: $a}')"
commit_sha="$(api POST "/projects/$project_id/repository/commits" -H 'content-type: application/json' --data "$commit_body" | jq -er .id)"
say "project root/fixture-probe ($project_id) has the template at $commit_sha"

resp="$(api POST /user/runners --data-urlencode runner_type=project_type --data-urlencode "project_id=$project_id" \
  --data-urlencode "tag_list=$runner_tag" --data-urlencode run_untagged=false --data-urlencode locked=true \
  --data-urlencode "description=socialweb-computer fixture runner ($name)")"
runner_id="$(printf '%s' "$resp" | jq -er .id)"
(umask 077; printf '%s' "$resp" | jq -er .token >"$secrets/runner-token")
unset resp

pipeline_id=""
for _ in $(seq 1 20); do
  pipeline_id="$(api GET "/projects/$project_id/pipelines?sha=$commit_sha" | jq -r '.[0].id // empty')"
  [ -n "$pipeline_id" ] && break
  sleep 1
done
[ -n "$pipeline_id" ] || { down; die "the template commit $commit_sha made no pipeline"; }

(umask 077; printf '%s\n' \
  "GITLAB_FIXTURE_NAME=$name" \
  "GITLAB_FIXTURE_URL=$url" \
  "GITLAB_FIXTURE_CONTAINER=$container" \
  "GITLAB_FIXTURE_PROJECT_ID=$project_id" \
  "GITLAB_FIXTURE_PROJECT_PATH=root/fixture-probe" \
  "GITLAB_FIXTURE_PIPELINE_ID=$pipeline_id" \
  "GITLAB_FIXTURE_RUNNER_ID=$runner_id" \
  "GITLAB_FIXTURE_RUNNER_TAG=$runner_tag" \
  "GITLAB_FIXTURE_RUNNER_TOKEN_FILE=$secrets/runner-token" \
  "GITLAB_FIXTURE_API_TOKEN_FILE=$secrets/api-token" \
  "GITLAB_FIXTURE_ROOT_PASSWORD_FILE=$secrets/root-password" >"$envfile")
say "fixture $name is up; pipeline $pipeline_id waits for a runner tagged $runner_tag; env file $envfile"
cat "$envfile"