require "lux/version" require 'pathname' require 'shellwords' require 'thor' require 'uri' class Lux::App < Thor # The Thor subclass containing useful tasks shared between the Lux # standalone executable and Rake tasks. # 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 '-' puts "Submodule at #{path} is not initialized!" nModuleWarn += 1 when '+' puts "Submodule at #{path} is not at correct commit!" nModuleErr += 1 when 'U' puts "Submodule at #{path} is conflicted!" nModuleErr += 1 else puts "Submodule at #{path} OK" 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 puts "Repository is clean" 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 "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 dockerenv 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 STDERR.puts msg exit(rc) end end # vim: ft=ruby sts=2 sw=2 ts=8