opswalrus 1.0.13 → 1.0.14

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: 1199faea66fca54073c2446271ef2c31a8e78e6dd55e7e9de5173baae5da7ac4
4
- data.tar.gz: 1bbe73114e2cf1449ad296245d4cf53a2c2f8c523a1652f0ce1776bf742e4438
3
+ metadata.gz: 9ee2d0af10d38f7722a8af82b44af64f3c0d906de21b4425d472d4e27b9495b0
4
+ data.tar.gz: e42c56ffcdf0c1cf1b08e68771dcf46364439c057393ed325b5325964816d1a8
5
5
  SHA512:
6
- metadata.gz: d4e9a452262cf8a3a82c967bfc5dc9e8b39f87f80b42fb9b641fa764811e2735a78b307cc0c2751c6c185c8223561c0d41b5913b7c62c78d76c020907c8f8286
7
- data.tar.gz: 15f772925cb50837543ee0277ca3e4fbee42c0c64e3c0ad28c940617c008d51c4507f3950a3fb28a96086ea8d759368f7b06e04b2f4aaee406a6883cc61956cf
6
+ metadata.gz: 39a22eed3d76788bb6d70ad4f4700d512a4e9612614508e407ee9d0187bd2f0aab3593845f5f6a8adb91020d330bd7d955bbe21c869c3aa7e6c1185af4204b6a
7
+ data.tar.gz: 63e635e997cead804423e1c966e7e429538e625532277b98178c19186345455b469222d5eabd4b7829998690753ba8d95d09788248b167e706d3512d05ece6de
data/Dockerfile CHANGED
@@ -39,5 +39,5 @@ RUN git config --global --add safe.directory /workdir
39
39
 
40
40
  # Set the entrypoint to run the installed binary in /workdir
41
41
  # Example: docker run -it -v "$PWD:/workdir" ops
42
- # ENTRYPOINT ["ops"]
43
- CMD ["ops"]
42
+ ENTRYPOINT ["ops"]
43
+ # CMD ["ops"]
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opswalrus (1.0.13)
4
+ opswalrus (1.0.14)
5
5
  bcrypt_pbkdf (~> 1.1)
6
6
  citrus (~> 3.0)
7
7
  ed25519 (~> 1.3)
data/README.md CHANGED
@@ -11,17 +11,19 @@ You have two options:
11
11
  ## Rubygems install
12
12
 
13
13
  ```shell
14
- gem install opswalrus
14
+ gem install opswalrus
15
15
 
16
- ops version
16
+ ops version
17
+ 1.0.13
17
18
  ```
18
19
 
19
20
  ## Docker install
20
21
 
21
22
  ```shell
22
- alias ops='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/opswalrus/ops'
23
+ alias ops='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/opswalrus/ops'
23
24
 
24
- ops version
25
+ ops version
26
+ 1.0.13
25
27
  ```
26
28
 
27
29
  # Examples
data/build.ops CHANGED
@@ -51,4 +51,5 @@ end
51
51
 
52
52
  totp = sh("Get Rubygems OTP") { 'bw get totp Rubygems' }
53
53
  sh("Push gem", input: {"You have enabled multi-factor authentication. Please enter OTP code." => "#{totp}\n"}) { 'gem push opswalrus-{{ version }}.gem' }
54
- sh("Build docker image") { 'docker build -t opswalrus/ops:{{ version }} .' }
54
+ sh("Build docker image") { 'docker build -t ghcr.io/opswalrus/ops:latest -t ghcr.io/opswalrus/ops:{{ version }} -t opswalrus/ops:latest -t opswalrus/ops:{{ version }} .' }
55
+ sh("Push docker image to ghcr.io/opswalrus/ops") { 'docker push ghcr.io/opswalrus/ops:latest' }
data/lib/opswalrus/app.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  require "citrus"
2
2
  require "io/console"
3
3
  require "json"
4
+ require "logger"
4
5
  require "random/formatter"
5
6
  require "shellwords"
6
7
  require "socket"
7
8
  require "stringio"
8
9
  require "yaml"
9
10
  require "pathname"
11
+ require_relative "errors"
10
12
  require_relative "patches"
11
13
  require_relative "git"
12
14
  require_relative "host"
@@ -18,9 +20,6 @@ require_relative "version"
18
20
 
19
21
 
20
22
  module OpsWalrus
21
- class Error < StandardError
22
- end
23
-
24
23
  class App
25
24
  def self.instance(*args)
26
25
  @instance ||= new(*args)
@@ -32,6 +31,8 @@ module OpsWalrus
32
31
  attr_reader :local_hostname
33
32
 
34
33
  def initialize(pwd = Dir.pwd)
34
+ @logger = Logger.new($stdout, level: Logger::INFO)
35
+
35
36
  @verbose = false
36
37
  @sudo_user = nil
37
38
  @sudo_password = nil
@@ -89,6 +90,7 @@ module OpsWalrus
89
90
 
90
91
  def set_verbose(verbose)
91
92
  @verbose = verbose
93
+ @logger.debug! if verbose?
92
94
  end
93
95
 
94
96
  def verbose?
@@ -99,6 +101,22 @@ module OpsWalrus
99
101
  @verbose == 2
100
102
  end
101
103
 
104
+ def error(msg)
105
+ @logger.error(msg)
106
+ end
107
+
108
+ def warn(msg)
109
+ @logger.warn(msg)
110
+ end
111
+
112
+ def log(msg)
113
+ @logger.info(msg)
114
+ end
115
+
116
+ def debug(msg)
117
+ @logger.debug(msg)
118
+ end
119
+
102
120
  def set_pwd(pwd)
103
121
  @pwd = pwd.to_pathname
104
122
  @bundler = Bundler.new(@pwd)
@@ -147,13 +165,9 @@ module OpsWalrus
147
165
  def run(package_operation_and_args)
