|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -o errexit |
| 3 | +set -o nounset |
| 4 | +set -o pipefail |
| 5 | + |
| 6 | +ECR_REGISTRY="850406765696.dkr.ecr.us-west-2.amazonaws.com" |
| 7 | +ECR_REPO="${ECR_REGISTRY}/lading-dev" |
| 8 | +AWS_VAULT_PROFILE="smp" |
| 9 | + |
| 10 | +usage() { |
| 11 | + cat <<EOF |
| 12 | +Usage: ci/container-build-and-push [OPTIONS] |
| 13 | +
|
| 14 | +Build and push lading Docker images to ECR. |
| 15 | +
|
| 16 | +Options: |
| 17 | + --tag TAG Image tag (default: git commit SHA) |
| 18 | + --arch ARCH Architecture(s): amd64, arm64, or amd64,arm64 (default: amd64,arm64) |
| 19 | + --profile PROFILE aws-vault profile (default: smp) |
| 20 | + --random-tag Use a random UUID as the image tag |
| 21 | + --no-push Build only, don't push to ECR |
| 22 | + -h, --help Show this help |
| 23 | +
|
| 24 | +Examples: |
| 25 | + ci/container-build-and-push # build both amd64+arm64, tag from git SHA |
| 26 | + ci/container-build-and-push --tag v0.32.0 # explicit tag |
| 27 | + ci/container-build-and-push --arch amd64,arm64 # multi-arch build |
| 28 | + ci/container-build-and-push --no-push # build only |
| 29 | +EOF |
| 30 | +} |
| 31 | + |
| 32 | +# Parse arguments |
| 33 | +TAG="" |
| 34 | +RANDOM_TAG=false |
| 35 | +ARCH="" |
| 36 | +PUSH=true |
| 37 | + |
| 38 | +while [[ $# -gt 0 ]]; do |
| 39 | + case "$1" in |
| 40 | + --tag) TAG="$2"; shift 2 ;; |
| 41 | + --arch) ARCH="$2"; shift 2 ;; |
| 42 | + --profile) AWS_VAULT_PROFILE="$2"; shift 2 ;; |
| 43 | + --random-tag) RANDOM_TAG=true; shift ;; |
| 44 | + --no-push) PUSH=false; shift ;; |
| 45 | + -h|--help) usage; exit 0 ;; |
| 46 | + *) echo "Error: Unknown option: $1" >&2; usage >&2; exit 1 ;; |
| 47 | + esac |
| 48 | +done |
| 49 | + |
| 50 | +if [[ -n "$TAG" && "$RANDOM_TAG" == "true" ]]; then |
| 51 | + echo "Error: --tag and --random-tag are mutually exclusive" >&2 |
| 52 | + exit 1 |
| 53 | +fi |
| 54 | + |
| 55 | +if [[ "$RANDOM_TAG" == "true" ]]; then |
| 56 | + TAG="$(uuidgen | tr '[:upper:]' '[:lower:]')" |
| 57 | + echo "Generated random tag: ${TAG}" |
| 58 | +fi |
| 59 | + |
| 60 | +# Check prerequisites |
| 61 | +REQUIRED_CMDS=(docker) |
| 62 | +if [[ "$PUSH" == "true" ]]; then |
| 63 | + REQUIRED_CMDS+=(aws aws-vault jq) |
| 64 | +fi |
| 65 | +for cmd in "${REQUIRED_CMDS[@]}"; do |
| 66 | + if ! command -v "$cmd" &>/dev/null; then |
| 67 | + echo "Error: $cmd is not installed" |
| 68 | + exit 1 |
| 69 | + fi |
| 70 | +done |
| 71 | + |
| 72 | +# Ensure Docker daemon is running |
| 73 | +if ! docker info &>/dev/null; then |
| 74 | + echo "Error: Docker daemon is not running. Start Docker Desktop and try again." |
| 75 | + exit 1 |
| 76 | +fi |
| 77 | + |
| 78 | +# Ensure buildx is available |
| 79 | +if ! docker buildx version &>/dev/null; then |
| 80 | + echo "Error: Docker Buildx is not available" |
| 81 | + exit 1 |
| 82 | +fi |
| 83 | + |
| 84 | +# Check Docker has enough memory for release builds with LTO |
| 85 | +MIN_MEMORY_GB=16 |
| 86 | +DOCKER_MEM_BYTES=$(docker info --format '{{.MemTotal}}' 2>/dev/null) |
| 87 | +DOCKER_MEM_GB=$(( DOCKER_MEM_BYTES / 1073741824 )) |
| 88 | +if [[ "$DOCKER_MEM_GB" -lt "$MIN_MEMORY_GB" ]]; then |
| 89 | + echo "Error: Docker has ${DOCKER_MEM_GB} GB of memory, but at least ${MIN_MEMORY_GB} GB is required" |
| 90 | + echo "Increase memory in Docker Desktop -> Settings -> Resources" |
| 91 | + exit 1 |
| 92 | +fi |
| 93 | +echo "Docker memory: ${DOCKER_MEM_GB} GB" |
| 94 | + |
| 95 | +# Default to both architectures if not specified |
| 96 | +if [[ -z "$ARCH" ]]; then |
| 97 | + ARCH="amd64,arm64" |
| 98 | + echo "Defaulting to both architectures: ${ARCH}" |
| 99 | +fi |
| 100 | + |
| 101 | +# Determine tag |
| 102 | +if [[ -z "$TAG" ]]; then |
| 103 | + TAG="sha-$(git rev-parse HEAD)" |
| 104 | + echo "Using git SHA tag: ${TAG}" |
| 105 | +fi |
| 106 | + |
| 107 | +# Parse architectures |
| 108 | +IFS=',' read -ra ARCHS <<< "$ARCH" |
| 109 | +MULTI_ARCH=false |
| 110 | +if [[ ${#ARCHS[@]} -gt 1 ]]; then |
| 111 | + MULTI_ARCH=true |
| 112 | +fi |
| 113 | + |
| 114 | +# Build platform string |
| 115 | +PLATFORMS="" |
| 116 | +for a in "${ARCHS[@]}"; do |
| 117 | + if [[ -n "$PLATFORMS" ]]; then |
| 118 | + PLATFORMS="${PLATFORMS}," |
| 119 | + fi |
| 120 | + PLATFORMS="${PLATFORMS}linux/${a}" |
| 121 | +done |
| 122 | + |
| 123 | +# Set up QEMU for cross-arch builds |
| 124 | +if [[ "$MULTI_ARCH" == "true" ]]; then |
| 125 | + echo "Setting up QEMU for multi-arch builds..." |
| 126 | + docker run --rm --privileged tonistiigi/binfmt --install all >/dev/null 2>&1 || true |
| 127 | +fi |
| 128 | + |
| 129 | +# Authenticate to ECR using a temporary docker config to avoid macOS credsStore issues. |
| 130 | +# We copy the real docker config (minus credsStore) and symlink cli-plugins so that |
| 131 | +# docker login can write credentials as plain JSON and buildx still works. |
| 132 | +DOCKER_CONFIG_DIR="" |
| 133 | +cleanup() { |
| 134 | + if [[ -n "$DOCKER_CONFIG_DIR" ]]; then |
| 135 | + rm -rf "$DOCKER_CONFIG_DIR" |
| 136 | + fi |
| 137 | +} |
| 138 | +trap cleanup EXIT |
| 139 | + |
| 140 | +if [[ "$PUSH" == "true" ]]; then |
| 141 | + echo "Authenticating to ECR..." |
| 142 | + REAL_DOCKER_CONFIG="${DOCKER_CONFIG:-${HOME}/.docker}" |
| 143 | + DOCKER_CONFIG_DIR="$(mktemp -d)" |
| 144 | + # Copy config without credsStore/credHelpers so docker login writes auth as plain JSON |
| 145 | + jq 'del(.credsStore, .credHelpers)' "${REAL_DOCKER_CONFIG}/config.json" \ |
| 146 | + > "${DOCKER_CONFIG_DIR}/config.json" |
| 147 | + # Symlink subdirectories so buildx CLI plugin and Docker contexts are found |
| 148 | + for subdir in cli-plugins contexts; do |
| 149 | + if [[ -d "${REAL_DOCKER_CONFIG}/${subdir}" ]]; then |
| 150 | + ln -s "${REAL_DOCKER_CONFIG}/${subdir}" "${DOCKER_CONFIG_DIR}/${subdir}" |
| 151 | + fi |
| 152 | + done |
| 153 | + aws-vault exec "$AWS_VAULT_PROFILE" -- \ |
| 154 | + aws ecr get-login-password --region us-west-2 \ |
| 155 | + | DOCKER_CONFIG="$DOCKER_CONFIG_DIR" docker login --username AWS --password-stdin "$ECR_REGISTRY" |
| 156 | + export DOCKER_CONFIG="$DOCKER_CONFIG_DIR" |
| 157 | +fi |
| 158 | + |
| 159 | +# Create/use a buildx builder that supports multi-platform |
| 160 | +BUILDER_NAME="lading-builder" |
| 161 | +if ! docker buildx inspect "$BUILDER_NAME" &>/dev/null; then |
| 162 | + echo "Creating buildx builder: ${BUILDER_NAME}" |
| 163 | + docker buildx create --name "$BUILDER_NAME" --driver docker-container --use |
| 164 | +else |
| 165 | + docker buildx use "$BUILDER_NAME" |
| 166 | +fi |
| 167 | + |
| 168 | +# Build image tags |
| 169 | +BUILD_TAGS=("--tag" "${ECR_REPO}:${TAG}") |
| 170 | + |
| 171 | +PUSH_FLAG="" |
| 172 | +if [[ "$PUSH" == "true" ]]; then |
| 173 | + PUSH_FLAG="--push" |
| 174 | +elif [[ "$MULTI_ARCH" != "true" ]]; then |
| 175 | + PUSH_FLAG="--load" |
| 176 | +else |
| 177 | + echo "Warning: multi-arch --no-push builds remain in cache only (use --arch amd64 or --arch arm64 for a loadable image)" |
| 178 | +fi |
| 179 | + |
| 180 | +# Build (multi-arch builds both platforms in parallel within a single buildx invocation) |
| 181 | +echo "Building for platform(s): ${PLATFORMS}" |
| 182 | +echo "Tag: ${TAG}" |
| 183 | + |
| 184 | +docker buildx build \ |
| 185 | + --platform "$PLATFORMS" \ |
| 186 | + --build-arg SCCACHE_BUCKET="" \ |
| 187 | + --build-arg SCCACHE_REGION="" \ |
| 188 | + --build-arg AWS_ACCESS_KEY_ID="" \ |
| 189 | + --build-arg AWS_SECRET_ACCESS_KEY="" \ |
| 190 | + --build-arg AWS_SESSION_TOKEN="" \ |
| 191 | + --secret "id=aws_access_key_id,src=/dev/null" \ |
| 192 | + --secret "id=aws_secret_access_key,src=/dev/null" \ |
| 193 | + --secret "id=aws_session_token,src=/dev/null" \ |
| 194 | + "${BUILD_TAGS[@]}" \ |
| 195 | + ${PUSH_FLAG} \ |
| 196 | + -f Dockerfile \ |
| 197 | + . |
| 198 | + |
| 199 | +if [[ "$PUSH" == "true" ]]; then |
| 200 | + echo "Pushed to ${ECR_REPO}:${TAG}" |
| 201 | +else |
| 202 | + echo "Build complete (not pushed)" |
| 203 | +fi |
0 commit comments