opswalrus 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a8289839c7b797b102518b31d579236e7e4750f6c0deea76a7afb8c378e0d98
4
- data.tar.gz: d126fcd38d30c21781cf631717aaea6b560927b9a62af6944beeca99bf6f05a6
3
+ metadata.gz: b31832ac2c06fb96c6abac86dacccce0f4fa280d679f2f12573eb37044d49b93
4
+ data.tar.gz: dc69618f8f83b0845f5e93a8e4699bcade24e37a3ce24fcecc49a427f2b471ff
5
5
  SHA512:
6
- metadata.gz: 602c6567a3a191fb9bc63085a84039e16df9199ce94579cd16c74ff846560737541ba53cdbfb612f49e076e465832ed868c817ae4240d65cb7d89b97f24d0cc0
7
- data.tar.gz: 0b56dc14e1ee53936e0649cbcd58ebfc3ecf6abff2f0e3e558c5254dd0e0ec396b0c54ce90e4cdc09d848f6bec9ea61a0a65460bcec3124931d77ed70b6cd93c
6
+ metadata.gz: c107c7165daa8536b4831c7f84d34c662dcc86cad1da29334412460e2995adad5bb076f14ae48b90578e2c349ea55411e4f2794371d417e714bffb093dca4f3b
7
+ data.tar.gz: d6e0983ef2d59ed545370dd670753436026244b0cc5c84dadb3745b188931839f5edd57425f65fa4a607324b4d1e17bd4e0431bb5bb2cbb825e8a95e1f9eca6c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opswalrus (1.0.0)
4
+ opswalrus (1.0.3)
5
5
  bcrypt_pbkdf
6
6
  citrus
7
7
  ed25519
data/lib/opswalrus/app.rb CHANGED
@@ -39,6 +39,8 @@ module OpsWalrus
39
39
  @instance ||= new(*args)
40
40
  end
41
41
 
42
+ LOCAL_SUDO_PASSWORD_PROMPT = "[ops] Enter sudo password to run sudo in local environment: "
43
+
42
44
 
43
45
  attr_reader :local_hostname
44
46
 
@@ -119,7 +121,7 @@ module OpsWalrus
119
121
  end
120
122
 
121
123
  def prompt_sudo_password
122
- password = IO::console.getpass("[ops] Enter sudo password to run sudo in local environment: ")
124
+ password = IO::console.getpass(LOCAL_SUDO_PASSWORD_PROMPT)
123
125
  set_sudo_password(password)
124
126
  nil
125
127
  end
@@ -207,46 +209,52 @@ module OpsWalrus
207
209
  tmp_dir = nil
208
210
 
209
211
  case
212
+ when Dir.exist?(package_or_ops_file_reference)
213
+ dir = package_or_ops_file_reference
214
+ ops_file_path, operation_kv_args = find_entry_point_ops_file_in_dir(dir, package_operation_and_args)
215
+ [ops_file_path, operation_kv_args, tmp_dir]
210
216
  when File.exist?(package_or_ops_file_reference)
211
217
  first_filepath = package_or_ops_file_reference.to_pathname.realpath
212
- ops_file_path = case first_filepath.extname.downcase
218
+
219
+ ops_file_path, operation_kv_args = case first_filepath.extname.downcase
213
220
  when ".ops"
214
- first_filepath
221
+ [first_filepath, package_operation_and_args]
215
222
  when ".zip"
216
- tmp_dir = Dir.mktmpdir.to_pathname
223
+ tmp_dir = Dir.mktmpdir.to_pathname # this is the temporary bundle root dir
217
224
 
218
225
  # unzip the bundle into the temp directory
219
226
  DirZipper.unzip(first_filepath, tmp_dir)
220
227
 
221
- relative_ops_path = package_operation_and_args.slice!(0, 1).first
222
-
223
- tmp_dir.join(relative_ops_path)
228
+ find_entry_point_ops_file_in_dir(tmp_dir, package_operation_and_args)
224
229
  else
