devbox_launcher 0.1.0 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47bd56cd1236592f71bf6b836ffd42bb2eaa79534f338af4e8218c7ec1597c22
4
- data.tar.gz: 2b22d988c80f27f2f3f3c3c23ac9b92bddd4ae258dda0f7979623daff9e982db
3
+ metadata.gz: 7ed54fc2716507cd48b6e65acb08d38f1399c7d2a626a898ea0bdbeb70317f9d
4
+ data.tar.gz: 13b0d4ba4c3b9ae2515040bddff831b746de65c1296d57be20df82bc7f99f722
5
5
  SHA512:
6
- metadata.gz: 56d053b7fec492b8399ad3e1aa170b4ba03cbeb242a49dab12486fff44d334928fd54605032c706143728447a18c6cfc1993154e427e17d6503f0787479f634d
7
- data.tar.gz: c02c5d508262e7e96f2654611352c330529abfb4cf7923517ec9bbc652ec700459cdd7806267f547c5074fc411be1bf25530442123210128ca616f8e314d43ce
6
+ metadata.gz: 0c9720ff3bc730ad33b4d225a24796eca616615534d23f5d01c57be8c8291f0b37a0fc2d13092b2f84799c66ea513d6cc3b00bef3fd3e3559fe8d6ea107596e2
7
+ data.tar.gz: a040326a3fe949841970a3d5fd26535dc2cbf06bec09bd30098f63744ff87ac7fd1184b3a79d4848571681b7cad7c91c6ebe89753781f6126c8b5372619790de
data/.gitignore CHANGED
File without changes
data/.rspec CHANGED
File without changes
File without changes
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.3.3]
8
+ ### Fixed
9
+ - Support launching multiple boxes at the same time
10
+
11
+ ## [0.3.2]
12
+ ### Fixed
13
+ - Fix: add missing file
14
+
15
+ ## [0.3.1]
16
+ ### Fixed
17
+ - Recover when the devbox is in a shutdown cycle
18
+
19
+ ## [0.3.0]
20
+ ### Changed
21
+ - Label sessions with devbox
22
+ - On linux, mutagen relies on watchman to detect changes
23
+
24
+ ### Fixed
25
+ - In Linux, mutagen no longer burns CPU every 10 seconds
26
+
27
+ ## [0.2.0]
8
28
  ### Added
9
- - Initial release
29
+ - Initial release
File without changes
data/Gemfile CHANGED
File without changes
@@ -1,34 +1,37 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- devbox_launcher (0.1.0)
5
- activesupport
6
- bcrypt_pbkdf
7
- ed25519
8
- net-ssh
9
- ssh-config
10
- thor
4
+ devbox_launcher (0.3.3)
5
+ activesupport (~> 6.0)
6
+ bcrypt_pbkdf (~> 1.0)
7
+ ed25519 (~> 1.2)
8
+ net-ssh (~> 5.2)
9
+ os (~> 1.0)
10
+ ruby-watchman (= 0.0.2)
11
+ ssh-config (= 0.1.3)
12
+ thor (~> 1.0)
11
13
 
12
14
  GEM
13
15
  remote: https://rubygems.org/
14
16
  specs:
15
- activesupport (6.0.0)
17
+ activesupport (6.0.3.1)
16
18
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
19
  i18n (>= 0.7, < 2)
18
20
  minitest (~> 5.1)
19
21
  tzinfo (~> 1.1)
20
- zeitwerk (~> 2.1, >= 2.1.8)
22
+ zeitwerk (~> 2.2, >= 2.2.2)
21
23
  bcrypt_pbkdf (1.0.1)
22
24
  byebug (11.0.1)
23
25
  coderay (1.1.2)
24
- concurrent-ruby (1.1.5)
26
+ concurrent-ruby (1.1.6)
25
27
  diff-lcs (1.3)
26
28
  ed25519 (1.2.4)
27
- i18n (1.6.0)
29
+ i18n (1.8.2)
28
30
  concurrent-ruby (~> 1.0)
29
31
  method_source (0.9.2)
30
- minitest (5.12.2)
32
+ minitest (5.14.1)
31
33
  net-ssh (5.2.0)
