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 +4 -4
- data/Gemfile.lock +18 -1
- data/lib/opswalrus/app.rb +51 -17
- data/lib/opswalrus/bootstrap.sh +1 -0
- data/lib/opswalrus/bundler.rb +18 -34
- data/lib/opswalrus/cli.rb +24 -10
- data/lib/opswalrus/host.rb +20 -10
- data/lib/opswalrus/interaction_handlers.rb +17 -16
- data/lib/opswalrus/invocation.rb +71 -41
- data/lib/opswalrus/local_non_blocking_backend.rb +0 -1
- data/lib/opswalrus/operation_runner.rb +32 -17
- data/lib/opswalrus/ops_file.rb +3 -2
- data/lib/opswalrus/ops_file_script.rb +81 -10
- data/lib/opswalrus/ops_file_script_dsl.rb +12 -69
- data/lib/opswalrus/package_file.rb +11 -11
- data/lib/opswalrus/runtime_environment.rb +21 -41
- data/lib/opswalrus/version.rb +1 -1
- data/lib/opswalrus.rb +1 -1
- data/opswalrus.gemspec +4 -2
- metadata +29 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2ce759171a0342644a1e3e2c94555299b093095b1684a69e11142fe29292da4
|
|
4
|
+
data.tar.gz: 75f4cab88e00038c73df02d69718b5cd067f3ed69150c192335894128ef912d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
@
|
|
129
|
+
@logger.level <= 1
|
|
101
130
|
end
|
|
102
131
|
|
|
103
132
|
def debug?
|
|
104
|
-
@
|
|
133
|
+
@logger.level <= 0
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def fatal(*args)
|
|
137
|
+
@logger.fatal(*args)
|
|
105
138
|
end
|
|
106
139
|
|
|
107
|
-
def error(
|
|
108
|
-
@logger.error(
|
|
140
|
+
def error(*args)
|
|
141
|
+
@logger.error(*args)
|
|
109
142
|
end
|
|
110
143
|
|
|
111
|
-
def warn(
|
|
112
|
-
@logger.warn(
|
|
144
|
+
def warn(*args)
|
|
145
|
+
@logger.warn(*args)
|
|
113
146
|
end
|
|
114
147
|
|
|
115
|
-
def log(
|
|
116
|
-
@logger.info(
|
|
148
|
+
def log(*args)
|
|
149
|
+
@logger.info(*args)
|
|
117
150
|
end
|
|
151
|
+
alias_method :info, :log
|
|
118
152
|
|
|
119
|
-
def debug(
|
|
120
|
-
@logger.debug(
|
|
153
|
+
def debug(*args)
|
|
154
|
+
@logger.debug(*args)
|
|
121
155
|
end
|
|
122
156
|
|
|
123
|
-
def trace(
|
|
124
|
-
@logger.trace(
|
|
157
|
+
def trace(*args)
|
|
158
|
+
@logger.trace(*args)
|
|
125
159
|
end
|
|
126
160
|
|
|
127
161
|
def set_pwd(pwd)
|
data/lib/opswalrus/bootstrap.sh
CHANGED
|
@@ -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
|
data/lib/opswalrus/bundler.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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["
|
|
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
|
-
|
|
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
|
|
data/lib/opswalrus/host.rb
CHANGED
|
@@ -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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
trace(Style.red("No interaction handler mapping for #{stream_name}: #{data} so no response was sent"))
|
|
69
67
|
else
|
|
70
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
133
|
+
nil
|
|
132
134
|
else
|
|
133
135
|
raise "Unexpected prompt: #{data} on stream #{stream_name} and channel #{channel.inspect}"
|
|
134
|
-
|
|
136
|
+
end
|
|
135
137
|
end
|
|
136
138
|
end
|
|
137
139
|
|
|
138
|
-
|
|
139
140
|
end
|
data/lib/opswalrus/invocation.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
97
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
130
|
-
|
|
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
|
|
@@ -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", "
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
memo[
|
|
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[
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
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
|
data/lib/opswalrus/ops_file.rb
CHANGED
|
@@ -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,
|
|
177
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
134
|
+
def _invoke(runtime_env, hashlike_params)
|
|
64
135
|
@runtime_env = runtime_env
|
|
65
|
-
@params = InvocationParams.new(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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)}
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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,
|
|
348
|
-
ops_file.invoke(self,
|
|
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)
|
data/lib/opswalrus/version.rb
CHANGED
data/lib/opswalrus.rb
CHANGED
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"] = "
|
|
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"] = "
|
|
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.
|
|
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
|