148
166
  return 0 if package_operation_and_args.empty?
149
167
 
150
- ops_file_path, operation_kv_args, tmp_dir = get_entry_point_ops_file_and_args(package_operation_and_args)
151
- ops_file = OpsFile.new(self, ops_file_path)
168
+ ops_file_path, operation_kv_args, tmp_bundle_root_dir = get_entry_point_ops_file_and_args(package_operation_and_args)
152
169
 
153
- # if the ops file is part of a package, then set the package directory as the app's pwd
154
- if ops_file.package_file && ops_file.package_file.dirname.to_s !~ /#{Bundler::BUNDLE_DIR}/
155
- ops_file = set_pwd_to_ops_file_package_directory(ops_file)
156
- end
170
+ ops_file = load_entry_point_ops_file(ops_file_path, tmp_bundle_root_dir)
157
171
 
158
172
  if @verbose
159
173
  puts "Running: #{ops_file.ops_file_path}"
@@ -177,19 +191,52 @@ module OpsWalrus
177
191
 
178
192
  exit_status
179
193
  ensure
180
- FileUtils.remove_entry(tmp_dir) if tmp_dir
194
+ FileUtils.remove_entry(tmp_bundle_root_dir) if tmp_bundle_root_dir
195
+ end
196
+
197
+ def load_entry_point_ops_file(ops_file_path, tmp_bundle_root_dir)
198
+ ops_file = OpsFile.new(self, ops_file_path)
199
+
200
+ # we are running the ops file from within a temporary bundle root directory created by unzipping a zip bundle workspace
201
+ if tmp_bundle_root_dir
202
+ return set_pwd_and_rebase_ops_file(tmp_bundle_root_dir, ops_file)
203
+ end
204
+
205
+ # if the ops file is contained within a bundle directory, then that means we're probably running this command invocation
206
+ # on a remote host, e.g. /home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops run --script /tmp/d20230822-18829-2j5ij2 opswalrus_bundle docker install install
207
+ # and the corresponding entry point, e.g. /tmp/d20230822-18829-2j5ij2/opswalrus_bundle/docker/install/install.ops
208
+ # is actually being run from a temporary zip bundle root directory
209
+ # so we want to set the app's pwd to be the parent directory of the Bundler::BUNDLE_DIR directory, e.g. /tmp/d20230822-18829-2j5ij2
210
+ if ops_file.ops_file_path.to_s =~ /#{Bundler::BUNDLE_DIR}/
211
+ return set_pwd_to_parent_of_bundle_dir(ops_file)
212
+ end
213
+
214
+ # if the ops file is part of a package, then set the package directory as the app's pwd
215
+ if ops_file.package_file && ops_file.package_file.dirname.to_s !~ /#{Bundler::BUNDLE_DIR}/
216
+ return set_pwd_to_ops_file_package_directory(ops_file)
217
+ end
218
+
219
+ ops_file
220
+ end
221
+
222
+ def set_pwd_to_parent_of_bundle_dir(ops_file)
223
+ match = /^(.*)#{Bundler::BUNDLE_DIR}.*$/.match(ops_file.ops_file_path.to_s)
224
+ parent_directory_path = match.captures.first.to_pathname.cleanpath
225
+ set_pwd_and_rebase_ops_file(parent_directory_path, ops_file)
181
226
  end
182
227
 
183
228
  # sets the App's pwd to the ops file's package directory and
184
- # returns a new OpsFile that points at the revised pathname with considered as relative to the package file's directory
229
+ # returns a new OpsFile that points at the revised pathname when considered as relative to the package file's directory
185
230
  def set_pwd_to_ops_file_package_directory(ops_file)
186
- # puts "set pwd: #{ops_file.package_file.dirname}"
187
- set_pwd(ops_file.package_file.dirname)
188
- rebased_ops_file_relative_path = ops_file.ops_file_path.relative_path_from(ops_file.package_file.dirname)
189
- # note: rebased_ops_file_relative_path is a relative path that is relative to ops_file.package_file.dirname
190
- # puts "rebased path: #{rebased_ops_file_relative_path}"
191
- absolute_ops_file_path = ops_file.package_file.dirname.join(rebased_ops_file_relative_path)
192
- # puts "absolute path: #{absolute_ops_file_path}"
231
+ set_pwd_and_rebase_ops_file(ops_file.package_file.dirname, ops_file)
232
+ end
233
+
234
+ # returns a new OpsFile that points at the revised pathname when considered as relative to the new working directory
235
+ def set_pwd_and_rebase_ops_file(new_working_directory, ops_file)
236
+ set_pwd(new_working_directory)
237
+ rebased_ops_file_relative_path = ops_file.ops_file_path.relative_path_from(new_working_directory)
238
+ # note: rebased_ops_file_relative_path is a relative path that is relative to new_working_directory
239
+ absolute_ops_file_path = new_working_directory.join(rebased_ops_file_relative_path)
193
240
  OpsFile.new(self, absolute_ops_file_path)
194
241
  end
195
242
 
@@ -203,18 +250,18 @@ module OpsWalrus
203
250
  # - ["../../my-package/operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
204
251
  # - ["../../my-package/operation1"]
205
252
  #
206
- # returns 3-tuple of the form: [ ops_file_path, operation_kv_args, optional_tmp_dir ]
207
- # such that the third item - optional_tmp_dir - if present, should be deleted after the script has completed running
253
+ # returns 3-tuple of the form: [ ops_file_path, operation_kv_args, optional_tmp_bundle_root_dir ]
254
+ # such that the third item - optional_tmp_bundle_root_dir - if present, should be deleted after the script has completed running
208
255
  def get_entry_point_ops_file_and_args(package_operation_and_args)
209
256
  package_operation_and_args = package_operation_and_args.dup