34
+ os (1.1.0)
32
35
  pry (0.12.2)
33
36
  coderay (~> 1.1.0)
34
37
  method_source (~> 0.9.0)
@@ -49,12 +52,13 @@ GEM
49
52
  diff-lcs (>= 1.2.0, < 2.0)
50
53
  rspec-support (~> 3.9.0)
51
54
  rspec-support (3.9.0)
55
+ ruby-watchman (0.0.2)
52
56
  ssh-config (0.1.3)
53
57
  thor (1.0.1)
54
58
  thread_safe (0.3.6)
55
- tzinfo (1.2.5)
59
+ tzinfo (1.2.7)
56
60
  thread_safe (~> 0.1)
57
- zeitwerk (2.1.10)
61
+ zeitwerk (2.3.0)
58
62
 
59
63
  PLATFORMS
60
64
  ruby
@@ -67,4 +71,4 @@ DEPENDENCIES
67
71
  rspec (~> 3.0)
68
72
 
69
73
  BUNDLED WITH
70
- 2.0.2
74
+ 2.1.4
File without changes
data/README.md CHANGED
@@ -38,6 +38,8 @@ devbox start your-username
38
38
 
39
39
  If you want to mosh in immediately, add the `--mosh` switch.
40
40
 
41
+ Note: Linux users that sync mutagen sessions need to install [Watchman](https://facebook.github.io/watchman/).
42
+
41
43
  ## Development
42
44
 
43
45
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
File without changes
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
16
 
17
- spec.metadata["homepage_uri"] = "https://github.com/bloom-solutions/devbox_launcher"
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = "https://github.com/bloom-solutions/devbox_launcher"
19
19
  spec.metadata["changelog_uri"] = "https://github.com/bloom-solutions/devbox_launcher/blob/master/CHANGELOG.md"
20
20
 
@@ -24,17 +24,21 @@ Gem::Specification.new do |spec|
24
24
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
25
  end
26
26
  spec.bindir = "bin"
27
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.executables =
28
+ spec.files.grep(%r{^exe/}) { |f| File.basename(f) } +
29
+ spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
28
30
  spec.require_paths = ["lib"]
29
31
 
30
32
  spec.add_development_dependency "bundler", "~> 2.0"
31
33
  spec.add_development_dependency "rake", "~> 10.0"
32
34
  spec.add_development_dependency "rspec", "~> 3.0"
33
35
 
34
- spec.add_runtime_dependency "thor"
35
- spec.add_runtime_dependency "net-ssh"
36
- spec.add_runtime_dependency "ed25519"
37
- spec.add_runtime_dependency "bcrypt_pbkdf"
38
- spec.add_runtime_dependency "ssh-config"
39
- spec.add_runtime_dependency "activesupport"
36
+ spec.add_runtime_dependency "thor", "~> 1.0"
37
+ spec.add_runtime_dependency "net-ssh", "~> 5.2"
38
+ spec.add_runtime_dependency "ed25519", "~> 1.2"
39
+ spec.add_runtime_dependency "bcrypt_pbkdf", "~> 1.0"
40
+ spec.add_runtime_dependency "ssh-config", "0.1.3"
41
+ spec.add_runtime_dependency "activesupport", "~> 6.0"
42
+ spec.add_runtime_dependency "os", "~> 1.0"
43
+ spec.add_runtime_dependency "ruby-watchman", "0.0.2"
40
44
  end
@@ -3,6 +3,10 @@ require "ssh-config"
3
3
  require "open3"
4
4
  require "thor"
5
5
  require "net/ssh"
6
+ require "os"
7
+ require "ruby-watchman"
8
+ require 'socket'
9
+ require 'pathname'
6
10
  require "yaml"
7
11
  require "devbox_launcher/version"
8
12
 
@@ -11,3 +15,7 @@ module DevboxLauncher
11
15
  end
12
16
 
13
17
  require "devbox_launcher/cli"
18
+ require "devbox_launcher/watchman"
19
+ require "devbox_launcher/models/description"
20
+ require "devbox_launcher/models/mutagen"
21
+ require "devbox_launcher/models/box"
@@ -0,0 +1,205 @@
1
+ module DevboxLauncher
2
+ class Box
3
+
4
+ WAIT_BOOT_RESCUED_EXCEPTIONS = [
5
+ Net::SSH::ConnectionTimeout,
6
+ Net::SSH::Disconnect,
7
+ Errno::ECONNRESET,
8
+ Errno::ETIMEDOUT,
9
+ ]
10
+ WAIT_BOOT_IN_SECONDS = 10.freeze
11
+ DEFAULT_IDENTIFY_FILE_PATH = "~/.ssh/google_compute_engine".freeze
12
+ SSH_CONFIG_PATH = File.expand_path("~/.ssh/config").freeze
13
+ CONFIG_PATH = File.expand_path("~/.devbox_launcher.yml").freeze
14
+ CONFIG = YAML.load_file(CONFIG_PATH).freeze
15
+
16
+ attr_reader :account
17
+
18
+ def initialize(account)
19
+ @account = account
20
+ end
21
+
22
+ def start
23
+ start_stdout, start_stderr, start_status =
24
+ Open3.capture3(start_cmd)
25
+
26
+ set_ssh_config!(hostname, {
27
+ username: username,
28
+ ip: description.ip,
29
+ })
30
+
31
+ wait_boot
32
+
33
+ reset_mutagen_session(
34
+ mutagen_config: config[:mutagen],
35
+ hostname: hostname,
36
+ )
37
+ end
38
+
39
+ def start_cmd
40
+ args = {
41
+ project: config[:project],
42
+ account: account,
43
+ }.map do |(key, val)|
44
+ ["--#{key}", val].join("=")
45
+ end.join(" ")
46
+
47
+ [
48
+ "gcloud",
49
+ "compute",
50
+ "instances",
51
+ "start",
52
+ name,
53
+ args
54
+ ].join(" ")
55
+ end
56
+
57
+ def wait_boot(tries: 1)
58
+ Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
59
+ puts "[#{ssh.exec!('date').chomp}] Machine booted"
60
+ end
61
+ rescue *WAIT_BOOT_RESCUED_EXCEPTIONS
62
+ puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
63
+
64
+ sleep WAIT_BOOT_IN_SECONDS
65
+
66
+ description = describe(name)
67
+ if !description.running?
68
+ puts "Detected that the machine is not running " \
69
+ "(status is #{description.status}). Booting it..."
70
+ start_box name, username
71
+ end
72
+
73
+ wait_boot name, username, tries: tries+1
74
+ end
75
+
76
+ def description(reload: false)
77
+ return @description if !reload && @description
78
+
79
+ puts "Fetching box's description..."
80
+
81
+ describe_command = %Q(gcloud compute instances describe #{name})
82
+ describe_stdout, describe_stderr, describe_status =
83
+ Open3.capture3(describe_command)
84
+
85
+ if !describe_status.success?
86
+ msg = "Problem fetching the description of #{name}. "
87
+ msg += "Please ensure you can call `#{describe_command}`.\n"
88
+ msg += "Error:\n"
89
+ msg += describe_stderr
90
+ fail msg
91
+ end
92
+
93
+ @description = Description.new(describe_stdout)
94
+ end
95
+
96
+ def set_ssh_config!(hostname, username:, ip:)
97
+ FileUtils.touch(SSH_CONFIG_PATH)
98
+ config = ConfigFile.new
99
+ args = {
100
+ "HostName" => ip,
101
+ "User" => username,
102
+ "IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
103
+ }
104
+ args.each do |key, value|
105
+ config.set(hostname, key, value)
106
+ end
107
+ config.save
108
+ end
109
+
110
+ def reset_mutagen_session
111
+ mutagen_config = config[:mutagen]
112
+ return if mutagen_config.nil?
113
+
114
+ alpha_dir = mutagen_config[:alpha]
115
+ beta_dir = mutagen_config[:beta]
116
+
117
+ return if alpha_dir.nil? || beta_dir.nil?
118
+
119
+ terminate_mutagen_session
120
+
121
+ create_mutagen_session(
122
+ alpha_dir: alpha_dir,
123
+ beta_dir: beta_dir,
124
+ hostname: hostname,
125
+ username: username,
126
+ )
127
+
128
+ if OS.linux?
129
+ watch_alpha(alpha_dir: alpha_dir, hostname: hostname)
130
+ end
131
+ end
132
+
133
+ def terminate_mutagen_session(username)
134
+ puts "Terminating mutagen session..."
135
+ terminate_mutagen_command =
136
+ %Q(mutagen terminate --label-selector=#{username})
137
+ terminate_mutagen_stdout,
138
+ terminate_mutagen_stderr,
139
+ terminate_mutagen_status =
140
+ Open3.capture3(terminate_mutagen_command)
141
+
142
+ if not terminate_mutagen_status.success?
143
+ # mutagen prints to stdout and stderr
144
+ msg = "Failed to terminate mutagen sessions: " \
145
+ "#{terminate_mutagen_stdout} -" \
146
+ "#{terminate_mutagen_stderr}"
147
+ fail msg
148
+ end
149
+ end
150
+
151
+ def create_mutagen_session(alpha_dir:, beta_dir:, hostname:, username:)
152
+ puts "Create mutagen session syncing local #{alpha_dir} " \
153
+ "with #{hostname} #{beta_dir}"
154
+
155
+ create_mutagen_command = [
156
+ "mutagen sync create",
157
+ alpha_dir,
158
+ "#{hostname}:#{beta_dir}",
159
+ "--label=#{username}",
160
+ ]
161
+ create_mutagen_command << "--watch-mode-alpha=no-watch" if OS.linux?
162
+
163
+ create_mutagen_stdout,
164
+ create_mutagen_stderr,
165
+ create_mutagen_status =
166
+ Open3.capture3(create_mutagen_command.join(" "))
167
+
168
+ if not create_mutagen_status.success?
169
+ # mutagen prints to stdout and stderr
170
+ msg = "Failed to create mutagen sessions: " \
171
+ "#{create_mutagen_stdout} -" \
172
+ "#{create_mutagen_stderr}"
173
+ fail msg
174
+ end
175
+ end
176
+
177
+ def watch_alpha(alpha_dir:, hostname:)
178
+ watchman = Watchman.new(dir: alpha_dir)
179
+ watchman.trigger("mutagen sync flush --label-selector=#{hostname}")
180
+ end
181
+
182
+ def name
183
+ @name ||= config[:box]
184
+ end
185
+
186
+ def hostname
187
+ [name, username, "devbox"].join("-")
188
+ end
189
+
190
+ def username
191
+ @username ||= account.gsub(/\W/, "_")
192
+ end
193
+
194
+ def config
195
+ return @config if @config
196
+
197
+ if not CONFIG.has_key?(account)
198
+ fail "No config in #{CONFIG_PATH} found for #{account}"
199
+ end
200
+
201
+ @config = CONFIG[account].with_indifferent_access
202
+ end
203
+
204
+ end
205
+ end
@@ -11,124 +11,7 @@ module DevboxLauncher
11
11
  option :mosh, type: :boolean, desc: "Mosh in"
12
12
 
13
13
  def start(account)
14
- if not CONFIG.has_key?(account)
15
- fail "No config in #{CONFIG_PATH} found for #{account}"
16
- end
17
-
18
- config = CONFIG[account].with_indifferent_access
19
-
20
- username = account.gsub(/\W/, "_")
21
-
22
- set_account_command = %Q(gcloud config set account #{account})
23
- set_account_stdout, set_account_stderr, set_account_status =
24
- Open3.capture3(set_account_command)
25
-
26
- set_project_command = %Q(gcloud config set project #{config[:project]})
27
- set_project_stdout, set_project_stderr, set_project_status =
28
- Open3.capture3(set_project_command)
29
-
30
- name = config[:box]
31
-
32
- start_command = %Q(gcloud compute instances start #{name})
33
- start_stdout, start_stderr, start_status = Open3.capture3(start_command)
34
-
35
- puts "Fetching IP..."
36
- describe_command = %Q(gcloud compute instances describe #{name})
37
- describe_stdout, describe_stderr, describe_status =
38
- Open3.capture3(describe_command)
39
-
40
- if !describe_status.success?
41
- msg = "Problem fetching the IP address. "
42
- msg += "Please ensure you can call `#{describe_command}`.\n"
43
- msg += "Error:\n"
44
- msg += describe_stderr
45
- fail msg
46
- end
47
-
48
- description = YAML.load(describe_stdout)
49
-
50
- ip = description["networkInterfaces"].first["accessConfigs"].
51
- find { |config| config["kind"] == "compute#accessConfig" }["natIP"]
52
-
53
- puts "IP: #{ip}"
54
-
55
- hostname = "#{name}-devbox"
56
-
57
- set_ssh_config!(hostname, {
58
- username: username,
59
- ip: ip,
60
- })
61
-
62
- wait_boot(hostname, username)
63
-
64
- mutagen_alpha_dir = config[:mutagen][:alpha] if config[:mutagen]
65
- mutagen_beta_dir = config[:mutagen][:beta] if config[:mutagen]
66
-
67
- if [mutagen_alpha_dir, mutagen_beta_dir].all?
68
- puts "Terminating all mutagen sessions..."
69
- terminate_mutagen_command = %Q(mutagen terminate --all)
70
- terminate_mutagen_stdout,
71
- terminate_mutagen_stderr,
72
- terminate_mutagen_status =
73
- Open3.capture3(terminate_mutagen_command)
74
-
75
- if not terminate_mutagen_status.success?
76
- # mutagen prints to stdout and stderr
77
- msg = "Failed to terminate mutagen sessions: " \
78
- "#{terminate_mutagen_stdout} -" \
79
- "#{terminate_mutagen_stderr}"
80
- fail msg
81
- end
82
-
83
- puts "Create mutagen session syncing local #{mutagen_alpha_dir} with #{hostname} #{mutagen_beta_dir}"
84
- create_mutagen_command = [
85
- "mutagen sync create",
86
- mutagen_alpha_dir,
87
- "#{hostname}:#{mutagen_beta_dir}",
88
- ].join(" ")
89
- create_mutagen_stdout,
90
- create_mutagen_stderr,
91
- create_mutagen_status =
92
- Open3.capture3(create_mutagen_command)
93
-
94
- if not create_mutagen_status.success?
95
- # mutagen prints to stdout and stderr
96
- msg = "Failed to create mutagen sessions: " \
97
- "#{create_mutagen_stdout} -" \
98
- "#{create_mutagen_stderr}"
99
- fail msg
100
- end
101
- end
102
-
103
- if options[:mosh]
104
- mosh_command = %Q(mosh #{hostname})
105
- system(mosh_command)
106
- end
107
- end
108
-
109
- no_commands do
110
- def wait_boot(hostname, username)
111
- Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
112
- puts "[#{ssh.exec!('date').chomp}] Machine booted"
113
- end
114
- rescue Net::SSH::ConnectionTimeout, Net::SSH::Disconnect, Errno::ECONNRESET
115
- puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
116
- wait_boot hostname, username
117
- end
118
-
119
- def set_ssh_config!(hostname, username:, ip:)
120
- FileUtils.touch(SSH_CONFIG_PATH)
121
- config = ConfigFile.new
122
- args = {
123
- "HostName" => ip,
124
- "User" => username,
125
- "IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
126
- }
127
- args.each do |key, value|
128
- config.set(hostname, key, value)
129
- end
130
- config.save
131
- end
14
+ Box.new(account, options).start
132
15
  end
133
16
 
134
17
  end
@@ -0,0 +1,222 @@
1
+ module DevboxLauncher
2
+ class Box
3
+
4
+ WAIT_BOOT_RESCUED_EXCEPTIONS = [
5
+ Net::SSH::ConnectionTimeout,
6
+ Net::SSH::Disconnect,
7
+ Errno::ECONNRESET,
8
+ Errno::ETIMEDOUT,
9
+ ]
10
+ WAIT_BOOT_IN_SECONDS = 10.freeze
11
+ MAX_BOOT_RETRIES = 10
12
+ DEFAULT_IDENTIFY_FILE_PATH = "~/.ssh/google_compute_engine".freeze
13
+ SSH_CONFIG_PATH = File.expand_path("~/.ssh/config").freeze
14
+ CONFIG_PATH = File.expand_path("~/.devbox_launcher.yml").freeze
15
+ CONFIG = YAML.load_file(CONFIG_PATH).freeze
16
+
17
+ attr_reader :account, :options
18
+
19
+ def initialize(account, options)
20
+ @account = account
21
+ @options = options
22
+ end
23
+
24
+ def start
25
+ start_stdout, start_stderr, start_status =
26
+ Open3.capture3(start_cmd)
27
+
28
+ set_ssh_config!
29
+
30
+ wait_boot
31
+
32
+ reset_mutagen_session
33
+
34
+ connect_mosh
35
+ end
36
+
37
+ def start_cmd
38
+ args = {
39
+ project: config[:project],
40
+ account: account,
41
+ }.map do |(key, val)|
42
+ ["--#{key}", val].join("=")
43
+ end.join(" ")
44
+
45
+ [
46
+ "gcloud",
47
+ "compute",
48
+ "instances",
49
+ "start",
50
+ name,
51
+ args
52
+ ].join(" ")
53
+ end
54
+
55
+ def connect_mosh
56
+ return if options[:mosh].nil?
57
+
58
+ mosh_cmd = %Q(mosh #{hostname})
59
+ system(mosh_cmd)
60
+ end
61
+
62
+ def wait_boot(tries: 1)
63
+ Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
64
+ puts "[#{ssh.exec!('date').chomp}] Machine booted"
65
+ end
66
+ rescue *WAIT_BOOT_RESCUED_EXCEPTIONS
67
+ puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
68
+
69
+ sleep WAIT_BOOT_IN_SECONDS
70
+
71
+ if !description(reload: true).running?
72
+ puts "Detected that the machine is not running " \
73
+ "(status is #{description.status}). Booting it..."
74
+ start
75
+ end
76
+
77
+ fail if tries >= MAX_BOOT_RETRIES
78
+
79
+ wait_boot tries: tries+1
80
+ end
81
+
82
+ def description(reload: false)
83
+ return @description if !reload && @description
84
+
85
+ puts "Fetching box's description..."
86
+
87
+ describe_stdout, describe_stderr, describe_status =
88
+ Open3.capture3(describe_cmd)
89
+
90
+ if !describe_status.success?
91
+ msg = "Problem fetching the description of #{name}. "
92
+ msg += "Please ensure you can call `#{describe_cmd}`.\n"
93
+ msg += "Error:\n"
94
+ msg += describe_stderr
95
+ fail msg
96
+ end
97
+
98
+ @description = Description.new(describe_stdout)
99
+ end
100
+
101
+ def describe_cmd
102
+ args = {
103
+ project: config[:project],
104
+ account: account,
105
+ }.map do |(key, val)|
106
+ ["--#{key}", val].join("=")
107
+ end.join(" ")
108
+
109
+ [
110
+ "gcloud",
111
+ "compute",
112
+ "instances",
113
+ "describe",
114
+ name,
115
+ args
116
+ ].join(" ")
117
+ end
118
+
119
+ def set_ssh_config!
120
+ FileUtils.touch(SSH_CONFIG_PATH)
121
+ config = ConfigFile.new
122
+ args = {
123
+ "HostName" => description.ip,
124
+ "User" => username,
125
+ "IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
126
+ }
127
+ args.each do |key, value|
128
+ config.set(hostname, key, value)
129
+ end
130
+ config.save
131
+ end
132
+
133
+ def reset_mutagen_session
134
+ return if !mutagen_config.configured?
135
+
136
+ terminate_mutagen_session
137
+ create_mutagen_session
138
+ watch_alpha if OS.linux?
139
+ end
140
+
141
+ def terminate_mutagen_session
142
+ puts "Terminating mutagen session..."
143
+ terminate_mutagen_cmd =
144
+ %Q(mutagen terminate --label-selector=#{label})
145
+ terminate_mutagen_stdout,
146
+ terminate_mutagen_stderr,
147
+ terminate_mutagen_status =
148
+ Open3.capture3(terminate_mutagen_cmd)
149
+
150
+ if not terminate_mutagen_status.success?
151
+ # mutagen prints to stdout and stderr
152
+ msg = "Failed to terminate mutagen sessions: " \
153
+ "#{terminate_mutagen_stdout} -" \
154
+ "#{terminate_mutagen_stderr}"
155
+ fail msg
156
+ end
157
+ end
158
+
159
+ def label
160
+ "#{username}=#{name}"
161
+ end
162
+
163
+ def create_mutagen_session
164
+ puts "Create mutagen session syncing local " \
165
+ "#{mutagen_config.alpha_dir} with " \
166
+ "#{hostname} #{mutagen_config.beta_dir}"
167
+
168
+ create_mutagen_cmd = [
169
+ "mutagen sync create",
170
+ mutagen_config.alpha_dir,
171
+ "#{hostname}:#{mutagen_config.beta_dir}",
172
+ "--label=#{label}",
173
+ ]
174
+ create_mutagen_cmd << "--watch-mode-alpha=no-watch" if OS.linux?
175
+
176
+ create_mutagen_stdout,
177
+ create_mutagen_stderr,
178
+ create_mutagen_status =
179
+ Open3.capture3(create_mutagen_cmd.join(" "))
180
+
181
+ if not create_mutagen_status.success?
182
+ # mutagen prints to stdout and stderr
183
+ msg = "Failed to create mutagen sessions: " \
184
+ "#{create_mutagen_stdout} -" \
185
+ "#{create_mutagen_stderr}"
186
+ fail msg
187
+ end
188
+ end
189
+
190
+ def watch_alpha
191
+ watchman = Watchman.new(dir: mutagen_config.alpha_dir)
192
+ watchman.trigger("mutagen sync flush --label-selector=#{label}")
193
+ end
194
+
195
+ def name
196
+ @name ||= config[:box]
197
+ end
198
+
199
+ def hostname
200
+ [name, username, "devbox"].join("-")
201
+ end
202
+
203
+ def username
204
+ @username ||= account.gsub(/\W/, "_")
205
+ end
206
+
207
+ def config
208
+ return @config if @config
209
+
210
+ if not CONFIG.has_key?(account)
211
+ fail "No config in #{CONFIG_PATH} found for #{account}"
212
+ end
213
+
214
+ @config = CONFIG[account].with_indifferent_access
215
+ end
216
+
217
+ def mutagen_config
218
+ @mutagen_config ||= Mutagen.new(config[:mutagen])
219
+ end
220
+
221
+ end
222
+ end
@@ -0,0 +1,32 @@
1
+ module DevboxLauncher
2
+ class Description
3
+
4
+ def initialize(yaml)
5
+ @desc = YAML.load(yaml)
6
+ end
7
+
8
+ def ip
9
+ return @ip if @ip
10
+ network_interface = network_interfaces.first
11
+ access_configs = network_interface["accessConfigs"]
12
+
13
+ access_config = access_configs.find do |c|
14
+ c["kind"] == "compute#accessConfig"
15
+ end
16
+ @ip = access_config["natIP"]
17
+ end
18
+
19
+ def status
20
+ @status ||= @desc["status"]
21
+ end
22
+
23
+ def network_interfaces
24
+ @network_interfaces ||= @desc["networkInterfaces"]
25
+ end
26
+
27
+ def running?
28
+ status == "RUNNING"
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module DevboxLauncher
2
+ class Mutagen
3
+
4
+ attr_reader :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def configured?
11
+ [config, alpha_dir, beta_dir].none?(&:nil?)
12
+ end
13
+
14
+ def alpha_dir
15
+ config[:alpha]
16
+ end
17
+
18
+ def beta_dir
19
+ config[:beta]
20
+ end
21
+
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module DevboxLauncher
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.3"
3
3
  end
@@ -0,0 +1,50 @@
1
+ module DevboxLauncher
2
+ class Watchman
3
+
4
+ attr_reader :dir
5
+
6
+ def initialize(dir:)
7
+ @dir = dir
8
+ end
9
+
10
+ def trigger(command)
11
+ UNIXSocket.open(sockname) do |socket|
12
+ root = Pathname.new(dir).expand_path.to_s
13
+ result = RubyWatchman.query(['watch-list'], socket)
14
+ roots = result['roots']
15
+ if !roots.include?(root)
16
+ # this path isn't being watched yet; try to set up watch
17
+ result = RubyWatchman.query(['watch-project', root], socket)
18
+
19
+ # root_restrict_files setting may prevent Watchman from working
20
+ raise "Unable to watch #{dir}" if result.has_key?('error')
21
+ end
22
+
23
+ query = ['trigger', root, {
24
+ 'name' => 'mutagen-sync',
25
+ 'expression' => ['match', '**/*', 'wholename'],
26
+ 'command' => command.split(" "),
27
+ }]
28
+ paths = RubyWatchman.query(query, socket)
29
+
30
+ # could return error if watch is removed
31
+ if paths.has_key?('error')
32
+ raise "Unable to set trigger. Error: #{paths['error']}"
33
+ end
34
+ end
35
+ end
36
+
37
+ def sockname
38
+ sockname = RubyWatchman.load(
39
+ %x{watchman --output-encoding=bser get-sockname}
40
+ )['sockname']
41
+
42
+ if !$?.exitstatus.zero?
43
+ raise "Failed to connect to watchman. Is it running?"
44
+ end
45
+
46
+ sockname
47
+ end
48
+
49
+ end
50
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devbox_launcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ramon Tayag
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-12 00:00:00.000000000 Z
11
+ date: 2020-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -56,90 +56,121 @@ dependencies:
56
56
  name: thor
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">="
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '1.0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ">="
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: '1.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: net-ssh
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '5.2'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '5.2'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: ed25519
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">="
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: '1.2'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ">="
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0'
96
+ version: '1.2'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: bcrypt_pbkdf
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ">="
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: '1.0'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '1.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: ssh-config
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ">="
115
+ - - '='
116
116
  - !ruby/object:Gem::Version
117
- version: '0'
117
+ version: 0.1.3
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ">="
122
+ - - '='
123
123
  - !ruby/object:Gem::Version
124
- version: '0'
124
+ version: 0.1.3
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: activesupport
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ">="
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '6.0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '6.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: os
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: ruby-watchman
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '='
130
158
  - !ruby/object:Gem::Version
131
- version: '0'
159
+ version: 0.0.2
132
160
  type: :runtime
133
161
  prerelease: false
134
162
  version_requirements: !ruby/object:Gem::Requirement
135
163
  requirements:
136
- - - ">="
164
+ - - '='
137
165
  - !ruby/object:Gem::Version
138
- version: '0'
166
+ version: 0.0.2
139
167
  description:
140
168
  email:
141
169
  - ramon.tayag@gmail.com
142
- executables: []
170
+ executables:
171
+ - console
172
+ - devbox
173
+ - setup
143
174
  extensions: []
144
175
  extra_rdoc_files: []
145
176
  files:
@@ -158,8 +189,13 @@ files:
158
189
  - bin/setup
159
190
  - devbox_launcher.gemspec
160
191
  - lib/devbox_launcher.rb
192
+ - lib/devbox_launcher/box.rb
161
193
  - lib/devbox_launcher/cli.rb
194
+ - lib/devbox_launcher/models/box.rb
195
+ - lib/devbox_launcher/models/description.rb
196
+ - lib/devbox_launcher/models/mutagen.rb
162
197
  - lib/devbox_launcher/version.rb
198
+ - lib/devbox_launcher/watchman.rb
163
199
  homepage: https://github.com/bloom-solutions/devbox_launcher
164
200
  licenses:
165
201
  - MIT
@@ -183,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
219
  - !ruby/object:Gem::Version
184
220
  version: '0'
185
221
  requirements: []
186
- rubygems_version: 3.1.2
222
+ rubygems_version: 3.0.8
187
223
  signing_key:
188
224
  specification_version: 4
189
225
  summary: Conveniently launch your devbox