#!/bin/sh set -u usage() { echo "build.sh: Builds a docker image. Requires docker and git" echo "Usage: ./build.sh [OPTIONS]" echo 'Options:' echo " -f|--env-file Path to env file. Default: ./.env" echo " -h|--help Help" echo "Environment variables:" echo " PIPELINE Build pipeline" echo " Type: string" echo " Possible values: 'build', 'update'" echo " GAME_UPDATE_COUNT Game update count (docker label). Applies only to PIPELINE=update" echo " Type: int" echo " GAME_VERSION Game version. For example, to get the latest CS:GO version:" echo " wget -qO- 'https://api.steampowered.com/ISteamApps/UpToDateCheck/v1?appid=730&version=0' | jq '.response.required_version'" echo " Type: int" echo " APPID AppID. See: https://developer.valvesoftware.com/wiki/Steam_Application_IDs" echo " Type: int" echo " CLIENT_APPID Client AppID. See: https://developer.valvesoftware.com/wiki/Steam_Application_IDs" echo " Type: int" echo " GAME Game" echo " Type: string" echo " MOD Mod. Needed when APPID=90, except for APPID=90 GAME=valve" echo " Type: string, optional" echo " FIX_APPMANIFEST Whether to apply appmanifest fixes on builds. Applies only to PIPELINE=build and APPID=90" echo " Type: bool, optional" echo " Possible values: 'true', 'false'" echo " INSTALL_COUNT Number of install attempts" echo " Type: int, optional" echo " LATEST Whether to tag the successfully built game image with the :latest tag. Applies only to PIPELINE=build" echo " Type: bool, optional" echo " Possible values: 'true', 'false'" echo " CACHE Whether to pull the game image by the :latest tag before build" echo " Type: bool, optional" echo " Possible values: 'true', 'false'" echo " NO_PULL Whether to skip pulling the game image by the :latest image before build. Applies only to PIPELINE=update" echo " Type: bool, optional" echo " Possible values: 'true', 'false'" echo " NO_CACHE Whether to use --no-cache for docker build" echo " Type: bool, optional" echo " Possible values: 'true', 'false'" echo " NO_TEST Whether to skip testing of the successfully built game image" echo " Type: bool, optional" echo " Possible values: 'true', 'false'" echo " NO_PUSH Whether to skip pushing of the successfully built game image" echo " Type: bool, optional" echo " Possible values: 'true', 'false'" echo " DOCKER_REPOSITORY Docker repository" echo " Type: string" echo " REGISTRY_USER Docker registry user" echo " Type: string" echo " REGISTRY_PASSWORD Docker registry password" echo " Type: string" echo " STEAM_LOGIN Whether to logon to Steam so that games download without rate limiting. Generally required for APPID=90" echo " Type: bool, optional" echo " STEAM_USERNAME Steam username" echo " Type: string" echo " STEAM_PASSWORD Steam password" echo " Type: string" echo "Examples:" echo " # Build hlds/cstrike image and push" echo " PIPELINE=build GAME_VERSION=1127 APPID=90 CLIENT_APPID=10 GAME=cstrike MOD=cstrike LATEST=true STEAM_LOGIN=true DOCKER_REPOSITORY=docker.io/mynamespace/cstrike REGISTRY_USER=xxx REGISTRY_PASSWORD=xxx STEAM_USERNAME=xxx STEAM_PASSWORD=xxx ./build.sh" echo echo " # Update hlds/cstrike image and push" echo " PIPELINE=update GAME_UPDATE_COUNT=1 GAME_VERSION=1128 APPID=90 CLIENT_APPID=10 GAME=cstrike MOD=cstrike STEAM_LOGIN=true DOCKER_REPOSITORY=docker.io/mynamespace/cstrike REGISTRY_USER=xxx REGISTRY_PASSWORD=xxx STEAM_USERNAME=xxx STEAM_PASSWORD=xxx ./build.sh" echo echo " # Build hlds/valve image and push" echo " PIPELINE=build GAME_VERSION=1122 APPID=90 CLIENT_APPID=10 GAME=valve MOD= LATEST=true STEAM_LOGIN=true DOCKER_REPOSITORY=docker.io/mynamespace/valve REGISTRY_USER=xxx REGISTRY_PASSWORD=xxx STEAM_USERNAME=xxx STEAM_PASSWORD=xxx ./build.sh" echo echo " # Update hlds/valve image and push" echo " PIPELINE=update GAME_UPDATE_COUNT=1 GAME_VERSION=1123 APPID=90 CLIENT_APPID=10 GAME=valve MOD= STEAM_LOGIN=true DOCKER_REPOSITORY=docker.io/mynamespace/valve REGISTRY_USER=xxx REGISTRY_PASSWORD=xxx STEAM_USERNAME=xxx STEAM_PASSWORD=xxx ./build.sh" echo echo " # Build srcds/csgo image and push" echo " PIPELINE=build GAME_VERSION=13840 APPID=740 CLIENT_APPID=730 GAME=csgo LATEST=true DOCKER_REPOSITORY=docker.io/mynamespace/csgo REGISTRY_USER=xxx REGISTRY_PASSWORD=xxx ./build.sh" echo echo " # Update srcds/csgo image and push" echo " PIPELINE=update GAME_UPDATE_COUNT=1 GAME_VERSION=13841 APPID=740 CLIENT_APPID=730 GAME=csgo DOCKER_REPOSITORY=docker.io/mynamespace/csgo REGISTRY_USER=xxx REGISTRY_PASSWORD=xxx ./build.sh" echo echo " # Build using an .env file in the same directory as build.sh" echo " ./build.sh" echo echo " # Build using a custom .env file in the same directory as build.sh" echo " ./build.sh --env-file path/to/.env" echo } # Get some options while test $# -gt 0; do case "$1" in -f|--env-file) shift ENV_FILE=$1 shift ;; -h|--help) usage exit 0 ;; *) echo "Invalid option '$1'" 1>&2 usage exit 1 ;; esac done ENV_FILE=${ENV_FILE:-.env} if [ -f "$ENV_FILE" ]; then ENV_FILE="$( cd "$( dirname "$ENV_FILE" )" && pwd )/$( basename "$ENV_FILE" )" echo "Reading env file $ENV_FILE" . "$ENV_FILE" else if [ "$ENV_FILE" = '.env' ]; then echo "Not reading from .env because it does not exist" else echo "env file does not exist: $ENV_FILE" exit 1 fi fi # Process job variables PIPELINE=${PIPELINE:?err} if [ "$PIPELINE" = 'build' ]; then GAME_VERSION=${GAME_VERSION:?err} APPID=${APPID:?err} CLIENT_APPID=${CLIENT_APPID:?err} GAME=${GAME:?err} MOD=${MOD:-} FIX_APPMANIFEST=${FIX_APPMANIFEST:-} INSTALL_COUNT=${INSTALL_COUNT:-} LATEST=${LATEST:-} CACHE=${CACHE:-} NO_CACHE=${NO_CACHE:-false} NO_TEST=${NO_TEST:-} NO_PUSH=${NO_PUSH:-} STEAM_LOGIN=${STEAM_LOGIN:-} elif [ "$PIPELINE" = 'update' ]; then GAME_VERSION=${GAME_VERSION:?err} APPID=${APPID:?err} GAME=${GAME:?err} GAME_UPDATE_COUNT=${GAME_UPDATE_COUNT:?err} INSTALL_COUNT=${INSTALL_COUNT:-} NO_PULL=${NO_PULL:-} NO_CACHE=${NO_CACHE:-false} NO_TEST=${NO_TEST:-} NO_PUSH=${NO_PUSH:-} STEAM_LOGIN=${STEAM_LOGIN:-} else echo "Invalid PIPELINE '$PIPELINE'" exit 1 fi # Process user variables DOCKER_REPOSITORY=${DOCKER_REPOSITORY:-} if [ ! "$NO_PUSH" = 'true' ]; then REGISTRY_USER=${REGISTRY_USER:?err} REGISTRY_PASSWORD=${REGISTRY_PASSWORD:?err} else REGISTRY_USER=${REGISTRY_USER:-} REGISTRY_PASSWORD=${REGISTRY_PASSWORD:-} fi if [ "$STEAM_LOGIN" = 'true' ]; then STEAM_USERNAME=${STEAM_USERNAME:?err} STEAM_PASSWORD=${STEAM_PASSWORD:?err} else STEAM_USERNAME=${STEAM_USERNAME:-} STEAM_PASSWORD=${STEAM_PASSWORD:-} fi # Process default job variables export DOCKER_BUILDKIT=1 if [ "$APPID" = 90 ]; then DOCKER_REPOSITORY="${DOCKER_REPOSITORY:-${REGISTRY_GOLDSOURCE:?err}/$GAME}" GAME_ENGINE='hlds' GAME_BIN='hlds_linux' else DOCKER_REPOSITORY="${DOCKER_REPOSITORY:-${REGISTRY_SOURCE:?err}/$GAME}" GAME_ENGINE='srcds' # srcds/cs2 if [ "$APPID" = 730 ]; then GAME_BIN='game/bin/linuxsteamrt64/cs2' else GAME_BIN='srcds_linux' fi fi if [ "$PIPELINE" = 'build' ]; then GAME_IMAGE_CLEAN="$DOCKER_REPOSITORY:$GAME_VERSION" BUILD_CONTEXT='build/' elif [ "$PIPELINE" = 'update' ]; then GAME_IMAGE_LAYERED="$DOCKER_REPOSITORY:$GAME_VERSION-layered" GAME_IMAGE_LAYERED_CALVER="$GAME_IMAGE_LAYERED-$( date -u '+%Y%m%d' )" BUILD_CONTEXT='update/' fi GAME_IMAGE_LATEST="$DOCKER_REPOSITORY:latest" COMMIT_SHA=$( git rev-parse HEAD ) date -Iseconds # Display docker env vars echo "DOCKER_HOST: ${DOCKER_HOST:-}" echo "DOCKER_BUILDKIT: ${DOCKER_BUILDKIT:-}" # Display pipeline echo "PIPELINE: $PIPELINE" # Display system info hostname whoami cat /etc/*release lscpu free df -h pwd docker info docker version # Terminate the build on errors set -e # Docker registry login if [ ! "$NO_PUSH" = 'true' ]; then echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin fi # Build / Update the game image if [ "$PIPELINE" = 'build' ]; then if [ "$CACHE" = 'true' ]; then date -Iseconds time docker pull "$GAME_IMAGE_CLEAN" || true fi date -Iseconds STEAM_USERNAME="$STEAM_USERNAME" STEAM_PASSWORD="$STEAM_PASSWORD" time docker build \ --progress plain \ --secret id=STEAM_USERNAME,env=STEAM_USERNAME \ --secret id=STEAM_PASSWORD,env=STEAM_PASSWORD \ --no-cache="$NO_CACHE" \ --build-arg APPID="$APPID" \ --build-arg MOD="$MOD" \ --build-arg FIX_APPMANIFEST="$FIX_APPMANIFEST" \ --build-arg CLIENT_APPID="$CLIENT_APPID" \ --build-arg INSTALL_COUNT="$INSTALL_COUNT" \ --build-arg STEAM_LOGIN="$STEAM_LOGIN" \ --build-arg CACHE_KEY="$GAME_VERSION" \ -t "$GAME_IMAGE_CLEAN" \ --label "appid=$APPID" \ --label "mod=$MOD" \ --label "client_appid=$CLIENT_APPID" \ --label "game=$GAME" \ --label "game_version=$GAME_VERSION" \ --label "game_version_base=$GAME_VERSION" \ --label 'game_update_count=0' \ --label "game_engine=$GAME_ENGINE" \ --label "commit_sha=$COMMIT_SHA" \ "$BUILD_CONTEXT" if [ "$LATEST" = 'true' ]; then docker tag "$GAME_IMAGE_CLEAN" "$GAME_IMAGE_LATEST" fi date -Iseconds GAME_IMAGE="$GAME_IMAGE_CLEAN" elif [ "$PIPELINE" = 'update' ]; then date -Iseconds if [ ! "$NO_PULL" = 'true' ]; then time docker pull "$GAME_IMAGE_LATEST" fi date -Iseconds STEAM_USERNAME="$STEAM_USERNAME" STEAM_PASSWORD="$STEAM_PASSWORD" time docker build \ --progress plain \ --secret id=STEAM_USERNAME,env=STEAM_USERNAME \ --secret id=STEAM_PASSWORD,env=STEAM_PASSWORD \ --no-cache="$NO_CACHE" \ --build-arg GAME_IMAGE="$GAME_IMAGE_LATEST" \ --build-arg INSTALL_COUNT="$INSTALL_COUNT" \ --build-arg STEAM_LOGIN="$STEAM_LOGIN" \ --build-arg CACHE_KEY="$GAME_VERSION" \ -t "$GAME_IMAGE_LAYERED" \ -t "$GAME_IMAGE_LAYERED_CALVER" \ -t "$GAME_IMAGE_LATEST" \ --label "game_version=$GAME_VERSION" \ --label "game_update_count=$GAME_UPDATE_COUNT" \ --label "commit_sha=$COMMIT_SHA" \ "$BUILD_CONTEXT" date -Iseconds GAME_IMAGE="$GAME_IMAGE_LAYERED" fi docker images docker inspect "$GAME_IMAGE" docker history "$GAME_IMAGE" # Test the game image if [ ! "$NO_TEST" = 'true' ]; then TEST_DIR=$( mktemp -d ) echo "Testing image" date -Iseconds time docker run -t --rm "$GAME_IMAGE" 'printenv && ls -al' date -Iseconds # srcds/cs2 if [ "$APPID" = 730 ]; then CONTAINER_ID=$( docker run -td "$GAME_IMAGE" "$GAME_BIN -dedicated -port 27015 +map de_dust2" ) i=0; while [ "$i" -lt 30 ]; do echo "Waiting for server to start" docker container inspect -f '{{.State.Running}}' "$CONTAINER_ID" | grep '^true$' > /dev/null || break docker logs "$CONTAINER_ID" | grep 'VAC secure mode is activated' && break || sleep 1 i=$(($i + 1)) done docker logs "$CONTAINER_ID" docker exec "$CONTAINER_ID" bash -c 'printf "\\xff\\xff\\xff\\xffTSource Engine Query\\x00" | nc -w1 -u 127.0.0.1 27015 | tr "[:cntrl:]" "\\n"' | tee "$TEST_DIR/test" docker rm -f "$CONTAINER_ID" > /dev/null else time docker run -t --rm "$GAME_IMAGE" "$GAME_BIN -game $GAME +version +exit" | tee "$TEST_DIR/test" fi date -Iseconds # Verify game version of the game image matches the value of GAME_VERSION echo 'Verifying game image game version' GAME_IMAGE_VERSION_LINES=$( if [ "$APPID" = 730 ]; then cat "$TEST_DIR/test" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' || true else cat "$TEST_DIR/test" | grep -iE '\bexe\b|version' || true fi ) echo 'GAME_IMAGE_VERSION_LINES:' echo "$GAME_IMAGE_VERSION_LINES" if echo "$GAME_IMAGE_VERSION_LINES" | sed 's/[^0-9]//g' | grep -E "^$GAME_VERSION" > /dev/null; then echo "Game version matches GAME_VERSION=$GAME_VERSION" else echo "Game version does not match GAME_VERSION=$GAME_VERSION" exit 1 fi rm -f "$TEST_DIR/test" fi # Push the game image if [ ! "$NO_PUSH" = 'true' ]; then date -Iseconds if [ "$PIPELINE" = 'build' ]; then time docker push "$GAME_IMAGE_CLEAN" if [ "$LATEST" = 'true' ]; then time docker push "$GAME_IMAGE_LATEST" fi elif [ "$PIPELINE" = 'update' ]; then time docker push "$GAME_IMAGE_LAYERED" time docker push "$GAME_IMAGE_LAYERED_CALVER" time docker push "$GAME_IMAGE_LATEST" fi date -Iseconds fi # Docker registry logout if [ ! "$NO_PUSH" = 'true' ]; then docker logout fi # Create .build.state artifact echo "Creating .build.state artifact" # Searching for 'WORKDIR /server' is a reliable way to locate the base image layers BASE_SIZE=0; for i in $( docker history "$GAME_IMAGE" --format='{{.Size}} {{.CreatedAt}} {{.CreatedBy}}' --no-trunc --human=false | grep 'WORKDIR /server' -A99999 | awk '{print $1}' ); do BASE_SIZE=$(( $BASE_SIZE + $i )); done # Searching for 'UPDATE' is a reliable way to determine the incremental image layers LAYERS_SIZE=0; for i in $( docker history "$GAME_IMAGE" --format='{{.Size}} {{.CreatedAt}} {{.CreatedBy}}' --no-trunc --human=false | grep UPDATE | awk '{print $1}' ); do LAYERS_SIZE=$(( $LAYERS_SIZE + $i )); done LAYERED_SIZE=$(( $BASE_SIZE + $LAYERS_SIZE )) cat - > .build.state <<EOF BASE_SIZE=$BASE_SIZE LAYERED_SIZE=$LAYERED_SIZE DIFF=$LAYERS_SIZE EOF cat .build.state ls -al .build.state date -Iseconds echo echo "Success:" docker inspect "$GAME_IMAGE" --format='{{ range .RepoTags }}{{ printf "%s\n" . }}{{ end }}'