require "lux/version" require 'pathname' require 'highline/import' require 'shellwords' require 'thor' require 'uri' # The Thor subclass containing useful tasks shared between the Lux # standalone executable and Rake tasks. # class Lux::App < Thor desc "version", "Show the version" def version puts "Version #{Lux::VERSION}" end desc "check", "Check the integrity of the Git repositories" def check repobranch = `git symbolic-ref -q HEAD`.chomp.gsub(%r{^.*/},'') puts "Repository is on branch #{repobranch}" `git fetch --recurse-submodules=on-demand` nModuleErr = 0 nModuleWarn = 0 # Inspect the submodules currently checked out in turtle submodules = Hash[`git submodule status --recursive`.split(/\n/).map do |s| die "Bad submodule status #{s}" unless /^(?[-+U\s])(?[0-9a-f]{40})\s(?\S+)(\s*\((?\S+)\))?$/ =~ s case flag when '-' Highline.say "Submodule at #{path} is <%= color('not initialized', RED) %>!" nModuleWarn += 1 when '+' Highline.say "Submodule at #{path} is <%= color('not at correct commit', RED) %>!" nModuleErr += 1 when 'U' Highline.say "Submodule at #{path} is <%= color('conflicted', RED) %>!" nModuleErr += 1 else Highline.say "Submodule at #{path} is <%= color('OK', GREEN) %>" end [path, [flag, sha, ref]] end ] die "There were #{nModuleErr} submodule errors and #{nModuleWarn} warnings" if nModuleErr > 0 # If the submodule status (above) was good, then we can ignore any submodule issues here changes = `git status --porcelain`.split(/\n/).reject do |ch| die "Bad status #{ch}" unless /^(?.)(?.)\s(?\S+)( -> (?\S+))?$/ =~ ch submodules.include? path1 end if changes.size > 0 die "Repository is not clean (#{changes.size} issues), use 'git status'" else HighLine.say "<%= color('Repository is clean', GREEN) %>" end end desc "start IMAGE", "Run a Docker image with user/home mapping" method_option :env, type: :string, aliases: '-e', desc: 'Path to environment file' method_option :name, type: :string, aliases: '-n', default: '', desc: 'Docker container name' def start(image) image = findimage image raise "no image" if image.empty? puts "Starting #{image} container..." me, setup_cmd = user_setup_cmd() args = ["-v #{ENV['HOME']}:#{ENV['HOME']}"] args << "--env-file=#{options.env}" if options.env args << "--name=#{options.name}" unless options.name == '' cid = `docker run -dit #{args.join(' ')} #{image} /bin/bash`.strip die "Container failed to start" unless cid =~ /^[a-z0-9]+$/ system "docker exec #{cid} bash -c #{setup_cmd.shellescape}" puts "Type 'su [-] #{me}' then hit enter to attach to the container as yourself:" Kernel.exec "docker attach #{cid}" end EXCLUDE_VARS = %w{ _ HOME PWD TMPDIR SSH_AUTH_SOCK SHLVL DISPLAY Apple_PubSub_Socket_Render SECURITYSESSIONID XPC_SERVICE_NAME XPC_FLAGS __CF_USER_TEXT_ENCODING TERM_PROGRAM TERM_PROGRAM_VERSION TERM_SESSION_ID } desc "exec IMAGE COMMAND", "Run a command inside a Docker container" method_option :env, type: :string, aliases: '-e', desc: 'Path to environment file' def exec(image, *command) image = findimage image me, setup_cmd = user_setup_cmd die "You must be within your home directory!" unless relwd = Pathname.pwd.to_s.gsub!(/^#{ENV['HOME']}/,'~') command.map!{|m| m.start_with?('/') ? Pathname.new(m).relative_path_from(Pathname.pwd) : m } env = ENV.reject{|k,v| EXCLUDE_VARS.include? k or v =~/\s+/}.map{|k,v| "#{k}=#{v.shellescape}"} env += IO.readlines(options.env).grep(/^(?!#)/).map(&:rstrip) if options.env cmd = setup_cmd + "su - #{me} -c 'cd #{relwd}; env -i #{env.join(' ')} #{command.join(' ')}'" args = ["-v #{ENV['HOME']}:#{ENV['HOME']}"] system "docker run --rm #{args.join(' ')} #{image} /bin/bash -c #{cmd.shellescape}" end desc "clean", "Destroy all exited containers" def clean exited = `docker ps -q -f status=exited`.gsub! /\n/,' ' if exited and not exited.empty? system "docker rm #{exited}" else puts "No exited containers" end end desc "tidy", "Remove dangling Docker images" def tidy images = `docker images -f "dangling=true" -q` if images.size > 0 system 'docker rmi $(docker images -f "dangling=true" -q)' else puts "No dangling images" end end desc "lsimages [PATTERN]", "List Docker images (with optional pattern)" method_option :regex, type: :boolean, aliases: '-r', default: false, desc: 'Pattern is a regular expression' def lsimages(pattern=nil) if options.regex pattern = '.*' unless pattern rx = Regexp.new(pattern) matcher = lambda {|s| rx.match(s)} else pattern = '*' unless pattern matcher = lambda {|s| File.fnmatch?(pattern, s)} end imagelines = `docker images`.split("\n")[1..-1].sort imagelines.each do |imageline| imageinfo = imageline.split(/\s+/) next if imageinfo[0] == '' imagename = imageinfo[0]+':'+imageinfo[1] imagesize = imageinfo[-2]+' '+imageinfo[-1] imageage = imageinfo[3..-3].join(' ') next unless matcher.call(imagename) printf "%-54s%-16s%10s\n", imagename, imageage, imagesize end end desc "rmimages [PATTERN]", "Remove Docker images (with optional pattern)" method_option :regex, type: :boolean, aliases: '-r', default: false, desc: 'Pattern is a regular expression' method_option :force, type: :boolean, aliases: '-f', default: false, desc: 'Do not prompt for confirmation' def rmimages(pattern=nil) if options.regex pattern = '.*' unless pattern rx = Regexp.new(pattern) matcher = lambda {|s| rx.match(s)} else pattern = '*' unless pattern matcher = lambda {|s| File.fnmatch?(pattern, s)} end imagelines = `docker images`.split("\n")[1..-1] imagelines.each do |imageline| imageinfo = imageline.split(/\s+/) next if imageinfo[0] == '' imagename = imageinfo[0]+':'+imageinfo[1] imagesize = imageinfo[-2]+' '+imageinfo[-1] imageage = imageinfo[3..-3].join(' ') next unless matcher.call(imagename) if options.force or (agree("Delete #{imagename} (#{imageage}, #{imagesize})? "){|q|q.echo=true}) `docker rmi #{imageinfo[2]}` HighLine.say "Image <%= color('#{imagename}', RED)%> deleted" end end end desc "clobber", "Destroy all containers (even if running!)" def clobber clean running = `docker ps -q -f status=running`.gsub! /\n/,' ' if running and not running.empty? system "docker rm -f #{running}" else puts "No running containers" end end desc "dockerip", "Show DOCKER_HOST IP address" def dockerip docker_url = URI(ENV['DOCKER_HOST']) name = docker_url.host if z = Socket.gethostbyname(name) rescue nil z.shift 3 z.each do |a| name = a.unpack('CCCC').join('.') if a.size == 4 end end puts "DOCKER_HOST=#{docker_url.to_s}" docker_url.host = name puts "export DOCKER_HOST=#{docker_url.to_s}" end private # Get the current list of images and make a guess at which one it is... # def findimage image local_images = `docker images`.strip.split("\n")[1..-1].map{|l| l.gsub!(/^(\S+)\s+(\S+).*/,'\1:\2')}.sort matching_images = local_images.select{|l| l.include? image } if matching_images.size > 0 if image.count(':') == 0 and image.count('/') > 0 matching_image = matching_images.select{|l| l.end_with? ':latest' }.first end unless matching_image matching_image = HighLine.choose do |menu| menu.header = 'List of matching (local) images' menu.choices(*matching_images) menu.choice('None of the above') { nil } end exit 2 unless matching_image end else matching_image = image end return matching_image end # Return two elements: # - user name (defaults to current user), and # - a bash script setup command # def user_setup_cmd user = `id -nu`.strip [user, <<-COMMAND.gsub(/^\s*/,'').gsub(/\n/,' ; ')] uid=$(echo $(stat -c %u:%g #{ENV['HOME']}) | cut -d: -f2) useradd -M -d #{ENV['HOME']} -u $uid -s #{ENV['SHELL']} #{user} echo "#{user} ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/#{user} COMMAND end def die msg, rc = 1 HighLine.say HighLine.color(msg, HighLine::RED) exit(rc) end end # vim: ft=ruby sts=2 sw=2 ts=8