boxen23 3.1.3a

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE +20 -0
  6. data/README.md +50 -0
  7. data/boxen.gemspec +29 -0
  8. data/lib/boxen/check.rb +72 -0
  9. data/lib/boxen/checkout.rb +25 -0
  10. data/lib/boxen/cli.rb +61 -0
  11. data/lib/boxen/config.rb +345 -0
  12. data/lib/boxen/error.rb +4 -0
  13. data/lib/boxen/flags.rb +291 -0
  14. data/lib/boxen/hook.rb +47 -0
  15. data/lib/boxen/hook/github_issue.rb +120 -0
  16. data/lib/boxen/hook/web.rb +56 -0
  17. data/lib/boxen/keychain.rb +61 -0
  18. data/lib/boxen/postflight.rb +13 -0
  19. data/lib/boxen/postflight/active.rb +16 -0
  20. data/lib/boxen/postflight/env.rb +29 -0
  21. data/lib/boxen/preflight.rb +13 -0
  22. data/lib/boxen/preflight/creds.rb +149 -0
  23. data/lib/boxen/preflight/directories.rb +32 -0
  24. data/lib/boxen/preflight/etc_my_cnf.rb +12 -0
  25. data/lib/boxen/preflight/identity.rb +16 -0
  26. data/lib/boxen/preflight/os.rb +33 -0
  27. data/lib/boxen/preflight/rbenv.rb +12 -0
  28. data/lib/boxen/preflight/rvm.rb +12 -0
  29. data/lib/boxen/project.rb +20 -0
  30. data/lib/boxen/puppeteer.rb +122 -0
  31. data/lib/boxen/runner.rb +149 -0
  32. data/lib/boxen/service.rb +58 -0
  33. data/lib/boxen/util.rb +19 -0
  34. data/lib/facter/boxen.rb +34 -0
  35. data/script/Boxen +0 -0
  36. data/script/bootstrap +7 -0
  37. data/script/build-keychain-helper +6 -0
  38. data/script/release +38 -0
  39. data/script/tests +10 -0
  40. data/src/keychain-helper.c +85 -0
  41. data/test/boxen/test.rb +7 -0
  42. data/test/boxen_check_test.rb +55 -0
  43. data/test/boxen_checkout_test.rb +42 -0
  44. data/test/boxen_cli_test.rb +39 -0
  45. data/test/boxen_config_test.rb +400 -0
  46. data/test/boxen_directories_test.rb +40 -0
  47. data/test/boxen_flags_test.rb +223 -0
  48. data/test/boxen_hook_github_issue_test.rb +294 -0
  49. data/test/boxen_hook_web_test.rb +58 -0
  50. data/test/boxen_keychain_test.rb +21 -0
  51. data/test/boxen_postflight_active_test.rb +29 -0
  52. data/test/boxen_postflight_env_test.rb +6 -0
  53. data/test/boxen_preflight_creds_test.rb +177 -0
  54. data/test/boxen_preflight_etc_my_cnf_test.rb +10 -0
  55. data/test/boxen_preflight_rvm_test.rb +10 -0
  56. data/test/boxen_project_test.rb +14 -0
  57. data/test/boxen_puppeteer_test.rb +101 -0
  58. data/test/boxen_runner_test.rb +171 -0
  59. data/test/boxen_service_test.rb +39 -0
  60. data/test/boxen_util_test.rb +21 -0
  61. data/test/fixtures/repo/modules/projects/manifests/first-project.pp +0 -0
  62. data/test/fixtures/repo/modules/projects/manifests/second-project.pp +0 -0
  63. metadata +257 -0
