Use docker buildx to build Docker images

Move away from docker-api gem which does not have support for `docker
buildx`. Add a wrapper to execute `docker buildx` commands in the shell
and use that for Docker operations.

Closes: https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/8469

Signed-off-by: Balasankar 'Balu' C <balasankar@gitlab.com>
This commit is contained in:
Balasankar 'Balu' C 2024-03-20 10:43:49 +05:30
parent a068eb2a10
commit f8c71c7a9c
No known key found for this signature in database
GPG Key ID: B77D2E2E23735427
5 changed files with 277 additions and 25 deletions

View File

@ -51,6 +51,15 @@ module Build
SkopeoHelper.copy_image(source, target)
end
def copy_image_to_gitlab_registry(final_tag)
source = source_image_address
target = gitlab_registry_image_address(tag: final_tag)
SkopeoHelper.login('gitlab-ci-token', Gitlab::Util.get_env('CI_JOB_TOKEN'), Gitlab::Util.get_env('CI_REGISTRY'))
SkopeoHelper.copy_image(source, target)
end
def source_image_address
raise NotImplementedError
end

View File

@ -0,0 +1,92 @@
require 'open3'
require_relative 'docker_operations'
# TODO: Deprecate DockerOperations
class DockerHelper < DockerOperations
class << self
def authenticate(username: nil, password: nil, registry: nil)
puts "Logging in to Docker registry"
stdout, stderr, status = Open3.popen3({}, *%W[docker login --username=#{username} --password-stdin #{registry}]) do |stdin, stdout, stderr, wait_thr|
stdin.puts(password)
stdin.close
[stdout.read, stderr.read, wait_thr.value]
end
return if status.success?
puts "Failed to login to Docker registry."
puts "Output is: #{stdout}"
puts stderr
Kernel.exit 1
end
# TODO: When multi-arch images are built by default, modify `platforms`
# array to include `linux/arm64` also
def build(location, image, tag, dockerfile: nil, buildargs: nil, platforms: %w[linux/amd64], push: true)
create_builder
commands = %W[docker buildx build #{location} -t #{image}:#{tag}]
if (env_var_platforms = Gitlab::Util.get_env('DOCKER_BUILD_PLATFORMS'))
platforms.append(env_var_platforms.split(",").map(&:strip))
end
platforms.uniq!
commands += %W[--platform=#{platforms.join(',')}]
# If specified to push, we must push to registry. Even if not, if the
# image being built is multiarch, we must push to registry.
commands += %w[--push] if push || platforms.length > 1
commands += %W[-f #{dockerfile}] if dockerfile
buildargs&.each do |arg|
commands += %W[--build-arg='#{arg}']
end
puts "Running command: #{commands.join(' ')}"
Open3.popen2e(*commands) do |_, stdout_stderr, status|
while line = stdout_stderr.gets
puts line
end
Kernel.exit 1 unless status.value.success?
end
end
def create_builder
cleanup_existing_builder
puts "Creating docker builder instance"
# TODO: For multi-arch builds, use Kubernetes driver for builder instance
_, stdout_stderr, status = Open3.popen2e(*%w[docker buildx create --bootstrap --use --name omnibus-gitlab-builder])
return if status.value.success?
puts "Creating builder instance failed."
puts "Output: #{stdout_stderr.read}"
raise
end
def cleanup_existing_builder
puts "Cleaning any existing builder instances."
_, _, status = Open3.popen2e(*%w[docker buildx ls | grep omnibus-gitlab-builder])
unless status.value.success?
puts "omnibus-gitlab-builder instance not found. Not attempting to clean."
return
end
_, stdout_stderr, status = Open3.popen2e(*%w[docker buildx rm --force omnibus-gitlab-builder])
if status.value.success?
puts "Successfully cleaned omnibus-gitlab-builder instance."
else
puts "Cleaning of omnibus-gitlab-builder instance failed."
puts "Output: #{stdout_stderr.read}"
end
end
end
end

View File

@ -5,6 +5,7 @@ require_relative '../build/gitlab_image'
require_relative '../build/info/ci'
require_relative '../build/info/docker'
require_relative '../docker_operations'
require_relative '../docker_helper'
require_relative '../util'
namespace :docker do
@ -14,11 +15,8 @@ namespace :docker do
Gitlab::Util.section('docker:build:image') do
Build::GitlabImage.write_release_file
location = File.absolute_path(File.join(File.dirname(File.expand_path(__FILE__)), "../../../docker"))
DockerOperations.build(
location,
Build::GitlabImage.gitlab_registry_image_address,
'latest'
)
DockerHelper.authenticate(username: "gitlab-ci-token", password: Gitlab::Util.get_env("CI_JOB_TOKEN"), registry: Gitlab::Util.get_env('CI_REGISTRY'))
DockerHelper.build(location, Build::GitlabImage.gitlab_registry_image_address, Build::Info::Docker.tag)
end
end
end
@ -28,11 +26,11 @@ namespace :docker do
# Only runs on dev.gitlab.org
task :staging do
Gitlab::Util.section('docker:push:staging') do
Build::GitlabImage.tag_and_push_to_gitlab_registry(Build::Info::Docker.tag)
# Also tag with CI_COMMIT_REF_SLUG so that manual testing using Docker
# can use the same image name/tag.
Build::GitlabImage.tag_and_push_to_gitlab_registry(Build::Info::CI.commit_ref_slug)
# As part of build, the image is already tagged and pushed to GitLab
# registry with `Build::Info::Docker.tag` as the tag. Also copy the
# image with `CI_COMMIT_REF_SLUG` as the tag so that manual testing
# using Docker can use the same image name/tag.
Build::GitlabImage.copy_image_to_gitlab_registry(Build::Info::CI.commit_ref_slug)
end
end
@ -88,11 +86,11 @@ namespace :docker do
desc "Push triggered Docker Image to GitLab Registry"
task :triggered do
Gitlab::Util.section('docker:push:triggered') do
Build::GitlabImage.tag_and_push_to_gitlab_registry(Build::Info::Docker.tag)
# Also tag with CI_COMMIT_REF_SLUG so that manual testing using Docker
# As part of build, the image is already tagged and pushed with
# `Build::Info::Docker.tag` as the tag. Also copy the image with
# `CI_COMMIT_REF_SLUG` as the tag so that manual testing using Docker
# can use the same image name/tag.
Build::GitlabImage.tag_and_push_to_gitlab_registry(Build::Info::CI.commit_ref_slug)
Build::GitlabImage.copy_image_to_gitlab_registry(Build::Info::CI.commit_ref_slug)
end
end
end

View File

@ -0,0 +1,146 @@
require 'spec_helper'
require 'gitlab/docker_helper'
RSpec.describe DockerHelper do
describe '.authenticate' do
before do
stdin_mock = double(puts: true, close: true)
stdout_mock = double(read: true)
stderr_mock = double(read: true)
wait_thr_mock = double(value: double(success?: true))
allow(Open3).to receive(:popen3).with({}, "docker", "login", any_args).and_yield(stdin_mock, stdout_mock, stderr_mock, wait_thr_mock)
end
context 'when a registry is not specified' do
it 'runs the command to login to docker.io' do
expect(Open3).to receive(:popen3).with({}, "docker", "login", "--username=dummy-username", "--password-stdin", "")
described_class.authenticate(username: 'dummy-username', password: 'dummy-password')
end
end
context 'when a registry is specified' do
it 'runs the command to login to specified registry' do
expect(Open3).to receive(:popen3).with({}, "docker", "login", "--username=dummy-username", "--password-stdin", "registry.gitlab.com")
described_class.authenticate(username: 'dummy-username', password: 'dummy-password', registry: 'registry.gitlab.com')
end
end
end
describe '.build' do
shared_examples 'docker build command invocation' do
end
before do
stdout_stderr_mock = double(gets: nil)
status_mock = double(value: double(success?: true))
allow(described_class).to receive(:create_builder).and_return(true)
allow(Open3).to receive(:popen2e).with("docker", "buildx", "build", any_args).and_yield(nil, stdout_stderr_mock, status_mock)
end
context 'when a single platform is specified' do
context 'when push is not explicitly disabled' do
let(:expected_args) { %w[docker buildx build /tmp/foo -t sample:value --platform=linux/amd64 --push] }
it 'calls docker build command with correct arguments' do
expect(Open3).to receive(:popen2e).with(*expected_args)
described_class.build('/tmp/foo', 'sample', 'value')
end
end
context 'when push is explicitly disabled' do
let(:expected_args) { %w[docker buildx build /tmp/foo -t sample:value --platform=linux/amd64] }
it 'calls docker build command with correct arguments' do
expect(Open3).to receive(:popen2e).with(*expected_args)
described_class.build('/tmp/foo', 'sample', 'value', push: false)
end
end
end
context 'when multiple platforms are specified via env vars' do
before do
stub_env_var('DOCKER_BUILD_PLATFORMS', 'linux/arm64')
end
let(:expected_args) { %w[docker buildx build /tmp/foo -t sample:value --platform=linux/amd64,linux/arm64 --push] }
it 'calls docker build command with correct arguments' do
expect(Open3).to receive(:popen2e).with(*expected_args)
described_class.build('/tmp/foo', 'sample', 'value')
end
context 'even if push is explicitly disabled' do
let(:expected_args) { %w[docker buildx build /tmp/foo -t sample:value --platform=linux/amd64,linux/arm64 --push] }
it 'calls docker build command with correct arguments' do
expect(Open3).to receive(:popen2e).with(*expected_args)
described_class.build('/tmp/foo', 'sample', 'value', push: false)
end
end
end
context 'when build_args are specified' do
let(:expected_args) { %w[docker buildx build /tmp/foo -t sample:value --platform=linux/amd64 --push --build-arg='FOO=BAR'] }
it 'calls docker build command with correct arguments' do
expect(Open3).to receive(:popen2e).with(*expected_args)
described_class.build('/tmp/foo', 'sample', 'value', buildargs: ["FOO=BAR"])
end
end
end
describe '.create_builder' do
before do
allow(described_class).to receive(:cleanup_existing_builder).and_return(true)
stdout_stderr_mock = double(gets: nil)
status_mock = double(value: double(success?: true))
allow(Open3).to receive(:popen2e).with("docker", "buildx", "create", any_args).and_return([nil, stdout_stderr_mock, status_mock])
end
it 'calls docker buildx create command with correct arguments' do
expect(Open3).to receive(:popen2e).with(*%w[docker buildx create --bootstrap --use --name omnibus-gitlab-builder])
described_class.create_builder
end
end
describe '.cleanup_existing_builder' do
context 'when no builder instance exist' do
before do
status_mock = double(value: double(success?: false))
allow(Open3).to receive(:popen2e).with("docker", "buildx", "ls", any_args).and_return([nil, nil, status_mock])
end
it 'does not call docker buildx rm' do
expect(Open3).not_to receive(:popen2e).with(*%w[docker buildx rm --force omnibus-gitlab-builder])
described_class.cleanup_existing_builder
end
end
context 'when builder instance exists' do
before do
status_mock = double(value: double(success?: true))
allow(Open3).to receive(:popen2e).with("docker", "buildx", "ls", any_args).and_return([nil, nil, status_mock])
allow(Open3).to receive(:popen2e).with("docker", "buildx", "rm", any_args).and_return([nil, nil, status_mock])
end
it 'calls docker buildx rm command with correct arguments' do
expect(Open3).to receive(:popen2e).with(*%w[docker buildx rm --force omnibus-gitlab-builder])
described_class.cleanup_existing_builder
end
end
end
end

View File

@ -12,14 +12,17 @@ RSpec.describe 'docker', type: :rake do
end
it 'calls build command with correct parameters' do
allow(ENV).to receive(:[]).with('CI_REGISTRY_IMAGE').and_return('dev.gitlab.org:5005/gitlab/omnibus-gitlab')
allow(ENV).to receive(:[]).with('CI_REGISTRY_IMAGE').and_return('registry.com/group/repo')
allow(Build::Info::Docker).to receive(:tag).and_return('9.0.0')
allow(Build::Info::Package).to receive(:name).and_return('gitlab-ce')
allow(Build::GitlabImage).to receive(:write_release_file).and_return(true)
allow(File).to receive(:expand_path).and_return('/tmp/omnibus-gitlab/lib/gitlab/tasks/docker_tasks.rake')
allow(DockerOperations).to receive(:build).and_call_original
expect(DockerOperations).to receive(:build).with("/tmp/omnibus-gitlab/docker", "dev.gitlab.org:5005/gitlab/omnibus-gitlab/gitlab-ce", "latest")
expect(Docker::Image).to receive(:build_from_dir).with("/tmp/omnibus-gitlab/docker", { t: "dev.gitlab.org:5005/gitlab/omnibus-gitlab/gitlab-ce:latest", pull: true })
allow(DockerHelper).to receive(:authenticate).and_return(true)
allow(DockerHelper).to receive(:build).and_return(true)
allow(DockerHelper).to receive(:create_builder).and_return(true)
expect(DockerHelper).to receive(:build).with("/tmp/omnibus-gitlab/docker", "registry.com/group/repo/gitlab-ce", '9.0.0')
Rake::Task['docker:build:image'].invoke
end
end
@ -82,11 +85,15 @@ RSpec.describe 'docker', type: :rake do
describe 'docker:push:staging' do
before do
Rake::Task['docker:push:staging'].reenable
allow(ENV).to receive(:[]).with('CI_COMMIT_REF_SLUG').and_return('foo-bar')
allow(ENV).to receive(:[]).with('CI_REGISTRY_IMAGE').and_return('registry.gitlab.com/gitlab-org/omnibus-gitlab')
allow(SkopeoHelper).to receive(:copy_image).and_return(true)
allow(Build::Info::Package).to receive(:name).and_return('gitlab-ce')
allow(Build::Info::Docker).to receive(:tag).and_return('1.2.3.4')
end
it 'pushes to staging correctly' do
expect(dummy_image).to receive(:push).with(dummy_creds, repo_tag: 'dev.gitlab.org:5005/gitlab/omnibus-gitlab/gitlab-ce:9.0.0')
expect(dummy_image).to receive(:push).with(dummy_creds, repo_tag: 'dev.gitlab.org:5005/gitlab/omnibus-gitlab/gitlab-ce:foo-bar')
it 'pushes triggered images correctly' do
expect(SkopeoHelper).to receive(:copy_image).with('registry.gitlab.com/gitlab-org/omnibus-gitlab/gitlab-ce:1.2.3.4', 'registry.gitlab.com/gitlab-org/omnibus-gitlab/gitlab-ce:foo-bar')
Rake::Task['docker:push:staging'].invoke
end
end
@ -183,13 +190,13 @@ RSpec.describe 'docker', type: :rake do
Rake::Task['docker:push:triggered'].reenable
allow(ENV).to receive(:[]).with('CI_COMMIT_REF_SLUG').and_return('foo-bar')
allow(ENV).to receive(:[]).with('CI_REGISTRY_IMAGE').and_return('registry.gitlab.com/gitlab-org/omnibus-gitlab')
allow(ENV).to receive(:[]).with("IMAGE_TAG").and_return("omnibus-12345")
allow(Build::Info::Docker).to receive(:tag).and_call_original
allow(SkopeoHelper).to receive(:copy_image).and_return(true)
allow(Build::Info::Package).to receive(:name).and_return('gitlab-ce')
allow(Build::Info::Docker).to receive(:tag).and_return('1.2.3.4')
end
it 'pushes triggered images correctly' do
expect(dummy_image).to receive(:push).with(dummy_creds, repo_tag: 'registry.gitlab.com/gitlab-org/omnibus-gitlab/gitlab-ce:omnibus-12345')
expect(dummy_image).to receive(:push).with(dummy_creds, repo_tag: 'registry.gitlab.com/gitlab-org/omnibus-gitlab/gitlab-ce:foo-bar')
expect(SkopeoHelper).to receive(:copy_image).with('registry.gitlab.com/gitlab-org/omnibus-gitlab/gitlab-ce:1.2.3.4', 'registry.gitlab.com/gitlab-org/omnibus-gitlab/gitlab-ce:foo-bar')
Rake::Task['docker:push:triggered'].invoke
end
end