210
257
  package_or_ops_file_reference = package_operation_and_args.slice!(0, 1).first
211
- tmp_dir = nil
258
+ tmp_bundle_root_dir = nil
212
259
 
213
260
  case
214
261
  when Dir.exist?(package_or_ops_file_reference)
215
262
  dir = package_or_ops_file_reference
216
263
  ops_file_path, operation_kv_args = find_entry_point_ops_file_in_dir(dir, package_operation_and_args)
217
- [ops_file_path, operation_kv_args, tmp_dir]
264
+ [ops_file_path, operation_kv_args, tmp_bundle_root_dir]
218
265
  when File.exist?(package_or_ops_file_reference)
219
266
  first_filepath = package_or_ops_file_reference.to_pathname.realpath
220
267
 
@@ -222,18 +269,18 @@ module OpsWalrus
222
269
  when ".ops"
223
270
  [first_filepath, package_operation_and_args]
224
271
  when ".zip"
225
- tmp_dir = Dir.mktmpdir.to_pathname # this is the temporary bundle root dir
272
+ tmp_bundle_root_dir = Dir.mktmpdir.to_pathname # this is the temporary bundle root dir
226
273
 
227
274
  # unzip the bundle into the temp directory
228
- DirZipper.unzip(first_filepath, tmp_dir)
275
+ DirZipper.unzip(first_filepath, tmp_bundle_root_dir)
229
276
 
230
- find_entry_point_ops_file_in_dir(tmp_dir, package_operation_and_args)
277
+ find_entry_point_ops_file_in_dir(tmp_bundle_root_dir, package_operation_and_args)
231
278
  else
232
279
  raise Error, "Unknown file type for entrypoint: #{first_filepath}"
233
280
  end
234
281
 
235
282
  # operation_kv_args = package_operation_and_args
236
- [ops_file_path, operation_kv_args, tmp_dir]
283
+ [ops_file_path, operation_kv_args, tmp_bundle_root_dir]
237
284
  when repo_url = Git.repo?(package_or_ops_file_reference)
238
285
  destination_package_path = bundler.download_git_package(repo_url)
239
286
 
@@ -241,7 +288,7 @@ module OpsWalrus
241
288
 
242
289
  # for an original package_operation_and_args of ["github.com/davidkellis/my-package", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
243
290
  # we return: [ "#{pwd}/#{Bundler::BUNDLE_DIR}/github-com-davidkellis-my-package/operation1.ops", ["arg1:val1", "arg2:val2", "arg3:val3"] ]
244
- [ops_file_path, operation_kv_args, tmp_dir]
291
+ [ops_file_path, operation_kv_args, tmp_bundle_root_dir]
245
292
  else
246
293
  raise Error, "Unknown operation reference: #{package_or_ops_file_reference.inspect}"
247
294
  end
@@ -0,0 +1,7 @@
1
+ module OpsWalrus
2
+ class Error < StandardError
3
+ end
4
+
5
+ class SymbolResolutionError < Error
6
+ end
7
+ end
@@ -173,6 +173,10 @@ module OpsWalrus
173
173
 
174
174
  # the subclasses of this class will define methods that handle method dispatch via HostProxyOpsFileInvocationBuilder objects
175
175
 
176
+ def to_s
177
+ @_host.to_s
178
+ end
179
+
176
180
  def method_missing(name, ...)
177
181
  @_host.send(name, ...)
178
182
  end
@@ -1,15 +1,3 @@
1
- # require 'json'
2
- # require 'set'
3
- # require 'shellwords'
4
- # require 'socket'
5
- # require 'stringio'
6
-
7
- # require 'sshkit'
8
- # require 'sshkit/dsl'
9
-
10
- # require_relative 'host'
11
- # require_relative 'sshkit_ext'
12
- # require_relative 'walrus_lang'
13
1
 
14
2
  module OpsWalrus
15
3
 
@@ -127,7 +115,7 @@ module OpsWalrus
127
115
  resolved_symbol = @namespace_or_ops_file.resolve_symbol(method_name)
128
116
  if resolved_symbol.is_a? OpsFile
129
117
  params_hash = resolved_symbol.build_params_hash(*args, **kwargs)
130
- resolved_symbol.invoke(runtime_env, params_hash)
118
+ resolved_symbol.invoke(@runtime_env, params_hash)
131
119
  else
132
120
  self
133
121
  end
@@ -142,434 +130,5 @@ module OpsWalrus
142
130
  _resolve_method_and_invoke(name, *args, **kwargs)
143
131
  end
144
132
  end