@@ -0,0 +1,56 @@
1
+ require "boxen/hook"
2
+ require "json"
3
+ require "net/http"
4
+
5
+ module Boxen
6
+ class Hook
7
+ class Web < Hook
8
+ def perform?
9
+ enabled?
10
+ end
11
+
12
+ private
13
+ def call
14
+ payload = {
15
+ :login => config.user,
16
+ :sha => checkout.sha,
17
+ :status => result.success? ? 'success' : 'failure',
18
+ :time => "#{Time.now.utc.to_i}"
19
+ }
20
+
21
+ post_web_hook payload
22
+ end
23
+
24
+ def post_web_hook(payload)
25
+ headers = { 'Content-Type' => 'application/json' }
26
+
27
+ uri = URI.parse(URI.escape(ENV['BOXEN_WEB_HOOK_URL']))
28
+
29
+ user, pass, host, port, path = \
30
+ uri.user, uri.pass, uri.host, uri.port, uri.path
31
+
32
+ request = Net::HTTP::Post.new(path, initheader = headers)
33
+
34
+ if uri.scheme =~ /https/
35
+ http.use_ssl = true
36
+ end
37
+
38
+ if user && pass
39
+ request.basic_auth user, pass
40
+ end
41
+
42
+ request.body = payload.to_json
43
+
44
+ response = Net::HTTP.new(host, port).start do |http|
45
+ http.request(request)
46
+ end
47
+ end
48
+
49
+ def required_environment_variables
50
+ ['BOXEN_WEB_HOOK_URL']
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ Boxen::Hook.register Boxen::Hook::Web
@@ -0,0 +1,61 @@
1
+ require "boxen/error"
2
+ require "shellwords"
3
+
4
+ module Boxen
5
+ class Keychain
6
+
7
+ # The keychain proxy we use to provide isolation and a friendly
8
+ # message in security prompts.
9
+
10
+ HELPER = File.expand_path "../../../script/Boxen", __FILE__
11
+
12
+ # The service name to use when loading/saving passwords.
13
+
14
+ PASSWORD_SERVICE = "GitHub Password"
15
+
16
+ # The service name to use when loading/saving API keys.
17
+
18
+ TOKEN_SERVICE = "GitHub API Token"
19
+
20
+ def initialize(login)
21
+ @login = login
22
+ # Clear the password. We're storing tokens now.
23
+ set PASSWORD_SERVICE, ""
24
+ end
25
+
26
+ def token
27
+ get TOKEN_SERVICE
28
+ end
29
+
30
+ def token=(token)
31
+ set TOKEN_SERVICE, token
32
+ end
33
+
34
+ protected
35
+
36
+ attr_reader :login
37
+
38
+ def get(service)
39
+ return nil if RUBY_PLATFORM !~ /darwin/
40
+ cmd = shellescape(HELPER, service, login)
41
+
42
+ result = `#{cmd}`.strip
43
+ $?.success? ? result : nil
44
+ end
45
+
46
+ def set(service, token)
47
+ return nil if RUBY_PLATFORM !~ /darwin/
48
+ cmd = shellescape(HELPER, service, login, token)
49
+
50
+ unless system *cmd
51
+ raise Boxen::Error, "Can't save #{service} in the keychain."
52
+ end
53
+
54
+ token
55
+ end
56
+
57
+ def shellescape(*args)
58
+ args.map { |s| Shellwords.shellescape s }.join " "
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ require "boxen/check"
2
+
3
+ module Boxen
4
+
5
+ # The superclass for postflight checks.
6
+
7
+ class Postflight < Boxen::Check
8
+
9
+ # Load all available postflight checks.
10
+
11
+ register File.expand_path("../postflight", __FILE__)
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ require "boxen/postflight"
2
+ require "boxen/util"
3
+
4
+ # Checks to see if the basic environment is loaded.
5
+
6
+ class Boxen::Postflight::Active < Boxen::Postflight
7
+ def ok?
8
+ Boxen::Util.active?
9
+ end
10
+
11
+ def run
12
+ warn "You haven't loaded Boxen's environment yet!",
13
+ "To permanently fix this, source #{config.envfile} at the end",
14
+ "of your shell's startup file."
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ require "boxen/postflight"
2
+
3
+ class Boxen::Postflight::Env < Boxen::Postflight
4
+
5
+ # Calculate an MD5 checksum for the current environment.
6
+
7
+ def self.checksum
8
+
9
+ # We can't get this from config 'cause it's static (gotta happen
10
+ # on load), and BOXEN_HOME might not be set.
11
+
12
+ home = ENV["BOXEN_HOME"] || "/opt/boxen"
13
+ return unless File.file? "#{home}/env.sh"
14
+
15
+ `find #{home}/env* -type f 2>&1 | sort | xargs /sbin/md5 | /sbin/md5 -q`.strip
16
+ end
17
+
18
+ # The checksum when this file was loaded.
19
+
20
+ CHECKSUM = self.checksum
21
+
22
+ def ok?
23
+ self.class.checksum == CHECKSUM
24
+ end
25
+
26
+ def run
27
+ warn "Run source #{config.envfile} or restart your shell for new stuff!"
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ require "boxen/check"
2
+
3
+ module Boxen
4
+
5
+ # The superclass for preflight checks.
6
+
7
+ class Preflight < Boxen::Check
8
+
9
+ # Load all available preflight checks.
10
+
11
+ register File.expand_path("../preflight", __FILE__)
12
+ end
13
+ end
@@ -0,0 +1,149 @@
1
+ require "boxen/preflight"
2
+ require "highline"
3
+ require "octokit"
4
+ require "digest"
5
+ require "socket"
6
+
7
+ # HACK: Unless this is `false`, HighLine has some really bizarre
8
+ # problems with empty/expended streams at bizarre intervals.
9
+
10
+ HighLine.track_eof = false
11
+
12
+ class Boxen::Preflight::Creds < Boxen::Preflight
13
+ attr :otp
14
+ attr :password
15
+
16
+ def ok?
17
+ if config.token && config.api.user
18
+ # There was a period of time when login wasn't geting set on first run.
19
+ # This should correct that.
20
+ config.login = config.api.user.login
21
+ true
22
+ end
23
+ rescue
24
+ nil
25
+ end
26
+
27
+ def tmp_api
28
+ @tmp_api ||= Octokit::Client.new :login => config.login, :password => password, :auto_paginate => true
29
+ end
30
+
31
+ def headers
32
+ otp.nil? ? {} : {"X-GitHub-OTP" => otp}
33
+ end
34
+
35
+ def get_otp
36
+ console = HighLine.new
37
+
38
+ # junk API call to send OTP until we implement PUT
39
+ tmp_api.create_authorization rescue nil
40
+
41
+ @otp = console.ask "One time password (via SMS or device):" do |q|
42
+ q.echo = '*'
43
+ end
44
+ end
45
+
46
+ # Attempt to use the username+password to get a list of the user's OAuth
47
+ # authorizations from the API. If it fails because of 2FA, ask the user for
48
+ # her OTP and try again.
49
+ #
50
+ # Returns a list of authorizations
51
+ def get_tokens
52
+ begin
53
+ tmp_api.authorizations(:headers => headers)
54
+ rescue Octokit::Unauthorized
55
+ abort "Sorry, I can't auth you on GitHub.",
56
+ "Please check your credentials and teams and give it another try."
57
+ rescue Octokit::OneTimePasswordRequired
58
+ puts
59
+ if otp.nil?
60
+ warn "It looks like you have two-factor auth enabled."
61
+ else
62
+ warn "That one time password didn't work. Let's try again."
63
+ end
64
+ get_otp
65
+ get_tokens
66
+ end
67
+ end
68
+
69
+ def run
70
+ fetch_login_and_password
71
+ tokens = get_tokens
72
+
73
+ # Boxen now supports the updated GitHub Authorizations API by using a unique
74
+ # `fingerprint` for each Boxen installation for a user. We delete any older
75
+ # authorization that does not make use of `fingerprint` so that the "legacy"
76
+ # authorization doesn't persist in the user's list of personal access
77
+ # tokens.
78
+ legacy_auth = tokens.detect { |a| a.note == "Boxen" && a.fingerprint == nil }
79
+ tmp_api.delete_authorization(legacy_auth.id, :headers => headers) if legacy_auth
80
+
81
+ # The updated GitHub authorizations API, in order to improve security, no
82
+ # longer returns a plaintext `token` for existing authorizations. So, if an
83
+ # authorization already exists for this machine we need to first delete it
84
+ # so that we can create a new one.
85
+ auth = tokens.detect { |a| a.note == note && a.fingerprint == fingerprint }
86
+ tmp_api.delete_authorization(auth.id, :headers => headers) if auth
87
+
88
+ auth = tmp_api.create_authorization(
89
+ :note => note,
90
+ :scopes => %w(repo user),
91
+ :fingerprint => fingerprint,
92
+ :headers => headers
93
+ )
94
+
95
+ config.token = auth.token
96
+
97
+ unless ok?
98
+ puts
99
+ abort "Something went terribly wrong.",
100
+ "I was able to get your OAuth token, but was unable to use it."
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def fetch_login_and_password
107
+ console = HighLine.new
108
+
109
+ config.login = fetch_from_env("login") || console.ask("GitHub login: ") do |q|
110
+ q.default = config.login || config.user
111
+ q.validate = /\A[^@]+\Z/
112
+ end
113
+
114
+ @password = fetch_from_env("password") || console.ask("GitHub password: ") do |q|
115
+ q.echo = "*"
116
+ end
117
+ end
118
+
119
+ def fetch_from_env(thing)
120
+ key = "BOXEN_GITHUB_#{thing.upcase}"
121
+ return unless found = ENV[key]
122
+ warn "Oh, looks like you've provided your #{thing} as environmental variable..."
123
+ found
124
+ end
125
+
126
+ def fingerprint
127
+ @fingerprint ||= begin
128
+ # See Apple technical note TN1103, "Uniquely Identifying a Macintosh
129
+ # Computer."
130
+ serial_number_match_data = IO.popen(
131
+ ["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"]
132
+ ).read.match(/"IOPlatformSerialNumber" = "([[:alnum:]]+)"/)
133
+ if serial_number_match_data
134
+ # The fingerprint must be unique across all personal access tokens for a
135
+ # given user. We prefix the serial number with the application name to
136
+ # differentiate between any other personal access token that uses the
137
+ # Mac serial number for the fingerprint.
138
+ Digest::SHA256.hexdigest("Boxen: #{serial_number_match_data[1]}")
139
+ else
140
+ abort "Sorry, I was unable to obtain your Mac's serial number.",
141
+ "Boxen requires access to your Mac's serial number in order to generate a unique GitHub personal access token."
142
+ end
143
+ end
144
+ end
145
+
146
+ def note
147
+ @note ||= "Boxen: #{Socket.gethostname}"
148
+ end
149
+ end
@@ -0,0 +1,32 @@
1
+ require "boxen/preflight"
2
+ require "boxen/util"
3
+
4
+ class Boxen::Preflight::Directories < Boxen::Preflight
5
+ def ok?
6
+ homedir_directory_exists? &&
7
+ homedir_owner == config.user &&
8
+ homedir_group == 'staff'
9
+ end
10
+
11
+ def run
12
+ Boxen::Util.sudo("/bin/mkdir", "-p", config.homedir) &&
13
+ Boxen::Util.sudo("/usr/sbin/chown", "#{config.user}:staff", config.homedir)
14
+ end
15
+
16
+ private
17
+ def homedir_directory_exists?
18
+ File.directory?(config.homedir)
19
+ end
20
+
21
+ def homedir_owner
22
+ Etc.getpwuid(homedir_stat.uid).name
23
+ end
24
+
25
+ def homedir_group
26
+ Etc.getgrgid(homedir_stat.gid).name
27
+ end
28
+
29
+ def homedir_stat
30
+ @homedir_stat ||= File.stat(config.homedir)
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ require "boxen/preflight"
2
+
3
+ class Boxen::Preflight::EtcMyCnf < Boxen::Preflight
4
+ def run
5
+ abort "You have an /etc/my.cnf file.",
6
+ "This will confuse Boxen's MySQL a lot. Please remove it."
7
+ end
8
+
9
+ def ok?
10
+ !File.file? "/etc/my.cnf"
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ require "boxen/preflight"
2
+
3
+ class Boxen::Preflight::Identity < Boxen::Preflight
4
+ def ok?
5
+ !user || (config.email && config.name)
6
+ end
7
+
8
+ def run
9
+ config.email = user.email
10
+ config.name = user.name
11
+ end
12
+
13
+ def user
14
+ @user ||= config.api.user rescue nil
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ require "boxen/preflight"
2
+
3
+ class Boxen::Preflight::OS < Boxen::Preflight
4
+ SUPPORTED_RELEASES = %w(10.8 10.9 10.10 10.11 10.12 10.13)
5
+
6
+ def ok?
7
+ osx? && (skip_os_check? || supported_release?)
8
+ end
9
+
10
+ def run
11
+ abort "You must be running one of the following OS X versions: #{SUPPORTED_RELEASES.join(' ')}."
12
+ end
13
+
14
+ private
15
+
16
+ def osx?
17
+ `uname -s`.chomp == "Darwin"
18
+ end
19
+
20
+ def supported_release?
21
+ SUPPORTED_RELEASES.any? do |r|
22
+ current_release.start_with? r
23
+ end
24
+ end
25
+
26
+ def current_release
27
+ @current_release ||= `sw_vers -productVersion`
28
+ end
29
+
30
+ def skip_os_check?
31
+ ENV['SKIP_OS_CHECK'] == '1'
32
+ end
33
+ end