shippy 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/bin/shippy +5 -0
- data/lib/shippy/cli/app.rb +207 -0
- data/lib/shippy/cli/base.rb +47 -0
- data/lib/shippy/cli/main.rb +76 -0
- data/lib/shippy/cli/prune.rb +21 -0
- data/lib/shippy/cli/server.rb +24 -0
- data/lib/shippy/cli/templates/shippy.yml +8 -0
- data/lib/shippy/cli.rb +10 -0
- data/lib/shippy/commander.rb +45 -0
- data/lib/shippy/compiler.rb +68 -0
- data/lib/shippy/compose.rb +88 -0
- data/lib/shippy/config.rb +44 -0
- data/lib/shippy/repo.rb +70 -0
- data/lib/shippy/service.rb +132 -0
- data/lib/shippy/version.rb +3 -0
- data/lib/shippy.rb +32 -0
- metadata +211 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 99b3140852a1c73552885b9c220f42211a2c86dd8500cf85ef0f0a7c30d88bec
|
4
|
+
data.tar.gz: 54f94d8901dd3b79e9ce150ae1caa5972d48061f0532d73fb51d1c6ecbcc806c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: efc12fd0b68d4cdad42c4cef32cb2e82674cd9a0f2e62d2c31964da77b010fefd3792088f19d204be109a1222198cdfabdc3d1ac66d0d677e30f8612bf47a7a7
|
7
|
+
data.tar.gz: 0d49e7f3a32c6a16175bce6ea75e60553a418ebe2a06742858488459085701b94b01ab141e023b8cdcc49cb37ba1d30c878769350cb0eadda365c38c65e61a1e
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Marius Bobin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Shippy
|
2
|
+
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/shippy`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
14
|
+
|
15
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
+
|
17
|
+
$ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Development
|
24
|
+
|
25
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
|
+
|
27
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/shippy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/shippy/blob/master/CODE_OF_CONDUCT.md).
|
32
|
+
|
33
|
+
## License
|
34
|
+
|
35
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
36
|
+
|
37
|
+
## Code of Conduct
|
38
|
+
|
39
|
+
Everyone interacting in the Shippy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/shippy/blob/master/CODE_OF_CONDUCT.md).
|
data/bin/shippy
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
class Shippy::Cli::App < Shippy::Cli::Base
|
2
|
+
argument :app_name, type: :string
|
3
|
+
|
4
|
+
def initialize(...)
|
5
|
+
super(...)
|
6
|
+
load_app
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "deploy", "Deploy application"
|
10
|
+
def deploy
|
11
|
+
say "Deploying #{app_name} on #{SHIPPY.host}...", :magenta
|
12
|
+
|
13
|
+
SHIPPY.compile
|
14
|
+
archive
|
15
|
+
backup
|
16
|
+
upload
|
17
|
+
create_network
|
18
|
+
refresh
|
19
|
+
stop_old
|
20
|
+
pre_start_hooks
|
21
|
+
start
|
22
|
+
cleanup
|
23
|
+
status
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "refresh", "Pull new images"
|
27
|
+
def refresh
|
28
|
+
say "Pulling images for #{app_name} on #{SHIPPY.host}...", :magenta
|
29
|
+
|
30
|
+
within_app do
|
31
|
+
execute :docker_compose, "pull"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "start", "Start application"
|
36
|
+
def start
|
37
|
+
say "Starting #{app_name} on #{SHIPPY.host}...", :magenta
|
38
|
+
|
39
|
+
within_app do
|
40
|
+
execute :docker_compose, "up", "-d", SHIPPY.app&.deploy_flags
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "stop", "Stop application"
|
45
|
+
def stop
|
46
|
+
say "Stopping #{app_name} on #{SHIPPY.host}...", :magenta
|
47
|
+
|
48
|
+
within_app do
|
49
|
+
execute :docker_compose, "down", "--remove-orphans"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "logs", "Show app logs"
|
54
|
+
def logs
|
55
|
+
within_app do
|
56
|
+
puts capture(:docker_compose, "logs", verbosity: Logger::INFO)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "status", "Show app status"
|
61
|
+
def status
|
62
|
+
say "Status for #{app_name} on #{SHIPPY.host}...", :magenta
|
63
|
+
|
64
|
+
within_app do
|
65
|
+
puts capture(:docker_compose, "ps", "-a")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def load_app
|
72
|
+
with_app do
|
73
|
+
SHIPPY.app = Shippy::Repo.load_app(app_name)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def archive
|
78
|
+
FileUtils.cd("builds/apps") do
|
79
|
+
Minitar.pack(app_name, Zlib::GzipWriter.new(File.open(archive_path, "wb")))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def prepare_directory
|
84
|
+
on(SHIPPY.host) do
|
85
|
+
execute :mkdir, "-p", SHIPPY.current_app_path
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def backup
|
90
|
+
backups_path = self.backups_path
|
91
|
+
|
92
|
+
on(SHIPPY.host) do
|
93
|
+
if test("[ -d #{SHIPPY.current_app_path} ]")
|
94
|
+
execute :mkdir, "-p", backups_path
|
95
|
+
execute :mv, SHIPPY.current_app_path, backups_path
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def stop_old
|
101
|
+
backup_path = backups_path.join(app_name)
|
102
|
+
|
103
|
+
on(SHIPPY.host) do
|
104
|
+
if test("[ -f #{backup_path.join("docker-compose.yml")} ]")
|
105
|
+
within backup_path do
|
106
|
+
execute :docker_compose, "down", "--remove-orphans"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def upload
|
113
|
+
source_path = SHIPPY.current_app_path.join("..").expand_path
|
114
|
+
archive_path = self.archive_path
|
115
|
+
prepare_directory
|
116
|
+
|
117
|
+
on(SHIPPY.host) do
|
118
|
+
tmp = capture "mktemp"
|
119
|
+
upload! archive_path, tmp
|
120
|
+
|
121
|
+
execute :tar, "-xzpf", tmp, "-C", source_path
|
122
|
+
execute :rm, tmp
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def cleanup
|
127
|
+
remove_archive
|
128
|
+
remove_old_releases
|
129
|
+
end
|
130
|
+
|
131
|
+
def remove_archive
|
132
|
+
archive_path = self.archive_path
|
133
|
+
|
134
|
+
run_locally do
|
135
|
+
execute :rm, archive_path
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def remove_old_releases
|
140
|
+
app_backups_paths = backups_path.join("..").expand_path
|
141
|
+
|
142
|
+
on(SHIPPY.host) do
|
143
|
+
if test("[ -d #{app_backups_paths} ]")
|
144
|
+
releases = capture(:ls, "-x", app_backups_paths).split
|
145
|
+
directories = (releases - releases.last(SHIPPY.config.keep_releases)).map do |release|
|
146
|
+
app_backups_paths.join(release).to_s
|
147
|
+
end
|
148
|
+
|
149
|
+
if directories.any?
|
150
|
+
directories.each_slice(100) do |directories_batch|
|
151
|
+
execute :rm, "-rf", *directories_batch
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def pre_start_hooks
|
159
|
+
say "Running #{app_name} hooks on #{SHIPPY.host}...", :magenta
|
160
|
+
|
161
|
+
within_app do
|
162
|
+
SHIPPY.app.each_hook do |service, options, command|
|
163
|
+
execute :docker_compose, "run", "--rm", options, service, command
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def create_network
|
169
|
+
on(SHIPPY.host) do
|
170
|
+
result = capture("docker", "network", "ls", "-q", "-f", "'name=lan_access'")
|
171
|
+
|
172
|
+
if result.empty?
|
173
|
+
execute :docker, "network", "create", "lan_access"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def archive_path
|
179
|
+
@archive_path ||= "/tmp/v#{Time.now.to_i}.tar.gz"
|
180
|
+
end
|
181
|
+
|
182
|
+
def backups_path
|
183
|
+
@backups_path ||= SHIPPY.deploy_path.join("backups", app_name, Time.now.to_i.to_s)
|
184
|
+
end
|
185
|
+
|
186
|
+
def with_app
|
187
|
+
path = Pathname.new("apps").join(app_name)
|
188
|
+
|
189
|
+
if path.exist?
|
190
|
+
yield
|
191
|
+
else
|
192
|
+
error_on_missing_app
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def error_on_missing_app
|
197
|
+
raise "No app by the name of '#{app_name}'"
|
198
|
+
end
|
199
|
+
|
200
|
+
def within_app(&block)
|
201
|
+
on(SHIPPY.host) do
|
202
|
+
within(SHIPPY.current_app_path) do
|
203
|
+
instance_eval(&block)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "sshkit"
|
3
|
+
|
4
|
+
module Shippy::Cli
|
5
|
+
class Base < Thor
|
6
|
+
include SSHKit::DSL
|
7
|
+
|
8
|
+
def self.exit_on_failure?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
class_option :config_file, aliases: "-c", default: "config/shippy.yml", desc: "Path to config file"
|
13
|
+
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
14
|
+
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
|
15
|
+
|
16
|
+
def initialize(*)
|
17
|
+
super
|
18
|
+
initialize_commander(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def initialize_commander(options)
|
24
|
+
SHIPPY.tap do |commander|
|
25
|
+
commander.config_file = Pathname.new(File.expand_path(options[:config_file]))
|
26
|
+
|
27
|
+
if options[:verbose]
|
28
|
+
ENV["VERBOSE"] = "1" # For backtraces via cli/start
|
29
|
+
commander.verbosity = :debug
|
30
|
+
end
|
31
|
+
|
32
|
+
if options[:quiet]
|
33
|
+
commander.verbosity = :error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def print_runtime
|
39
|
+
started_at = Time.now
|
40
|
+
yield
|
41
|
+
Time.now - started_at
|
42
|
+
ensure
|
43
|
+
runtime = Time.now - started_at
|
44
|
+
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
class Shippy::Cli::Main < Shippy::Cli::Base
|
4
|
+
desc "init", "Create config stub in config/shippy.yml"
|
5
|
+
option :bundle, type: :boolean, default: false, desc: "Add SHIPPY to the Gemfile and create a bin/shippy binstub"
|
6
|
+
def init
|
7
|
+
require "fileutils"
|
8
|
+
|
9
|
+
if (config_file = Pathname.new(File.expand_path("config/shippy.yml"))).exist?
|
10
|
+
puts "Config file already exists in config/shippy.yml (remove first to create a new one)"
|
11
|
+
else
|
12
|
+
FileUtils.mkdir_p config_file.dirname
|
13
|
+
FileUtils.cp_r Pathname.new(File.expand_path("templates/shippy.yml", __dir__)), config_file
|
14
|
+
puts "Created configuration file in config/shippy.yml"
|
15
|
+
end
|
16
|
+
|
17
|
+
if Pathname.new(File.expand_path("apps")).exist?
|
18
|
+
puts "Apps directory already exists in ./apps (remove first to create a new one)"
|
19
|
+
else
|
20
|
+
FileUtils.mkdir_p "apps"
|
21
|
+
puts "Created apps directory in ./apps"
|
22
|
+
end
|
23
|
+
|
24
|
+
if options[:bundle]
|
25
|
+
if Pathname.new(File.expand_path("bin/shippy")).exist?
|
26
|
+
puts "Binstub already exists in bin/shippy (remove first to create a new one)"
|
27
|
+
else
|
28
|
+
puts "Adding SHIPPY to Gemfile and bundle..."
|
29
|
+
run_locally do
|
30
|
+
execute :bundle, :add, :shippy
|
31
|
+
execute :bundle, :binstubs, :shippy
|
32
|
+
end
|
33
|
+
puts "Created binstub file in bin/shippy"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "version", "Show SHIPPY version"
|
39
|
+
def version
|
40
|
+
puts Shippy::VERSION
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "setup", "Setup all accessories and deploy apps to server"
|
44
|
+
def setup
|
45
|
+
invoke "shippy:cli:server:bootstrap"
|
46
|
+
invoke "shippy:cli:server:setup"
|
47
|
+
deploy
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "deploy", "Deploy all apps to server"
|
51
|
+
def deploy
|
52
|
+
print_runtime do
|
53
|
+
Dir.glob("*", base: "apps").each do |name|
|
54
|
+
Shippy::Cli::App.send :dispatch, :deploy, [name], {}, {}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "stop", "Stop all apps on server"
|
60
|
+
def stop
|
61
|
+
print_runtime do
|
62
|
+
Dir.glob("*", base: "apps").each do |name|
|
63
|
+
Shippy::Cli::App.send :dispatch, :stop, [name], {}, {}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "app", "Manage application"
|
69
|
+
subcommand "app", Shippy::Cli::App
|
70
|
+
|
71
|
+
desc "server", "Bootstrap servers with curl and Docker"
|
72
|
+
subcommand "server", Shippy::Cli::Server
|
73
|
+
|
74
|
+
desc "prune", "Prune old application images and containers"
|
75
|
+
subcommand "prune", Shippy::Cli::Prune
|
76
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Shippy::Cli::Prune < Shippy::Cli::Base
|
2
|
+
desc "all", "Prune unused images and stopped containers"
|
3
|
+
def all
|
4
|
+
containers
|
5
|
+
images
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "images", "Prune unused images older than 7 days"
|
9
|
+
def images
|
10
|
+
on(SHIPPY.host) do
|
11
|
+
execute :docker, :image, :prune, "--all", "--force", "--filter", "until=#{7.days.in_hours}h"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "containers", "Prune stopped containers older than 3 days"
|
16
|
+
def containers
|
17
|
+
on(SHIPPY.host) do
|
18
|
+
execute :docker, :container, :prune, "--force", "--filter", "until=#{3.days.in_hours}h"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Shippy::Cli::Server < Shippy::Cli::Base
|
2
|
+
desc "bootstrap", "Ensure curl and Docker are installed on server"
|
3
|
+
def bootstrap
|
4
|
+
on(SHIPPY.host) do
|
5
|
+
dependencies_to_install = [].tap do |dependencies|
|
6
|
+
dependencies << "curl" unless execute "which curl", raise_on_non_zero_exit: false
|
7
|
+
dependencies << "docker.io" unless execute "which docker", raise_on_non_zero_exit: false
|
8
|
+
dependencies << "docker-compose" unless execute "which docker-compose", raise_on_non_zero_exit: false
|
9
|
+
end
|
10
|
+
|
11
|
+
if dependencies_to_install.any?
|
12
|
+
execute "apt-get update -y && apt-get install #{dependencies_to_install.join(" ")} -y"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "setup", "Generate directories structure"
|
18
|
+
def setup
|
19
|
+
on(SHIPPY.host) do
|
20
|
+
execute(:mkdir, "-p", SHIPPY.config.deploy_to + "/apps")
|
21
|
+
execute(:mkdir, "-p", SHIPPY.config.deploy_to + "/backups")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/shippy/cli.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Shippy
|
2
|
+
class Commander
|
3
|
+
attr_accessor :config_file, :verbosity, :repo, :app
|
4
|
+
delegate :host, :deploy_path, to: :config
|
5
|
+
|
6
|
+
def initialize(config_file: nil, verbosity: :info)
|
7
|
+
@config_file, @verbosity = config_file, verbosity
|
8
|
+
end
|
9
|
+
|
10
|
+
def config
|
11
|
+
@config ||= Shippy::Config
|
12
|
+
.new(config_file)
|
13
|
+
.tap { |config| configure_sshkit_with(config) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_verbosity(level)
|
17
|
+
old_level = verbosity
|
18
|
+
|
19
|
+
self.verbosity = level
|
20
|
+
SSHKit.config.output_verbosity = level
|
21
|
+
|
22
|
+
yield
|
23
|
+
ensure
|
24
|
+
self.verbosity = old_level
|
25
|
+
SSHKit.config.output_verbosity = old_level
|
26
|
+
end
|
27
|
+
|
28
|
+
def compile
|
29
|
+
Shippy::Compiler.new(app, config: config).compile
|
30
|
+
end
|
31
|
+
|
32
|
+
def current_app_path
|
33
|
+
deploy_path.join("apps", app.name)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def configure_sshkit_with(config)
|
39
|
+
SSHKit::Backend::Netssh.configure { |ssh| ssh.ssh_options = config.ssh_options }
|
40
|
+
SSHKit.config.command_map[:docker] = "docker"
|
41
|
+
SSHKit.config.command_map[:docker_compose] = "docker-compose"
|
42
|
+
SSHKit.config.output_verbosity = verbosity
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Shippy
|
2
|
+
class Compiler
|
3
|
+
attr_reader :config, :app
|
4
|
+
|
5
|
+
def initialize(app, config:)
|
6
|
+
@app = app
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def compile
|
11
|
+
prep_build_dir
|
12
|
+
write_compose_file
|
13
|
+
write_configuration_files
|
14
|
+
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def build_dir
|
21
|
+
@build_dir ||= Pathname.pwd.join("builds", "apps", app.name).expand_path
|
22
|
+
end
|
23
|
+
|
24
|
+
def prep_build_dir
|
25
|
+
FileUtils.rm_rf(build_dir)
|
26
|
+
FileUtils.mkdir_p(build_dir)
|
27
|
+
end
|
28
|
+
|
29
|
+
def write_compose_file
|
30
|
+
path = build_dir.join("docker-compose.yml")
|
31
|
+
File.binwrite(path, app.to_yaml)
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_configuration_files
|
35
|
+
Dir.glob("apps/#{app.name}/**/*", File::FNM_DOTMATCH)
|
36
|
+
.reject { |file| File.directory?(file) }
|
37
|
+
.reject { |file| file.match?(/docker-compose\.(rb)?(yml)?/) }
|
38
|
+
.each { |file| create_directory(file) }
|
39
|
+
.each { |file| copy_file(file) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_directory(file)
|
43
|
+
path = build_dest(file)
|
44
|
+
directory = File.dirname(path)
|
45
|
+
FileUtils.mkdir_p(directory)
|
46
|
+
end
|
47
|
+
|
48
|
+
def copy_file(file)
|
49
|
+
if file.ends_with?(".erb")
|
50
|
+
copy_template_files(file)
|
51
|
+
else
|
52
|
+
FileUtils.cp(file, build_dest(file))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def copy_template_files(file)
|
57
|
+
new_path = build_dest(file.gsub(".erb", ""))
|
58
|
+
mode = File.stat(file).mode
|
59
|
+
template = ERB.new(File.read(file))
|
60
|
+
result = template.result(app.get_binding)
|
61
|
+
File.open(new_path, "wb", mode) { |f| f.write(result) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_dest(path)
|
65
|
+
build_dir.join("..", "..", path).expand_path
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Shippy
|
2
|
+
class Compose
|
3
|
+
attr_reader :services, :name
|
4
|
+
|
5
|
+
def initialize(name, &block)
|
6
|
+
@name = name
|
7
|
+
@services = []
|
8
|
+
instance_eval(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def service(name, &block)
|
12
|
+
service = Service.new(name, app: self, &block)
|
13
|
+
@services << service
|
14
|
+
end
|
15
|
+
|
16
|
+
def xservice(name, &block)
|
17
|
+
puts "Service #{name} is disabled"
|
18
|
+
end
|
19
|
+
|
20
|
+
def deploy_flags
|
21
|
+
if block_given?
|
22
|
+
@deploy_flags = yield
|
23
|
+
else
|
24
|
+
@deploy_flags
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_hash
|
29
|
+
data = {}
|
30
|
+
data[:version] = "3"
|
31
|
+
data[:services] = services.map(&:to_hash).reduce(:merge)
|
32
|
+
data[:volumes] = volumes_hash.presence
|
33
|
+
data[:networks] = networks_hash.presence
|
34
|
+
data.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_yaml
|
38
|
+
to_hash.deep_stringify_keys.to_yaml
|
39
|
+
end
|
40
|
+
|
41
|
+
def each_hook(&block)
|
42
|
+
@services
|
43
|
+
.flat_map(&:hooks)
|
44
|
+
.each(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def secrets(secret_name)
|
48
|
+
Shippy.x.secrets(name, secret_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_binding
|
52
|
+
binding
|
53
|
+
end
|
54
|
+
|
55
|
+
def local_domains
|
56
|
+
@services.flat_map(&:local_domains).uniq
|
57
|
+
end
|
58
|
+
|
59
|
+
delegate :x, to: Shippy
|
60
|
+
delegate :wildcard_domain, :local_domain, to: :x
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def networks
|
65
|
+
services.flat_map(&:networks).uniq.compact
|
66
|
+
end
|
67
|
+
|
68
|
+
def networks_hash
|
69
|
+
data = networks.reduce({}) { |acc, net| acc.merge(net => nil) }
|
70
|
+
data.deep_symbolize_keys!
|
71
|
+
data[:lan_access] = {external: true} if data.key?(:lan_access)
|
72
|
+
data
|
73
|
+
end
|
74
|
+
|
75
|
+
def volumes
|
76
|
+
services
|
77
|
+
.flat_map(&:volumes)
|
78
|
+
.uniq
|
79
|
+
.compact
|
80
|
+
.select { |v| v.start_with?(/[a-z]/) }
|
81
|
+
.map { |v| v.split(":", 2).first }
|
82
|
+
end
|
83
|
+
|
84
|
+
def volumes_hash
|
85
|
+
volumes.reduce({}) { |acc, v| acc.merge(v => nil) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Shippy
|
2
|
+
class Config
|
3
|
+
attr_reader :data
|
4
|
+
|
5
|
+
def initialize(file)
|
6
|
+
@data = YAML.load_file(file).deep_symbolize_keys
|
7
|
+
end
|
8
|
+
|
9
|
+
def secrets(app, name)
|
10
|
+
@data
|
11
|
+
.fetch(:secrets)
|
12
|
+
.fetch(app.to_sym)
|
13
|
+
.fetch(name.to_sym)
|
14
|
+
end
|
15
|
+
|
16
|
+
def ssh_options
|
17
|
+
{user: ssh_user, auth_methods: ["publickey"]}.compact
|
18
|
+
end
|
19
|
+
|
20
|
+
def ssh_user
|
21
|
+
if data[:ssh].present?
|
22
|
+
data.dig(:ssh, :user) || "root"
|
23
|
+
else
|
24
|
+
"root"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def deploy_path
|
29
|
+
Pathname.new(deploy_to)
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(name, *args)
|
33
|
+
if @data.key?(name)
|
34
|
+
@data.fetch(name)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond_to_missing?(name, include_private = false)
|
41
|
+
@data.key?(name) || super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/shippy/repo.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Shippy
|
2
|
+
class Repo
|
3
|
+
attr_reader :data
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def current
|
7
|
+
Thread.current[:repo]
|
8
|
+
end
|
9
|
+
|
10
|
+
def current=(repo)
|
11
|
+
Thread.current[:repo] = repo
|
12
|
+
end
|
13
|
+
|
14
|
+
def with(repo)
|
15
|
+
old_repo = current
|
16
|
+
|
17
|
+
self.current = repo
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
self.current = old_repo
|
21
|
+
end
|
22
|
+
|
23
|
+
def define(&block)
|
24
|
+
current.define(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_app(name)
|
28
|
+
repo = new
|
29
|
+
repo.load_definitions(pattern: name)
|
30
|
+
repo.data.fetch(name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@data = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def define(&block)
|
39
|
+
name = caller
|
40
|
+
.reject { |path| path.match(/lib\/shippy/) }
|
41
|
+
.find { |path| path.match(/docker-compose\.rb/) }.split(":", 2).first
|
42
|
+
name = File.basename(File.dirname(name))
|
43
|
+
data[name] = Compose.new(name, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def load_definitions(pattern: "**")
|
47
|
+
self.class.with(self) do
|
48
|
+
Pathname.glob("apps/#{pattern}/docker-compose.rb")
|
49
|
+
.map(&:expand_path)
|
50
|
+
.each { |path| load path }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def hooks_for(app)
|
55
|
+
data.fetch(app).hooks
|
56
|
+
end
|
57
|
+
|
58
|
+
def each(&block)
|
59
|
+
data.each(&block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def each_app(&block)
|
63
|
+
data.values.each(&block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def app_names
|
67
|
+
data.keys
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Shippy
|
2
|
+
class Service
|
3
|
+
ATTRIBUTES = %i[
|
4
|
+
build command depends_on devices entrypoint environment hostname image
|
5
|
+
labels logging networks ports restart volumes volumes_from working_dir
|
6
|
+
security_opt
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
attr_reader :name, :app
|
10
|
+
|
11
|
+
ATTRIBUTES.each do |name|
|
12
|
+
define_method(name) do |&block|
|
13
|
+
if block
|
14
|
+
@data.merge!({name => block.call}.compact)
|
15
|
+
else
|
16
|
+
@data[name]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(name, app:, &block)
|
22
|
+
@name = name
|
23
|
+
@app = app
|
24
|
+
@data = {}
|
25
|
+
|
26
|
+
instance_eval(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_hash
|
30
|
+
{@name => @data}
|
31
|
+
end
|
32
|
+
|
33
|
+
def use_default_options
|
34
|
+
use_default_restart
|
35
|
+
use_default_networks
|
36
|
+
use_default_logging
|
37
|
+
end
|
38
|
+
|
39
|
+
def use_default_restart
|
40
|
+
restart { "unless-stopped" }
|
41
|
+
end
|
42
|
+
|
43
|
+
def use_default_logging
|
44
|
+
logging do
|
45
|
+
{
|
46
|
+
driver: "json-file",
|
47
|
+
options: {
|
48
|
+
"max-size": "10m",
|
49
|
+
"max-file": "5"
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def use_default_networks
|
56
|
+
networks { ["lan_access"] }
|
57
|
+
end
|
58
|
+
|
59
|
+
def use_traefik(name:, alt: nil, port: nil, healthcheck: nil)
|
60
|
+
labels do
|
61
|
+
all_labels = ["traefik.enable=true"]
|
62
|
+
all_labels += build_traefik_labels(name: name, port: port, healthcheck: healthcheck)
|
63
|
+
all_labels += build_traefik_labels(name: alt, port: port, healthcheck: healthcheck) if alt
|
64
|
+
|
65
|
+
all_labels
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_traefik_labels(name:, port: nil, healthcheck: nil)
|
70
|
+
[
|
71
|
+
"traefik.http.routers.websecure-#{name}.rule=Host(`#{name}.#{wildcard_domain}`)",
|
72
|
+
"traefik.http.routers.websecure-#{name}.entrypoints=websecure",
|
73
|
+
"traefik.http.routers.websecure-#{name}.tls=true",
|
74
|
+
"traefik.http.routers.websecure-#{name}.tls.certresolver=ssl-resolver",
|
75
|
+
"traefik.http.routers.websecure-#{name}.tls.domains[0].main=#{wildcard_domain}",
|
76
|
+
"traefik.http.routers.websecure-#{name}.tls.domains[0].sans=*.#{wildcard_domain}",
|
77
|
+
|
78
|
+
"traefik.http.routers.web-#{name}.rule=Host(`#{name}.#{wildcard_domain}`)",
|
79
|
+
"traefik.http.routers.web-#{name}.entrypoints=web",
|
80
|
+
"traefik.http.routers.web-#{name}.middlewares=secure-redirect-scheme@file",
|
81
|
+
|
82
|
+
"traefik.http.routers.#{name}-local.rule=Host(`#{name}.#{local_domain}`)",
|
83
|
+
"traefik.http.routers.#{name}-local.entrypoints=web",
|
84
|
+
"traefik.http.middlewares.#{name}-local.redirectregex.regex=^http://#{name}.#{local_domain}",
|
85
|
+
"traefik.http.middlewares.#{name}-local.redirectregex.replacement=https://#{name}.#{wildcard_domain}",
|
86
|
+
"traefik.http.middlewares.#{name}-local.redirectregex.permanent=true",
|
87
|
+
"traefik.http.routers.#{name}-local.middlewares=#{name}-local@docker"
|
88
|
+
].tap do |labels|
|
89
|
+
if port
|
90
|
+
labels << "traefik.http.services.#{name}.loadbalancer.server.port=#{port}"
|
91
|
+
labels << "traefik.http.routers.websecure-#{name}.service=#{name}"
|
92
|
+
labels << "traefik.http.routers.web-#{name}.service=#{name}"
|
93
|
+
labels << "traefik.http.routers.#{name}-local.service=#{name}"
|
94
|
+
end
|
95
|
+
|
96
|
+
if healthcheck
|
97
|
+
labels << "traefik.http.services.#{name}.loadbalancer.healthcheck.path=#{healthcheck}"
|
98
|
+
labels << "traefik.http.services.#{name}.loadbalancer.healthcheck.interval=5s"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def secrets(name)
|
104
|
+
@app.secrets(name)
|
105
|
+
end
|
106
|
+
|
107
|
+
delegate :x, to: Shippy
|
108
|
+
delegate :wildcard_domain, :local_domain, to: :x
|
109
|
+
|
110
|
+
def hooks
|
111
|
+
if block_given?
|
112
|
+
@hooks = normalize_hooks(yield)
|
113
|
+
else
|
114
|
+
@hooks.to_a.map { |data| [name, data[:options], data[:cmd]] }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def local_domains
|
119
|
+
labels.to_a.filter_map do |label|
|
120
|
+
label.match(/Host\(`(\w+\.#{local_domain})`\)/).to_a[1]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def normalize_hooks(data)
|
127
|
+
Array
|
128
|
+
.wrap(data)
|
129
|
+
.map { |data| data.is_a?(String) ? {cmd: data} : data }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/shippy.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "active_support/all"
|
5
|
+
require "fileutils"
|
6
|
+
require "erb"
|
7
|
+
require "thor"
|
8
|
+
require "minitar"
|
9
|
+
require "zlib"
|
10
|
+
|
11
|
+
require_relative "shippy/version"
|
12
|
+
require_relative "shippy/config"
|
13
|
+
require_relative "shippy/commander"
|
14
|
+
require_relative "shippy/cli"
|
15
|
+
require_relative "shippy/compose"
|
16
|
+
require_relative "shippy/service"
|
17
|
+
require_relative "shippy/repo"
|
18
|
+
require_relative "shippy/compiler"
|
19
|
+
|
20
|
+
module Shippy
|
21
|
+
class Error < StandardError; end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def define(&block)
|
25
|
+
Shippy::Repo.define(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def x
|
29
|
+
SHIPPY.config
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shippy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marius Bobin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-03-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 7.0.4.2
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '7.0'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 7.0.4.2
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: capistrano
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.17'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.17'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: ed25519
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: bcrypt_pbkdf
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: minitar
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.9'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0.9'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: thor
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :runtime
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: sshkit
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.21'
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '1.21'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: rake
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '13.0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '13.0'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: rspec
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '3.0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '3.0'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: standard
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '1.3'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '1.3'
|
159
|
+
description:
|
160
|
+
email:
|
161
|
+
- marius@mbobin.me
|
162
|
+
executables:
|
163
|
+
- shippy
|
164
|
+
extensions: []
|
165
|
+
extra_rdoc_files: []
|
166
|
+
files:
|
167
|
+
- LICENSE.txt
|
168
|
+
- README.md
|
169
|
+
- bin/shippy
|
170
|
+
- lib/shippy.rb
|
171
|
+
- lib/shippy/cli.rb
|
172
|
+
- lib/shippy/cli/app.rb
|
173
|
+
- lib/shippy/cli/base.rb
|
174
|
+
- lib/shippy/cli/main.rb
|
175
|
+
- lib/shippy/cli/prune.rb
|
176
|
+
- lib/shippy/cli/server.rb
|
177
|
+
- lib/shippy/cli/templates/shippy.yml
|
178
|
+
- lib/shippy/commander.rb
|
179
|
+
- lib/shippy/compiler.rb
|
180
|
+
- lib/shippy/compose.rb
|
181
|
+
- lib/shippy/config.rb
|
182
|
+
- lib/shippy/repo.rb
|
183
|
+
- lib/shippy/service.rb
|
184
|
+
- lib/shippy/version.rb
|
185
|
+
homepage: https://mbobin.me/shippy
|
186
|
+
licenses:
|
187
|
+
- MIT
|
188
|
+
metadata:
|
189
|
+
allowed_push_host: https://rubygems.org
|
190
|
+
homepage_uri: https://mbobin.me/shippy
|
191
|
+
source_code_uri: https://mbobin.me/shippy
|
192
|
+
post_install_message:
|
193
|
+
rdoc_options: []
|
194
|
+
require_paths:
|
195
|
+
- lib
|
196
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: 3.0.0
|
201
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
202
|
+
requirements:
|
203
|
+
- - ">="
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
requirements: []
|
207
|
+
rubygems_version: 3.4.6
|
208
|
+
signing_key:
|
209
|
+
specification_version: 4
|
210
|
+
summary: Deployment wrapper around docker-compose and SSH Kit
|
211
|
+
test_files: []
|