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:
parent
a068eb2a10
commit
f8c71c7a9c
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue