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", "Run a Docker container with user/home mapping (default is 'base')" method_option :image, type: :string, aliases: '-i', default: 'base', desc: 'Docker image' 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 = findimage options.image puts "Starting #{image} container..." me, setup_cmd = user_setup_cmd() args = ["-v /home/#{me}:/home/#{me}"] 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 COMMAND", "Run a command inside a Docker container" method_option :image, type: :string, aliases: '-i', default: 'base', desc: 'Docker image' method_option :env, type: :string, aliases: '-e', desc: 'Path to environment file' def exec(*command) image = findimage options.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 /home/#{me}:/home/#{me}"] 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 "rmimages", "Remove named Docker images (with optional pattern)" method_option :match, type: :string, aliases: '-m', default: '*', desc: 'Matching files' method_option :pattern, type: :string, aliases: '-p', default: nil, desc: 'Matching pattern (regex)' method_option :confirm, type: :boolean, aliases: '-c', default: true, desc: 'Confirm removal' def rmimages if options.pattern rx = Regexp.new(options.pattern) matcher = lambda {|s| rx.match(s)} else matcher = lambda {|s| File.fnmatch?(options.match, 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.confirm 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 if image.count('/') == 0 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 =~ %r[/#{image}] } if matching_images.size > 0 if image.count(':') == 0 matching_image = matching_images.select{|l| l =~ /:latest$/ }.first end unless matching_image matching_image = matching_images.first end else matching_image = nil end image = matching_image ? matching_image : "lightside/"+image end return 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 /home/#{user}) | cut -d: -f2) useradd -M -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