225
230
  raise Error, "Unknown file type for entrypoint: #{first_filepath}"
226
231
  end
227
- operation_kv_args = package_operation_and_args
228
- [ops_file_path, operation_kv_args, tmp_dir ]
232
+
233
+ # operation_kv_args = package_operation_and_args
234
+ [ops_file_path, operation_kv_args, tmp_dir]
229
235
  when repo_url = git_repo?(package_or_ops_file_reference)
230
236
  destination_package_path = bundler.download_git_package(repo_url)
231
237
 
232
- ops_file_path = nil
233
- base_path = Pathname.new(destination_package_path)
234
- path_parts = 0
235
- package_operation_and_args.each do |candidate_path_arg|
236
- candidate_base_path = base_path.join(candidate_path_arg)
237
- candidate_ops_file = candidate_base_path.sub_ext(".ops")
238
- if candidate_ops_file.exist?
239
- path_parts += 1
240
- ops_file_path = candidate_ops_file
241
- break
242
- elsif candidate_base_path.exist?
243
- path_parts += 1
244
- else
245
- raise Error, "Operation not found in #{repo_url}: #{candidate_base_path}"
246
- end
247
- base_path = candidate_base_path
248
- end
249
- operation_kv_args = package_operation_and_args.drop(path_parts)
238
+ ops_file_path, operation_kv_args = find_entry_point_ops_file_in_dir(destination_package_path, package_operation_and_args)
239
+
240
+ # ops_file_path = nil
241
+ # base_path = Pathname.new(destination_package_path)
242
+ # path_parts = 0
243
+ # package_operation_and_args.each do |candidate_path_arg|
244
+ # candidate_base_path = base_path.join(candidate_path_arg)
245
+ # candidate_ops_file = candidate_base_path.sub_ext(".ops")
246
+ # if candidate_ops_file.exist?
247
+ # path_parts += 1
248
+ # ops_file_path = candidate_ops_file
249
+ # break
250
+ # elsif candidate_base_path.exist?
251
+ # path_parts += 1
252
+ # else
253
+ # raise Error, "Operation not found in #{repo_url}: #{candidate_base_path}"
254
+ # end
255
+ # base_path = candidate_base_path
256
+ # end
257
+ # operation_kv_args = package_operation_and_args.drop(path_parts)
250
258
 
251
259
  # for an original package_operation_and_args of ["github.com/davidkellis/my-package", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
252
260
  # we return: [ "#{pwd}/#{Bundler::BUNDLE_DIR}/github-com-davidkellis-my-package/operation1.ops", ["arg1:val1", "arg2:val2", "arg3:val3"] ]
@@ -256,6 +264,31 @@ module OpsWalrus
256
264
  end
257
265
  end
258
266
 
267
+ # returns pair of the form: [ ops_file_path, operation_kv_args ]
268
+ def find_entry_point_ops_file_in_dir(base_dir, package_operation_and_args)
269
+ ops_file_path = nil
270
+ base_path = Pathname.new(base_dir)
271
+
272
+ path_parts = 0
273
+ package_operation_and_args.each do |candidate_path_arg|
274
+ candidate_base_path = base_path.join(candidate_path_arg)
275
+ candidate_ops_file = candidate_base_path.sub_ext(".ops")
276
+ if candidate_ops_file.exist?
277
+ path_parts += 1
278
+ ops_file_path = candidate_ops_file
279
+ break
280
+ elsif candidate_base_path.exist?
281
+ path_parts += 1
282
+ else
283
+ raise Error, "Operation not found in: #{candidate_base_path}"
284
+ end
285
+ base_path = candidate_base_path
286
+ end
287
+ operation_kv_args = package_operation_and_args.drop(path_parts)
288
+
289
+ [ops_file_path, operation_kv_args]
290
+ end
291
+
259
292
  # git_repo?("davidkellis/arborist") -> "https://github.com/davidkellis/arborist"
