shelter 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +92 -0
- data/bin/shelter +8 -0
- data/bin/shelter-inventory +24 -0
- data/lib/ansible/inventory.rb +26 -0
- data/lib/cli/ansible_helper.rb +52 -0
- data/lib/cli/app.rb +51 -0
- data/lib/cli/command/ansible.rb +39 -0
- data/lib/cli/stack/cloud_formation.rb +111 -0
- data/lib/configuration.rb +50 -0
- data/lib/shelter.rb +34 -0
- data/lib/version.rb +5 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c6e01fa6f3c4c557007039ec6a00801fe964e56e
|
4
|
+
data.tar.gz: e836545cd87009fcc5c14c4a2f5051df4430a244
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3623a12c17917629240e4afc16f1bbd5b6377346bfe3f34c1c0ae867a76d782c79b0e06cc81528016a88d1039f6ea9abc84cc02c5492efd33498acbabda19555
|
7
|
+
data.tar.gz: 6f935734fdabcc64abb92e1eac449cdcefc58e1a1e55cd3a6ba2fd4ae9e291f5e9cda0ab6937e431e8c99f05373e1372604861180cddd7b9a15fe08e65cc7e64
|
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Configuration
|
2
|
+
|
3
|
+
Create `Shelterfile.rb`:
|
4
|
+
|
5
|
+
```
|
6
|
+
Shelter::CLI::App.config do |c|
|
7
|
+
c.ansible_directory = 'ansible'
|
8
|
+
c.stack_directory = 'stack'
|
9
|
+
c.plugin_directory = 'plugin'
|
10
|
+
c.inventory_directory = 'inventory'
|
11
|
+
c.secure_root = ENV.fetch('CHEPPERS_SECURE')
|
12
|
+
end
|
13
|
+
```
|
14
|
+
|
15
|
+
# Inventory script
|
16
|
+
|
17
|
+
Create a ruby file under your inventory directory:
|
18
|
+
|
19
|
+
```
|
20
|
+
module Inventory
|
21
|
+
class MyInventory
|
22
|
+
def load(inventory)
|
23
|
+
inventory.add_group_vars 'groupname', ch_env: 'value'
|
24
|
+
inventory.add_server 'groupname', 'ip-address'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
# Define a stack
|
31
|
+
|
32
|
+
Create a directory under your `stack` directory (eg: `random`)
|
33
|
+
and create your template there. `cli.rb` will be loaded.
|
34
|
+
|
35
|
+
|
36
|
+
cli.rb:
|
37
|
+
```
|
38
|
+
module Stack
|
39
|
+
class RandomStack < Shelter::CLI::Stack::CloudFormation
|
40
|
+
set_attr :stack_name, 'random'
|
41
|
+
set_attr :template_file, File.expand_path('template.yaml', File.dirname(__FILE__))
|
42
|
+
set_attr :meta, {
|
43
|
+
type: 'development',
|
44
|
+
client: 'cheppers',
|
45
|
+
application: 'random'
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
# Create plugin
|
52
|
+
|
53
|
+
Create a directory under your `plugin` directory and place your code there.
|
54
|
+
`main.rb` will be loaded.
|
55
|
+
|
56
|
+
### Example #1: extra command
|
57
|
+
|
58
|
+
```
|
59
|
+
# plugin/check/main.rb
|
60
|
+
require 'thor'
|
61
|
+
|
62
|
+
module Plugin
|
63
|
+
class Check < Thor
|
64
|
+
desc 'environment', 'Check environment'
|
65
|
+
def environment
|
66
|
+
puts "check"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
Shelter::CLI::App.register(Plugin::Check, 'check', 'check [COMMAND]', 'check plugin')
|
72
|
+
```
|
73
|
+
|
74
|
+
### Example #2: extra command under a specific namespace
|
75
|
+
|
76
|
+
```
|
77
|
+
# plugin/ansible_scanner/main.rb
|
78
|
+
require 'thor'
|
79
|
+
|
80
|
+
module Plugin
|
81
|
+
class Ansible < Thor
|
82
|
+
desc 'scanner', 'scanner'
|
83
|
+
def scanner
|
84
|
+
puts "scan"
|
85
|
+
end
|
86
|
+
|
87
|
+
default_task :scanner
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
Shelter::CLI::Command::Ansible.register(Plugin::Ansible, 'scanner', 'scanner', 'Scan')
|
92
|
+
```
|
data/bin/shelter
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('../../lib', __FILE__)
|
4
|
+
|
5
|
+
require 'shelter'
|
6
|
+
|
7
|
+
inventory = Shelter::Ansible::Inventory.new
|
8
|
+
|
9
|
+
Shelter::CLI::App.config.inventory.each do |script|
|
10
|
+
begin
|
11
|
+
require File.join(Shelter::CLI::App.config.project_root, script)
|
12
|
+
class_name = "Inventory::#{File.basename(script, File.extname(script)).camelize}"
|
13
|
+
inv = Object.const_get(class_name).new()
|
14
|
+
inv.load(inventory)
|
15
|
+
rescue Exception => e
|
16
|
+
$stderr.puts "[x] Error with #{script}"
|
17
|
+
$stderr.puts e.message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
puts inventory.to_json
|
22
|
+
|
23
|
+
# vim: set filetype=ruby:
|
24
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Shelter
|
2
|
+
module Ansible
|
3
|
+
# Ansible Inventory representation
|
4
|
+
class Inventory
|
5
|
+
def initialize
|
6
|
+
@inv = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_group_vars(group, vars)
|
10
|
+
@inv[group] ||= { hosts: [], vars: {} }
|
11
|
+
@inv[group][:vars].merge! vars
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_server(group, ip, vars: {})
|
15
|
+
@inv[group] ||= { hosts: [], vars: {} }
|
16
|
+
|
17
|
+
@inv[group][:hosts] << ip
|
18
|
+
#@inv[group][:vars] = { ansible_host: ip }.merge(vars)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_json
|
22
|
+
@inv.to_json
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Shelter
|
2
|
+
module CLI
|
3
|
+
# Mixin module for Ansible
|
4
|
+
module AnsibleHelpers
|
5
|
+
def vault_password_file
|
6
|
+
[
|
7
|
+
'--vault-password-file',
|
8
|
+
"#{App.config.secure_root}/ansible_vault_password"
|
9
|
+
].join(' ')
|
10
|
+
end
|
11
|
+
|
12
|
+
def inventory_file(inventory)
|
13
|
+
inventory ||= App.config.inventory_script
|
14
|
+
"--inventory #{inventory}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def command_bin
|
18
|
+
'ansible-playbook'
|
19
|
+
end
|
20
|
+
|
21
|
+
def new_server_params(server_user)
|
22
|
+
command = []
|
23
|
+
command << "--user #{server_user} --ask-pass" unless server_user.nil?
|
24
|
+
command
|
25
|
+
end
|
26
|
+
|
27
|
+
def optional_params(tags: [], skip: [], limit: nil)
|
28
|
+
command = []
|
29
|
+
command << "--tags '#{tags.join(',')}'" unless tags.empty?
|
30
|
+
command << "--skip-tags '#{skip.join(',')}'" unless skip.empty?
|
31
|
+
command << "--limit '#{limit}'" unless limit.nil?
|
32
|
+
command
|
33
|
+
end
|
34
|
+
|
35
|
+
def ansible_execute(
|
36
|
+
playbook,
|
37
|
+
server_user: nil, inventory: nil,
|
38
|
+
tags: [], skip: [], limit: nil
|
39
|
+
)
|
40
|
+
command = [command_bin, inventory_file(inventory), vault_password_file]
|
41
|
+
command += new_server_params(server_user)
|
42
|
+
command += optional_params(tags: tags, skip: skip, limit: limit)
|
43
|
+
command << "#{App.config.ansible_directory}/#{playbook}.yaml"
|
44
|
+
|
45
|
+
full_command = "#{command.join(' ')}"
|
46
|
+
$stdout.puts full_command
|
47
|
+
system full_command
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
data/lib/cli/app.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
require_all(File.join(File.dirname(__FILE__), 'command'))
|
4
|
+
require_all(File.join(File.dirname(__FILE__), 'stack'))
|
5
|
+
|
6
|
+
module Shelter
|
7
|
+
module CLI
|
8
|
+
# Main Shelter app
|
9
|
+
class App < Thor
|
10
|
+
class << self
|
11
|
+
attr_writer :config
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.config
|
15
|
+
@config ||= Shelter::Configuration.new
|
16
|
+
@config.load_shelterfile
|
17
|
+
block_given? ? yield(@config) : @config
|
18
|
+
end
|
19
|
+
|
20
|
+
register(
|
21
|
+
Shelter::CLI::Command::Ansible,
|
22
|
+
'ansible',
|
23
|
+
'ansible [COMMAND]',
|
24
|
+
'Ansible related commands'
|
25
|
+
)
|
26
|
+
|
27
|
+
# Register server managers
|
28
|
+
path = File.join(App.config.stack_directory, '*')
|
29
|
+
Dir.glob(path).each do |path|
|
30
|
+
next unless File.exists? "#{path}/cli.rb"
|
31
|
+
require File.join(App.config.project_root, path, 'cli.rb')
|
32
|
+
|
33
|
+
stack_name = File.basename(path)
|
34
|
+
register(
|
35
|
+
Object.const_get("Stack::#{stack_name.camelize}"),
|
36
|
+
stack_name,
|
37
|
+
"#{stack_name} [COMMAND]",
|
38
|
+
"Manage #{stack_name.camelize} stack"
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Register server managers
|
43
|
+
path = File.join(App.config.plugin_directory, '*')
|
44
|
+
Dir.glob(path).each do |path|
|
45
|
+
next unless File.exists? "#{path}/main.rb"
|
46
|
+
require File.join(App.config.project_root, path, 'main.rb')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Shelter
|
4
|
+
module CLI
|
5
|
+
module Command
|
6
|
+
# Ansible subcommand for Shelter
|
7
|
+
class Ansible < Thor
|
8
|
+
desc 'update', 'update existing infrastructure'
|
9
|
+
method_option :only,
|
10
|
+
default: [], type: :array,
|
11
|
+
desc: 'Execute only these tags'
|
12
|
+
method_option :skip,
|
13
|
+
default: [], type: :array,
|
14
|
+
desc: 'Skip these tags'
|
15
|
+
method_option :limit,
|
16
|
+
type: :string,
|
17
|
+
desc: 'Limit to a specific group or host'
|
18
|
+
method_option :inventory,
|
19
|
+
type: :string,
|
20
|
+
default: nil,
|
21
|
+
desc: 'Specify inventory file/script'
|
22
|
+
def update
|
23
|
+
ansible_execute(
|
24
|
+
'configuration',
|
25
|
+
tags: options[:only],
|
26
|
+
skip: options[:skip],
|
27
|
+
limit: options[:limit],
|
28
|
+
inventory: options[:inventory]
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
no_commands do
|
33
|
+
include Shelter::CLI::AnsibleHelpers
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'aws-sdk'
|
3
|
+
|
4
|
+
module Shelter
|
5
|
+
module CLI
|
6
|
+
module Stack
|
7
|
+
class CloudFormation < Thor
|
8
|
+
desc 'status', 'Stack status'
|
9
|
+
def status
|
10
|
+
target_stack = cf_client.describe_stacks(stack_name: get_attr(:stack_name)).stacks.first
|
11
|
+
|
12
|
+
puts "#{target_stack.stack_name} exists..."
|
13
|
+
puts "Created: #{target_stack.creation_time}"
|
14
|
+
|
15
|
+
resources = cf_client.describe_stack_resources(
|
16
|
+
stack_name: target_stack.stack_name
|
17
|
+
)
|
18
|
+
resources.stack_resources.each do |r|
|
19
|
+
puts ' __________________________________________________________________________________________'
|
20
|
+
puts '| Stack Name | Resource ID | Resource Type | Resource Status |'
|
21
|
+
puts "| #{r.stack_name} | #{r.physical_resource_id} | #{r.resource_type} | #{r.resource_status} |"
|
22
|
+
puts ' ------------------------------------------------------------------------------------------'
|
23
|
+
end
|
24
|
+
rescue Exception => e
|
25
|
+
fail Thor::Error, e.message
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'create', 'Create stack'
|
29
|
+
def create
|
30
|
+
cf_client.create_stack(
|
31
|
+
stack_name: get_attr(:stack_name),
|
32
|
+
template_body: File.open(get_attr(:template_file)).read,
|
33
|
+
capabilities: ['CAPABILITY_IAM'],
|
34
|
+
tags: [
|
35
|
+
{ key: 'client', value: get_attr(:meta)[:client] },
|
36
|
+
{ key: 'type', value: get_attr(:meta)[:type] },
|
37
|
+
{ key: 'application', value: get_attr(:meta)[:application] }
|
38
|
+
]
|
39
|
+
)
|
40
|
+
i = 0
|
41
|
+
cf_client.wait_until(:stack_create_complete, stack_name: get_attr(:stack_name)) do |w|
|
42
|
+
w.before_attempt do
|
43
|
+
i = (i + 1) % SPINNER.size
|
44
|
+
print "\r#{SPINNER[i]}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
rescue Exception => e
|
48
|
+
fail Thor::Error, e.message
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'update', 'Update stack'
|
52
|
+
def update
|
53
|
+
cf_client.update_stack(
|
54
|
+
stack_name: get_attr(:stack_name),
|
55
|
+
template_body: File.open(get_attr(:template_file)).read,
|
56
|
+
capabilities: ['CAPABILITY_IAM']
|
57
|
+
)
|
58
|
+
i = 0
|
59
|
+
cf_client.wait_until(:stack_update_complete, stack_name: get_attr(:stack_name)) do |w|
|
60
|
+
w.before_attempt do
|
61
|
+
i = (i + 1) % SPINNER.size
|
62
|
+
print "\r#{SPINNER[i]}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rescue Exception => e
|
66
|
+
fail Thor::Error, e.message
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
no_commands do
|
72
|
+
class << self
|
73
|
+
def set_attr(name, value)
|
74
|
+
@@_attr ||= {}
|
75
|
+
@@_attr[name] = value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_attr(name)
|
80
|
+
@@_attr[name] if @@_attr.key? name
|
81
|
+
end
|
82
|
+
|
83
|
+
def cf_client
|
84
|
+
@cf_client ||= Aws::CloudFormation::Client.new(
|
85
|
+
credentials: Aws::Credentials.new(
|
86
|
+
ENV.fetch('AWS_ACCESS_KEY_ID'),
|
87
|
+
ENV.fetch('AWS_SECRET_ACCESS_KEY')
|
88
|
+
)
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_public_ip
|
93
|
+
target_stack = cf_client.describe_stacks(stack_name: get_attr(:stack_name)).stacks.first
|
94
|
+
|
95
|
+
resources = cf_client.describe_stack_resources(
|
96
|
+
stack_name: target_stack.stack_name
|
97
|
+
)
|
98
|
+
|
99
|
+
eip = resources.stack_resources.select { |r|
|
100
|
+
r.resource_type == 'AWS::EC2::EIP'
|
101
|
+
}.first
|
102
|
+
|
103
|
+
eip.physical_resource_id
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Shelter
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :ansible_directory,
|
4
|
+
:stack_directory,
|
5
|
+
:secure_root,
|
6
|
+
:inventory_script,
|
7
|
+
:inventory_directory,
|
8
|
+
:plugin_directory
|
9
|
+
|
10
|
+
attr_reader :project_root
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@ansible_directory = 'ansible'
|
14
|
+
@stack_directory = 'stacks'
|
15
|
+
@secure_root = ENV.fetch('SECURE', 'secure')
|
16
|
+
@inventory_script = File.join(File.dirname($PROGRAM_NAME), 'shelter-inventory')
|
17
|
+
@inventory_directory = 'inventory'
|
18
|
+
@plugin_directory = 'plugin'
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_shelterfile
|
22
|
+
load shelterfile if @shelterfile.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def inventory
|
26
|
+
path = File.join(@inventory_directory, '*')
|
27
|
+
Dir.glob(path)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def shelterfile
|
32
|
+
@shelterfile ||= File.join(find_project_root, 'Shelterfile.rb')
|
33
|
+
end
|
34
|
+
|
35
|
+
def find_project_root
|
36
|
+
@project_root unless @project_root.nil?
|
37
|
+
|
38
|
+
dir = Dir.pwd
|
39
|
+
loop do
|
40
|
+
break if File.exist?('Shelterfile.rb')
|
41
|
+
fail 'No Shelterfile.rb found' if Dir.pwd == '/'
|
42
|
+
Dir.chdir('..')
|
43
|
+
end
|
44
|
+
@project_root = Dir.pwd
|
45
|
+
Dir.chdir dir
|
46
|
+
@project_root
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
data/lib/shelter.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'version'
|
2
|
+
require 'configuration'
|
3
|
+
|
4
|
+
directories = %w(
|
5
|
+
ansible
|
6
|
+
cli
|
7
|
+
)
|
8
|
+
|
9
|
+
class String
|
10
|
+
def camelize(uppercase_first_letter = true)
|
11
|
+
string = self
|
12
|
+
if uppercase_first_letter
|
13
|
+
string = string.sub(/^[a-z\d]*/) { $&.capitalize }
|
14
|
+
else
|
15
|
+
string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase }
|
16
|
+
end
|
17
|
+
string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
LIB_ROOT = File.expand_path('../', __FILE__)
|
22
|
+
|
23
|
+
def require_all(dir)
|
24
|
+
path = File.join(dir, '*')
|
25
|
+
Dir.glob(path).each do |file|
|
26
|
+
require file if File.file?(file)
|
27
|
+
require_all(file) if File.directory?(file)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
directories.each do |dir|
|
32
|
+
require_all(File.join(File.dirname(__FILE__), dir))
|
33
|
+
end
|
34
|
+
|
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shelter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Balazs Nadasdi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.20.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.20.0
|
41
|
+
description: Shelter tries to be a framework to manage internal infrastructure
|
42
|
+
email: balazs.nadasdi@cheppers.com
|
43
|
+
executables:
|
44
|
+
- shelter
|
45
|
+
- shelter-inventory
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- README.md
|
50
|
+
- bin/shelter
|
51
|
+
- bin/shelter-inventory
|
52
|
+
- lib/ansible/inventory.rb
|
53
|
+
- lib/cli/ansible_helper.rb
|
54
|
+
- lib/cli/app.rb
|
55
|
+
- lib/cli/command/ansible.rb
|
56
|
+
- lib/cli/stack/cloud_formation.rb
|
57
|
+
- lib/configuration.rb
|
58
|
+
- lib/shelter.rb
|
59
|
+
- lib/version.rb
|
60
|
+
homepage: https://github.com/yitsushi/shelter
|
61
|
+
licenses:
|
62
|
+
- MIT
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '2.3'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 2.6.11
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: Shelter is a tool for you
|
84
|
+
test_files: []
|