# Extend rake with file-like Docker tasks # require 'docker' require 'lux' require 'rake' require 'socket' module Rake module DSL def dockerimage(*args, &block) Lux::DockerImageTask.define_task(*args, &block) end def container(*args, &block) Lux::DockerContainerTask.define_task(*args, &block) end end end module Lux version = Docker.version rescue nil DISABLED = case when version.nil? error "Docker server not found, disabling Docker tasks!" true else begin # Set this to avoid 5 sec timeouts with .local addresses # which don't resolve in IPv6 Docker.options = { family: Socket::Constants::AF_INET, connect_timeout: 5 } Docker.validate_version! false rescue Exception => e error "Docker problem (#{e.message}), tasks will be disabled" true end end class DockerImageTask < Rake::FileTask # This task checks to see if the image is present in the local Docker Server # If present it returns the creation date as the timestamp, if not then it # returns Rake::Early. This allows dependencies to execute correctly # The action block should build the container. # def initialize(task_name, app) super(task_name, app) @imagename = @name @imagename += ':latest' unless @imagename.index(':') @image = DISABLED ? nil : Docker::Image.all.select{|i| i.info['RepoTags']&.include? @imagename}.first end def needed? return false if DISABLED ! @image || out_of_date?(timestamp) || @application.options.build_all end def timestamp return Time.now if DISABLED if @image = Docker::Image.all.select{|i| i.info['RepoTags'].include? @imagename}.first Time.at(@image.info['Created']) else Rake::EARLY end end end class DockerContainerTask < Rake::FileTask # This task checks whether a named container is running from a given image. # The name of the task is @ # The container name must match /?[a-zA-Z0-9_-]+ # # If a container is already running for this image, the creation time # is checked against the current image modification date. # If the container is not running or out-of-date it is restarted. # def initialize(task_name, app) super(task_name, app) @containername, sep, @imagename = task_name.partition('@') raise "Task #{task_name} must be name@image" if @containername.empty? or @imagename.empty? unless DISABLED @container = Docker::Container.get(@containername) rescue nil @imagename += ':latest' unless @imagename.index(':') @image = Docker::Image.all.select{|i| i.info['RepoTags'].include? @imagename}.first end end def needed? return false if DISABLED not @container or @container.info["Image"] != @image.id or not @container.info["State"]["Running"] or out_of_date?(timestamp) or @application.options.build_all end # Before the task actions are done, remove the container def execute(args) destroy_container("it is out-of-date") { out_of_date?(timestamp) } destroy_container("it uses the wrong image") { @container.info["Image"] != @image.id } destroy_container("it is not running") { not @container.info["State"]["Running"] } destroy_container("it exists!") { @container } super args end # Destroy the container if the block yields true def destroy_container msg, &block return if !@container or (block_given? and not yield) application.trace "** Prepare #{name} (destroying container: #{msg})" if application.options.trace return if application.options.dryrun # It may be hard to kill this container, try but if something goes wrong # then print a message and proceed with the action block. The start command # in there will fail in a more user-friendly way. begin @container.stop # @container.kill(signal: 'SIGTERM') better? @container.wait(10) @container.delete rescue Exception => e puts "Exception destroying container: #{e.to_s}" end @container = nil end def timestamp return Time.now if DISABLED if @container = Docker::Container.get(@containername) rescue nil Time.iso8601(@container.info['Created']) else Rake::EARLY end end end end