require 'picsolve_docker_builder/helpers/ssh_auth_forwarding' require 'picsolve_docker_builder/composer/registry' require 'picsolve_docker_builder/base' require 'docker' require 'logger' require 'psych' module PicsolveDockerBuilder # Docker image building template # rubocop:disable Metrics/ClassLength class Frame include PicsolveDockerBuilder::Base def initialize # TODO: @christiansimon please report that upstream, second time # i am facing that bug Docker.options[:read_timeout] = 3600 Excon.defaults[:read_timeout] = 3600 config end def default_config { 'docker' => {} } end def execute(cmd) r = container.exec(cmd) fail "Execution of cmd=#{cmd} failed" unless r[2] == 0 log.debug "executed container id=#{container.id} cmd=#{cmd} result=#{r}" r end def execute_attach(cmd) log.debug "execute and attach container id=#{container.id} cmd=#{cmd}" r = container.exec( cmd ) do |_stream, chunk| $stdout.write chunk $stdout.flush end fail "Execution of cmd=#{cmd} failed" unless r[2] == 0 log.debug 'executed and attached container ' \ "id=#{container.id} cmd=#{cmd} exitcode=#{r[2]}" end def build_mode return :template unless image_name.nil? return :dockerfile if dockerfile_exists? log.fatal 'No image_name configured and no Dockerfile present' end def dockerfile_hooks_docker_build_early config['docker']['dockerfile_hooks']['docker_build']['early'] rescue NoMethodError '' end def dockerfile_hooks_docker_build_late config['docker']['dockerfile_hooks']['docker_build']['late'] rescue NoMethodError '' end def dockerfile_hooks_asset_build_early config['docker']['dockerfile_hooks']['asset_build']['early'] rescue NoMethodError '' end def dockerfile_hooks_asset_build_late config['docker']['dockerfile_hooks']['asset_build']['late'] rescue NoMethodError '' end def jenkins_build_number n = ENV['BUILD_NUMBER'] return nil if n.nil? n.to_i end def tags t = ['latest'] t << "jenkins-#{jenkins_build_number}" unless jenkins_build_number.nil? t end def tag fail 'No image found to tag' if @docker_build.nil? tags.each do |tag| log.info "tagging image with #{dest_image_name}:#{tag}" @docker_build.tag( repo: dest_image_name, tag: tag, force: true ) end end def push fail 'No image found to be pushed' if @docker_build.nil? tags.each do |tag| repotag = "#{dest_image_name}:#{tag}" log.info "pushing image with #{repotag}" @docker_build.push( Composer::Registry.creds, tag: tag ) do |resp| resp = JSON.parse resp if resp.key? 'errorDetail' message = "pushing image #{repotag} failed: #{resp['errorDetail']}" log.fatal message fail message end log.info "status pushing image #{repotag}: #{resp['status']}" \ if resp.key? 'status' end end end def docker_build_build Docker::Image.build_from_dir(base_dir) do |stream| s = JSON.parse(stream)['stream'] log.debug s.strip unless s.nil? end rescue StandardError => e log.fatal "docker building failed: #{e}" exit 1 end def docker_build dockerfile log.info "start docker image building with path #{base_dir}" @docker_build = docker_build_build end def asset_build end def build asset_build if dest_image_name.nil? log.info 'Skip building docker image as no dest_image is set' return end docker_build end def dockerfile_path File.join(base_dir, 'Dockerfile') end def dockerfile return unless build_mode == :template File.open('Dockerfile', 'w') do |file| dockerfile_template.each_line do |line| log.debug "Dockerfile: #{line.strip}" end file.write(dockerfile_template) end end def dockerfile_exists? File.exist? dockerfile_path end def dockerfile_template fail NotImplementedError end def dockerignore_template fail NotImplementedError end def validate_config(c) validate_config_docker(c) end def validate_config_docker(c) c end def start container.start('Binds' => volumes) log.debug "started container id=#{container.id} volumes=#{volumes}" end def stop return if @container.nil? container.stop log.debug "stopped container id=#{container.id}" container.remove log.debug "removed container id=#{container.id}" @container = nil end def volume_workspace [ base_dir, build_dir ] end def volumes_array volumes = [volume_workspace] volumes << volume_ssh_auth_forwarding if ssh_auth_forwarding? volumes end def volumes volumes_array.map do |volume| volume.join ':' end end def build_user_uid Process.uid.to_s end def build_user_home '/home/build' end def build_user 'build' end def build_dir '/_build' end def container @container ||= create_container end def environment blacklist = %w( SSH_CLIENT SSH_CONNECTION LD_LIBRARY_PATH PATH NVM_DIR NVM_NODEJS_ORG_MIRROR) keys = ENV.keys keys = keys.reject do |key| blacklist.include? key end env = keys.map do |key| "#{key}=#{ENV[key]}" end env << "SSH_AUTH_SOCK=#{ssh_auth_forwarding_path}" if ssh_auth_forwarding? env end def create_container command = ['/bin/sleep', '3600'] c = Docker::Container.create( 'Image' => asset_image.id, 'Cmd' => command, 'OpenStdin' => false, 'WorkingDir' => '/_build', 'Env' => environment ) at_exit do stop end log.debug "created a new container image=#{image_name} " \ "id=#{c.id} cmd=#{command}" c end def dest_image_name config['docker']['image_name'] end def runtime_image_name name = config['docker']['runtime_image'] || image_name if name.match(/:[a-z0-9\-_]+$/) name else "#{name}:latest" end rescue NoMethodError nil end def image_name name = config['docker']['base_image'] if name.match(/:[a-z0-9\-_]+$/) name else "#{name}:latest" end rescue NoMethodError nil end def ssh_auth_forwarding? return true if config['docker']['ssh_auth_forwarding'] false end def ssh_auth_forwarding_dockerfile return "\n" unless ssh_auth_forwarding? # add ssh known hosts to the image if using forwards "ADD ssh_known_hosts #{File.join(build_user_home, '.ssh/known_hosts')}" end def ssh_auth_forwarding_path '/tmp/ssh_auth_sock/ssh_auth_sock' end def volume_ssh_auth_forwarding @ssh_auth_forwarding = Helpers::SshAuthForwarding.new [ @ssh_auth_forwarding.dir, File.dirname(ssh_auth_forwarding_path) ] end def ssh_known_hosts File.open(File.join(Dir.home, '.ssh/known_hosts')).read end def asset_image_dockerfile [ "FROM #{image_name}", 'MAINTAINER Picsolve Onlineops ', dockerfile_hooks_asset_build_early, "RUN useradd -m -d #{build_user_home} \\", " -u #{build_user_uid} #{build_user}", "ADD .gitconfig #{File.join(build_user_home, '.gitconfig')}", ssh_auth_forwarding_dockerfile, "RUN chown -cR #{build_user} #{build_user_home}", dockerfile_hooks_asset_build_late ] end def asset_image_build tar_contents = { 'Dockerfile' => asset_image_dockerfile.join("\n"), '.gitconfig' => [ '[user]', 'name = Jenkins London Picsolve', 'email = jenkins@picsolve.com' ].join("\n") } tar_contents['ssh_known_hosts'] = ssh_known_hosts if ssh_auth_forwarding? tar = StringIO.new Docker::Util.create_tar(tar_contents) begin Docker::Image.build_from_tar(tar) do |stream| s = JSON.parse(stream)['stream'] log.debug s.strip unless s.nil? end rescue StandardError => e log.fatal "asset building failed: #{e}" exit 1 end end def fetch_asset_image log.debug "pulling image '#{image_name}' from registry" Docker::Image.create( { 'fromImage' => image_name }, Composer::Registry.creds ) log.debug "building asset image from '#{image_name}'" asset_image_build end def asset_image @asset_image ||= fetch_asset_image end def create_logger log = Logger.new(STDOUT) log.level = Logger::DEBUG log end def log @logger ||= create_logger end end end