Skip to content

Commit

Permalink
Add docker rake tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
lloeki committed Nov 14, 2024
1 parent 7c2672e commit 424292d
Showing 1 changed file with 218 additions and 0 deletions.
218 changes: 218 additions & 0 deletions tasks/docker.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# frozen_string_literal: true

# @type self: Rake::DSL

# TODO: use rake dependency + satisfaction mechanism via `needs?`
# See: https://github.com/ruby/rake/blob/03cb03474b4eb008b2d62ad96d07de0d6239c7ab/lib/rake/file_task.rb#L16

namespace :docker do
def repository # TODO: rename to registry/registry host/user/path
"ghcr.io/datadog/images-rb"
end

def targets
@targets ||= Dir.glob("src/**/Dockerfile*").map do |f|
dockerfile = f
context = File.dirname(dockerfile)

image = "#{repository}/#{File.dirname(f.sub(/^src\//, "").sub(/\/Dockerfile(.*)/, ""))}"
tag = f.sub(/.*\/(\d+(?:\.\d+))+\//, "\\1").sub(/Dockerfile(.*)$/) { |m| m.sub("Dockerfile", "").tr(".", "-") }

{
dockerfile: dockerfile,
context: context,
image: image, # TODO: rename to repository
tag: tag
# TODO: rename to image/tag/tagged_image/name/alias: "#{repo}:#{tag}"
}
end
end

def dependencies
@dependencies ||= Dir.glob("src/**/Dockerfile*").each_with_object({}) do |path, h|
h[path] = File.read(path).each_line.with_object([]) { |l, a| l =~ /^FROM (\S+)(?:\s+AS|\s*$)/ && a << $1 }
end
end

def local_dependencies
@local_dependencies ||= dependencies.each_with_object({}) { |(k, v), h| h[k] = v.select { |from| from.start_with?(repository) } }
end

def target_for(args)
targets_for(args).tap { |a| a.size > 1 and fail "multiple args passed to task" }.first
end

def glob_match?(pattern, str)
re = Regexp.new("^#{Regexp.escape(pattern).gsub("\\*\\*", "[^:]*?").gsub("\\*", "[^/:]*?")}$")

!!(str =~ re)
end

def targets_for(args)
images = args.to_a

images.map do |image|
image = "#{repository}/#{image}" unless image.start_with?(repository)

found = targets.select { |e| glob_match?(image, "#{e[:image]}:#{e[:tag]}") }

fail "#{image} not found" if found.nil?

found
end.flatten
end

def dockerfiles_for(*images)
images.map do |image|
targets.each_with_object([]) { |t, a| a << t[:dockerfile] if "#{t[:image]}:#{t[:tag]}" == image }
end.flatten
end

def satisfied?(result, deps = [])
result_time = case result
when String
File.ctime(result).to_datetime
when Proc
result.call
else
raise ArgumentError, "invalid type: #{dep.class}"
end

return false if result_time.nil?
return true if deps.empty?

deps.map do |dep|
dep_time = case dep
when String
File.ctime(dep).to_datetime
when Proc
dep.call
else
raise ArgumentError, "invalid type: #{dep.class}"
end

result_time > dep_time
end.reduce(:&)
end

def docker_platform
if RUBY_PLATFORM =~ /^(?:universal\.|)(x86_64|aarch64|arm64)/
cpu = $1.sub(/arm64(:?e|)/, "aarch64")
else
raise ArgumentError, "unsupported platform: #{RUBY_PLATFORM}"
end

os = "linux"

"#{os}/#{cpu}"
end

def image_time(image)
require "time"

last_tag_time = `docker image inspect -f '{{ .Metadata.LastTagTime }}' '#{image}'`.chomp

if $?.to_i == 0
# "0001-01-01 00:00:00 +0000 UTC"
last_tag_time.sub!(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})(\s+)/, "\\1.0\\2")

DateTime.strptime(last_tag_time, "%Y-%m-%d %H:%M:%S.%N %z")
end
end

def volume_time(volume)
require "time"

volume_creation_time = `docker volume inspect -f '{{ .CreatedAt }}' '#{volume}'`.chomp

if $?.to_i == 0
volume_creation_time.sub!(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})(\s+)/, "\\1.0\\2")

DateTime.strptime(volume_creation_time, "%Y-%m-%dT%H:%M:%S.%N %z")
end
end

desc "List image targets."
task :list do
targets.each do |image|
puts "#{image[:image]}:#{image[:tag]}"
end
end

desc "Pull image."
task :pull do |_, args|
targets = targets_for(args)

targets.each do |target|
image = target[:image]
tag = target[:tag]
platform = docker_platform

sh "docker pull --platform #{platform} #{image}:#{tag} || true"
end
end

desc "Build image."
task :build do |_, args|
targets = targets_for(args)

targets.each do |target|
dockerfile = target[:dockerfile]
context = target[:context]
image = target[:image]
tag = target[:tag]
platform = docker_platform

deps = [
dockerfile
] + dockerfiles_for(*local_dependencies[dockerfile])

compatible_platforms = deps.map do |dep|
File.read(dep).lines.select { |l| l =~ /^\s*#\s*platforms:/ }.map { |l| l =~ /platforms: (.*)/ && $1 }
end.flatten

if compatible_platforms.any? && !compatible_platforms.include?(platform)
warn "skip build: dockerfile: #{dockerfile.inspect}, incompatible platform: #{platform.inspect}, compatible platforms: #{compatible_platforms.inspect}"
next
end

local_dependencies[dockerfile].each { |dep| Rake::Task[:"docker:build"].execute(Rake::TaskArguments.new([], [dep])) }

next if satisfied?(-> { image_time("#{image}:#{tag}") }, deps)

sh "docker buildx build --platform #{platform} --cache-from=type=registry,ref=#{image}:#{tag} -f #{dockerfile} -t #{image}:#{tag} #{context}"
end
end

desc "Run container with default CMD."
task cmd: :build do |_, args|
target = target_for(args)

image = target[:image]
tag = target[:tag]
platform = docker_platform

exec "docker run --rm -it --platform #{platform} -v #{Dir.pwd}:#{Dir.pwd} -w #{Dir.pwd} #{image}:#{tag}"
end

desc "Run container with shell."
task shell: :build do |_, args|
target = target_for(args)

image = target[:image]
tag = target[:tag]
platform = docker_platform

exec "docker run --rm -it --platform #{platform} -v #{Dir.pwd}:#{Dir.pwd} -w #{Dir.pwd} #{image}:#{tag} /bin/sh"
end

desc "Run container with irb."
task irb: :build do |_, args|
target = target_for(args)

image = target[:image]
tag = target[:tag]

exec "docker run --rm -it --platform #{platform} -v #{Dir.pwd}:#{Dir.pwd} -w #{Dir.pwd} #{image}:#{tag} irb"
end
end

0 comments on commit 424292d

Please sign in to comment.