260
293
  # returns the repo URL
261
294
  def git_repo?(repo_reference)
@@ -268,6 +301,10 @@ module OpsWalrus
268
301
  working_repo_reference
269
302
  end
270
303
 
304
+ # def is_dir_git_repo?(dir_path)
305
+ # Git.ls_remote(reference) rescue nil
306
+ # end
307
+
271
308
  def bundle_status
272
309
  end
273
310
 
data/lib/opswalrus/cli.rb CHANGED
@@ -15,10 +15,10 @@ module OpsWalrus
15
15
 
16
16
  # this is invoked on an unhandled exception or a call to exit_now!
17
17
  on_error do |exception|
18
- puts "*" * 80
19
- puts "catchall exception handler:"
20
- puts exception.message
21
- puts exception.backtrace.join("\n")
18
+ # puts "*" * 80
19
+ # puts "catchall exception handler:"
20
+ # puts exception.message
21
+ # puts exception.backtrace.join("\n")
22
22
  false # disable built-in exception handling
23
23
  end
24
24
 
@@ -132,6 +132,7 @@ module OpsWalrus
132
132
 
133
133
  c.default_command :status
134
134
  end
135
+
135
136
  end
136
137
  end
137
138
 
@@ -53,18 +53,71 @@ module OpsWalrus
53
53
  # execute('eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"')
54
54
  # end
55
55
 
56
- def run_ops(cmd)
56
+ # runs the specified ops command with the specified command arguments
57
+ def run_ops(command, command_arguments, in_bundle_root_dir: true, verbose: false)
57
58
  # e.g. /home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops bundle unzip tmpops.zip
58
- shell!("/home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops #{cmd}")
59
+ # e.g. /home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops run echo.ops args:foo args:bar
60
+
61
+ cmd = "/home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops"
62
+ cmd << " -v" if verbose
63
+ cmd << " #{command.to_s}"
64
+ cmd << " #{@tmp_bundle_root_dir}" if in_bundle_root_dir
65
+ cmd << " #{command_arguments}" unless command_arguments.empty?
66
+
67
+ shell!(cmd)
59
68
  end
60
69
 
61
70
  end
62
71
 
72
+ class HostProxyOpsFileInvocationBuilder
73
+ def initialize(host_proxy, is_invocation_a_call_to_package_in_bundle_dir = false)
74
+ @host_proxy = host_proxy
75
+ @is_invocation_a_call_to_package_in_bundle_dir = is_invocation_a_call_to_package_in_bundle_dir
76
+ @method_chain = []
77
+ end
78
+
79
+ def method_missing(method_name, *args, **kwargs)
80
+ @method_chain << method_name.to_s
81
+
82
+ if args.empty? && kwargs.empty? # when there are no args and no kwargs, we are just drilling down through another namespace
83
+ self
84
+ else
85
+ # when there are args or kwargs, then the method invocation represents an attempt to run an OpsFile on a remote host,
86
+ # so we want to build up a command and send it to the remote host via HostDSL#run_ops
87
+ @method_chain.unshift(Bundler::BUNDLE_DIR) if @is_invocation_a_call_to_package_in_bundle_dir
88
+
89
+ # path_to_ops_file_basename = @method_chain.join(File::Separator)
90
+ # remote_run_command_args << path_to_ops_file_basename
91
+
92
+ remote_run_command_args = @method_chain.join(" ")
93
+
94
+ unless args.empty?
95
+ remote_run_command_args << " "
96
+ remote_run_command_args << args.join(" ")
97
+ end
98
+
99
+ unless kvargs.empty?
100
+ remote_run_command_args << " "
101
+ remote_run_command_args << kvargs.map{|k,v| "#{k}=#{v}" }.join(" ") unless kvargs.empty?
102
+ end
103
+
104
+ @host_proxy.run_ops(:run, remote_run_command_args)
105
+ end
106
+ end
107
+ end
108
+
109
+ # the subclasses of HostProxy will define methods that handle method dispatch via HostProxyOpsFileInvocationBuilder objects
63
110
  class HostProxy
