opswalrus 1.0.0
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 +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