diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e43e9cf..03ec3d1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,16 +4,74 @@ stages: - deploy variables: - DOCKER_IMAGE_ENVS: "production test" + # Disable the shared image build jobs from turniere-infra; this repo owns buildx builds below. + DOCKER_IMAGE_ENVS: "" + DOCKER_BUILDKIT: "1" + BUILDX_PLATFORMS: "linux/amd64,linux/arm64" include: - project: 'turniere/turniere-infra' file: '/ci/pipeline.yaml' +.buildx-image: + stage: build + tags: + - shell + before_script: + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" + - export BUILDX_BUILDER_NAME="turniere-buildx-$CI_PIPELINE_ID-$CI_JOB_ID" + - docker buildx create --name "$BUILDX_BUILDER_NAME" --driver docker-container --use + - docker buildx inspect --builder "$BUILDX_BUILDER_NAME" --bootstrap + after_script: + - export BUILDX_BUILDER_NAME="turniere-buildx-$CI_PIPELINE_ID-$CI_JOB_ID" + - docker buildx rm "$BUILDX_BUILDER_NAME" || true + - docker logout "$CI_REGISTRY" || true + +build production image: + extends: .buildx-image + script: + - >- + docker buildx build + --builder "$BUILDX_BUILDER_NAME" + --file Dockerfile + --target production + --platform "$BUILDX_PLATFORMS" + --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production" + --cache-to "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production,mode=max" + --tag "$CI_REGISTRY_IMAGE/production/commits:$CI_COMMIT_SHA" + --push + . + rules: + - if: $CI_PIPELINE_SOURCE != "push" + when: never + - when: always + +build test image: + extends: .buildx-image + script: + - >- + docker buildx build + --builder "$BUILDX_BUILDER_NAME" + --file Dockerfile + --target test + --platform "$BUILDX_PLATFORMS" + --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:production" + --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:test" + --cache-to "type=registry,ref=$CI_REGISTRY_IMAGE/buildcache:test,mode=max" + --tag "$CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA" + --push + . + rules: + - if: $CI_PIPELINE_SOURCE != "push" + when: never + - when: always + rails spec: stage: test tags: - docker + needs: + - build test image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA parallel: matrix: @@ -42,6 +100,9 @@ rails e2e spec: stage: test tags: - docker + needs: + - build production image + - build test image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA services: - name: postgres:16 @@ -93,6 +154,9 @@ rails follow sync e2e spec: stage: test tags: - docker + needs: + - build production image + - build test image image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA services: - name: postgres:16 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..00f2006 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,100 @@ +# syntax=docker/dockerfile:1.7 + +ARG RUBY_VERSION=3.1.2 +ARG BUNDLER_VERSION=2.3.13 + +FROM ruby:${RUBY_VERSION}-slim AS runtime-base + +ARG BUNDLER_VERSION + +ENV LANG=C.UTF-8 \ + BUNDLE_JOBS=4 \ + BUNDLE_RETRY=3 \ + BUNDLE_PATH=/usr/local/bundle + +WORKDIR /app + +RUN apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + libpq5 \ + nodejs \ + openssl \ + && rm -rf /var/lib/apt/lists/* \ + && gem install bundler -v ${BUNDLER_VERSION} \ + && gem install tzinfo-data + +FROM runtime-base AS build-base + +RUN apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + libpq-dev \ + libsqlite3-dev \ + pkg-config \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +COPY Gemfile Gemfile.lock ./ + +FROM build-base AS bundle-production + +RUN bundle config set deployment 'true' \ + && bundle config set without 'development test' \ + && bundle install \ + && rm -rf /usr/local/bundle/cache/*.gem + +FROM build-base AS bundle-test + +RUN bundle config set with 'development test' \ + && bundle install \ + && rm -rf /usr/local/bundle/cache/*.gem + +FROM runtime-base AS production + +ENV RAILS_ENV=production + +COPY --from=bundle-production /usr/local/bundle /usr/local/bundle +COPY app /app/app +COPY bin /app/bin +COPY config /app/config +COPY db /app/db +COPY public /app/public +COPY script /app/script +COPY config.ru Rakefile /app/ +# Duplicate production environment to beta environment +COPY config/environments/production.rb /app/config/environments/beta.rb + +EXPOSE 3000 + +CMD ["sh", "-lc", "bundle exec rails db:migrate && bundle exec rails s -p 3000 -b 0.0.0.0"] + +FROM runtime-base AS test + +ENV RAILS_ENV=test + +RUN apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + libsqlite3-0 \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* \ + && python3 -m pip install --no-cache-dir invoke + +COPY --from=bundle-test /usr/local/bundle /usr/local/bundle +COPY app /app/app +COPY bin /app/bin +COPY config /app/config +COPY db /app/db +COPY public /app/public +COPY script /app/script +COPY config.ru Rakefile /app/ +COPY config/environments/production.rb /app/config/environments/beta.rb +COPY tasks.py /app/tasks.py +COPY e2e /app/e2e +COPY lib /app/lib +COPY spec /app/spec +COPY .rspec /app/.rspec + +CMD ["sh", "-lc", "bundle exec rails db:migrate && bundle exec rails spec"] diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile deleted file mode 100644 index f4d11ff..0000000 --- a/docker/production/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -ARG base_commit -FROM ruby:3.1.2-slim -ENV RAILS_ENV production -# RUN apk add --no-cache build-base tzdata sqlite sqlite-dev postgresql-dev git && gem install tzinfo-data -RUN apt-get update -qq && apt-get install -y openssl git build-essential libpq-dev nodejs && apt-get clean && gem install tzinfo-data -WORKDIR /app -COPY Gemfile* /app/ -RUN gem install bundler:2.3.13 && bundle config set deployment 'true' && bundle install -COPY app /app/app -COPY bin /app/bin -COPY config /app/config -COPY db /app/db -COPY public /app/public -COPY script /app/script -COPY config.ru Rakefile /app/ -# Duplicate production environment to beta environment -COPY config/environments/production.rb config/environments/beta.rb -EXPOSE 3000 -CMD bundle exec rails db:migrate && bundle exec rails s -p 3000 -b 0.0.0.0 diff --git a/docker/test/Dockerfile b/docker/test/Dockerfile deleted file mode 100644 index a2446ba..0000000 --- a/docker/test/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# Build production container locally first with following tag -ARG base_commit -FROM registry.gitlab.com/turniere/turniere-backend/production/commits:$base_commit -WORKDIR /app -RUN apt-get update -qq && apt-get install -y python3 python3-pip && apt-get clean && python3 -m pip install --no-cache-dir invoke -RUN bundle config set with 'development test' && bundle install -COPY tasks.py /app/tasks.py -COPY e2e /app/e2e -COPY lib /app/lib -COPY spec /app/spec -COPY .rspec /app/.rspec -ENV RAILS_ENV test -CMD bundle exec rails db:migrate && bundle exec rails spec diff --git a/tasks.py b/tasks.py index 6fd9f3d..af6fa28 100644 --- a/tasks.py +++ b/tasks.py @@ -31,6 +31,9 @@ E2E_ALT_USERNAME = "e2e-alt-user" PRODUCTION_TAG = "registry.gitlab.com/turniere/turniere-backend/production/commits:local" TEST_TAG = "registry.gitlab.com/turniere/turniere-backend/test/commits:local" +DOCKERFILE_PATH = "Dockerfile" +PRODUCTION_DOCKER_TARGET = "production" +TEST_DOCKER_TARGET = "test" BLACKBOX_COMPOSE_FILE = "docker-compose.blackbox.yml" BLACKBOX_PROJECT = "turniere-blackbox" BLACKBOX_FOLLOW_COMPOSE_FILE = "docker-compose.blackbox-follow.yml" @@ -228,6 +231,31 @@ def _format_command(command): return " ".join(shlex.quote(part) for part in command) +def _docker_build_command(tag, target, platforms=None, push=False): + if platforms and "," in str(platforms) and not push: + raise ValueError("multi-platform buildx builds require push=True") + + if platforms or push: + command = [ + "docker", + "buildx", + "build", + "--target", + target, + "-t", + tag, + "-f", + DOCKERFILE_PATH, + ] + if platforms: + command.extend(["--platform", platforms]) + command.append("--push" if push else "--load") + command.append(".") + return _format_command(command) + + return f"docker build --target {target} -t {tag} -f {DOCKERFILE_PATH} ." + + def _rspec_command(*paths): command = ["bundle", "exec", "rspec", *paths] if os.environ.get("CI"): @@ -814,15 +842,15 @@ def scenario_render_profile( @task(name="docker-build-production") -def docker_build_production(c, tag=PRODUCTION_TAG): +def docker_build_production(c, tag=PRODUCTION_TAG, platforms=None, push=False): """Build the production Docker image.""" - c.run(f"docker build -t {tag} -f docker/production/Dockerfile .", pty=True) + c.run(_docker_build_command(tag, PRODUCTION_DOCKER_TARGET, platforms=platforms, push=push), pty=True) @task(name="docker-build-test") -def docker_build_test(c, tag=TEST_TAG): +def docker_build_test(c, tag=TEST_TAG, platforms=None, push=False): """Build the test Docker image.""" - c.run(f"docker build --build-arg base_commit=local -t {tag} -f docker/test/Dockerfile .", pty=True) + c.run(_docker_build_command(tag, TEST_DOCKER_TARGET, platforms=platforms, push=push), pty=True) @task(name="docker-build-all")