145
- # class ArrayOrHashNavigationProxy
146
- # def initialize(array_or_hash)
147
- # @obj = array_or_hash
148
- # end
149
- # def [](index, *args, **kwargs, &block)
150
- # @obj.method(:[]).call(index, *args, **kwargs, &block)
151
- # end
152
- # def respond_to_missing?(method, *)
153
- # @obj.is_a?(Hash) && @obj.respond_to?(method)
154
- # end
155
- # def method_missing(name, *args, **kwargs, &block)
156
- # case @obj
157
- # when Array
158
- # @obj.method(name).call(*args, **kwargs, &block)
159
- # when Hash
160
- # if @obj.respond_to?(name)
161
- # @obj.method(name).call(*args, **kwargs, &block)
162
- # else
163
- # value = self[name.to_s]
164
- # case value
165
- # when Array, Hash
166
- # ArrayOrHashNavigationProxy.new(value)
167
- # else
168
- # value
169
- # end
170
- # end
171
- # end
172
- # end
173
- # end
174
-
175
- # class InvocationParams
176
- # # params : Hash
177
- # def initialize(params)
178
- # @params = params
179
- # end
180
-
181
- # def [](key)
182
- # key = key.to_s if key.is_a? Symbol
183
- # @params[key]
184
- # end
185
-
186
- # def dig(*keys)
187
- # # keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
188
- # @params.dig(*keys)
189
- # end
190
-
191
- # def method_missing(name, *args, **kwargs, &block)
192
- # if @params.respond_to?(name)
193
- # @params.method(name).call(*args, **kwargs, &block)
194
- # else
195
- # value = self[name]
196
- # case value
197
- # when Array, Hash
198
- # ArrayOrHashNavigationProxy.new(value)
199
- # else
200
- # value
201
- # end
202
- # end
203
- # end
204
- # end
205
-
206
-
207
- # # BootstrapLinuxHostShellScript = <<~SCRIPT
208
- # # #!/usr/bin/env bash
209
- # # ...
210
- # # SCRIPT
211
-
212
- # module InvocationDSL
213
- # def ssh(*args, **kwargs, &block)
214
- # host_proxy_class = @ops_file_script.host_proxy_class
215
- # hosts = inventory(*args, **kwargs).map {|host| host_proxy_class.new(host) }
216
- # sshkit_hosts = hosts.map(&:sshkit_host)
217
- # sshkit_host_to_ops_host_map = sshkit_hosts.zip(hosts).to_h
218
- # runtime_env = @runtime_env
219
- # local_host = self
220
- # # bootstrap_shell_script = BootstrapLinuxHostShellScript
221
- # # on sshkit_hosts do |sshkit_host|
222
- # SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
223
-
224
- # # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
225
-
226
- # host = sshkit_host_to_ops_host_map[sshkit_host]
227
- # # puts "#{host.alias} / #{host}:"
228
-
229
- # begin
230
- # host.set_runtime_env(runtime_env)
231
- # host.set_ssh_session_connection(self) # self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
232
-
233
- # # copy over bootstrap shell script
234
- # # io = StringIO.new(bootstrap_shell_script)
235
- # io = File.open(__FILE__.to_pathname.dirname.join("bootstrap.sh"))
236
- # upload_success = host.upload(io, "tmpopsbootstrap.sh")
237
- # io.close
238
- # raise Error, "Unable to upload bootstrap shell script to remote host" unless upload_success
239
- # host.execute(:chmod, "755", "tmpopsbootstrap.sh")
240
- # host.execute(:sh, "tmpopsbootstrap.sh")
241
-
242
- # # copy over ops bundle zip file
243
- # zip_bundle_path = runtime_env.zip_bundle_path
244
- # upload_success = host.upload(zip_bundle_path, "tmpops.zip")
245
- # raise Error, "Unable to upload ops bundle to remote host" unless upload_success
246
-
247
- # stdout, stderr, exit_status = host.run_ops(:bundle, "unzip tmpops.zip", in_bundle_root_dir: false)
248
- # raise Error, "Unable to unzip ops bundle on remote host" unless exit_status == 0
249
- # tmp_bundle_root_dir = stdout.strip
250
- # host.set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
251
-
252
- # # we run the block in the context of the host, s.t. `self` within the block evaluates to `host`
253
- # retval = host.instance_exec(local_host, &block) # host is passed as the argument to the block
254
-
255
- # # puts retval.inspect
256
-
257
- # # cleanup
258
- # if tmp_bundle_root_dir =~ /tmp/ # sanity check the temp path before we blow away something we don't intend
259
- # host.execute(:rm, "-rf", "tmpopsbootstrap.sh", "tmpops.zip", tmp_bundle_root_dir)
260
- # else
261
- # host.execute(:rm, "-rf", "tmpopsbootstrap.sh", "tmpops.zip")
262
- # end
263
-
264
- # retval
265
- # rescue SSHKit::Command::Failed => e
266
- # puts "[!] Command failed:"
267
- # puts e.message
268
- # rescue Net::SSH::ConnectionTimeout
269
- # puts "[!] The host '#{host}' not alive!"
270
- # rescue Net::SSH::Timeout
271
- # puts "[!] The host '#{host}' disconnected/timeouted unexpectedly!"
272
- # rescue Errno::ECONNREFUSED
273
- # puts "[!] Incorrect port #{port} for #{host}"
274
- # rescue Net::SSH::HostKeyMismatch => e
275
- # puts "[!] The host fingerprint does not match the last observed fingerprint for #{host}"
276
- # puts e.message
277
- # puts "You might try `ssh-keygen -f ~/.ssh/known_hosts -R \"#{host}\"`"
278
- # rescue Net::SSH::AuthenticationFailed
279
- # puts "Wrong Password: #{host} | #{user}:#{password}"
280
- # rescue Net::SSH::Authentication::DisallowedMethod
281
- # puts "[!] The host '#{host}' doesn't accept password authentication method."
282
- # rescue Errno::EHOSTUNREACH => e
283
- # puts "[!] The host '#{host}' is unreachable"
284
- # rescue => e
285
- # puts e.class
286
- # puts e.message
287
- # # puts e.backtrace.join("\n")
288
- # ensure
289
- # host.clear_ssh_session
290
- # end
291
- # end
292
- # end
293
-
294
- # def inventory(*args, **kwargs)
295
- # tags = args.map(&:to_s)
296
-
297
- # kwargs = kwargs.transform_keys(&:to_s)
298
- # tags.concat(kwargs["tags"]) if kwargs["tags"]
299
-
300
- # @runtime_env.app.inventory(tags)
301
- # end
302
-
303
- # def exit(exit_status, message = nil)
304
- # if message
305
- # puts message
306
- # end
307
- # result = if exit_status == 0
308
- # Invocation::Success.new(nil)
309
- # else
310
- # Invocation::Error.new(nil, exit_status)
311
- # end
312
- # throw :exit_now, result
313
- # end
314
-
315
- # def env(*keys)
316
- # keys = keys.map(&:to_s)
317
- # if keys.empty?
318
- # @env
319
- # else
320
- # @env.dig(*keys)
321
- # end
322
- # end
323
-
324
- # # currently, import may only be used to import a package that is referenced in the script's package file
325
- # # I may decide to extend this to work with dynamic package references
326
- # #
327
- # # local_package_name is the local package name defined for the package dependency that is attempting to be referenced
328
- # def import(local_package_name)
329
- # local_package_name = local_package_name.to_s
330
- # package_reference = @ops_file_script.ops_file.package_file&.dependency(local_package_name)
331
- # raise Error, "Unknown package reference: #{local_package_name}" unless package_reference
332
- # import_reference = PackageDependencyReference.new(local_package_name, package_reference)
333
- # # puts "import: #{import_reference.inspect}"
334
- # @runtime_env.resolve_import_reference(@ops_file_script.ops_file, import_reference)
335
- # end
336
-
337
- # def params(*keys, default: nil)
338
- # keys = keys.map(&:to_s)
339
- # if keys.empty?
340
- # @params
341
- # else
342
- # @params.dig(*keys) || default
343
- # end
344
- # end
345
-
346
- # # returns the stdout from the command
347
- # def sh(desc_or_cmd = nil, cmd = nil, input: nil, &block)
348
- # out, err, status = *shell!(desc_or_cmd, cmd, block, input: input)
349
- # out
350
- # end
351
-
352
- # # returns the tuple: [stdout, stderr, exit_status]
353
- # def shell(desc_or_cmd = nil, cmd = nil, input: nil, &block)
354
- # shell!(desc_or_cmd, cmd, block, input: input)
355
- # end
356
-
357
- # # returns the tuple: [stdout, stderr, exit_status]
358
- # def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil)
359
- # # description = nil
360
-
361
- # return ["", "", 0] if !desc_or_cmd && !cmd && !block # we were told to do nothing; like hitting enter at the bash prompt; we can do nothing successfully
362
-
363
- # description = desc_or_cmd if cmd || block
364
- # cmd = block.call if block
365
- # cmd ||= desc_or_cmd
366
-
367
- # cmd = WalrusLang.render(cmd, block.binding) if block && cmd =~ /{{.*}}/
368
-
369
- # #cmd = Shellwords.escape(cmd)
370
-
371
- # # puts "shell! self: #{self.inspect}"
372
-
373
- # if App.instance.report_mode?
374
- # print "[#{@runtime_env.local_hostname}] "
375
- # print "#{description}: " if description
376
- # puts cmd
377
- # end
378
-
379
- # return unless cmd && !cmd.strip.empty?
380
-
381
- # sshkit_cmd = @runtime_env.handle_input(input) do |interaction_handler|
382
- # # self is a Module instance that is serving as the evaluation context in an instance of a subclass of an Invocation; see Invocation#evaluate
383
- # backend.execute_cmd(cmd, interaction_handler: interaction_handler, verbosity: :info)
384
- # end
385
-
386
- # [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
387
- # end
388
-
389
- # # def init_brew
390
- # # execute('eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"')
391
- # # end
392
-
393
- # end
394
-
395
- # # An Invocation object represents a stack frame, and the params_hash represents the
396
- # # arguments that the caller has supplied for that stack frame to reference
397
- # class Invocation
398
- # class Result
399
- # attr_accessor :value
400
- # attr_accessor :exit_status
401
- # def initialize(value, exit_status = 0)
402
- # @value = value
403
- # @exit_status = exit_status
404
- # end
405
- # def success?
406
- # !failure?
407
- # end
408
- # def failure?
409
- # !success?
410
- # end
411
- # end
412
- # class Success < Result
413
- # def initialize(value)
414
- # super(value, 0)
415
- # end
416
- # def success?
417
- # true
418
- # end
419
- # end
420
- # class Error < Result
421
- # def initialize(value, exit_status = 1)
422
- # super(value, exit_status == 0 ? 1 : exit_status)
423
- # end
424
- # def failure?
425
- # true
426
- # end
427
- # end
428
-
429
-
430
- # def self.define_invocation_class(ops_file)
431
- # klass = Class.new(Invocation)
432
-
433
- # methods_defined = Set.new
434
-
435
- # # define methods for every import in the script
436
- # ops_file.local_symbol_table.each do |symbol_name, import_reference|
437
- # unless methods_defined.include? symbol_name
438
- # klass.define_method(symbol_name) do |*args, **kwargs, &block|
439
- # # puts "0" * 80
440
- # # puts "@runtime_env.resolve_import_reference(@ops_file_script.ops_file, #{import_reference.inspect})"
441
- # # puts @ops_file_script.ops_file.ops_file_path
442
- # # puts symbol_name
443
- # namespace_or_ops_file = @runtime_env.resolve_import_reference(@ops_file_script.ops_file, import_reference)
444
- # # puts namespace_or_ops_file.inspect
445
- # # puts "0" * 80
446
- # case namespace_or_ops_file
447
- # when Namespace
448
- # namespace_or_ops_file
449
- # when OpsFile
450
- # params_hash = namespace_or_ops_file.build_params_hash(*args, **kwargs)
451
- # namespace_or_ops_file.invoke(@runtime_env, params_hash)
452
- # end
453
- # end
454
- # methods_defined << symbol_name
455
- # end
456
- # end
457
-
458
- # # define methods for every Namespace or OpsFile within the namespace that the OpsFile resides within
459
- # sibling_symbol_table = Set.new
460
- # sibling_symbol_table |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
461
- # sibling_symbol_table |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
462
- # sibling_symbol_table.each do |symbol_name|
463
- # unless methods_defined.include? symbol_name
464
- # klass.define_method(symbol_name) do |*args, **kwargs, &block|
465
- # # puts "0" * 80
466
- # # puts "@runtime_env.resolve_symbol(@ops_file_script.ops_file, #{symbol_name})"
467
- # # puts @ops_file_script.ops_file.ops_file_path
468
- # # puts symbol_name
469
- # namespace_or_ops_file = @runtime_env.resolve_sibling_symbol(@ops_file_script.ops_file, symbol_name)
470
- # # puts namespace_or_ops_file.inspect
471
- # # puts "0" * 80
472
- # case namespace_or_ops_file
473
- # when Namespace
474
- # namespace_or_ops_file
475
- # when OpsFile
476
- # params_hash = namespace_or_ops_file.build_params_hash(*args, **kwargs)
477
- # namespace_or_ops_file.invoke(@runtime_env, params_hash)
478
- # end
479
- # end
480
- # methods_defined << symbol_name
481
- # end
482
- # end
483
-
484
- # klass
485
- # end
486
-
487
- # include InvocationDSL
488
-
489
- # def initialize(ops_file_script, runtime_env, params_hash)
490
- # @ops_file_script = ops_file_script
491
- # @runtime_env = runtime_env
492
- # @params = InvocationParams.new(params_hash)
493
- # end
494
-
495
- # def backend
496
- # @runtime_env.pty
497
- # end
498
-
499
- # def debug?
500
- # @runtime_env.debug?
501
- # end
502
-
503
- # def verbose?
504
- # @runtime_env.verbose?
505
- # end
506
-
507
- # # def evaluate
508
- # # # the evaluation context needs to be a module with all of the following:
509
- # # # - InvocationDSL methods
510
- # # # - @ops_file_script
511
- # # # - @runtime_env
512
- # # # - @params
513
- # # # - #backend
514
- # # # - #debug?
515
- # # # - #verbose?
516
- # # # - all the dynamically defined methods in the subclass of Invocation
517
- # # end
518
-
519
- # def evaluate
520
- # eval(@ops_file_script.script, nil, @ops_file_script.ops_file.ops_file_path.to_s, @ops_file_script.ops_file.script_line_offset)
521
- # end
522
-
523
- # # def evaluate
524
- # # ops_file_script = @ops_file_script
525
- # # runtime_env = @runtime_env
526
- # # invocation_params = @params
527
-
528
- # # # we use a Module as the evaluation context instead of this #evaluate method so that classes and other structures may be defined in an .ops file
529
- # # evaluation_context = Module.new
530
- # # evaluation_context.module_exec {
531
- # # include InvocationDSL
532
-
533
- # # @ops_file_script = ops_file_script
534
- # # @runtime_env = runtime_env
535
- # # @params = invocation_params
536
-
537
- # # def backend
538
- # # @runtime_env.pty
539
- # # end
540
-
541
- # # def debug?
542
- # # @runtime_env.debug?
543
- # # end
544
-
545
- # # def verbose?
546
- # # @runtime_env.verbose?
547
- # # end
548
- # # }
549
-
550
- # # evaluation_context.module_eval(@ops_file_script.script, @ops_file_script.ops_file.ops_file_path.to_s, @ops_file_script.ops_file.script_line_offset)
551
- # # end
552
-
553
- # # def method_missing(name, *args, **kwargs, &block)
554
- # # puts "1" * 80
555
- # # import_reference = @ops_file_script.ops_file.resolve_import(name)
556
- # # if import_reference
557
- # # puts "2" * 80
558
- # # resolved_value = @runtime_env.resolve_import_reference(@ops_file_script.ops_file, import_reference)
559
- # # return resolved_value if resolved_value
560
- # # end
561
133
 
