ezpaas-server 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -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
+ ![server starting](./assets/server-start.gif)
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).
@@ -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)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,9 @@
1
+ module EzPaaS
2
+ module Helpers
3
+ class Config
4
+ class << self;
5
+ attr_accessor :data_dir
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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,7 @@
1
+ class File
2
+ def self.ez_cp(src, dst)
3
+ while buffer = src.read(4096)
4
+ dst << buffer
5
+ end
6
+ end
7
+ 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,15 @@
1
+ require 'active_pstore'
2
+
3
+ module EzPaaS
4
+ module Models
5
+ class Model < ActivePStore::Base
6
+ def to_hash
7
+ {}
8
+ end
9
+
10
+ def to_json
11
+ to_hash.to_json
12
+ end
13
+ end
14
+ end
15
+ 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,14 @@
1
+ require 'grape'
2
+
3
+ module EzPaaS
4
+ module Server
5
+ module Routes
6
+ class API < Grape::API
7
+ def self.configure_api
8
+ format :json
9
+ default_format :json
10
+ end
11
+ end
12
+ end
13
+ end
14
+ 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
@@ -0,0 +1,6 @@
1
+ module EzPaaS
2
+ module Server
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
6
+
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: []