require 'lux/version' require 'highline/import' require 'pathname' require 'shellwords' require 'thor' # 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 current Git repository" 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 any submodules currently checked out submodules = Hash[`git submodule status --recursive`.split(/\n/).map do |s| Lux.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 ] Lux.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| Lux.die "Bad status #{ch}" unless /^(?.)(?.)\s(?\S+)( -> (?\S+))?$/ =~ ch submodules.include? path1 end if changes.size > 0 Lux.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' method_option :port, type: :string, aliases: '-p', default: nil, desc: 'Expose ports (default all)' def start(image) image = Lux.findimage image raise "no image" if image.empty? puts "Starting #{image} container..." me, setup_cmd = Lux.user_setup_cmd() args = ["-v #{ENV['HOME']}:#{ENV['HOME']}"] args << "--env-file=#{options.env}" if options.env args << "--name=#{options.name}" unless options.name == '' args << (options.port ? "-p #{options.port}" : "-P") cid = `docker run -dit #{args.join(' ')} #{image} /bin/bash`.strip Lux.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 = Lux.findimage image me, setup_cmd = user_setup_cmd Lux.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 #{options.force ? '-f':''} #{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 # This was originally designed to allow the easy export of an IPv4 address URL for Docker # as early Docker implementations did not honor .local addresses on macOS # desc "dockerip", "Show DOCKER_HOST IP address in exportable format. Use $(lux dockerip) to set it" def dockerip name, uri, info = Lux.dockerip return Lux.info "Docker is not available" unless name var = 'DOCKER_HOST' case uri.scheme when 'tcp' uri.host = name if STDOUT.isatty Lux.info "Please export: #{var}=#{uri.to_s}" Lux.info "(You can use '$(lux dockerip)' to do this)" else Lux.info "Exported: #{var}=#{uri.to_s}" puts "export #{var}=#{uri.to_s}" end when 'unix' Lux.info "Docker is running on a Unix socket" end Lux.info "Version #{info['Version']}, OS: #{info['Os']}, Arch: #{info['Arch']}, Kernel: #{info['KernelVersion']}" if info end end # vim: ft=ruby sts=2 sw=2 ts=8