562
- # # puts "3" * 80
563
- # # case namespace_or_ops_file = @runtime_env.resolve_symbol(@ops_file_script.ops_file, name.to_s)
564
- # # when Namespace
565
- # # puts "4" * 80
566
- # # namespace_or_ops_file
567
- # # when OpsFile
568
- # # puts "5" * 80
569
- # # namespace_or_ops_file.invoke(@runtime_env, *args, **kwargs, &block)
570
- # # else
571
- # # raise NoMethodError, "No method named '#{name}'"
572
- # # end
573
- # # end
574
- # end
575
134
  end
@@ -58,25 +58,25 @@ module OpsWalrus
58
58
  Invocation::Success.new(ruby_script_return)
59
59
  end
60
60
  rescue SSHKit::Command::Failed => e
61
- puts "[!] Command failed: #{e.message}"
61
+ App.instance.error "[!] Command failed: #{e.message}"
62
62
  rescue Error => e
63
- $stderr.puts "Error: Ops script crashed."
64
- $stderr.puts e.message
65
- $stderr.puts e.backtrace.join("\n")
63
+ App.instance.error "Error: Ops script crashed."
64
+ App.instance.error e.message
65
+ App.instance.error e.backtrace.take(5).join("\n")
66
66
  Invocation::Error.new(e)
