ezpaas-server 0.1.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/LICENSE.txt +21 -0
- data/README.md +61 -0
- data/bin/ezpaasd +37 -0
- data/bin/setup +8 -0
- data/lib/ezpaas/helpers/config.rb +9 -0
- data/lib/ezpaas/helpers/container_manager.rb +232 -0
- data/lib/ezpaas/helpers/file_extensions.rb +7 -0
- data/lib/ezpaas/models/app.rb +17 -0
- data/lib/ezpaas/models/init.rb +13 -0
- data/lib/ezpaas/models/model.rb +15 -0
- data/lib/ezpaas/server/app.rb +29 -0
- data/lib/ezpaas/server/routes/api.rb +14 -0
- data/lib/ezpaas/server/routes/apps.rb +60 -0
- data/lib/ezpaas/server/routes/deployments.rb +139 -0
- data/lib/ezpaas/server/routes/proxy.rb +69 -0
- data/lib/ezpaas/server/version.rb +6 -0
- metadata +271 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8b7cfb3850b8555809276ef35f10a0b9536e9c0e
|
4
|
+
data.tar.gz: 20a74dcd8ea53b1bb2ce28b769d8071dca2936b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7481619c52d0918f9f37cb129152a2a467b6462ab677999c4a819ea5bbab34dfaf000683f1e416b8fe1bf97d86cd322a4a1949ae61d9675ae18a4f54651bf80d
|
7
|
+
data.tar.gz: 812d5b1f0bd4fcc33656aea727560b13837b751f12826bb1b56a06dc975b6ee82319b09c5466ce7613492d331a45f64956feb3194490cbc94978a4620fd89226
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Nick Lee
|
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,61 @@
|
|
1
|
+
# EzPaaS Server
|
2
|
+
### A miniature Heroku clone for easy in-house deployments, powered by Docker.
|
3
|
+
|
4
|
+
## What Is It?
|
5
|
+
|
6
|
+
At [Tendigi](http://www.tendigi.com), we build applications for a variety of clients, often simultaneously, and those applications usually require server-side infrastructure. We also build [random things internally](https://blog.tendigi.com/people-who-are-really-serious-about-software-should-make-their-own-hardware-6983007e7427) from time to time, and these often depend on services that have to live *somewhere*.
|
7
|
+
|
8
|
+
For production deployments, we love [Heroku](https://heroku.com) (when it makes financial sense) as well as systems like [Deis](https://deis.com/) which can be deployed on AWS / DigitalOcean / etc.
|
9
|
+
|
10
|
+
We longed for a simple, on-site [PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service) solution that we could hack on as our needs evolved. [Dokku](https://github.com/dokku/dokku) is a great project, but we ran into some issues with it (problems updating to newer versions, discrepancies in application behavior compared to our other Deis deployments, a little annoying to work on because it's a collection of shell scripts, etc). As a result, we built EzPaaS: a mini Heroku clone, built in Ruby, powered by Deis images running on Docker.
|
11
|
+
|
12
|
+
## Prerequsites
|
13
|
+
|
14
|
+
#### Docker
|
15
|
+
|
16
|
+
EzPaaS requires [Docker](https://www.docker.com/) to be installed. We recommend following the Docker Community Edition (CE) installation instructions for your platform [here](https://docs.docker.com/engine/installation/).
|
17
|
+
|
18
|
+
#### Ruby
|
19
|
+
|
20
|
+
EzPaaS also requires [Ruby 2.2 or newer](https://www.ruby-lang.org/en/downloads/). It may work with older versions, but they have not been tested.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Install the gem. The easiest way is to install it for all users with `sudo`:
|
25
|
+
|
26
|
+
`$ sudo gem install ezpaas-server`
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
#### Starting The Server
|
31
|
+
|
32
|
+
The server runs on port 3000 by default, and can be started by running `ezpaasd` with no arguments at the command line.
|
33
|
+
|
34
|
+
All data is stored in the filesystem. Everything is stored in `~/.ezpaas/` by default, but you can override this by passing the `--data-dir` option to `ezpaasd`.
|
35
|
+
|
36
|
+
Every time you `ezpaasd` starts, it checks for updates to the two Docker images used for building and running your applications: [deis/slugbuilder]() and [deis/slugrunner]().
|
37
|
+
|
38
|
+

|
39
|
+
|
40
|
+
After ensuring it has the latest images, the server starts and you're ready to start deploying applications using the [CLI](https://github.com/TENDIGI/ezpaas-cli)!
|
41
|
+
|
42
|
+
## Project Status
|
43
|
+
|
44
|
+
- [x] Container compilation from repository archive
|
45
|
+
- [x] Application deployment
|
46
|
+
- [x] Container scaling
|
47
|
+
- [x] Control over HTTP API
|
48
|
+
- [x] Control from [CLI](https://github.com/TENDIGI/ezpaas-cli)
|
49
|
+
- [ ] Authentication / access control
|
50
|
+
- [ ] [Config Storage](https://12factor.net/config)
|
51
|
+
- [ ] Virtual hosts
|
52
|
+
- [ ] Complete documentation
|
53
|
+
- [ ] Tests! Lots of tests.
|
54
|
+
|
55
|
+
## Contributing
|
56
|
+
|
57
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/tendigi/ezpaas-server)
|
58
|
+
|
59
|
+
## License
|
60
|
+
|
61
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/bin/ezpaasd
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'thin'
|
4
|
+
require 'ezpaas/server/app'
|
5
|
+
require 'ezpaas/models/init'
|
6
|
+
require 'ezpaas/helpers/container_manager'
|
7
|
+
require 'ezpaas/helpers/config'
|
8
|
+
require 'thor'
|
9
|
+
|
10
|
+
class CLI < Thor
|
11
|
+
|
12
|
+
desc 'serve', 'runs the server'
|
13
|
+
option :address, :type => :string, :default => '0.0.0.0'
|
14
|
+
option :port, :type => :numeric, :default => 3000
|
15
|
+
option :data_dir, :type => :string, :default => File.join(Etc.getpwuid.dir, '.ezpaas')
|
16
|
+
def serve
|
17
|
+
|
18
|
+
EzPaaS::Helpers::Config.data_dir = options[:data_dir]
|
19
|
+
|
20
|
+
EzPaaS::Models.connect
|
21
|
+
|
22
|
+
EzPaaS::Helpers::ContainerManager.new.pull_images
|
23
|
+
|
24
|
+
server = Thin::Server.new(options[:address], options[:port]) do |server|
|
25
|
+
use Rack::CommonLogger
|
26
|
+
run EzPaaS::Server.app
|
27
|
+
end
|
28
|
+
|
29
|
+
server.maximum_connections = 1
|
30
|
+
|
31
|
+
server.start
|
32
|
+
|
33
|
+
end
|
34
|
+
default_task :serve
|
35
|
+
end
|
36
|
+
|
37
|
+
CLI.start(ARGV)
|
data/bin/setup
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'docker'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'rubygems/package'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'socket'
|
7
|
+
require 'awesome_print'
|
8
|
+
require 'yaml'
|
9
|
+
require 'json'
|
10
|
+
require 'ezpaas/helpers/file_extensions'
|
11
|
+
require 'ezpaas/helpers/config'
|
12
|
+
require 'tty'
|
13
|
+
|
14
|
+
module EzPaaS
|
15
|
+
module Helpers
|
16
|
+
class ContainerManager
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
ensure_paths
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_slug(src_tar_stream, emitter = nil)
|
23
|
+
|
24
|
+
temp_tar = Tempfile.new('ezpaas') # Temporary place to copy the slug tarball
|
25
|
+
|
26
|
+
emitter.emit :message, '-----> Creating slug compilation container' if emitter
|
27
|
+
container = Docker::Container.create({'Image' => 'deis/slugbuilder', 'OpenStdin' => true, 'StdinOnce' => true}, connection)
|
28
|
+
|
29
|
+
begin
|
30
|
+
# Start the container
|
31
|
+
emitter.emit :message, '-----> Starting slug compilation container' if emitter
|
32
|
+
container.start
|
33
|
+
|
34
|
+
# Redirect container output to HTTP stream
|
35
|
+
container.attach(stdin: src_tar_stream) do |_fd, chunk|
|
36
|
+
emitter.emit :message, chunk if emitter
|
37
|
+
end
|
38
|
+
|
39
|
+
# Wait for container to finish compiling the slug
|
40
|
+
container.wait
|
41
|
+
|
42
|
+
# Kill the container
|
43
|
+
emitter.emit :message, '-----> Stopping slug compilation container' if emitter
|
44
|
+
begin
|
45
|
+
container.stop
|
46
|
+
end
|
47
|
+
|
48
|
+
# Copy the tarred slug out of the container
|
49
|
+
emitter.emit :message, "-----> Temporarily copying slug from container to #{temp_tar.path}" if emitter
|
50
|
+
container.copy('/tmp/slug.tgz') { |chunk| temp_tar.write(chunk) }
|
51
|
+
|
52
|
+
temp_tar.rewind # rewind the temp file for immediate reading
|
53
|
+
|
54
|
+
# Decompress the tarred slug to our final destination
|
55
|
+
slug_name = SecureRandom.uuid
|
56
|
+
slug_destination = slug_path(slug_name)
|
57
|
+
emitter.emit :message, "-----> Untarring slug to #{slug_destination}" if emitter
|
58
|
+
tar_extract = Gem::Package::TarReader.new(temp_tar)
|
59
|
+
slug_entry = tar_extract.find { |e| e.full_name == 'slug.tgz' }
|
60
|
+
File.open(slug_destination, 'wb') do |file|
|
61
|
+
File.ez_cp(slug_entry, file)
|
62
|
+
end
|
63
|
+
temp_tar.close
|
64
|
+
rescue Exception => ex
|
65
|
+
raise
|
66
|
+
else
|
67
|
+
slug_name
|
68
|
+
ensure
|
69
|
+
emitter.emit :message, "-----> Removing container" if emitter
|
70
|
+
# Try to remove the container
|
71
|
+
begin
|
72
|
+
container.remove
|
73
|
+
rescue
|
74
|
+
raise
|
75
|
+
end
|
76
|
+
emitter.emit :message, "-----> Deleting temporary files" if emitter
|
77
|
+
temp_tar.close
|
78
|
+
temp_tar.unlink
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def deploy_app(name, slug, config, emitter = nil)
|
84
|
+
|
85
|
+
emitter.emit :message, "-----> Creating temporary slug container to snapshot" if emitter
|
86
|
+
|
87
|
+
# Load the slug into a temporary container to produce an image
|
88
|
+
slug_labels = {
|
89
|
+
'com.tendigi.appname' => name,
|
90
|
+
}
|
91
|
+
|
92
|
+
slug_file = File.open(slug_path(slug), 'rb')
|
93
|
+
slug_container = Docker::Container.create({'Image' => 'deis/slugrunner', 'OpenStdin' => true, 'StdinOnce' => true, 'Labels' => slug_labels}, connection)
|
94
|
+
slug_container.start
|
95
|
+
slug_container.attach(stdin: slug_file) do |_fd, chunk|
|
96
|
+
# puts chunk
|
97
|
+
end
|
98
|
+
|
99
|
+
slug_image_name = "ezpass/#{name}"
|
100
|
+
|
101
|
+
emitter.emit :message, "-----> Imaging container #{slug_container.id}" if emitter
|
102
|
+
slug_image = slug_container.commit(repo: slug_image_name, 'Labels' => slug_labels)
|
103
|
+
emitter.emit :message, "-----> Created slug image #{slug_image.id}" if emitter
|
104
|
+
|
105
|
+
slug_container.remove(force: true)
|
106
|
+
emitter.emit :message, "-----> Removing temporary slug container #{slug_container.id}" if emitter
|
107
|
+
|
108
|
+
emitter.emit :message, "-----> Starting deployment..." if emitter
|
109
|
+
|
110
|
+
# Start up instances using that image
|
111
|
+
config.each do |process, count|
|
112
|
+
|
113
|
+
# iterate over the desired number of instances
|
114
|
+
for i in 0 ... count
|
115
|
+
container_name = "#{name}-#{process}-#{i}"
|
116
|
+
labels = {
|
117
|
+
'com.tendigi.appname' => name,
|
118
|
+
'com.tendigi.instance' => container_name,
|
119
|
+
'com.tendigi.slug' => slug,
|
120
|
+
'com.tendigi.process' => process
|
121
|
+
}
|
122
|
+
container = slug_image.run("start #{process}", { name: container_name, 'Labels' => labels, 'PublishAllPorts' => true })
|
123
|
+
emitter.emit :message, "-----> Deployed container #{i + 1} for process type `#{process}`: #{container.id}" if emitter
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
def undeploy_app(name, emitter = nil)
|
130
|
+
|
131
|
+
emitter.emit :message, "-----> Un-deploying #{name}" if emitter
|
132
|
+
|
133
|
+
containers = Docker::Container.all(all: true, filters: { label: [ "com.tendigi.appname=#{name}" ] }.to_json)
|
134
|
+
|
135
|
+
containers.each do |c|
|
136
|
+
emitter.emit :message, "-----> Removing container #{c.id}" if emitter
|
137
|
+
c.remove(force: true)
|
138
|
+
end
|
139
|
+
|
140
|
+
emitter.emit :message, "-----> Deleting slug images for #{name}" if emitter
|
141
|
+
|
142
|
+
images = Docker::Image.all(all: true, filters: { label: [ "com.tendigi.appname=#{name}" ] }.to_json)
|
143
|
+
|
144
|
+
images.each do |i|
|
145
|
+
emitter.emit :message, "-----> Removing image #{i.id}" if emitter
|
146
|
+
i.remove
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
def http_destinations(name)
|
152
|
+
containers = Docker::Container.all(filters: { label: [ "com.tendigi.appname=#{name}", "com.tendigi.process=web"] }.to_json)
|
153
|
+
|
154
|
+
ports = {}
|
155
|
+
|
156
|
+
for container in containers
|
157
|
+
for port_info in (container.info['Ports'] || [])
|
158
|
+
if port_info['PrivatePort'] == 5000
|
159
|
+
ports[container.id] = port_info['PublicPort']
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
ports
|
165
|
+
end
|
166
|
+
|
167
|
+
def read_procfile(slug_name)
|
168
|
+
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open(slug_path(slug_name)))
|
169
|
+
tar_extract.rewind # The extract has to be rewinded after every iteration
|
170
|
+
tar_extract.each do |entry|
|
171
|
+
if entry.full_name == './Procfile' && entry.file?
|
172
|
+
return YAML.load(entry.read)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
ensure
|
176
|
+
tar_extract.close
|
177
|
+
end
|
178
|
+
|
179
|
+
def pull_images
|
180
|
+
|
181
|
+
images = [
|
182
|
+
'deis/slugbuilder',
|
183
|
+
'deis/slugrunner'
|
184
|
+
]
|
185
|
+
|
186
|
+
puts
|
187
|
+
|
188
|
+
images.each do |name|
|
189
|
+
|
190
|
+
pastel = Pastel.new
|
191
|
+
puts 'Downloading latest image: ' + pastel.blue(name)
|
192
|
+
|
193
|
+
Docker::Image.create({'fromImage' => name, 'tag' => 'latest'}, connection) do |chunk|
|
194
|
+
fragments = chunk.split "\r\n"
|
195
|
+
fragments.each do |fragment|
|
196
|
+
data = JSON.parse(fragment)
|
197
|
+
if id = data['id']
|
198
|
+
# puts "#{id}: #{data['status']}" #off for now
|
199
|
+
else
|
200
|
+
puts pastel.green(data['status'].rstrip)
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
puts
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
attr_reader :connection
|
214
|
+
attr_reader :slug_dir
|
215
|
+
|
216
|
+
def connection
|
217
|
+
@connection ||= Docker::Connection.new(Docker.url, {:chunk_size => 1})
|
218
|
+
@connection
|
219
|
+
end
|
220
|
+
|
221
|
+
def ensure_paths
|
222
|
+
@slug_dir ||= File.join(EzPaaS::Helpers::Config.data_dir, 'slugs')
|
223
|
+
FileUtils.mkdir_p @slug_dir
|
224
|
+
end
|
225
|
+
|
226
|
+
def slug_path(slug_name)
|
227
|
+
File.join(slug_dir, slug_name + '.tgz')
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'ezpaas/models/model'
|
2
|
+
|
3
|
+
module EzPaaS
|
4
|
+
module Models
|
5
|
+
class App < Model
|
6
|
+
|
7
|
+
attr_accessor :name
|
8
|
+
attr_accessor :slug
|
9
|
+
attr_accessor :scale
|
10
|
+
|
11
|
+
def to_hash
|
12
|
+
super.merge(name: name, slug: slug, scale: scale)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_pstore'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'ezpaas/helpers/config'
|
4
|
+
|
5
|
+
module EzPaaS
|
6
|
+
module Models
|
7
|
+
def self.connect
|
8
|
+
db_folder = File.join(EzPaaS::Helpers::Config.data_dir, 'db')
|
9
|
+
FileUtils.mkdir_p db_folder
|
10
|
+
ActivePStore::Base.establish_connection(database: File.join(db_folder, 'database.db0'))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'ezpaas/models/init'
|
3
|
+
require 'ezpaas/server/routes/apps'
|
4
|
+
require 'ezpaas/server/routes/deployments'
|
5
|
+
require 'ezpaas/server/routes/proxy'
|
6
|
+
|
7
|
+
module EzPaaS
|
8
|
+
module Server
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :app
|
12
|
+
end
|
13
|
+
|
14
|
+
self.app = Rack::Builder.new {
|
15
|
+
map '/apps' do
|
16
|
+
run EzPaaS::Server::Routes::Apps
|
17
|
+
end
|
18
|
+
|
19
|
+
map '/deployments' do
|
20
|
+
run EzPaaS::Server::Routes::Deployments
|
21
|
+
end
|
22
|
+
|
23
|
+
map '/proxy' do
|
24
|
+
run EzPaaS::Server::Routes::Proxy
|
25
|
+
end
|
26
|
+
}
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'to_slug'
|
3
|
+
require 'awesome_print'
|
4
|
+
require 'ezpaas/server/routes/api'
|
5
|
+
require 'ezpaas/helpers/container_manager'
|
6
|
+
require 'ezpaas/models/app'
|
7
|
+
|
8
|
+
module EzPaaS
|
9
|
+
module Server
|
10
|
+
module Routes
|
11
|
+
class Apps < API
|
12
|
+
|
13
|
+
configure_api
|
14
|
+
|
15
|
+
desc 'Returns all apps'
|
16
|
+
get do
|
17
|
+
{ apps: Models::App.all.to_a.map { |e| e.to_hash } }
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'Create an app'
|
21
|
+
params do
|
22
|
+
requires :name, type: String, desc: "The new app's name"
|
23
|
+
end
|
24
|
+
post do
|
25
|
+
name = params[:name]
|
26
|
+
slug = name.to_slug
|
27
|
+
error!("App named #{slug} already exists", 409) unless Models::App.where(name: slug).empty?
|
28
|
+
a = Models::App.new
|
29
|
+
a.name = slug
|
30
|
+
a.save
|
31
|
+
{
|
32
|
+
app: a.to_hash
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'Delete an app'
|
37
|
+
params do
|
38
|
+
requires :name, type: String, desc: "The app's name"
|
39
|
+
end
|
40
|
+
delete do
|
41
|
+
name = params[:name]
|
42
|
+
slug = name.to_slug
|
43
|
+
error!("Could not find app named #{slug}", 404) unless app = Models::App.where(name: slug).first
|
44
|
+
|
45
|
+
scale = app.scale || {}
|
46
|
+
container_count = scale.values.reduce(0) { |x, y| x + y }
|
47
|
+
|
48
|
+
error!("App #{slug} currently has #{container_count} containers running. Please scale it to 0 before trying to delete it.", 409) unless container_count == 0
|
49
|
+
|
50
|
+
# ensure images are cleared out
|
51
|
+
manager = Helpers::ContainerManager.new
|
52
|
+
manager.undeploy_app(slug)
|
53
|
+
|
54
|
+
app.destroy
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'sinatra/sse'
|
3
|
+
require 'awesome_print'
|
4
|
+
require 'to_slug'
|
5
|
+
require 'json'
|
6
|
+
require 'emittr'
|
7
|
+
|
8
|
+
require 'ezpaas/helpers/container_manager'
|
9
|
+
require 'ezpaas/models/app'
|
10
|
+
|
11
|
+
module EzPaaS
|
12
|
+
module Server
|
13
|
+
module Routes
|
14
|
+
class Deployments < Sinatra::Application
|
15
|
+
|
16
|
+
include Sinatra::SSE
|
17
|
+
|
18
|
+
post '/' do
|
19
|
+
ensure_app
|
20
|
+
manager = Helpers::ContainerManager.new
|
21
|
+
body = request.body # String IO object containing the source tarball
|
22
|
+
|
23
|
+
open_message_stream do |emitter|
|
24
|
+
slug = manager.create_slug(body, emitter)
|
25
|
+
|
26
|
+
emitter.emit :message, '-----> Saving new slug name to database'
|
27
|
+
@app.slug = slug
|
28
|
+
|
29
|
+
procfile = manager.read_procfile(@app.slug)
|
30
|
+
@app.scale ||= {}
|
31
|
+
procfile.keys.each do |key|
|
32
|
+
@app.scale[key] ||= 1
|
33
|
+
end
|
34
|
+
|
35
|
+
redeploy(emitter)
|
36
|
+
|
37
|
+
@app.save
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
patch '/' do
|
42
|
+
ensure_app
|
43
|
+
|
44
|
+
scale = request.params['scale']
|
45
|
+
if scale.nil? || !scale.is_a?(Hash)
|
46
|
+
content_type :json
|
47
|
+
halt [400, { error: 'scale parameter missing or malformed' }.to_json]
|
48
|
+
end
|
49
|
+
|
50
|
+
@app.scale.keys.each do |key|
|
51
|
+
if (newcount = scale[key]) && scale[key] == scale[key].to_i.to_s
|
52
|
+
@app.scale[key] = newcount.to_i
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
open_message_stream do |emitter|
|
57
|
+
redeploy(emitter)
|
58
|
+
@app.save
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
delete '/' do
|
63
|
+
ensure_app
|
64
|
+
|
65
|
+
@app.scale.keys.each do |key|
|
66
|
+
@app.scale[key] = 0
|
67
|
+
end
|
68
|
+
|
69
|
+
manager = Helpers::ContainerManager.new
|
70
|
+
|
71
|
+
open_message_stream do |emitter|
|
72
|
+
scale_strings = @app.scale.map { |k, v| "#{k}: #{v}" }
|
73
|
+
emitter.emit :message, "-----> App scaled to: #{scale_strings.join(', ')}"
|
74
|
+
manager.undeploy_app(@app.name, emitter)
|
75
|
+
@app.save
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def ensure_app
|
82
|
+
app_name = request.params['app']
|
83
|
+
|
84
|
+
if app_name.nil?
|
85
|
+
app_not_found
|
86
|
+
end
|
87
|
+
|
88
|
+
app_slug = app_name.to_slug
|
89
|
+
|
90
|
+
app = Models::App.where(name: app_slug).first
|
91
|
+
|
92
|
+
if app.nil?
|
93
|
+
app_not_found
|
94
|
+
end
|
95
|
+
|
96
|
+
@app = app
|
97
|
+
end
|
98
|
+
|
99
|
+
def open_message_stream
|
100
|
+
sse_stream do |out|
|
101
|
+
begin
|
102
|
+
emitter = Emittr::Emitter.new
|
103
|
+
|
104
|
+
emitter.on :message do |message|
|
105
|
+
out.push :event => 'message', :data => message
|
106
|
+
end
|
107
|
+
|
108
|
+
yield emitter
|
109
|
+
rescue Exception => ex
|
110
|
+
raise
|
111
|
+
ensure
|
112
|
+
out.close
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def redeploy(emitter)
|
118
|
+
manager = Helpers::ContainerManager.new
|
119
|
+
|
120
|
+
scale_strings = @app.scale.map { |k, v| "#{k}: #{v}" }
|
121
|
+
emitter.emit :message, "-----> App scaled to: #{scale_strings.join(', ')}"
|
122
|
+
|
123
|
+
emitter.emit :message, '-----> Deploying application'
|
124
|
+
manager.undeploy_app(@app.name, emitter)
|
125
|
+
manager.deploy_app(@app.name, @app.slug, @app.scale, emitter)
|
126
|
+
|
127
|
+
emitter.emit :message, '-----> Application deployed'
|
128
|
+
end
|
129
|
+
|
130
|
+
def app_not_found
|
131
|
+
content_type :json
|
132
|
+
halt [404, { error: 'app not found' }.to_json]
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'pathname'
|
3
|
+
require 'uri'
|
4
|
+
require 'awesome_print'
|
5
|
+
require 'excon'
|
6
|
+
|
7
|
+
require 'ezpaas/helpers/container_manager'
|
8
|
+
|
9
|
+
module EzPaaS
|
10
|
+
module Server
|
11
|
+
module Routes
|
12
|
+
class Proxy < Sinatra::Application
|
13
|
+
|
14
|
+
%i(get post delete patch put head options).each do |method|
|
15
|
+
send method, '/:app/*' do
|
16
|
+
full_path = request.path_info
|
17
|
+
|
18
|
+
components = Pathname.new(full_path).each_filename.to_a
|
19
|
+
app = components.shift
|
20
|
+
|
21
|
+
path = Pathname.new('/').join(components.join('/')).to_s
|
22
|
+
|
23
|
+
unless request.query_string.empty?
|
24
|
+
path += '?' + request.query_string
|
25
|
+
end
|
26
|
+
|
27
|
+
(container, port) = get_container(app)
|
28
|
+
|
29
|
+
unless container and port
|
30
|
+
return [404, 'container not found']
|
31
|
+
end
|
32
|
+
|
33
|
+
destination = "http://127.0.0.1:#{port}"
|
34
|
+
|
35
|
+
connection = Excon.new(destination)
|
36
|
+
|
37
|
+
response = connection.request(method: method, path: path)
|
38
|
+
|
39
|
+
headers = response.headers
|
40
|
+
headers['X-Container'] = container
|
41
|
+
|
42
|
+
[response.status, headers, response.body]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def headers_hash
|
49
|
+
# https://stackoverflow.com/questions/6317705/rackrequest-how-do-i-get-all-headers
|
50
|
+
Hash[*env.select {|k,v| k.start_with? 'HTTP_'}
|
51
|
+
.collect {|k,v| [k.sub(/^HTTP_/, ''), v]}
|
52
|
+
.collect {|k,v| [k.split('_').collect(&:capitalize).join('-'), v]}
|
53
|
+
.sort
|
54
|
+
.flatten]
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_container(app)
|
58
|
+
manager = Helpers::ContainerManager.new
|
59
|
+
options = manager.http_destinations(app)
|
60
|
+
return nil if options.empty?
|
61
|
+
key = options.keys.sample
|
62
|
+
[key, options[key]]
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
metadata
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ezpaas-server
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Lee
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sinatra-sse
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: grape
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.0.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: docker-api
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.33.6
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.33.6
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: active_pstore
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.5.2
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.5.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: awesome_print
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.7.0
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.7.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rack
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.0.0
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.0.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: thin
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.7.2
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.7.2
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: excon
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.58.0
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.58.0
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: to_slug
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 1.0.8
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 1.0.8
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: emittr
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 0.1.0
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 0.1.0
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: thor
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: 0.19.4
|
174
|
+
type: :runtime
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: 0.19.4
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: tty
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: 0.7.0
|
188
|
+
type: :runtime
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: 0.7.0
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: bundler
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '1.15'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '1.15'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: rake
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - "~>"
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '10.0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - "~>"
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '10.0'
|
223
|
+
description:
|
224
|
+
email:
|
225
|
+
- nick@tendigi.com
|
226
|
+
executables:
|
227
|
+
- ezpaasd
|
228
|
+
extensions: []
|
229
|
+
extra_rdoc_files: []
|
230
|
+
files:
|
231
|
+
- LICENSE.txt
|
232
|
+
- README.md
|
233
|
+
- bin/ezpaasd
|
234
|
+
- bin/setup
|
235
|
+
- lib/ezpaas/helpers/config.rb
|
236
|
+
- lib/ezpaas/helpers/container_manager.rb
|
237
|
+
- lib/ezpaas/helpers/file_extensions.rb
|
238
|
+
- lib/ezpaas/models/app.rb
|
239
|
+
- lib/ezpaas/models/init.rb
|
240
|
+
- lib/ezpaas/models/model.rb
|
241
|
+
- lib/ezpaas/server/app.rb
|
242
|
+
- lib/ezpaas/server/routes/api.rb
|
243
|
+
- lib/ezpaas/server/routes/apps.rb
|
244
|
+
- lib/ezpaas/server/routes/deployments.rb
|
245
|
+
- lib/ezpaas/server/routes/proxy.rb
|
246
|
+
- lib/ezpaas/server/version.rb
|
247
|
+
homepage: https://github.com/TENDIGI/ezpaas-server
|
248
|
+
licenses:
|
249
|
+
- MIT
|
250
|
+
metadata: {}
|
251
|
+
post_install_message:
|
252
|
+
rdoc_options: []
|
253
|
+
require_paths:
|
254
|
+
- lib
|
255
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
256
|
+
requirements:
|
257
|
+
- - ">="
|
258
|
+
- !ruby/object:Gem::Version
|
259
|
+
version: '0'
|
260
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
261
|
+
requirements:
|
262
|
+
- - ">="
|
263
|
+
- !ruby/object:Gem::Version
|
264
|
+
version: '0'
|
265
|
+
requirements: []
|
266
|
+
rubyforge_project:
|
267
|
+
rubygems_version: 2.4.8
|
268
|
+
signing_key:
|
269
|
+
specification_version: 4
|
270
|
+
summary: A miniature Heroku clone for easy in-house deployments, powered by Docker
|
271
|
+
test_files: []
|