opswalrus 1.0.16 → 1.0.17

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: f6d990ac1fac1a918ebca3eac342377ee61702f0864e635e378ad8e69d7ec0d5
4
- data.tar.gz: 900661d04774a42b0a960624fd7fc51b9690c23ad15fba5aaf94f8a3e1241de6
3
+ metadata.gz: f2ce759171a0342644a1e3e2c94555299b093095b1684a69e11142fe29292da4
4
+ data.tar.gz: 75f4cab88e00038c73df02d69718b5cd067f3ed69150c192335894128ef912d0
5
5
  SHA512:
6
- metadata.gz: 88e94b27da26afb8b925f1b363c6ccf889f3e105d28e3f7d2ccfd1d2d8fe1b61d443d64dc1829c42b8644216e19bad1c4e384757246414b62dc18104421cb8fe
7
- data.tar.gz: 3e5b2b2062a592102bed054b7bae779a66b676ca8d47beb42cbab0a6cc6096e191129b3fd9a2c6fc815f8964bf78270b15687cbba99787c0323fb51c87e82c9c
6
+ metadata.gz: 6d47f83ae4dde1c9007f0d4591da7d6d9501218b15dfed542bdf9eda8538cfca14e22baa0d0a9e7dd244ef0db3fc683f862f296e296f713f500a3b084de02dac
7
+ data.tar.gz: 96877f5975e10dd6415194f5f130f3b7b31fcee66199c354cee93386116b4c0e918749c4158ec6da4a9fe64d9414410032c47d5c78c41caf2a36683cab259a03
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opswalrus (1.0.16)
4
+ opswalrus (1.0.17)
5
5
  amazing_print (~> 1.5)
6
6
  bcrypt_pbkdf (~> 1.1)
7
7
  citrus (~> 3.0)
@@ -9,8 +9,10 @@ PATH
9
9
  git (~> 1.18)
10
10
  gli (~> 2.21)
11
11
  ougai (~> 2.0)
12
+ pastel (~> 0.8)
12
13
  rubyzip (~> 2.3)
13
14
  sshkit (~> 1.21)
15
+ tty-editor (~> 0.7)
14
16
 
15
17
  GEM
16
18
  remote: https://rubygems.org/
@@ -32,6 +34,8 @@ GEM
32
34
  oj (3.16.0)
33
35
  ougai (2.0.0)
34
36
  oj (~> 3.10)
37
+ pastel (0.8.0)
38
+ tty-color (~> 0.5)
35
39
  public_suffix (5.0.3)
36
40
  rake (13.0.6)
37
41
  rchardet (1.8.0)
@@ -52,6 +56,19 @@ GEM
52
56
  sshkit (1.21.5)
53
57
  net-scp (>= 1.1.2)
54
58
  net-ssh (>= 2.8.0)
59
+ tty-color (0.6.0)
60
+ tty-cursor (0.7.1)
61
+ tty-editor (0.7.0)
62
+ tty-prompt (~> 0.22)
63
+ tty-prompt (0.23.1)
64
+ pastel (~> 0.8)
65
+ tty-reader (~> 0.8)
66
+ tty-reader (0.9.0)
67
+ tty-cursor (~> 0.7)
68
+ tty-screen (~> 0.8)
69
+ wisper (~> 2.0)
70
+ tty-screen (0.8.1)
71
+ wisper (2.0.1)
55
72
 
56
73
  PLATFORMS
57
74
  x86_64-linux
data/lib/opswalrus/app.rb CHANGED
@@ -4,11 +4,12 @@ require "json"
4
4
  # require "logger"
5
5
  require "random/formatter"
6
6
  require "ougai"
7
+ require "pastel"
8
+ require "pathname"
7
9
  require "shellwords"
8
10
  require "socket"
9
11
  require "stringio"
10
12
  require "yaml"
11
- require "pathname"
12
13
  require_relative "errors"
13
14
  require_relative "patches"
14
15
  require_relative "git"
@@ -21,6 +22,8 @@ require_relative "version"
21
22
 
22
23
 
23
24
  module OpsWalrus
25
+ Style = Pastel.new(enabled: $stdout.tty?)
26
+
24
27
  class App
25
28
  def self.instance(*args)
26
29
  @instance ||= new(*args)
@@ -32,9 +35,14 @@ module OpsWalrus
32
35
  attr_reader :local_hostname
33
36
 
34
37
  def initialize(pwd = Dir.pwd)
35
- @logger = Ougai::Logger.new($stdout, level: Logger::INFO) # Logger.new($stdout, level: Logger::INFO)
38
+ @logger = Ougai::Logger.new($stdout) # Logger.new($stdout, level: Logger::INFO)
39
+ @logger.level = :info # , :trace or 'trace'
36
40
  @logger.formatter = Ougai::Formatters::Readable.new
37
41
 
42
+ # @logger.warn Style.yellow("warn"), foo: "bar", baz: {qux: "quux"}
43
+ # @logger.info Style.yellow("info"), foo: "bar", baz: {qux: "quux"}
44
+ # @logger.debug Style.yellow("debug"), foo: "bar", baz: {qux: "quux"}
45
+ # @logger.trace Style.yellow("trace"), foo: "bar", baz: {qux: "quux"}
38
46
 
39
47
  @verbose = false
40
48
  @sudo_user = nil
@@ -46,6 +54,7 @@ module OpsWalrus
46
54
  @bundler = Bundler.new(@pwd)
47
55
  @local_hostname = "localhost"
48
56
  @mode = :report # :report | :script
57
+ @dry_run = false
49
58
  end
50
59
 
51
60
  def to_s
@@ -68,6 +77,14 @@ module OpsWalrus
68
77
  @mode == :script
69
78
  end
70
79
 
80
+ def dry_run?
81
+ @dry_run
82
+ end
83
+
84
+ def dry_run!
85
+ @dry_run = true
86
+ end
87
+
71
88
  def set_local_hostname(hostname)
72
89
  hostname = hostname.strip
73
90
  @local_hostname = hostname.empty? ? "localhost" : hostname
@@ -91,37 +108,54 @@ module OpsWalrus
91
108
  @bundler.bundle_dir
92
109
  end
93
110
 
94
- def set_verbose(verbose)
95
- @verbose = verbose
96
- @logger.debug! if verbose?
111
+ # log_level = :fatal, :error, :warn, :info, :debug, :trace
112
+ # irb(main):018:0> Ougai::Logger::TRACE
113
+ # => -1
114
+ # irb(main):019:0> Ougai::Logger::DEBUG
115
+ # => 0
116
+ # irb(main):020:0> Ougai::Logger::INFO
117
+ # => 1
118
+ # irb(main):021:0> Ougai::Logger::WARN
119
+ # => 2
120
+ # irb(main):022:0> Ougai::Logger::ERROR
121
+ # => 3
122
+ # irb(main):023:0> Ougai::Logger::FATAL
123
+ # => 4
124
+ def set_log_level(log_level)
125
+ @logger.level = log_level
97
126
  end
98
127
 
99
128
  def verbose?