67
67
  rescue => e
68
- $stderr.puts "Unhandled Error: Ops script crashed."
69
- $stderr.puts e.class
70
- $stderr.puts e.message
71
- $stderr.puts e.backtrace.join("\n")
68
+ App.instance.error "Unhandled Error: Ops script crashed."
69
+ App.instance.error e.class
70
+ App.instance.error e.message
71
+ App.instance.error e.backtrace.take(10).join("\n")
72
72
  Invocation::Error.new(e)
73
73
  end
74
74
 
75
75
  if app.debug? && result.failure?
76
- puts "Ops script error details:"
77
- puts "Error: #{result.value}"
78
- puts "Status code: #{result.exit_status}"
79
- puts @entry_point_ops_file.script
76
+ App.instance.debug "Ops script error details:"
77
+ App.instance.debug "Error: #{result.value}"
78
+ App.instance.debug "Status code: #{result.exit_status}"
79
+ App.instance.debug @entry_point_ops_file.script
80
80
  end
81
81
 
82
82
  result
@@ -14,22 +14,15 @@ module OpsWalrus
14
14
  # define methods for the OpsFile's local_symbol_table: local imports and private lib directory
15
15
  ops_file.local_symbol_table.each do |symbol_name, import_reference|
16
16
  unless methods_defined.include? symbol_name
