diff --git a/Gemfile.lock b/Gemfile.lock index 8f8b856..ce79eef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,14 +2,31 @@ PATH remote: . specs: boxen (0.0.0) + json_pure + octokit GEM remote: http://rubygems.org/ specs: + addressable (2.3.2) + faraday (0.8.4) + multipart-post (~> 1.1) + faraday_middleware (0.8.8) + faraday (>= 0.7.4, < 0.9) + hashie (1.2.0) + json_pure (1.7.5) metaclass (0.0.1) minitest (3.5.0) mocha (0.12.6) metaclass (~> 0.0.1) + multi_json (1.3.6) + multipart-post (1.1.5) + octokit (1.15.1) + addressable (~> 2.2) + faraday (~> 0.8) + faraday_middleware (~> 0.8) + hashie (~> 1.2) + multi_json (~> 1.3) PLATFORMS ruby diff --git a/boxen.gemspec b/boxen.gemspec index 2717d4d..ddef975 100644 --- a/boxen.gemspec +++ b/boxen.gemspec @@ -13,6 +13,9 @@ Gem::Specification.new do |gem| gem.test_files = gem.files.grep /^test/ gem.require_paths = ["lib"] + gem.add_dependency "json_pure" + gem.add_dependency "octokit" + gem.add_development_dependency "minitest", "3.5.0" gem.add_development_dependency "mocha" end diff --git a/lib/boxen/config.rb b/lib/boxen/config.rb new file mode 100644 index 0000000..b08700c --- /dev/null +++ b/lib/boxen/config.rb @@ -0,0 +1,235 @@ +require "fileutils" +require "json" +require "octokit" +require "boxen/project" + +module Boxen + + # All configuration for Boxen, whether it's loaded from command-line + # args, environment variables, config files, or the keychain. + + class Config + + # The service name to use when loading/saving config in the Keychain. + + KEYCHAIN_SERVICE = "Boxen" + + # Load config. Yields config if `block` is given. + + def self.load(&block) + new do |config| + home = ENV["GH_HOME"] || "/opt/boxen" + file = "#{home}/config/boxen/defaults.json" + + if File.file? file + attrs = JSON.parse File.read file + + attrs.each do |key, value| + if value && config.respond_to?(selector = "#{key}=") + config.send selector, value + end + end + end + + cmd = "security find-generic-password " + + "-a #{config.user} -s '#{KEYCHAIN_SERVICE}' -w 2>/dev/null" + + password = `#{cmd}`.strip + password = nil unless $?.success? + + config.password = password + + yield config if block_given? + end + end + + # Save `config`. Returns `config`. Note that this only saves data, + # not flags. For example, `login` will be saved, but `stealth?` + # won't. + + def self.save(config) + attrs = { + :email => config.email, + :homedir => config.homedir, + :login => config.login, + :name => config.name, + :srcdir => config.srcdir, + :user => config.user + } + + file = "#{config.homedir}/config/boxen/defaults.json" + FileUtils.mkdir_p File.dirname file + + File.open file, "wb" do |f| + f.write JSON.generate Hash[attrs.reject { |k, v| v.nil? }] + end + + cmd = ["security", "add-generic-password", + "-a", config.user, "-s", KEYCHAIN_SERVICE, "-U", "-w", config.password] + + unless system *cmd + raise Boxen::Error, "Can't save config in the Keychain." + end + + config + end + + # Create a new instance. Yields `self` if `block` is given. + + def initialize(&block) + @fde = true + @pull = true + + yield self if block_given? + end + + # Create an API instance using the current user creds. A new + # instance is created any time `login` or `password` change. + + def api + @api ||= Octokit::Client.new :login => login, :password => password + end + + # Spew a bunch of debug logging? Default is `false`. + + def debug? + !!@debug + end + + attr_writer :debug + + # A GitHub user's public email. + + attr_accessor :email + + # The shell script that loads Boxen's environment. + + def envfile + "#{homedir}/env.sh" + end + + # Is full disk encryption required? Default is `true`. Respects + # the `BOXEN_NO_FDE` environment variable. + + def fde? + !ENV["BOXEN_NO_FDE"] && @fde + end + + attr_writer :fde + + # Boxen's home directory. Default is `"/opt/boxen"`. Respects the + # `BOXEN_HOME` environment variable. + + def homedir + @homedir || ENV["BOXEN_HOME"] || "/opt/boxen" + end + + attr_writer :homedir + + # Boxen's log file. Default is `"/tmp/boxen.log"`. Respects the + # `BOXEN_LOG_FILE` environment variable. + + def logfile + @logfile || ENV["BOXEN_LOG_FILE"] || "/tmp/boxen.log" + end + + attr_writer :logfile + + # A GitHub user login. Default is `nil`. + + attr_reader :login + + def login=(login) + @api = nil + @login = login + end + + # Is Boxen running on the `master` branch? + + def master? + `git symbolic-ref HEAD`.chomp == "refs/heads/master" + end + + # A GitHub user's profile name. + + attr_accessor :name + + # A GitHub user password. Default is `nil`. + + attr_reader :password + + def password=(password) + @api = nil + @password = password + end + + # Just go through the motions? Default is `false`. + + def pretend? + !!@pretend + end + + attr_writer :pretend + + # Run a profiler on Puppet? Default is `false`. + + def profile? + !!@profile + end + + attr_writer :profile + + # Dirty tree? + def dirty? + changes.empty? + end + + def changes + `git status --porcelain`.strip + end + + # An Array of Boxen::Project entries, one for each project Boxen + # knows how to manage. + # + # FIX: Revisit this once we restructure template projects. It's + # broken for several reasons: It assumes paths that won't be + # right, and it assumes projects live in the same repo as this + # file. + + def projects + root = File.expand_path "../../..", __FILE__ + files = Dir["modules/github/manifests/projects/*.pp"] + names = (files.map { |m| File.basename m, ".pp" } - %w(all)).sort + + names.map do |name| + Boxen::Project.new "#{srcdir}/#{name}" + end + end + + # The directory where repos live. Default is + # `"/Users/#{user}/src"`. + + def srcdir + @srcdir || "/Users/#{user}/src" + end + + attr_writer :srcdir + + # Don't auto-create issues on failure? Default is `false`. + # Respects the `BOXEN_NO_ISSUE` environment variable. + + def stealth? + !!ENV["BOXEN_NO_ISSUE"] || @stealth + end + + attr_writer :stealth + + # A local user login. Default is the `USER` environment variable. + + def user + @user || ENV["USER"] + end + + attr_writer :user + end +end diff --git a/test/boxen_config_test.rb b/test/boxen_config_test.rb new file mode 100644 index 0000000..b9874c4 --- /dev/null +++ b/test/boxen_config_test.rb @@ -0,0 +1,139 @@ +require "boxen/test" +require "boxen/config" + +class BoxenConfigTest < Boxen::Test + def setup + @config = Boxen::Config.new + end + + def test_debug? + refute @config.debug? + + @config.debug = true + assert @config.debug? + end + + def test_email + assert_nil @config.email + + @config.email = "foo" + assert_equal "foo", @config.email + end + + def test_fde? + assert @config.fde? + + @config.fde = false + refute @config.fde? + end + + def test_fde_env_var + ENV.expects(:[]).with("BOXEN_NO_FDE").returns "1" + refute @config.fde? + end + + def test_homedir + assert_equal "/opt/boxen", @config.homedir + + @config.homedir = "foo" + assert_equal "foo", @config.homedir + end + + def test_homedir_env_var_boxen_home + ENV.expects(:[]).with("BOXEN_HOME").returns "foo" + assert_equal "foo", @config.homedir + end + +def test_initialize + config = Boxen::Config.new do |c| + c.homedir = "foo" + end + + assert_equal "foo", config.homedir + end + + def test_logfile + assert_equal "/tmp/boxen.log", @config.logfile + + @config.logfile = "foo" + assert_equal "foo", @config.logfile + end + + def test_logfile_env_var + ENV.expects(:[]).with("BOXEN_LOG_FILE").returns "foo" + assert_equal "foo", @config.logfile + end + + def test_login + assert_nil @config.login + + @config.login = "foo" + assert_equal "foo", @config.login + end + + def test_name + assert_nil @config.name + + @config.name = "foo" + assert_equal "foo", @config.name + end + + def test_password + assert_nil @config.password + + @config.password = "foo" + assert_equal "foo", @config.password + end + + def test_pretend? + refute @config.pretend? + + @config.pretend = true + assert @config.pretend? + end + + def test_profile? + refute @config.profile? + + @config.profile = true + assert @config.profile? + end + + def test_projects + root = File.expand_path "../..", __FILE__ + files = Dir["#{root}/modules/github/manifests/projects/*.pp"] + + # all.pp is autogenerated + files.reject! { |f| File.basename(f) == "all.pp" } + + assert_equal files.size, @config.projects.size + end + + def test_srcdir + @config.expects(:user).returns "foo" + assert_equal "/Users/foo/src", @config.srcdir + + @config.srcdir = "elsewhere" + assert_equal "elsewhere", @config.srcdir + end + + def test_stealth? + refute @config.stealth? + + @config.stealth = true + assert @config.stealth? + end + + def test_stealth_env_var + ENV.expects(:[]).with("BOXEN_NO_ISSUE").returns "1" + assert @config.stealth? + end + + def test_user + ENV.expects(:[]).with("USER").returns "foo" + assert_equal "foo", @config.user + + @config.user = "bar" + assert_equal "bar", @config.user + end +end diff --git a/test/system_timer.rb b/test/system_timer.rb new file mode 100644 index 0000000..364620f --- /dev/null +++ b/test/system_timer.rb @@ -0,0 +1,10 @@ +# THIS IS SUCH HAX. Faraday helpfully reminds you to install +# `system_timer` if you're running Ruby 1.8, since Timeout can give +# unreliable results. We can't do this during first-time runs, since +# there's no C compiler available. +# +# To squash the message and stop confusing people, this shim just +# exposes Timeout as SystemTimer. I'm a bad person. + +require "timeout" +SystemTimer = Timeout