100
- @verbose
129
+ @logger.level <= 1
101
130
  end
102
131
 
103
132
  def debug?
104
- @verbose == 2
133
+ @logger.level <= 0
134
+ end
135
+
136
+ def fatal(*args)
137
+ @logger.fatal(*args)
105
138
  end
106
139
 
107
- def error(msg)
108
- @logger.error(msg)
140
+ def error(*args)
141
+ @logger.error(*args)
109
142
  end
110
143
 
111
- def warn(msg)
112
- @logger.warn(msg)
144
+ def warn(*args)
145
+ @logger.warn(*args)
113
146
  end
114
147
 
115
- def log(msg)
116
- @logger.info(msg)
148
+ def log(*args)
149
+ @logger.info(*args)
117
150
  end
151
+ alias_method :info, :log
118
152
 
119
- def debug(msg)
120
- @logger.debug(msg)
153
+ def debug(*args)
154
+ @logger.debug(*args)
121
155
  end
122
156
 
123
- def trace(msg)
124
- @logger.trace(msg)
157
+ def trace(*args)
158
+ @logger.trace(*args)
125
159
  end
126
160
 
127
161
  def set_pwd(pwd)
@@ -9,6 +9,7 @@ if [ -x "$(command -v /home/linuxbrew/.linuxbrew/bin/brew)" ]; then
9
9
  echo 'Ruby is already installed.' >&2
10
10
 
11
11
  # make sure the latest opswalrus gem is installed
12
+ # todo: figure out how to install this differently, so that test versions will work
12
13
  gem install opswalrus
13
14
 
14
15
  exit 0
@@ -26,6 +26,10 @@ module OpsWalrus
26
26
  FileUtils.mkdir_p(@bundle_dir) unless @bundle_dir.exist?
27
27
  end
28
28
 
29
+ def delete_pwd_bundle_directory
30
+ FileUtils.remove_dir(@bundle_dir) if @bundle_dir.exist?
31
+ end
32
+
29
33
  # # returns the OpsFile within the bundle directory that represents the given ops_file (which is outside of the bundle directory)
30
34
  # def build_bundle_for_ops_file(ops_file)
31
35
  # if ops_file.package_file # ops_file is part of larger package
@@ -50,6 +54,7 @@ module OpsWalrus
50
54
  # end
51
55
 
52
56
  def update
57
+ delete_pwd_bundle_directory
53
58
  ensure_pwd_bundle_directory_exists
54
59
 
55
60
  package_yaml_files = pwd.glob("./**/package.yaml") - pwd.glob("./**/#{BUNDLE_DIR}/**/package.yaml")
@@ -110,36 +115,16 @@ module OpsWalrus
110
115
  version = package_reference.version
111
116
 
112
117
  destination_package_path = @bundle_dir.join(package_reference.import_resolution_dirname)
113
- FileUtils.remove_dir(destination_package_path) if destination_package_path.exist?
118
+
119
+ # we return early here under the assumption that an already downloaded package/version combo will not
120
+ # differ if we download it again multiple times to the same location
121
+ if destination_package_path.exist?
122
+ App.instance.log("Skipping #{package_reference} referenced in #{package_file.package_file_path} since it already has been downloaded to #{destination_package_path}")
123
+ return destination_package_path
124
+ end
125
+ # FileUtils.remove_dir(destination_package_path) if destination_package_path.exist?
114
126
 
115
127
  download_package_contents(package_file, local_name, package_url, version, destination_package_path)
116
- # case
117
- # when package_url =~ /\.git/ # git reference
118
- # download_git_package(package_url, version, destination_package_path)
119
- # when package_url.start_with?("file://") # local path
120
- # path = package_url.sub("file://", "")
121
- # path = path.to_pathname
122
- # package_path_to_download = if path.relative? # relative path
123
- # package_file.containing_directory.join(path)
124
- # else # absolute path
125
- # path.realpath
126
- # end
127
-
128
- # raise Error, "Package not found: #{package_path_to_download}" unless package_path_to_download.exist?
129
- # FileUtils.cp_r(package_path_to_download, destination_package_path)
130
- # when package_url.to_pathname.exist? || package_file.containing_directory.join(package_url).exist? # local path
131
- # path = package_url.to_pathname
132
- # package_path_to_download = if path.relative? # relative path
133
- # package_file.containing_directory.join(path)
134
- # else # absolute path
135
- # path.realpath
136
- # end
137
-
138
- # raise Error, "Package not found: #{package_path_to_download}" unless File.exist?(package_path_to_download)
139
- # FileUtils.cp_r(package_path_to_download, destination_package_path)
140
- # else # git reference
141
- # download_git_package(package_url, version, destination_package_path)
142
- # end
143
128
 
144
129
  destination_package_path
145
130
  end
@@ -147,10 +132,12 @@ module OpsWalrus
147
132
  def download_package_contents(package_file, local_name, package_url, version, destination_package_path)
148
133
  package_path = package_url.to_pathname
149
134
  package_path = package_path.to_s.gsub(/^~/, Dir.home).to_pathname
135
+ App.instance.trace("download_package_contents #{package_path}")
150
136
  if package_path.absolute? && package_path.exist? # absolute path reference
151
137
  return case
152
138
  when package_path.directory?
153
139
  package_path_to_download = package_path.realpath
140
+ App.instance.debug("Copying #{package_path_to_download} to #{destination_package_path}")
154
141
  FileUtils.cp_r(package_path_to_download, destination_package_path)
155
142
  when package_path.file?
156
143
  raise Error, "Package reference must be a directory, not a file:: #{local_name}: #{package_path}"
@@ -163,6 +150,7 @@ module OpsWalrus
163
150
  if rebased_path.exist?
164
151
  return case
165
152
  when rebased_path.directory?
153
+ App.instance.debug("Copying #{package_path_to_download} to #{destination_package_path}")
166
154
  package_path_to_download = rebased_path.realpath
167
155
  FileUtils.cp_r(package_path_to_download, destination_package_path)
168
156
  when rebased_path.file?
@@ -174,6 +162,7 @@ module OpsWalrus
174
162
  end
175
163
 
176
164
  if package_uri = Git.repo?(package_url) # git repo
165
+ App.instance.debug("Cloning repo #{package_uri} into #{destination_package_path}")
177
166
  download_git_package(package_uri, version, destination_package_path)
178
167
  end
179
168
  end
@@ -195,15 +184,10 @@ module OpsWalrus
195
184
  end
196
185
 
197
186
  def dynamic_package_path_for_git_package(package_url, version = nil)
198
- package_reference_dirname = sanitize_path(package_url)
187
+ package_reference_dirname = DynamicPackageReference.import_resolution_dirname(package_url, version)
199
188
  bundle_dir.join(package_reference_dirname)
200
189
  end
201
190
 
202
- def sanitize_path(path)
203
- # found this at https://apidock.com/rails/v5.2.3/ActiveStorage/Filename/sanitized
204
- path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
205
- end
206
-
207
191
  # returns the directory that the zip file is unzipped into
208
192
  def unzip(zip_bundle_file, output_dir = nil)