17
- # puts "defining method for local symbol table entry: #{symbol_name}"
17
+ App.instance.debug "defining method for local symbol table entry: #{symbol_name}"
18
18
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
19
- # puts "resolving local symbol table entry: #{symbol_name}"
19
+ App.instance.debug "resolving local symbol table entry: #{symbol_name}"
20
20
  namespace_or_ops_file = @runtime_env.resolve_import_reference(ops_file, import_reference)
21
- # puts "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
21
+ App.instance.debug "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
22
22
 
23
23
  invocation_context = LocalImportInvocationContext.new(@runtime_env, namespace_or_ops_file)
24
24
  invocation_context._invoke(*args, **kwargs)
25
25
 
26
- # case namespace_or_ops_file
27
- # when Namespace
28
- # namespace_or_ops_file._invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
29
- # when OpsFile
30
- # params_hash = namespace_or_ops_file.build_params_hash(*args, **kwargs)
31
- # namespace_or_ops_file.invoke(@runtime_env, params_hash)
32
- # end
33
26
  end
34
27
  methods_defined << symbol_name
35
28
  end
@@ -40,26 +33,18 @@ module OpsWalrus
40
33
  sibling_symbol_table_names |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
41
34
  sibling_symbol_table_names |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
42
35
  # puts "sibling_symbol_table_names=#{sibling_symbol_table_names}"
43
- # puts "methods_defined=#{methods_defined}"
36
+ App.instance.debug "methods_defined=#{methods_defined}"
44
37
  sibling_symbol_table_names.each do |symbol_name|
45
38
  unless methods_defined.include? symbol_name
46
- # puts "defining method for implicit imports: #{symbol_name}"
39
+ App.instance.debug "defining method for implicit imports: #{symbol_name}"
47
40
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
48
- # puts "resolving implicit import: #{symbol_name}"
41
+ App.instance.debug "resolving implicit import: #{symbol_name}"
49
42
  namespace_or_ops_file = @runtime_env.resolve_sibling_symbol(ops_file, symbol_name)
50
- # puts "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
43
+ App.instance.debug "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
51
44
 
52
45
  invocation_context = LocalImportInvocationContext.new(@runtime_env, namespace_or_ops_file)
53
46
  invocation_context._invoke(*args, **kwargs)
54
47
 
55
-
56
- # case namespace_or_ops_file
57
- # when Namespace
58
- # namespace_or_ops_file._invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
59
- # when OpsFile
60
- # params_hash = namespace_or_ops_file.build_params_hash(*args, **kwargs)
61
- # namespace_or_ops_file.invoke(@runtime_env, params_hash)
62
- # end
63
48
  end
64
49
  methods_defined << symbol_name
65
50
  end
@@ -37,6 +37,17 @@ module OpsWalrus
37
37
  local_name
38
38
  end
39
39
 
40
+ def to_s
41
+ "PackageReference(local_name=#{@local_name}, package_uri=#{@package_uri}, version=#{@version})"
42
+ end
43
+
44
+ def summary
45
+ if version
46
+ "#{package_uri}:#{version}"
47
+ else
48
+ package_uri
49
+ end
50
+ end
40
51
  end
41
52
 
42
53
  # these are dynamic package references defined at runtime when an OpsFile's imports are being evaluated.
@@ -23,6 +23,12 @@ module OpsWalrus
23
23
  super(local_name)
24
24
  @package_reference = package_reference
25
25
  end
26
+ def to_s
27
+ "PackageDependencyReference(local_name=#{@local_name}, package_reference=#{@package_reference})"
28
+ end
29
+ def summary
30
+ "#{local_name}: #{package_reference.summary}"
31
+ end
26
32
  end
27
33
 
28
34
  class DirectoryReference < ImportReference
@@ -31,6 +37,12 @@ module OpsWalrus
31
37
  super(local_name)
32
38
  @dirname = dirname
33
39
  end
40
+ def to_s
41
+ "DirectoryReference(local_name=#{@local_name}, dirname=#{@dirname})"
42
+ end
43
+ def summary
44
+ "#{local_name}: #{dirname}"
45
+ end
34
46
  end
35
47
 
36
48
  class DynamicPackageImportReference < ImportReference
@@ -39,6 +51,12 @@ module OpsWalrus
39
51
  super(local_name)
40
52
  @package_reference = package_reference
41
53
  end
54
+ def to_s
55
+ "DynamicPackageImportReference(local_name=#{@local_name}, package_reference=#{@package_reference})"
56
+ end
57
+ def summary
58
+ "#{local_name}: #{package_reference.summary}"
59
+ end
42
60
  end
43
61
 
44
62
  class OpsFileReference < ImportReference
@@ -47,6 +65,12 @@ module OpsWalrus
47
65
  super(local_name)
48
66
  @ops_file_path = ops_file_path
49
67
  end
68
+ def to_s
69
+ "DirectoryReference(local_name=#{@local_name}, ops_file_path=#{@ops_file_path})"
70
+ end
71
+ def summary
72
+ "#{local_name}: #{ops_file_path}"
73
+ end
50
74
  end
51
75
 
52
76
  # Namespace is really just a Map of symbol_name -> (Namespace | OpsFile) pairs
@@ -178,6 +202,10 @@ module OpsWalrus
178
202
  path_map
179
203
  end
180
204
 
205
+ def includes_path?(path)
206
+ !!@path_map[path]
207
+ end
208
+
181
209
  def dynamically_add_new_package_dir(new_package_dir)
182
210
  # patch the symbol resolution (namespace) tree