111
+ attr_accessor :_host
112
+
64
113
  def initialize(host)
65
- @host = host
114
+ @_host = host
66
115
  end
67
- def method_missing()
116
+
117
+ # the subclasses of this class will define methods that handle method dispatch via HostProxyOpsFileInvocationBuilder objects
118
+
119
+ def method_missing(name, ...)
120
+ @_host.send(name, ...)
68
121
  end
69
122
  end
70
123
 
@@ -142,12 +195,17 @@ module OpsWalrus
142
195
  })
143
196
  end
144
197
 
145
- def set_ssh_connection(sshkit_backend)
198
+ def set_ssh_session_connection(sshkit_backend)
146
199
  @sshkit_backend = sshkit_backend
147
200
  end
148
201
 
149
- def clear_ssh_connection
150
- @sskit_backend = nil
202
+ def set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
203
+ @tmp_bundle_root_dir = tmp_bundle_root_dir
204
+ end
205
+
206
+ def clear_ssh_session
207
+ @sshkit_backend = nil
208
+ @tmp_bundle_root_dir = nil
151
209
  end
152
210
 
153
211
  def execute(*args)
@@ -169,10 +227,6 @@ module OpsWalrus
169
227
  @sshkit_backend.download!(remote_path.to_s, local_path.to_s)
170
228
  end
171
229
 
172
- def run
173
-
174
- end
175
-
176
230
  end
177
231
 
178
232
  end
@@ -25,6 +25,7 @@ module OpsWalrus
25
25
  def interaction_handler
26
26
  SSHKit::MappingInteractionHandler.new({
27
27
  /\[sudo\] password for .*?:\s*/ => "#{@sudo_password}\n",
28
+ App::LOCAL_SUDO_PASSWORD_PROMPT => "#{@sudo_password}\n",
28
29
  # /\s+/ => nil, # unnecessary
29
30
  }, :info)
30
31
  end
@@ -27,6 +27,10 @@ module OpsWalrus
27
27
  @yaml || (load_file && @yaml)
28
28
  end
29
29
 
30
+ def script_line_offset
31
+ @script_line_offset || (load_file && @script_line_offset)
32
+ end
33
+
30
34
  def script
31
35
  @script || (load_file && @script)
32
36
  end
@@ -35,6 +39,7 @@ module OpsWalrus
35
39
  yaml, ruby_script = if @ops_file_path.exist?
36
40
  parse(File.read(@ops_file_path))
37
41
  end || ["", ""]
42
+ @script_line_offset = yaml.lines.size + 1 # +1 to account for the ... line
38
43
  @yaml = YAML.load(yaml) || {} # post_invariant: @yaml is a Hash
39
44
  @script = OpsFileScript.new(self, ruby_script)
40
45
  end
@@ -115,7 +120,7 @@ module OpsWalrus
115
120
  end
116
121
 
117
122
  def invoke(runtime_env, params_hash)
118
- puts "invoking: #{ops_file_path}"
123
+ # puts "invoking: #{ops_file_path}"
119
124
  script.invoke(runtime_env, params_hash)
120
125
  end
121
126
 
@@ -146,7 +151,7 @@ module OpsWalrus
146
151
  @local_symbol_table ||= begin
147
152
  local_symbol_table = {}
148
153
 
149
- local_symbol_table.merge(imports)
154
+ local_symbol_table.merge!(imports)
150
155
 
151
156
  # this is the import for the private lib directory if it exists
152
157
  if private_lib_dir.exist?
@@ -8,6 +8,7 @@ require 'stringio'
8
8
  require 'sshkit'