209
193
  if zip_bundle_file.to_pathname.exist?
data/lib/opswalrus/cli.rb CHANGED
@@ -9,7 +9,7 @@ module OpsWalrus
9
9
 
10
10
  pre do |global_options, command, options, args|
11
11
  $app = App.instance(Dir.pwd)
12
- $app.set_local_hostname(ENV["WALRUS_LOCAL_HOSTNAME"]) if ENV["WALRUS_LOCAL_HOSTNAME"]
12
+ $app.set_local_hostname(ENV["OPSWALRUS_LOCAL_HOSTNAME"]) if ENV["OPSWALRUS_LOCAL_HOSTNAME"]
13
13
  true
14
14
  end
15
15
 
@@ -31,6 +31,10 @@ module OpsWalrus
31
31
  desc 'Turn on debug mode'
32
32
  switch [:d, :debug]
33
33
 
34
+ switch :noop, desc: "Perform a dry run"
35
+ switch :dryrun, desc: "Perform a dry run"
36
+ switch :dry_run, desc: "Perform a dry run"
37
+
34
38
  flag [:h, :hosts], multiple: true, desc: "Specify the hosts.yaml file"
35
39
  flag [:t, :tags], multiple: true, desc: "Specify a set of tags to filter the hosts by"
36
40
 
@@ -48,7 +52,8 @@ module OpsWalrus
48
52
  hosts = global_options[:hosts]
49
53
  tags = global_options[:tags]
50
54
 
51
- $app.set_verbose(global_options[:debug] || global_options[:verbose])
55
+ log_level = global_options[:debug] && :trace || global_options[:verbose] && :debug || :info
56
+ $app.set_log_level(log_level)
52
57
 
53
58
  $app.report_inventory(hosts, tags: tags)
54
59
  end
@@ -63,28 +68,31 @@ module OpsWalrus
63
68
  c.flag [:p, :params], desc: "JSON string that represents the input parameters for the operation. The JSON string must conform to the params schema for the operation."
64
69
  c.switch :script, desc: "Script mode"
65
70
 
71
+ # dry run
72
+ c.switch :noop, desc: "Perform a dry run"
73
+ c.switch :dryrun, desc: "Perform a dry run"
74
+ c.switch :dry_run, desc: "Perform a dry run"
75
+
66
76
  c.action do |global_options, options, args|
77
+ log_level = global_options[:debug] && :trace || global_options[:verbose] && :debug || :info
78
+ $app.set_log_level(log_level)
79
+
67
80
  hosts = global_options[:hosts] || []
68
81
  tags = global_options[:tags] || []
69
82
 
70
83
  $app.set_inventory_hosts(hosts)
71
84
  $app.set_inventory_tags(tags)
72
85
 
73
- verbose = case
74
- when global_options[:debug]
75
- 2
76
- when global_options[:verbose]
77
- 1
78
- end
79
-
80
86
  user = options[:user]
81
87
  params = options[:params]
82
88
 
83
- $app.set_verbose(verbose)
84
89
  $app.set_params(params)
85
90
 
86
91
  $app.set_sudo_user(user) if user
87
92
 
93
+ dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
94
+ $app.dry_run! if dry_run
95
+
88
96
  if options[:pass]
89
97
  $app.prompt_sudo_password
90
98
  end
@@ -107,6 +115,9 @@ module OpsWalrus
107
115
  long_desc 'Download and bundle the latest versions of dependencies for the current package'
108
116
  c.command :update do |update|
109
117
  update.action do |global_options, options, args|
118
+ log_level = global_options[:debug] && :trace || global_options[:verbose] && :debug || :info
119
+ $app.set_log_level(log_level)
120
+
110
121
  $app.bundle_update
111
122
  end
112
123
  end
@@ -125,6 +136,9 @@ module OpsWalrus
125
136
  unzip.flag [:o, :output], desc: "Specify the output directory"
126
137
 
127
138
  unzip.action do |global_options, options, args|
139
+ log_level = global_options[:debug] && :trace || global_options[:verbose] && :debug || :info
140
+ $app.set_log_level(log_level)
141
+
128
142
  output_dir = options[:output]
129
143
  zip_file_path = args.first
130
144
 
@@ -26,12 +26,12 @@ module OpsWalrus
26
26
  # we know we're dealing with a package dependency reference, so we want to run an ops file contained within the bundle directory,
27
27
  # therefore, we want to reference the specified ops file with respect to the bundle dir
28
28
  when PackageDependencyReference
29
- RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, true)
29
+ RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, true, prompt_for_sudo_password: !!ssh_password)
30
30
 
31
31
  # we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to reference
32
32
  # the specified ops file with respect to the root directory, and not with respect to the bundle dir
33
33
  when DirectoryReference, OpsFileReference
34
- RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false)
34
+ RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false, prompt_for_sudo_password: !!ssh_password)
35
35
  end
36
36
 
37
37
  invocation_context._invoke(*args, **kwargs)
@@ -49,7 +49,7 @@ module OpsWalrus
49
49
  unless methods_defined.include? symbol_name
50
50
  # puts "2. defining: #{symbol_name}(...)"
51
51
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
52
- invocation_context = RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false)
52
+ invocation_context = RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false, prompt_for_sudo_password: !!ssh_password)
53
53
  invocation_context._invoke(*args, **kwargs)
54
54
  end
55
55
  methods_defined << symbol_name
@@ -107,13 +107,14 @@ module OpsWalrus
107
107
  #cmd = Shellwords.escape(cmd)
108
108
 
109
109
  if App.instance.report_mode?
110
+ puts Style.green("*" * 80)
110
111
  if self.alias
111
- print "[#{self.alias} | #{host}] "
112
+ print "[#{Style.blue(self.alias)} | #{Style.blue(host)}] "
112
113
  else
113
- print "[#{host}] "
114
+ print "[#{Style.blue(host)}] "
114
115
  end
115
116
  print "#{description}: " if description
116
- puts cmd
117
+ puts Style.yellow(cmd)
117
118
  end
118
119
 
119
120
  return unless cmd && !cmd.strip.empty?
@@ -122,9 +123,12 @@ module OpsWalrus
122
123
  # puts "shell: #{cmd.inspect}"
123
124
  # puts "sudo_password: #{sudo_password}"
124
125
 
