opswalrus 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +59 -0
- data/LICENSE +674 -0
- data/README.md +256 -0
- data/Rakefile +8 -0
- data/exe/ops +6 -0
- data/exe/run_ops_bundle +22 -0
- data/lib/opswalrus/app.rb +328 -0
- data/lib/opswalrus/bootstrap.sh +57 -0
- data/lib/opswalrus/bootstrap_linux_host1.sh +12 -0
- data/lib/opswalrus/bootstrap_linux_host2.sh +37 -0
- data/lib/opswalrus/bootstrap_linux_host3.sh +21 -0
- data/lib/opswalrus/bundler.rb +175 -0
- data/lib/opswalrus/cli.rb +143 -0
- data/lib/opswalrus/host.rb +177 -0
- data/lib/opswalrus/hosts_file.rb +55 -0
- data/lib/opswalrus/interaction_handlers.rb +53 -0
- data/lib/opswalrus/local_non_blocking_backend.rb +132 -0
- data/lib/opswalrus/local_pty_backend.rb +89 -0
- data/lib/opswalrus/operation_runner.rb +85 -0
- data/lib/opswalrus/ops_file.rb +235 -0
- data/lib/opswalrus/ops_file_script.rb +472 -0
- data/lib/opswalrus/package_file.rb +102 -0
- data/lib/opswalrus/runtime_environment.rb +258 -0
- data/lib/opswalrus/sshkit_ext.rb +51 -0
- data/lib/opswalrus/traversable.rb +15 -0
- data/lib/opswalrus/version.rb +3 -0
- data/lib/opswalrus/walrus_lang.rb +83 -0
- data/lib/opswalrus/zip.rb +57 -0
- data/lib/opswalrus.rb +10 -0
- data/opswalrus.gemspec +45 -0
- data/sig/opswalrus.rbs +4 -0
- metadata +178 -0
@@ -0,0 +1,258 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'socket'
|
4
|
+
require 'sshkit'
|
5
|
+
|
6
|
+
require_relative 'traversable'
|
7
|
+
require_relative 'walrus_lang'
|
8
|
+
|
9
|
+
module OpsWalrus
|
10
|
+
|
11
|
+
class ImportReference
|
12
|
+
attr_accessor :local_name
|
13
|
+
|
14
|
+
def initialize(local_name)
|
15
|
+
@local_name = local_name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class PackageDependencyReference < ImportReference
|
20
|
+
attr_accessor :package_reference
|
21
|
+
def initialize(local_name, package_reference)
|
22
|
+
super(local_name)
|
23
|
+
@package_reference = package_reference
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class DirectoryReference < ImportReference
|
28
|
+
attr_accessor :dirname
|
29
|
+
def initialize(local_name, dirname)
|
30
|
+
super(local_name)
|
31
|
+
@dirname = dirname
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class OpsFileReference < ImportReference
|
36
|
+
attr_accessor :ops_file_path
|
37
|
+
def initialize(local_name, ops_file_path)
|
38
|
+
super(local_name)
|
39
|
+
@ops_file_path = ops_file_path
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Namespace is really just a Map of symbol_name -> (Namespace | OpsFile) pairs
|
44
|
+
class Namespace
|
45
|
+
attr_accessor :runtime_env
|
46
|
+
attr_accessor :dirname
|
47
|
+
attr_accessor :symbol_table
|
48
|
+
|
49
|
+
# dirname is an absolute path
|
50
|
+
def initialize(runtime_env, dirname)
|
51
|
+
@runtime_env = runtime_env
|
52
|
+
@dirname = dirname
|
53
|
+
@symbol_table = {} # "symbol_name" => ops_file_or_child_namespace
|
54
|
+
end
|
55
|
+
|
56
|
+
def add(symbol_name, ops_file_or_child_namespace)
|
57
|
+
@symbol_table[symbol_name.to_s] = ops_file_or_child_namespace
|
58
|
+
end
|
59
|
+
|
60
|
+
def resolve_symbol(symbol_name)
|
61
|
+
@symbol_table[symbol_name.to_s]
|
62
|
+
end
|
63
|
+
|
64
|
+
def method_missing(name, *args, **kwargs, &block)
|
65
|
+
resolved_symbol = resolve_symbol(name)
|
66
|
+
case resolved_symbol
|
67
|
+
when Namespace
|
68
|
+
resolved_symbol
|
69
|
+
when OpsFile
|
70
|
+
params_hash = resolved_symbol.build_params_hash(*args, **kwargs)
|
71
|
+
resolved_symbol.invoke(runtime_env, params_hash)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# the assumption is that we have a bundle directory with all the packages in it
|
77
|
+
# and the bundle directory is the root directory
|
78
|
+
class LoadPath
|
79
|
+
include Traversable
|
80
|
+
|
81
|
+
attr_accessor :dir
|
82
|
+
attr_accessor :runtime_env
|
83
|
+
|
84
|
+
def initialize(runtime_env, dir)
|
85
|
+
@runtime_env = runtime_env
|
86
|
+
@dir = dir
|
87
|
+
@root_namespace = build_symbol_resolution_tree(@dir)
|
88
|
+
@path_map = build_path_map(@root_namespace)
|
89
|
+
end
|
90
|
+
|
91
|
+
# returns a tree of Namespace -> {Namespace* -> {Namespace* -> ..., OpsFile*}, OpsFile*}
|
92
|
+
def build_symbol_resolution_tree(directory_path)
|
93
|
+
namespace = Namespace.new(runtime_env, directory_path)
|
94
|
+
|
95
|
+
directory_path.glob("*.ops").each do |ops_file_path|
|
96
|
+
ops_file = OpsFile.new(@app, ops_file_path)
|
97
|
+
namespace.add(ops_file.basename, ops_file)
|
98
|
+
end
|
99
|
+
|
100
|
+
directory_path.glob("*").
|
101
|
+
select(&:directory?).
|
102
|
+
reject {|dir| dir.basename.to_s.downcase == Bundler::BUNDLE_DIR }.
|
103
|
+
each do |dir_path|
|
104
|
+
dir_basename = dir_path.basename
|
105
|
+
unless namespace.resolve_symbol(dir_basename)
|
106
|
+
child_namespace = build_symbol_resolution_tree(dir_path)
|
107
|
+
namespace.add(dir_basename, child_namespace)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
namespace
|
112
|
+
end
|
113
|
+
|
114
|
+
# returns a Map of path -> (Namespace | OpsFile) pairs
|
115
|
+
def build_path_map(root_namespace)
|
116
|
+
path_map = {}
|
117
|
+
|
118
|
+
pre_order_traverse(root_namespace) do |namespace_or_ops_file|
|
119
|
+
case namespace_or_ops_file
|
120
|
+
when Namespace
|
121
|
+
path_map[namespace_or_ops_file.dirname] = namespace_or_ops_file
|
122
|
+
when OpsFile
|
123
|
+
path_map[namespace_or_ops_file.ops_file_path] = namespace_or_ops_file
|
124
|
+
end
|
125
|
+
|
126
|
+
namespace_or_ops_file.symbol_table.values if namespace_or_ops_file.is_a?(Namespace)
|
127
|
+
end
|
128
|
+
|
129
|
+
path_map
|
130
|
+
end
|
131
|
+
|
132
|
+
# returns a Namespace
|
133
|
+
def lookup_namespace(ops_file)
|
134
|
+
@path_map[ops_file.dirname]
|
135
|
+
end
|
136
|
+
|
137
|
+
# returns a Namespace or OpsFile
|
138
|
+
def resolve_symbol(origin_ops_file, symbol_name)
|
139
|
+
lookup_namespace(origin_ops_file)&.resolve_symbol(symbol_name)
|
140
|
+
end
|
141
|
+
|
142
|
+
# returns a Namespace | OpsFile
|
143
|
+
def resolve_import_reference(origin_ops_file, import_reference)
|
144
|
+
case import_reference
|
145
|
+
when PackageDependencyReference
|
146
|
+
# puts "root namespace: #{@root_namespace.symbol_table}"
|
147
|
+
@root_namespace.resolve_symbol(import_reference.package_reference.local_name) # returns the Namespace associated with the bundled package dirname (i.e. the local name)
|
148
|
+
when DirectoryReference
|
149
|
+
@path_map[import_reference.dirname]
|
150
|
+
when OpsFileReference
|
151
|
+
@path_map[import_reference.ops_file_path]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class RuntimeEnvironment
|
157
|
+
include Traversable
|
158
|
+
|
159
|
+
attr_accessor :app
|
160
|
+
attr_accessor :pty
|
161
|
+
|
162
|
+
def initialize(app)
|
163
|
+
@app = app
|
164
|
+
@bundle_load_path = LoadPath.new(self, @app.bundle_dir)
|
165
|
+
@app_load_path = LoadPath.new(self, @app.pwd)
|
166
|
+
|
167
|
+
configure_sshkit
|
168
|
+
end
|
169
|
+
|
170
|
+
# configure sshkit globally
|
171
|
+
def configure_sshkit
|
172
|
+
SSHKit.config.use_format :blackhole
|
173
|
+
SSHKit.config.output_verbosity = :info
|
174
|
+
|
175
|
+
if app.debug?
|
176
|
+
SSHKit.config.use_format :pretty
|
177
|
+
# SSHKit.config.use_format :simpletext
|
178
|
+
SSHKit.config.output_verbosity = :debug
|
179
|
+
elsif app.verbose?
|
180
|
+
# SSHKit.config.use_format :dot
|
181
|
+
SSHKit.config.output_verbosity = :info
|
182
|
+
end
|
183
|
+
|
184
|
+
SSHKit::Backend::Netssh.configure do |ssh|
|
185
|
+
ssh.pty = true # necessary for interaction with sudo on the remote host
|
186
|
+
ssh.connection_timeout = 60 # seconds
|
187
|
+
ssh.ssh_options = { # this hash is passed in as the options hash (3rd argument) to Net::SSH.start(host, user, options) - see https://net-ssh.github.io/net-ssh/Net/SSH.html
|
188
|
+
auth_methods: %w(publickey password), # :auth_methods => an array of authentication methods to try
|
189
|
+
# :forward_agent => set to true if you want the SSH agent connection to be forwarded
|
190
|
+
# :keepalive => set to true to send a keepalive packet to the SSH server when there's no traffic between the SSH server and Net::SSH client for the keepalive_interval seconds. Defaults to false.
|
191
|
+
# :keepalive_interval => the interval seconds for keepalive. Defaults to 300 seconds.
|
192
|
+
# :keepalive_maxcount => the maximun number of keepalive packet miss allowed. Defaults to 3
|
193
|
+
timeout: 2, # :timeout => how long to wait for the initial connection to be made
|
194
|
+
}
|
195
|
+
end
|
196
|
+
SSHKit::Backend::Netssh.pool.idle_timeout = 1 # seconds
|
197
|
+
end
|
198
|
+
|
199
|
+
def debug?
|
200
|
+
@app.debug?
|
201
|
+
end
|
202
|
+
|
203
|
+
def verbose?
|
204
|
+
@app.verbose?
|
205
|
+
end
|
206
|
+
|
207
|
+
def local_hostname
|
208
|
+
@app.local_hostname
|
209
|
+
end
|
210
|
+
|
211
|
+
def sudo_user
|
212
|
+
@app.sudo_user
|
213
|
+
end
|
214
|
+
|
215
|
+
def sudo_password
|
216
|
+
@app.sudo_password
|
217
|
+
end
|
218
|
+
|
219
|
+
def zip_bundle_path
|
220
|
+
app.zip
|
221
|
+
end
|
222
|
+
|
223
|
+
def run(entry_point_ops_file, params_hash)
|
224
|
+
runtime_env = self
|
225
|
+
SSHKit::Backend::LocalPty.new do
|
226
|
+
runtime_env.pty = self
|
227
|
+
retval = runtime_env.invoke(entry_point_ops_file, params_hash)
|
228
|
+
runtime_env.pty = nil
|
229
|
+
retval
|
230
|
+
end.run
|
231
|
+
end
|
232
|
+
|
233
|
+
def invoke(ops_file, params_hash)
|
234
|
+
ops_file.invoke(self, params_hash)
|
235
|
+
end
|
236
|
+
|
237
|
+
# returns a Namespace or OpsFile
|
238
|
+
def resolve_sibling_symbol(origin_ops_file, symbol_name)
|
239
|
+
@app_load_path.resolve_symbol(origin_ops_file, symbol_name)
|
240
|
+
end
|
241
|
+
|
242
|
+
# returns a Namespace | OpsFile
|
243
|
+
def resolve_import_reference(origin_ops_file, import_reference)
|
244
|
+
case import_reference
|
245
|
+
|
246
|
+
# we know we're dealing with a package dependency reference, so we want to do the lookup in the bundle load path, where package dependencies live
|
247
|
+
when PackageDependencyReference
|
248
|
+
@bundle_load_path.resolve_import_reference(origin_ops_file, import_reference)
|
249
|
+
|
250
|
+
# we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to do the lookup in the app load path
|
251
|
+
when DirectoryReference, OpsFileReference
|
252
|
+
@app_load_path.resolve_import_reference(origin_ops_file, import_reference)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'logger'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'sshkit'
|
5
|
+
|
6
|
+
require_relative 'local_non_blocking_backend'
|
7
|
+
require_relative 'local_pty_backend'
|
8
|
+
|
9
|
+
module SSHKit
|
10
|
+
module Backend
|
11
|
+
class Abstract
|
12
|
+
def execute_cmd(*args)
|
13
|
+
# puts "args: #{args.inspect}"
|
14
|
+
options = { verbosity: :debug, strip: true }.merge(args.extract_options!)
|
15
|
+
# puts "options: #{options.inspect}"
|
16
|
+
create_command_and_execute(args, options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Runner
|
22
|
+
class Sequential < Abstract
|
23
|
+
def run_backend(host, &block)
|
24
|
+
backend(host, &block).run
|
25
|
+
# rescue ::StandardError => e
|
26
|
+
# e2 = ExecuteError.new e
|
27
|
+
# raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Command
|
33
|
+
# Initialize a new Command object
|
34
|
+
#
|
35
|
+
# @param [Array] A list of arguments, the first is considered to be the
|
36
|
+
# command name, with optional variadaric args
|
37
|
+
# @return [Command] An un-started command object with no exit staus, and
|
38
|
+
# nothing in stdin or stdout
|
39
|
+
#
|
40
|
+
def initialize(*args)
|
41
|
+
raise ArgumentError, "Must pass arguments to Command.new" if args.empty?
|
42
|
+
@options = default_options.merge(args.extract_options!)
|
43
|
+
# @command = sanitize_command(args.shift)
|
44
|
+
@command = args.shift
|
45
|
+
@args = args
|
46
|
+
@options.symbolize_keys! # @options.transform_keys!(&:to_sym)
|
47
|
+
@stdout, @stderr, @full_stdout, @full_stderr = String.new, String.new, String.new, String.new
|
48
|
+
@uuid = Digest::SHA1.hexdigest(SecureRandom.random_bytes(10))[0..7]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module OpsWalrus
|
2
|
+
module Traversable
|
3
|
+
# the yield block visits the node and returns children that should be visited
|
4
|
+
def pre_order_traverse(root, observed_nodes = Set.new, &visit_fn_block)
|
5
|
+
# there shouldn't be any cycles in a tree, but we're going to make sure!
|
6
|
+
return if observed_nodes.include?(root)
|
7
|
+
observed_nodes << root
|
8
|
+
|
9
|
+
children = visit_fn_block.call(root)
|
10
|
+
children&.each do |child_node|
|
11
|
+
pre_order_traverse(child_node, observed_nodes, &visit_fn_block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "citrus"
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
module WalrusLang
|
5
|
+
module Templates
|
6
|
+
def render(binding)
|
7
|
+
captures(:template).map{|t| t.render(binding) }.join
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Template
|
12
|
+
def render(binding)
|
13
|
+
s = StringIO.new
|
14
|
+
if mustache = capture(:mustache)
|
15
|
+
s << capture(:pre).value
|
16
|
+
s << mustache.render(binding)
|
17
|
+
s << capture(:post).value
|
18
|
+
else
|
19
|
+
s << capture(:fallthrough).value
|
20
|
+
end
|
21
|
+
s.string
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module Mustache
|
26
|
+
def render(binding)
|
27
|
+
eval(capture(:expr).render(binding), binding)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Grammar = <<-GRAMMAR
|
32
|
+
grammar WalrusLang::Parser
|
33
|
+
rule templates
|
34
|
+
(template*) <WalrusLang::Templates>
|
35
|
+
end
|
36
|
+
|
37
|
+
rule template
|
38
|
+
((pre:(non_mustache?) mustache post:(non_mustache?)) | fallthrough:non_mustache) <WalrusLang::Template>
|
39
|
+
end
|
40
|
+
|
41
|
+
rule before_mustache
|
42
|
+
~'{{'
|
43
|
+
end
|
44
|
+
|
45
|
+
rule non_mustache
|
46
|
+
~('{{' | '}}')
|
47
|
+
end
|
48
|
+
|
49
|
+
rule mustache
|
50
|
+
('{{' expr:templates '}}') <WalrusLang::Mustache>
|
51
|
+
end
|
52
|
+
end
|
53
|
+
GRAMMAR
|
54
|
+
|
55
|
+
Citrus.eval(Grammar)
|
56
|
+
|
57
|
+
def self.render(template, binding)
|
58
|
+
ast = WalrusLang::Parser.parse(template)
|
59
|
+
ast.render(binding)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Binding
|
64
|
+
def local_vars_hash
|
65
|
+
local_variables.map {|s| [s.to_s, local_variable_get(s)] }.to_h
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def mustache(&block)
|
70
|
+
template_string = block.call
|
71
|
+
template_string =~ /{{.*}}/ ? WalrusLang.render(block.call, block.binding) : template_string
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# foo = 1
|
76
|
+
# bar = 2
|
77
|
+
# # m = TemplateLang.parse("abc; {{ 'foo' * bar }} def ")
|
78
|
+
# m = WalrusLang::Parser.parse("abc; {{ 'foo{{1+2}}' * bar }} def {{ 4 * 4 }}; def")
|
79
|
+
# # m = TemplateLang.parse("a{{b{{c}}d}}e{{f}}g{{h{{i{{j{{k{{l}}m{{n}}o}}p}}}}}}")
|
80
|
+
# # puts m.dump
|
81
|
+
# puts m.render(binding)
|
82
|
+
|
83
|
+
# puts(mustache { "abc {{ 1 + 2 }} def" })
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'zip'
|
2
|
+
|
3
|
+
module OpsWalrus
|
4
|
+
class DirZipper
|
5
|
+
|
6
|
+
# Zip the input directory.
|
7
|
+
# returns output_file
|
8
|
+
def self.zip(input_dir, output_file)
|
9
|
+
entries = Dir.entries(input_dir) - %w[. ..]
|
10
|
+
|
11
|
+
::Zip::File.open(output_file, create: true) do |zipfile|
|
12
|
+
write_entries(input_dir, entries, '', zipfile)
|
13
|
+
end
|
14
|
+
|
15
|
+
output_file
|
16
|
+
end
|
17
|
+
|
18
|
+
# returns output_dir
|
19
|
+
def self.unzip(input_file, output_dir)
|
20
|
+
if !File.exist?(output_dir) || File.directory?(output_dir)
|
21
|
+
FileUtils.mkdir_p(output_dir)
|
22
|
+
::Zip::File.foreach(input_file) do |entry|
|
23
|
+
path = File.join(output_dir, entry.name)
|
24
|
+
entry.extract(path) unless File.exist?(path)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
raise Error, "#{output_dir} is not a directory"
|
28
|
+
end
|
29
|
+
|
30
|
+
output_dir
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.write_entries(input_dir, entries, path, zipfile)
|
34
|
+
entries.each do |e|
|
35
|
+
zipfile_path = path == '' ? e : File.join(path, e)
|
36
|
+
disk_file_path = File.join(input_dir, zipfile_path)
|
37
|
+
|
38
|
+
if File.directory?(disk_file_path)
|
39
|
+
zipfile.mkdir(zipfile_path)
|
40
|
+
subdir = Dir.entries(disk_file_path) - %w[. ..]
|
41
|
+
write_entries(input_dir, subdir, zipfile_path, zipfile)
|
42
|
+
else
|
43
|
+
zipfile.add(zipfile_path, disk_file_path)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# this is just to test the zip function
|
52
|
+
def main
|
53
|
+
OpsWalrus::DirZipper.zip("../example/davidinfra", "test.zip")
|
54
|
+
OpsWalrus::DirZipper.unzip("test.zip", "test")
|
55
|
+
end
|
56
|
+
|
57
|
+
main if __FILE__ == $0
|
data/lib/opswalrus.rb
ADDED
data/opswalrus.gemspec
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/opswalrus/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "opswalrus"
|
7
|
+
spec.version = OpsWalrus::VERSION
|
8
|
+
spec.authors = ["David Ellis"]
|
9
|
+
spec.email = ["david@conquerthelawn.com"]
|
10
|
+
|
11
|
+
spec.summary = "opswalrus is a tool that runs scripts against a fleet of hosts"
|
12
|
+
spec.description = "opswalrus is a tool that runs scripts against a fleet of hosts hosts. It's kind of like Ansible, but aims to be simpler to use."
|
13
|
+
spec.homepage = "https://github.com/opswalrus/opswalrus"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
|
16
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/opswalrus/opswalrus"
|
20
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
# gem dependencies
|
34
|
+
spec.add_dependency "citrus"
|
35
|
+
spec.add_dependency "gli"
|
36
|
+
spec.add_dependency "git"
|
37
|
+
spec.add_dependency "rubyzip"
|
38
|
+
|
39
|
+
spec.add_dependency "bcrypt_pbkdf"
|
40
|
+
spec.add_dependency "ed25519"
|
41
|
+
spec.add_dependency "sshkit"
|
42
|
+
|
43
|
+
# For more information and examples about making a new gem, check out our
|
44
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
45
|
+
end
|
data/sig/opswalrus.rbs
ADDED