9
9
  require 'sshkit/dsl'
10
10
 
11
+ require_relative 'host'
11
12
  require_relative 'sshkit_ext'
12
13
  require_relative 'walrus_lang'
13
14
 
@@ -71,24 +72,24 @@ module OpsWalrus
71
72
  end
72
73
 
73
74
  class Env
74
- # params : Hash
75
- def initialize(params)
76
- @params = params
75
+ # env : Hash
76
+ def initialize(env)
77
+ @env = env
77
78
  end
78
79
 
79
80
  def [](key)
80
81
  key = key.to_s if key.is_a? Symbol
81
- @params[key]
82
+ @env[key]
82
83
  end
83
84
 
84
85
  def dig(*keys)
85
86
  keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
86
- @params.dig(*keys)
87
+ @env.dig(*keys)
87
88
  end
88
89
 
89
90
  def method_missing(name, *args, **kwargs, &block)
90
- if @params.respond_to?(name)
91
- @params.method(name).call(*args, **kwargs, &block)
91
+ if @env.respond_to?(name)
92
+ @env.method(name).call(*args, **kwargs, &block)
92
93
  else
93
94
  value = self[name]
94
95
  case value
@@ -107,10 +108,9 @@ module OpsWalrus
107
108
  # SCRIPT
108
109
 
109
110
  module DSL
110
- # include SSHKit::DSL
111
-
112
111
  def ssh(*args, **kwargs, &block)
113
- hosts = inventory(*args, **kwargs)
112
+ host_proxy_class = @ops_file_script.host_proxy_class
113
+ hosts = inventory(*args, **kwargs).map {|host| host_proxy_class.new(host) }
114
114
  sshkit_hosts = hosts.map(&:sshkit_host)
115
115
  sshkit_host_to_ops_host_map = sshkit_hosts.zip(hosts).to_h
116
116
  runtime_env = @runtime_env
@@ -124,7 +124,7 @@ module OpsWalrus
124
124
 
125
125
  begin
126
126
  # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
127
- host.set_ssh_connection(self)
127
+ host.set_ssh_session_connection(self)
128
128
 
129
129
  # copy over bootstrap shell script
130
130
  # io = StringIO.new(bootstrap_shell_script)
@@ -140,9 +140,10 @@ module OpsWalrus
140
140
  upload_success = host.upload(zip_bundle_path, "tmpops.zip")
141
141
  raise Error, "Unable to upload ops bundle to remote host" unless upload_success
142
142
 
143
- stdout, stderr, exit_status = host.run_ops("bundle unzip tmpops.zip")
143
+ stdout, stderr, exit_status = host.run_ops(:bundle, "unzip tmpops.zip", in_bundle_root_dir: false)
144
144
  raise Error, "Unable to unzip ops bundle on remote host" unless exit_status == 0
145
- tmp_bundle_dir = stdout.strip
145
+ tmp_bundle_root_dir = stdout.strip
146
+ host.set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
146
147
 
147
148
  # we run the block in the context of the host, s.t. `self` within the block evaluates to `host`
148
149
  retval = host.instance_exec(local_host, &block) # host is passed as the argument to the block
@@ -179,7 +180,7 @@ module OpsWalrus
179
180
  puts e.message
180
181
  # puts e.backtrace.join("\n")
181
182
  ensure
182
- host.clear_ssh_connection
183
+ host.clear_ssh_session
183
184
  end
184
185
  end
185
186
  end
@@ -291,6 +292,7 @@ module OpsWalrus
291
292
  @ops_file = ops_file
292
293
  @ruby_script = ruby_script
293
294
  @invocation_class = define_invocation_class
295
+ @host_proxy_class = define_host_proxy_class
294
296
  end
295
297
 
296
298
  def define_invocation_class
@@ -350,6 +352,56 @@ module OpsWalrus
350
352
  klass
351
353
  end
352
354
 