125
- sshkit_cmd = execute_cmd(cmd, input: input)
126
-
127
- [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
126
+ if App.instance.dry_run?
127
+ ["", "", 0]
128
+ else
129
+ sshkit_cmd = execute_cmd(cmd, input: input)
130
+ [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
131
+ end
128
132
  end
129
133
 
130
134
  # def init_brew
@@ -137,7 +141,13 @@ module OpsWalrus
137
141
  # e.g. /home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops run echo.ops args:foo args:bar
138
142
 
139
143
  # cmd = "/home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops"
140
- cmd = "/home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops"
144
+ local_hostname_for_remote_host = if self.alias
145
+ "#{self.alias} | #{host}"
146
+ else
147
+ host
148
+ end
149
+
150
+ cmd = "OPSWALRUS_LOCAL_HOSTNAME='#{local_hostname_for_remote_host}'; /home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops"
141
151
  cmd << " -v" if verbose
142
152
  cmd << " #{ops_command.to_s}"
143
153
  cmd << " #{ops_command_options.to_s}" if ops_command_options
@@ -5,6 +5,7 @@ module OpsWalrus
5
5
  class ScopedMappingInteractionHandler
6
6
  attr_accessor :input_mappings # Hash[ String | Regex => String ]
7
7
 
8
+ # log_level is one of: :fatal, :error, :warn, :info, :debug, :trace
8
9
  def initialize(mapping, log_level = nil)
9
10
  @log_level = log_level
10
11
  @input_mappings = mapping
@@ -21,11 +22,10 @@ module OpsWalrus
21
22
  # end
22
23
 
23
24
  # sudo_password : String
24
- def mapping_for_sudo_password(sudo_password)
25
+ def self.mapping_for_sudo_password(sudo_password)
25
26
  {
26
27
  /\[sudo\] password for .*?:\s*/ => "#{sudo_password}\n",
27
28
  App::LOCAL_SUDO_PASSWORD_PROMPT => "#{sudo_password}\n",
28
- # /\s+/ => nil, # unnecessary
29
29
  }
30
30
  end
31
31
 
@@ -39,7 +39,7 @@ module OpsWalrus
39
39
  raise ArgumentError.new("mapping must be a Hash") unless mapping.is_a?(Hash)
40
40
 
41
41
  if sudo_password
42
- mapping.merge!(mapping_for_sudo_password(sudo_password))
42
+ mapping.merge!(ScopedMappingInteractionHandler.mapping_for_sudo_password(sudo_password))
43
43
  end
44
44
 
45
45
  if mapping.empty?
@@ -57,17 +57,16 @@ module OpsWalrus
57
57
  end
58
58
 
59
59
  def on_data(_command, stream_name, data, channel)
60
- log("Looking up response for #{stream_name} message #{data.inspect}")
61
-
62
60
  response_data = begin
63
61
  first_matching_key_value_pair = @input_mappings.find {|k, _v| k === data }
64
62
  first_matching_key_value_pair&.last
65
63
  end
66
64
 
67
65
  if response_data.nil?
68
- log("Unable to find interaction handler mapping for #{stream_name}: #{data.inspect} so no response was sent")
66
+ trace(Style.red("No interaction handler mapping for #{stream_name}: #{data} so no response was sent"))
69
67
  else
70
- log("Sending #{response_data.inspect}")
68
+ debug(Style.cyan("Handling #{stream_name} message #{data}"))
69
+ debug(Style.cyan("Sending response #{response_data}"))
71
70
  if channel.respond_to?(:send_data) # Net SSH Channel
72
71
  channel.send_data(response_data)
73
72
  elsif channel.respond_to?(:write) # Local IO
@@ -80,16 +79,21 @@ module OpsWalrus
80
79
 
81
80
  private
82
81
 
83
- def log(message)
84
- # puts message
85
- SSHKit.config.output.send(@log_level, message) unless @log_level.nil?
82
+ def trace(message)
83
+ App.instance.trace(message)
84
+ end
85
+
86
+ def debug(message)
87
+ App.instance.debug(message)
88
+ if [:fatal, :error, :warn, :info, :debug, :trace].include? @log_level
89
+ SSHKit.config.output.send(@log_level, message)
90
+ end
86
91
  end
87
92
 
88
93
  end
89
94
 
90
95
  class PasswdInteractionHandler
91
96
  def on_data(command, stream_name, data, channel)
92
- # puts data
93
97
  case data
94
98
  when '(current) UNIX password: '
95
99
  channel.send_data("old_pw\n")
@@ -118,8 +122,6 @@ module OpsWalrus
118
122
 
119
123
  class SudoPromptInteractionHandler
120
124
  def on_data(command, stream_name, data, channel)
121
- # puts "0" * 80
122
- # puts data.inspect
123
125
  case data
124
126
  when /\[sudo\] password for/
125
127
  if channel.respond_to?(:send_data) # Net::SSH channel
@@ -128,12 +130,11 @@ module OpsWalrus
128
130
  channel.write("conquer\n")
129
131
  end
130
132
  when /\s+/
131
- puts 'space, do nothing'
133
+ nil
132
134
  else
133
135
  raise "Unexpected prompt: #{data} on stream #{stream_name} and channel #{channel.inspect}"
134
- end
136
+ end
135
137
  end
136
138
  end
137
139
 
138
-
139
140
  end
@@ -13,10 +13,18 @@ module OpsWalrus
13
13
  def method_missing(name, *args, **kwargs, &block)
14
14
  raise "Not implemented in base class"
15
15
  end
16
+
17
+ def _bang_method?(name)
18
+ name.to_s.end_with?("!")
19
+ end
20
+
21
+ def _non_bang_method(name)
22
+ name.to_s.sub(/!$/, '')
23
+ end
16
24
  end
17
25
 
18
26
  class RemoteImportInvocationContext < ImportInvocationContext
19
- def initialize(runtime_env, host_proxy, namespace_or_ops_file, is_invocation_a_call_to_package_in_bundle_dir = false)
27
+ def initialize(runtime_env, host_proxy, namespace_or_ops_file, is_invocation_a_call_to_package_in_bundle_dir = false, prompt_for_sudo_password: nil)
20
28
  @runtime_env = runtime_env
21
29
  @host_proxy = host_proxy
22
30
  @initial_namespace_or_ops_file = @namespace_or_ops_file = namespace_or_ops_file
@@ -24,12 +32,46 @@ module OpsWalrus
24
32
 
25
33
  initial_method_name = @namespace_or_ops_file.dirname.basename
26
34
  @method_chain = [initial_method_name]
35
+ @prompt_for_sudo_password = prompt_for_sudo_password
36
+ end
37
+
38
+ def method_missing(name, *args, **kwargs, &block)
39
+ _resolve_method_and_invoke(name, *args, **kwargs)
40
+ end
41
+
42
+ def _resolve_method_and_invoke(name, *args, **kwargs)
43
+ if _bang_method?(name) # foo! is an attempt to invoke the module's default entrypoint
44
+ method_name = _non_bang_method(name)
45
+
46
+ @method_chain << method_name
47
+
48
+ @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(method_name)
49
+ _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
50
+ else
51
+ @method_chain << name.to_s
52
+
53
+ @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(name)
54
+ _invoke(*args, **kwargs)
55
+ end
56
+ end
57
+
58
+ # if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
59
+ # method invokes the OpsFile of that same name and returns the result;
60
+ # otherwise we return this namespace object
61
+ def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
62
+ method_name = @namespace_or_ops_file.dirname.basename
63
+ resolved_symbol = @namespace_or_ops_file.resolve_symbol(method_name)
64
+ if resolved_symbol.is_a? OpsFile
65
+ _resolve_method_and_invoke(method_name)
66
+ else
67
+ self
68
+ end
27
69
  end
28
70
 
29
71
  def _invoke(*args, **kwargs)
30
72
  case @namespace_or_ops_file
31
73
  when Namespace
32
- _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
74
+ self
33
75
  when OpsFile
34
76
  _invoke_remote(*args, **kwargs)
35
77
  end
@@ -59,32 +101,13 @@ module OpsWalrus
59
101
  end.join(" ")
60
102
  end
61
103
 
62
- @host_proxy.run_ops(:run, "--script", remote_run_command_args)
63
- end
64
-
65
- # if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
66
- # method invokes the OpsFile of that same name and returns the result;
67
- # otherwise we return this namespace object
68
- def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
69
- method_name = @namespace_or_ops_file.dirname.basename
70
- resolved_symbol = @namespace_or_ops_file.resolve_symbol(method_name)
71
- if resolved_symbol.is_a? OpsFile
72
- _resolve_method_and_invoke(method_name)
104
+ # @host_proxy.run_ops(:run, "--script", remote_run_command_args)
105
+ if @prompt_for_sudo_password
106
+ @host_proxy.run_ops(:run, "--pass", remote_run_command_args)
73
107
  else
74
- self
108
+ @host_proxy.run_ops(:run, remote_run_command_args)
75
109
  end
76
110
  end
77
-
78
- def _resolve_method_and_invoke(name, *args, **kwargs)
79
- @method_chain << name.to_s
80
-
81
- @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(name)
82
- _invoke(*args, **kwargs)
83
- end
84
-
85
- def method_missing(name, *args, **kwargs, &block)
86
- _resolve_method_and_invoke(name, *args, **kwargs)
87
- end
88
111
  end
89
112
 
90
113
  class LocalImportInvocationContext < ImportInvocationContext
@@ -93,24 +116,25 @@ module OpsWalrus
93
116
  @initial_namespace_or_ops_file = @namespace_or_ops_file = namespace_or_ops_file
94
117
  end
95
118
 
96
- def _invoke(*args, **kwargs)
97
- case @namespace_or_ops_file
98
- when Namespace
99
- _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
100
- when OpsFile
101
- _invoke_local(*args, **kwargs)
102
- end
119
+ def method_missing(name, *args, **kwargs, &block)
120
+ _resolve_method_and_invoke(name, *args, **kwargs)
103
121
  end
104
122
 
105
- def _invoke_local(*args, **kwargs)
106
- params_hash = @namespace_or_ops_file.build_params_hash(*args, **kwargs)
107
- @namespace_or_ops_file.invoke(@runtime_env, params_hash)
123
+ def _resolve_method_and_invoke(name, *args, **kwargs)
124
+ if _bang_method?(name) # foo! is an attempt to invoke the module's default entrypoint
125
+ method_name = _non_bang_method(name)
126
+ @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(method_name)
127
+ _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
128
+ else
129
+ @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(name)
130
+ _invoke(*args, **kwargs)
131
+ end
108
132
  end
109
133
 
110
134
  # if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
111
135
  # method invokes the OpsFile of that same name and returns the result;
112
136
  # otherwise we return this namespace object
113
- def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
137
+ def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
114
138
  method_name = @namespace_or_ops_file.dirname.basename
115
139
  resolved_symbol = @namespace_or_ops_file.resolve_symbol(method_name)
116
140
  if resolved_symbol.is_a? OpsFile
@@ -121,14 +145,20 @@ module OpsWalrus
121
145
  end
122
146
  end
123
147
 
124
- def _resolve_method_and_invoke(name, *args, **kwargs)
125
- @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(name)
126
- _invoke(*args, **kwargs)
148
+ def _invoke(*args, **kwargs)
149
+ case @namespace_or_ops_file
150
+ when Namespace
151
+ self
152
+ when OpsFile
153
+ _invoke_local(*args, **kwargs)
154
+ end
127
155
  end
128
156
 
129
- def method_missing(name, *args, **kwargs, &block)
130
- _resolve_method_and_invoke(name, *args, **kwargs)
157
+ def _invoke_local(*args, **kwargs)
158
+ params_hash = @namespace_or_ops_file.build_params_hash(*args, **kwargs)
159
+ @namespace_or_ops_file.invoke(@runtime_env, params_hash)
131
160
  end
161
+
132
162
  end
133
163
 
134
164
  end
@@ -112,7 +112,6 @@ module SSHKit
112
112
  buffer || ""
113
113
  end
114
114
 
115
-
116
115
  def handle_data_for_stderr(output, cmd, buffer, stdin, is_blocked)
117
116
  # we're blocked on reading, so let's process the buffer
118
117
  lines, buffer = split_buffer(buffer)
@@ -28,24 +28,39 @@ module OpsWalrus
28
28
  @app.sudo_password
29
29
  end
30
30
 
31
- # runtime_kv_args is an Array(String) of the form: ["arg1:val1", "arg2:val2", ...]
32
- # params_json_hash is a Hash representation of a JSON string
33
- def run(runtime_kv_args, params_json_hash: nil)
34
- params_hash = runtime_kv_args.reduce(params_json_hash || {}) do |memo, kv_pair_string|
35
- str_key, str_value = kv_pair_string.split(":", 2)
36
- if pre_existing_value = memo[str_key]
37
- array = pre_existing_value.is_a?(Array) ? pre_existing_value : [pre_existing_value]
38
- array << str_value
39
- memo[str_key] = array
31
+ # runtime_kv_args is an Array(String) of the form: ["arg1:val1", "arg1:val2", ...]
32
+ # irb(main):057:0> build_params_hash(["names:foo", "names:bar", "names:baz", "age:5", "profile:name:corge", "profile:language:en", "height:5ft8in"])
33
+ # => {"names"=>["foo", "bar", "baz"], "age"=>"5", "profile"=>{"name"=>"corge", "language"=>"en"}, "height"=>"5ft8in"}
34
+ def build_params_hash(runtime_kv_args, params_json_hash: nil)
35
+ runtime_kv_args.reduce(params_json_hash || {}) do |memo, kv_pair_string|
36
+ param_name, str_value = kv_pair_string.split(":", 2)
37
+ key, value = str_value.split(":", 2)
38
+ if pre_existing_value = memo[param_name]
39
+ memo[param_name] = if value # we're dealing with a Hash parameter value
40
+ pre_existing_value.merge(key => value)
41
+ else # we're dealing with an Array parameter value or a scalar parameter value
42
+ array = pre_existing_value.is_a?(Array) ? pre_existing_value : [pre_existing_value]
43
+ array << str_value
44
+ end
40
45
  else
41
- memo[str_key] = str_value
46
+ memo[param_name] = if value # we're dealing with a Hash parameter value
47
+ {key => value}
48
+ else # we're dealing with an Array parameter value or a scalar parameter value
49
+ str_value
50
+ end
42
51
  end
43
52
  memo
44
53
  end
54
+ end
55
+
56
+ # runtime_kv_args is an Array(String) of the form: ["arg1:val1", "arg1:val2", ...]
57
+ # params_json_hash is a Hash representation of a JSON string
58
+ def run(runtime_kv_args, params_json_hash: nil)
59
+ params_hash = build_params_hash(runtime_kv_args, params_json_hash: params_json_hash)
45
60
 
46
61
  if app.debug?
47
- puts "Script:"
48
- puts @entry_point_ops_file.script
62
+ App.instance.trace "Script:"
63
+ App.instance.trace @entry_point_ops_file.script
49
64
  end
50
65
 
51
66
  result = begin
@@ -61,14 +76,14 @@ module OpsWalrus
61
76
  App.instance.error "[!] Command failed: #{e.message}"
62
77
  rescue Error => e
63
78
  App.instance.error "Error: Ops script crashed."
64
- App.instance.error e.message
65
- App.instance.error e.backtrace.take(5).join("\n")
79
+ App.instance.error e
80
+ # App.instance.error e.backtrace.take(5).join("\n")
66
81
  Invocation::Error.new(e)
67
82
  rescue => e
68
83
  App.instance.error "Unhandled Error: Ops script crashed."
69
84
  App.instance.error e.class
70
- App.instance.error e.message
71
- App.instance.error e.backtrace.take(10).join("\n")
85
+ App.instance.error e
86
+ # App.instance.error e.backtrace.take(10).join("\n")
72
87
  Invocation::Error.new(e)
73
88
  end
74
89
 
@@ -76,7 +91,7 @@ module OpsWalrus
76
91
  App.instance.debug "Ops script error details:"
77
92
  App.instance.debug "Error: #{result.value}"
78
93
  App.instance.debug "Status code: #{result.exit_status}"
79
- App.instance.debug @entry_point_ops_file.script
94
+ App.instance.debug @entry_point_ops_file.script.to_s
80
95
  end
81
96
 
82
97
  result
@@ -173,8 +173,9 @@ module OpsWalrus
173
173
  raise Error, "Unknown import reference: #{local_name}: #{import_str.inspect}"
174
174
  end
175
175
 
176
- def invoke(runtime_env, params_hash)
177
- script._invoke(runtime_env, params_hash)
176
+ def invoke(runtime_env, hashlike_params)
177
+ # this invokes the dynamically generated _invoke method that is defined at runtime within OpsFileScript.define_for(...)
178
+ script._invoke(runtime_env, hashlike_params)
178
179
  end
179
180
 
180
181
  def build_params_hash(*args, **kwargs)
@@ -1,9 +1,80 @@
1
+ require 'forwardable'
1
2
  require 'set'
2
3
  require_relative 'invocation'
3
4
  require_relative 'ops_file_script_dsl'
4
5
 
5
6
  module OpsWalrus
6
7
 
8
+ class ArrayOrHashNavigationProxy
9
+ extend Forwardable
10
+
11
+ def initialize(array_or_hash)
12
+ @obj = array_or_hash
13
+ end
14
+
15
+ def_delegators :@obj, :to_s, :inspect, :hash, :===, :eql?, :kind_of?, :is_a?, :instance_of?, :respond_to?, :<=>
16
+
17
+ def [](index, *args, **kwargs, &block)
18
+ @obj.method(:[]).call(index, *args, **kwargs, &block)
19
+ end
20
+ def respond_to_missing?(method, *)
21
+ @obj.is_a?(Hash) && @obj.respond_to?(method)
22
+ end
23
+ def method_missing(name, *args, **kwargs, &block)
24
+ case @obj
25
+ when Array
26
+ @obj.method(name).call(*args, **kwargs, &block)
27
+ when Hash
28
+ if @obj.respond_to?(name)
29
+ @obj.method(name).call(*args, **kwargs, &block)
30
+ else
31
+ value = self[name.to_s]
32
+ case value
33
+ when Array, Hash
34
+ ArrayOrHashNavigationProxy.new(value)
35
+ else
36
+ value
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class InvocationParams
44
+ # @params : Hash
45
+
46
+ # params : Hash | ArrayOrHashNavigationProxy
47
+ def initialize(hashlike_params)
48
+ # this doesn't seem to make any difference
49
+ @params = hashlike_params.to_h
50
+ # @params = hashlike_params
51
+ end
52
+
53
+ def [](key)
54
+ key = key.to_s if key.is_a? Symbol
55
+ @params[key]
56
+ end
57
+
58
+ def dig(*keys)
59
+ # keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
60
+ @params.dig(*keys)
61
+ end
62
+
63
+ def method_missing(name, *args, **kwargs, &block)
64
+ if @params.respond_to?(name)
65
+ @params.method(name).call(*args, **kwargs, &block)
66
+ else
67
+ value = self[name]
68
+ case value
69
+ when Array, Hash
70
+ ArrayOrHashNavigationProxy.new(value)
71
+ else
72
+ value
73
+ end
74
+ end
75
+ end
76
+ end
77
+
7
78
  class OpsFileScript
8
79
 
9
80
  def self.define_for(ops_file, ruby_script)
@@ -14,11 +85,11 @@ module OpsWalrus
14
85
  # define methods for the OpsFile's local_symbol_table: local imports and private lib directory
15
86
  ops_file.local_symbol_table.each do |symbol_name, import_reference|
16
87
  unless methods_defined.include? symbol_name
17
- App.instance.debug "defining method for local symbol table entry: #{symbol_name}"
88
+ App.instance.trace "defining method for local symbol table entry: #{symbol_name}"
18
89
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
19
- App.instance.debug "resolving local symbol table entry: #{symbol_name}"
90
+ App.instance.trace "resolving local symbol table entry: #{symbol_name}"
20
91
  namespace_or_ops_file = @runtime_env.resolve_import_reference(ops_file, import_reference)
21
- App.instance.debug "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
92
+ App.instance.trace "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
22
93
 
23
94
  invocation_context = LocalImportInvocationContext.new(@runtime_env, namespace_or_ops_file)
24
95
  invocation_context._invoke(*args, **kwargs)
@@ -33,14 +104,14 @@ module OpsWalrus
33
104
  sibling_symbol_table_names |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
34
105
  sibling_symbol_table_names |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
35
106
  # puts "sibling_symbol_table_names=#{sibling_symbol_table_names}"
36
- App.instance.debug "methods_defined=#{methods_defined}"
107
+ App.instance.trace "methods_defined=#{methods_defined}"
37
108
  sibling_symbol_table_names.each do |symbol_name|
38
109
  unless methods_defined.include? symbol_name
39
- App.instance.debug "defining method for implicit imports: #{symbol_name}"
110
+ App.instance.trace "defining method for implicit imports: #{symbol_name}"
40
111
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
41
- App.instance.debug "resolving implicit import: #{symbol_name}"
112
+ App.instance.trace "resolving implicit import: #{symbol_name}"
42
113
  namespace_or_ops_file = @runtime_env.resolve_sibling_symbol(ops_file, symbol_name)
43
- App.instance.debug "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
114
+ App.instance.trace "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
44
115
 
45
116
  invocation_context = LocalImportInvocationContext.new(@runtime_env, namespace_or_ops_file)
46
117
  invocation_context._invoke(*args, **kwargs)
@@ -60,9 +131,9 @@ module OpsWalrus
60
131
  # - #verbose?
61
132
  # - all the dynamically defined methods in the subclass of Invocation
62
133
  invoke_method_definition = <<~INVOKE_METHOD
63
- def _invoke(runtime_env, params_hash)
134
+ def _invoke(runtime_env, hashlike_params)
64
135
  @runtime_env = runtime_env
65
- @params = InvocationParams.new(params_hash)
136
+ @params = InvocationParams.new(hashlike_params)
66
137
  #{ruby_script}
67
138
  end
68
139
  INVOKE_METHOD
@@ -101,7 +172,7 @@ module OpsWalrus
101
172
  end
102
173
 
103
174
  # The _invoke method is dynamically defined as part of OpsFileScript.define_for
104
- def _invoke(runtime_env, params_hash)
175
+ def _invoke(runtime_env, hashlike_params)
105
176
  raise "Not implemented in base class."
106
177
  end
107
178
 
@@ -12,67 +12,6 @@ require_relative 'walrus_lang'
12
12
 
13
13
  module OpsWalrus
14
14
 
15
- class ArrayOrHashNavigationProxy
16
- def initialize(array_or_hash)
17
- @obj = array_or_hash
18
- end
19
- def [](index, *args, **kwargs, &block)
20
- @obj.method(:[]).call(index, *args, **kwargs, &block)
21
- end
22
- def respond_to_missing?(method, *)
23
- @obj.is_a?(Hash) && @obj.respond_to?(method)
24
- end
25
- def method_missing(name, *args, **kwargs, &block)
26
- case @obj
27
- when Array
28
- @obj.method(name).call(*args, **kwargs, &block)
29
- when Hash
30
- if @obj.respond_to?(name)
31
- @obj.method(name).call(*args, **kwargs, &block)
32
- else
33
- value = self[name.to_s]
34
- case value
35
- when Array, Hash
36
- ArrayOrHashNavigationProxy.new(value)
37
- else
38
- value
39
- end
40
- end
41
- end
42
- end
43
- end
44
-
45
- class InvocationParams
46
- # params : Hash
47
- def initialize(params)
48
- @params = params
49
- end
50
-
51
- def [](key)
52
- key = key.to_s if key.is_a? Symbol
53
- @params[key]
54
- end
55
-
56
- def dig(*keys)
57
- # keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
58
- @params.dig(*keys)
59
- end
60
-
61
- def method_missing(name, *args, **kwargs, &block)
62
- if @params.respond_to?(name)
63
- @params.method(name).call(*args, **kwargs, &block)
64
- else
65
- value = self[name]
66
- case value
67
- when Array, Hash
68
- ArrayOrHashNavigationProxy.new(value)
69
- else
70
- value
71
- end
72
- end
73
- end
74
- end
75
-
76
15
  module Invocation
77
16
  class Result
78
17
  attr_accessor :value
@@ -157,7 +96,7 @@ module OpsWalrus
157
96
 
158
97
  # puts retval.inspect
159
98
 
160
- # cleanup
99
+ # todo: cleanup
161
100
  # if tmp_bundle_root_dir =~ /tmp/ # sanity check the temp path before we blow away something we don't intend
162
101
  # host.execute(:rm, "-rf", "tmpopsbootstrap.sh", "tmpops.zip", tmp_bundle_root_dir)
163
102
  # else
@@ -275,19 +214,23 @@ module OpsWalrus
275
214
  # puts "shell! self: #{self.inspect}"
276
215
 
277
216
  if App.instance.report_mode?
278
- print "[#{@runtime_env.local_hostname}] "
217
+ puts Style.green("*" * 80)
218
+ print "[#{Style.blue(@runtime_env.local_hostname)}] "
279
219
  print "#{description}: " if description
280
- puts cmd
220
+ puts Style.yellow(cmd)
281
221
  end
282
222
 
283
223
  return unless cmd && !cmd.strip.empty?
284
224
 
285
- sshkit_cmd = @runtime_env.handle_input(input) do |interaction_handler|
286
- # self is a Module instance that is serving as the evaluation context in an instance of a subclass of an Invocation; see Invocation#evaluate
287
- backend.execute_cmd(cmd, interaction_handler: interaction_handler, verbosity: :info)
225
+ if App.instance.dry_run?
226
+ ["", "", 0]
227
+ else
228
+ sshkit_cmd = @runtime_env.handle_input(input) do |interaction_handler|
229
+ # self is a Module instance that is serving as the evaluation context in an instance of a subclass of an Invocation; see Invocation#evaluate
230
+ backend.execute_cmd(cmd, interaction_handler: interaction_handler, verbosity: :info)
231
+ end
232
+ [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
288
233
  end
289
-
290
- [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
291
234
  end
292
235
 
293
236
  # def init_brew
@@ -15,15 +15,6 @@ module OpsWalrus
15
15
  @local_name, @package_uri, @version = local_name, package_uri, version
16
16
  end
17
17
 
18
- def sanitized_package_uri
19
- sanitize_path(@package_uri)
20
- end
21
-
22
- def sanitize_path(path)
23
- # found this at https://apidock.com/rails/v5.2.3/ActiveStorage/Filename/sanitized
24
- path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
25
- end
26
-
27
18
  # important: the import_resolution_dirname implemented as the local_name is critical because Bundler#download_package downloads
28
19
  # package dependencies to the name that this method returns, which must match the package reference's local name
29
20
  # so that later, when the package is being looked up on the load path (in LoadPath#resolve_import_reference),
@@ -34,7 +25,7 @@ module OpsWalrus
34
25
  # change in order for the three things to reconcile with respect to one another, since all three bits of logic are
35
26
  # what make bundling package dependencies and loading them function properly.
36
27
  def import_resolution_dirname
37
- local_name
28
+ "pkg_#{local_name}_version_#{version}"
38
29
  end
39
30
 
40
31
  def to_s
@@ -53,8 +44,17 @@ module OpsWalrus
53
44
  # these are dynamic package references defined at runtime when an OpsFile's imports are being evaluated.
54
45
  # this will usually be the case when an ops file does not belong to a package
55
46
  class DynamicPackageReference < PackageReference
47
+ def self.import_resolution_dirname(package_uri, version)
48
+ sanitized_package_uri = sanitize_path(package_uri || raise(Error, "Unspecified package reference"))
49
+ sanitized_version = sanitize_path(version || "")
50
+ "pkg_#{sanitized_package_uri}_version_#{sanitized_version}"
51
+ end
52
+ def self.sanitize_path(path)
53
+ # found this at https://apidock.com/rails/v5.2.3/ActiveStorage/Filename/sanitized
54
+ path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
55
+ end
56
56
  def import_resolution_dirname
57
- sanitized_package_uri
57
+ DynamicPackageReference.import_resolution_dirname(@package_uri, @version)
58
58
  end
59
59
  end
60
60
 
@@ -90,7 +90,7 @@ module OpsWalrus
90
90
  str = "Namespace: #{@dirname.to_s}\n"
91
91
  @symbol_table.each do |k, v|
92
92
  if v.is_a? Namespace
93
- str << "#{' ' * (indent)}|- #{k} : #{v.to_s(indent + 1)}\n"
93
+ str << "#{' ' * (indent)}|- #{k} : #{v.to_s(indent + 1)}"
94
94
  else
95
95
  str << "#{' ' * (indent)}|- #{k} : #{v.to_s}\n"
96
96
  end
@@ -106,31 +106,6 @@ module OpsWalrus
106
106
  @symbol_table[symbol_name.to_s]
107
107
  end
108
108
 
109
- # # if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
110
- # # method invokes the OpsFile of that same name and returns the result;
111
- # # otherwise we return this namespace object
112
- # def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
113
- # resolved_symbol = resolve_symbol(@dirname.basename)
114
- # if resolved_symbol.is_a? OpsFile
115
- # params_hash = resolved_symbol.build_params_hash(*args, **kwargs)
116
- # resolved_symbol.invoke(runtime_env, params_hash)
117
- # else
118
- # self
119
- # end
120
- # end
121
-
122
- # def method_missing(name, *args, **kwargs, &block)
123
- # # puts "method_missing: #{name}"
124
- # # puts caller
125
- # resolved_symbol = resolve_symbol(name)
126
- # case resolved_symbol
127
- # when Namespace
128
- # resolved_symbol._invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
129
- # when OpsFile
130
- # params_hash = resolved_symbol.build_params_hash(*args, **kwargs)
131
- # resolved_symbol.invoke(runtime_env, params_hash)
132
- # end
133
- # end
134
109
  end
135
110
 
136
111
  # the assumption is that we have a bundle directory with all the packages in it
@@ -147,16 +122,13 @@ module OpsWalrus
147
122
  @root_namespace = build_symbol_resolution_tree(@dir)
148
123
  @path_map = build_path_map(@root_namespace)
149
124
 
150
- # puts "*" * 80
151
- # puts "load path for #{@dir}"
152
- # puts "-" * 80
153
- # puts 'root namespace'
154
- # puts @root_namespace.to_s
155
- # puts "-" * 80
156
- # puts 'path map'
157
- # @path_map.each do |k,v|
158
- # puts "#{k.to_s}: #{v.to_s}"
159
- # end
125
+ App.instance.trace "LoadPath for #{@dir} ************************************************************"
126
+ App.instance.trace 'root namespace ******************************************************************'
127
+ App.instance.trace @root_namespace.to_s
128
+ App.instance.trace 'path map ************************************************************************'
129
+ @path_map.each do |k,v|
130
+ App.instance.trace "#{k.to_s}: #{v.to_s}"
131
+ end
160
132
 
161
133
  @dynamic_package_additions_memo = {}
162
134
  end
@@ -268,9 +240,16 @@ module OpsWalrus
268
240
  @bundle_load_path = LoadPath.new(self, @app.bundle_dir)
269
241
  @app_load_path = LoadPath.new(self, @app.pwd)
270
242
 
271
- @interaction_handler = ScopedMappingInteractionHandler.new({
272
- /\[sudo\] password for .*?:\s*/ => "#{sudo_password}\n",
273
- })
243
+ # we include this sudo password mapping by default because if a bundled script is being run on a remote host,
244
+ # then the remote command invocation will include the --pass flag when being run on the remote host, which will
245
+ # interactively prompt for a sudo password, and that will be the only opportunity for the command host
246
+ # that is running the bundled script on the remote host to interactively enter a sudo password for the remote context
247
+ # since the remote ops command will be running within a PTY, and the interactive prompts running on the remote
248
+ # host will be managed by the local ScopedMappingInteractionHandler running within the instance of the ops command
249
+ # process on the remote host, and the command host will not have any further opportunity to interactively enter
250
+ # any prompts on the remote host
251
+ interaction_handler_mapping_for_sudo_password = ScopedMappingInteractionHandler.mapping_for_sudo_password(sudo_password)
252
+ @interaction_handler = ScopedMappingInteractionHandler.new(interaction_handler_mapping_for_sudo_password)
274
253
 
275
254
  configure_sshkit
276
255
  end
@@ -291,6 +270,7 @@ module OpsWalrus
291
270
  # SSHKit.config.use_format :simpletext
292
271
  SSHKit.config.output_verbosity = :debug
293
272
  elsif app.verbose?
273
+ SSHKit.config.use_format :pretty
294
274
  # SSHKit.config.use_format :dot
295
275
  SSHKit.config.output_verbosity = :info
296
276
  end
@@ -344,8 +324,8 @@ module OpsWalrus
344
324
  end.run
345
325
  end
346
326
 
347
- def invoke(ops_file, params_hash)
348
- ops_file.invoke(self, params_hash)
327
+ def invoke(ops_file, hashlike_params)
328
+ ops_file.invoke(self, hashlike_params)
349
329
  end
350
330
 
351
331
  def find_load_path_that_includes_path(path)
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.16"
2
+ VERSION = "1.0.17"
3
3
  end
data/lib/opswalrus.rb CHANGED
@@ -6,5 +6,5 @@ require_relative 'opswalrus/app'
6
6
  require_relative 'opswalrus/cli'
7
7
  require_relative 'opswalrus/version'
8
8
 
9
- module OpsWalrusWalrus
9
+ module OpsWalrus
10
10
  end
data/opswalrus.gemspec CHANGED
@@ -14,11 +14,11 @@ Gem::Specification.new do |spec|
14
14
  spec.license = "EPL-2.0"
15
15
  spec.required_ruby_version = ">= 2.6.0"
16
16
 
17
- # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server - https://example.com"
17
+ # spec.metadata["allowed_push_host"] = "Set to your gem server - https://example.com"
18
18
 
19
19
  spec.metadata["homepage_uri"] = spec.homepage
20
20
  spec.metadata["source_code_uri"] = "https://github.com/opswalrus/opswalrus"
21
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
21
+ # spec.metadata["changelog_uri"] = "Put your gem's CHANGELOG.md URL here."
22
22
 
23
23
  # Specify which files should be added to the gem when it is released.
24
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -37,7 +37,9 @@ Gem::Specification.new do |spec|
37
37
  spec.add_dependency "gli", "~> 2.21"
38
38
  spec.add_dependency "git", "~> 1.18"
39
39
  spec.add_dependency "ougai", "~> 2.0"
40
+ spec.add_dependency "pastel", "~> 0.8"
40
41
  spec.add_dependency "rubyzip", "~> 2.3"
42
+ spec.add_dependency "tty-editor", "~> 0.7"
41
43
 
42
44
  spec.add_dependency "bcrypt_pbkdf", "~> 1.1"
43
45
  spec.add_dependency "ed25519", "~> 1.3"
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.16
4
+ version: 1.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Ellis
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pastel
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.8'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.8'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rubyzip
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,20 @@ dependencies:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '2.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: tty-editor
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.7'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.7'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: bcrypt_pbkdf
99
127
  requirement: !ruby/object:Gem::Requirement