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.
@@ -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: []