355
+ def define_host_proxy_class
356
+ klass = Class.new(HostProxy)
357
+
358
+ methods_defined = Set.new
359
+
360
+ # define methods for every import in the script
361
+ ops_file.local_symbol_table.each do |symbol_name, import_reference|
362
+ unless methods_defined.include? symbol_name
363
+ # puts "1. defining: #{symbol_name}(...)"
364
+ klass.define_method(symbol_name) do |*args, **kwargs, &block|
365
+ invocation_builder = case import_reference
366
+ # we know we're dealing with a package dependency reference, so we want to run an ops file contained within the bundle directory,
367
+ # therefore, we want to reference the specified ops file with respect to the bundle dir
368
+ when PackageDependencyReference
369
+ HostProxyOpsFileInvocationBuilder.new(self, true)
370
+
371
+ # we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to reference
372
+ # the specified ops file with respect to the root directory, and not with respect to the bundle dir
373
+ when DirectoryReference, OpsFileReference
374
+ HostProxyOpsFileInvocationBuilder.new(self, false)
375
+ end
376
+
377
+ invocation_builder.send(symbol_name, *args, **kwargs, &block)
378
+ end
379
+ methods_defined << symbol_name
380
+ end
381
+ end
382
+
383
+ # define methods for every Namespace or OpsFile within the namespace that the OpsFile resides within
384
+ sibling_symbol_table = Set.new
385
+ sibling_symbol_table |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
386
+ sibling_symbol_table |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
387
+ sibling_symbol_table.each do |symbol_name|
388
+ unless methods_defined.include? symbol_name
389
+ # puts "2. defining: #{symbol_name}(...)"
390
+ klass.define_method(symbol_name) do |*args, **kwargs, &block|
391
+ invocation_builder = HostProxyOpsFileInvocationBuilder.new(self, false)
392
+ invocation_builder.invoke(symbol_name, *args, **kwargs, &block)
393
+ end
394
+ methods_defined << symbol_name
395
+ end
396
+ end
397
+
398
+ klass
399
+ end
400
+
401
+ def host_proxy_class
402
+ @host_proxy_class
403
+ end
404
+
353
405
  def script
354
406
  @ruby_script
355
407
  end
@@ -423,7 +475,7 @@ module OpsWalrus
423
475
 
424
476
  def evaluate
425
477
  # catch(:exit_now) do
426
- eval(@ops_file_script.script)
478
+ eval(@ops_file_script.script, nil, @ops_file_script.ops_file.ops_file_path.to_s, @ops_file_script.ops_file.script_line_offset)
427
479
  # end
428
480
  end
429
481
 
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opswalrus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Ellis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-08-13 00:00:00.000000000 Z
11
+ date: 2023-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: citrus
@@ -114,7 +114,6 @@ email:
114
114
  - david@conquerthelawn.com
115
115
  executables:
116
116
  - ops
117
- - run_ops_bundle
118
117
  extensions: []
119
118
  extra_rdoc_files: []
120
119
  files:
@@ -125,7 +124,6 @@ files:
125
124
  - README.md
126
125
  - Rakefile
127
126
  - exe/ops
128
- - exe/run_ops_bundle
129
127
  - lib/opswalrus.rb
130
128
  - lib/opswalrus/app.rb
131
129
  - lib/opswalrus/bootstrap.sh
data/exe/run_ops_bundle DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'opswalrus'
4
-
5
- def main
6
- zip_file_path = ARGV.shift.to_pathname
7
-
8
- exit_status = 0
9
-
10
- Dir.mktmpdir do |dir|
11
- dir = dir.to_pathname
12
-
13
- # unzip the bundle into the temp directory
14
- OpsWalrus::DirZipper.unzip(zip_file_path, dir)
15
-
16
- exit_status = OpsWalrus::Cli.run(ARGV)
17
- end
18
-
19
- exit exit_status
20
- end
21
-
22
- main