opswalrus 1.0.1 → 1.0.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: 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