183
211
  dir_basename = new_package_dir.basename
@@ -198,19 +226,22 @@ module OpsWalrus
198
226
 
199
227
  # returns a Namespace or OpsFile
200
228
  def resolve_symbol(origin_ops_file, symbol_name)
201
- lookup_namespace(origin_ops_file)&.resolve_symbol(symbol_name)
229
+ resolved_namespace_or_ops_file = lookup_namespace(origin_ops_file)&.resolve_symbol(symbol_name)
230
+ App.instance.debug("LoadPath#resolve_symbol(#{origin_ops_file}, #{symbol_name}) -> #{resolved_namespace_or_ops_file}")
231
+ resolved_namespace_or_ops_file
202
232
  end
203
233
 
204
234
  # returns a Namespace | OpsFile
205
235
  def resolve_import_reference(origin_ops_file, import_reference)
206
- case import_reference
236
+ resolved_namespace_or_ops_file = case import_reference
207
237
  when PackageDependencyReference
208
238
  # puts "root namespace: #{@root_namespace.symbol_table}"
209
- @root_namespace.resolve_symbol(import_reference.package_reference.import_resolution_dirname) # returns the Namespace associated with the bundled package dirname (i.e. the local name)
239
+ @root_namespace.resolve_symbol(import_reference.package_reference.import_resolution_dirname) # returns the Namespace associated with the bundled package import_resolution_dirname (i.e. the local name)
210
240
  when DynamicPackageImportReference
211
241
  dynamic_package_reference = import_reference.package_reference
212
242
  @dynamic_package_additions_memo[dynamic_package_reference] ||= begin
213
243
  # puts "Downloading dynamic package: #{dynamic_package_reference.inspect}"
244
+ App.instance.debug("Downloading dynamic package: #{dynamic_package_reference}")
214
245
  dynamically_added_package_dir = @runtime_env.app.bundler.download_git_package(dynamic_package_reference.package_uri, dynamic_package_reference.version)
215
246
  dynamically_add_new_package_dir(dynamically_added_package_dir)
216
247
  dynamically_added_package_dir
@@ -221,6 +252,8 @@ module OpsWalrus
221
252
  when OpsFileReference
222
253
  @path_map[import_reference.ops_file_path]
223
254
  end
255
+ App.instance.debug("LoadPath#resolve_import_reference(#{origin_ops_file}, #{import_reference}) -> #{resolved_namespace_or_ops_file}")
256
+ resolved_namespace_or_ops_file
224
257
  end
225
258
  end
226
259
 
@@ -315,23 +348,41 @@ module OpsWalrus
315
348
  ops_file.invoke(self, params_hash)
316
349
  end
317
350
 
351
+ def find_load_path_that_includes_path(path)
352
+ load_path = [@bundle_load_path, @app_load_path].find {|load_path| load_path.includes_path?(path) }
353
+ raise SymbolResolutionError, "No load path includes the path: #{path}" unless load_path
354
+ load_path
355
+ end
356
+
318
357
  # returns a Namespace | OpsFile
319
358
  def resolve_sibling_symbol(origin_ops_file, symbol_name)
320
- @app_load_path.resolve_symbol(origin_ops_file, symbol_name)
359
+ # if the origin_ops_file's file path is contained within a Bundler::BUNDLE_DIR directory, then we want to consult the @bundle_load_path
360
+ # otherwise, we want to consult the @app_load_path
361
+ load_path = find_load_path_that_includes_path(origin_ops_file.ops_file_path)
362
+ namespace_or_ops_file = load_path.resolve_symbol(origin_ops_file, symbol_name)
363
+ raise SymbolResolutionError, "Symbol '#{symbol_name}' not in load path for #{origin_ops_file.ops_file_path}" unless namespace_or_ops_file
364
+ namespace_or_ops_file
321
365
  end
322
366
 
323
367
  # returns a Namespace | OpsFile
324
368
  def resolve_import_reference(origin_ops_file, import_reference)
325
- case import_reference
369
+ load_path = case import_reference
326
370
 
327
- # we know we're dealing with a package dependency reference, so we want to do the lookup in the bundle load path, where package dependencies live
371
+ # We know we're dealing with a package dependency reference, so we want to do the lookup in the bundle load path, where package dependencies live.
372
+ # Package references are guaranteed to live in the bundle dir
328
373
  when PackageDependencyReference, DynamicPackageImportReference
329
- @bundle_load_path.resolve_import_reference(origin_ops_file, import_reference)
330
-
331
- # we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to do the lookup in the app load path
332
- when DirectoryReference, OpsFileReference
333
- @app_load_path.resolve_import_reference(origin_ops_file, import_reference)
374
+ @bundle_load_path
375
+ when DirectoryReference
376
+ find_load_path_that_includes_path(import_reference.dirname)
377
+ when OpsFileReference
378
+ find_load_path_that_includes_path(import_reference.ops_file_path)
334
379
  end
380
+
381
+ namespace_or_ops_file = load_path.resolve_import_reference(origin_ops_file, import_reference)
382
+
383
+ raise SymbolResolutionError, "Import reference '#{import_reference.summary}' not in load path for #{origin_ops_file.ops_file_path}" unless namespace_or_ops_file
384
+
385
+ namespace_or_ops_file
335
386
  end
336
387
 
337
388
  end
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.13"
2
+ VERSION = "1.0.14"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opswalrus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.13
4
+ version: 1.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Ellis
@@ -132,6 +132,7 @@ files:
132
132
  - lib/opswalrus/bootstrap.sh
133
133
  - lib/opswalrus/bundler.rb
134
134
  - lib/opswalrus/cli.rb
135
+ - lib/opswalrus/errors.rb
135
136
  - lib/opswalrus/git.rb
136
137
  - lib/opswalrus/host.rb
137
138
  - lib/opswalrus/hosts_file.rb