Commit ff012e3a authored by Terence Lee's avatar Terence Lee

rearchitect tests to support a real proxy API and routing frontend for SSL termination

parent a60c0ad2
require "fileutils"
require_relative "spec_helper"
require_relative "support/app_runner"
require_relative "support/router_runner"
require_relative "support/buildpack_builder"
require_relative "support/router_builder"
require_relative "support/proxy_builder"
require_relative "support/proxy_runner"
require_relative "support/path_helper"
RSpec.describe "Simple" do
before(:all) do
@debug = true
BuildpackBuilder.new(@debug, ENV['CIRCLECI'])
RouterBuilder.new(@debug, ENV['CIRCLECI'])
ProxyBuilder.new(@debug, ENV["CIRCLE_CI"])
end
after do
app.destroy
end
let(:app) { AppRunner.new(name, env, @debug) }
let(:proxy) { nil }
let(:app) { AppRunner.new(name, proxy, env, @debug) }
let(:name) { "hello_world" }
let(:env) { Hash.new }
......@@ -89,7 +96,7 @@ RSpec.describe "Simple" do
it "should redirect and respect the http code & remove the port" do
response = app.get("/old/gone")
expect(response.code).to eq("302")
expect(response["location"]).to eq("http://#{AppRunner::HOST_IP}/")
expect(response["location"]).to eq("http://#{RouterRunner::HOST_IP}/")
end
context "interpolation" do
......@@ -102,7 +109,7 @@ RSpec.describe "Simple" do
it "should redirect using interpolated urls" do
response = app.get("/old/interpolation")
expect(response.code).to eq("302")
expect(response["location"]).to eq("http://#{AppRunner::HOST_IP}/interpolation.html")
expect(response["location"]).to eq("http://#{RouterRunner::HOST_IP}/interpolation.html")
end
end
end
......@@ -133,6 +140,7 @@ RSpec.describe "Simple" do
include PathHelper
let(:name) { "proxies" }
let(:proxy) { true }
let(:static_json_path) { fixtures_path("proxies/static.json") }
let(:setup_static_json) do
Proc.new do |path|
......@@ -141,7 +149,7 @@ RSpec.describe "Simple" do
{
"proxies": {
"/api/": {
"origin": "http://#{AppRunner::HOST_IP}:#{AppRunner::HOST_PORT}#{path}"
"origin": "http://#{@proxy_ip_address}#{path}"
}
}
}
......@@ -151,6 +159,10 @@ STATIC_JSON
end
end
before do
@proxy_ip_address = app.proxy.ip_address
end
after do
FileUtils.rm(static_json_path)
end
......@@ -186,10 +198,10 @@ STATIC_JSON
{
"proxies": {
"/api/": {
"origin": "http://#{AppRunner::HOST_IP}:#{AppRunner::HOST_PORT}/foo"
"origin": "http://#{@proxy_ip_address}/foo"
},
"/proxy/": {
"origin": "http://#{AppRunner::HOST_IP}:#{AppRunner::HOST_PORT}/foo"
"origin": "http://#{@proxy_ip_address}/foo"
}
},
"routes": {
......@@ -214,6 +226,14 @@ STATIC_JSON
end
context "env var substitution" do
let(:proxy) do
<<CONFIG_RU
get "/foo/bar/" do
"api"
end
CONFIG_RU
end
before do
File.open(static_json_path, "w") do |file|
file.puts <<STATIC_JSON
......@@ -230,7 +250,7 @@ STATIC_JSON
let(:env) do
{
"PROXY_HOST" => "#{AppRunner::HOST_IP}:#{AppRunner::HOST_PORT}"
"PROXY_HOST" => "${PROXY_IP_ADDRESS}"
}
end
......@@ -347,6 +367,17 @@ STATIC_JSON
let(:name) { "proxies" }
let(:static_json_path) { fixtures_path("proxies/static.json") }
let(:proxy) do
<<PROXY
get "/foo/bar/" do
"api"
end
get "/foo/baz/" do
"baz"
end
PROXY
end
let(:setup_static_json) do
Proc.new do |path|
File.open(static_json_path, "w") do |file|
......@@ -354,7 +385,7 @@ STATIC_JSON
{
"proxies": {
"/api/": {
"origin": "http://#{AppRunner::HOST_IP}:#{AppRunner::HOST_PORT}#{path}"
"origin": "http://#{@proxy_ip_address}#{path}"
}
},
"headers": {
......@@ -370,6 +401,7 @@ STATIC_JSON
end
before do
@proxy_ip_address = app.proxy.ip_address
setup_static_json.call("/foo/")
end
......
......@@ -6,38 +6,56 @@ require "docker"
require "concurrent/atomic/count_down_latch"
require_relative "path_helper"
require_relative "buildpack_builder"
require_relative "router_runner"
require_relative "../../scripts/config/lib/nginx_config_util"
class AppRunner
include PathHelper
def self.boot2docker_ip
%x(boot2docker ip).match(/([0-9]{1,3}\.){3}[0-9]{1,3}/)[0]
rescue Errno::ENOENT
end
HOST_PORT = "3000"
HOST_IP = boot2docker_ip || "127.0.0.1"
CONTAINER_PORT = "3000"
attr_reader :proxy
def initialize(fixture, env = {}, debug = false)
def initialize(fixture, proxy = nil, env = {}, debug = false)
@run = false
@debug = debug
env.merge!("STATIC_DEBUG" => true) if @debug
@tmpdir = nil
@proxy = nil
env.merge!("STATIC_DEBUG" => "true") if @debug
@container = Docker::Container.create(
app_options = {
"name" => "app",
"Image" => BuildpackBuilder::TAG,
# Env format is [KEY1=VAL1 KEY2=VAL2]
"Env" => env.to_a.map {|i| i.join("=") },
"HostConfig" => {
"Binds" => ["#{fixtures_path(fixture)}:/src"],
"PortBindings" => {
"#{CONTAINER_PORT}/tcp" => [{
"HostIp" => HOST_IP,
"HostPort" => HOST_PORT,
}]
"Binds" => ["#{fixtures_path(fixture)}:/src"]
}
}
)
if proxy
app_options["Links"] = ["proxy:proxy"]
if proxy.is_a?(String)
@tmpdir = Dir.mktmpdir
File.open("#{@tmpdir}/config.ru", "w") do |file|
file.puts %q{require "sinatra"}
file.puts proxy
file.puts "run Sinatra::Application"
end
end
@proxy = ProxyRunner.new(@tmpdir)
@proxy.start
# need to interpolate the PROXY_IP_ADDRESS since env is a parameter to this constructor and
# the proxy app needs to be started first to get the ip address docker provides.
# it's a bootstrapping problem to do env var substitution
env.select {|_, value| value.include?("${PROXY_IP_ADDRESS}") }.each do |key, value|
env[key] = NginxConfigUtil.interpolate(value, {"PROXY_IP_ADDRESS" => @proxy.ip_address})
app_options["Env"] = env.to_a.map {|i| i.join("=") }
end
end
@app = Docker::Container.create(app_options)
@router = RouterRunner.new
end
def run(capture_io = false)
......@@ -47,16 +65,17 @@ class AppRunner
io_stream = StringIO.new
run_thread = Thread.new {
latch.wait(0.5)
yield(@container)
yield
}
container_thread = Thread.new {
@container.tap(&:start).attach do |stream, chunk|
@app.tap(&:start).attach do |stream, chunk|
io_message = "#{stream}: #{chunk}"
puts io_message if @debug
io_stream << io_message if capture_io
latch.count_down if chunk.include?("Starting nginx...")
end
}
@router.start
retn = run_thread.value
......@@ -66,7 +85,8 @@ class AppRunner
retn
end
ensure
@container.stop
@app.stop
@router.stop
container_thread.join
io_stream.close_write
@run = false
......@@ -81,15 +101,22 @@ class AppRunner
end
def destroy
@container.delete(force: true) unless @debug
if @proxy
@proxy.stop
@proxy.destroy
end
@router.destroy
@app.delete(force: true)
ensure
FileUtils.rm_rf(@tmpdir) if @tmpdir
end
private
def get_retry(path, max_retries)
network_retry(max_retries) do
uri = URI(path)
uri.host = HOST_IP if uri.host.nil?
uri.port = HOST_PORT if (uri.host == HOST_IP && uri.port != HOST_PORT) || uri.port.nil?
uri.host = RouterRunner::HOST_IP if uri.host.nil?
uri.port = RouterRunner::HOST_PORT if (uri.host == RouterRunner::HOST_IP && uri.port != RouterRunner::HOST_PORT) || uri.port.nil?
uri.scheme = "http" if uri.scheme.nil?
Net::HTTP.get_response(URI(uri.to_s))
......
require "docker"
require_relative "path_helper"
require_relative "docker_builder"
class BuildpackBuilder
include PathHelper
include DockerBuilder
TAG = "hone/static:cedar-14"
def initialize(debug = false, intermediates = false)
@debug = debug
@intermediates = intermediates
@image = build_image
end
def build_image
print_output =
if @debug
-> (chunk) {
json = JSON.parse(chunk)
puts json["stream"]
}
else
-> (chunk) { nil }
end
Docker::Image.build_from_dir(buildpack_path.to_s, 't' => TAG, 'rm' => !@intermediates, 'dockerfile' => "spec/support/docker/Dockerfile", &print_output)
@image = build(
context: buildpack_path.to_s,
dockerfile: docker_path("app/Dockerfile").relative_path_from(buildpack_path),
tag: TAG,
intermediates: @intermediates,
debug: @debug
)
end
end
require "fiber"
require "docker"
class ContainerRunner
attr_reader :ip_address
def initialize(options)
@container = Docker::Container.create(options)
@ip_address = nil
@thread = nil
end
def start
@thread = Fiber.new {
@container.start
Fiber.yield @container.json["NetworkSettings"]["IPAddress"]
}
@ip_address = @thread.resume
end
def stop
@container.stop
@thread.resume if @thread.alive?
end
def destroy
@container.delete(force: true)
end
end
......@@ -14,6 +14,6 @@ EXPOSE 3000
WORKDIR /app
COPY ./spec/support/docker/init.sh /usr/bin/init.sh
COPY ./spec/support/docker/app/init.sh /usr/bin/init.sh
ENTRYPOINT ["/usr/bin/init.sh"]
CMD "/app/bin/boot"
FROM ruby:2.3.1-alpine
RUN mkdir -p /app
WORKDIR /app
ADD Gemfile* /app/
RUN bundle install --path /app/vendor/bundle
ADD config.ru /app/config/
EXPOSE 80
CMD bundle exec rackup /app/config/config.ru --host 0.0.0.0 -p 80
source "https://rubygems.org"
gem "sinatra"
GEM
remote: https://rubygems.org/
specs:
rack (1.6.4)
rack-protection (1.5.3)
rack
sinatra (1.4.7)
rack (~> 1.5)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
tilt (2.0.5)
PLATFORMS
ruby
DEPENDENCIES
sinatra
BUNDLED WITH
1.11.2
require "sinatra"
get "/*" do
"api"
end
run Sinatra::Application
FROM matsumotory/ngx-mruby:latest
RUN echo $'\nUS\nTexas\nAustin\nHeroku\n\nexample.com\n\n' \
| openssl req -x509 -nodes -days 365 -newkey rsa:1024 \
-keyout /etc/ssl/private/myssl.key \
-out /etc/ssl/certs/myssl.crt
RUN mkdir -p /root/conf/ && \
touch /root/conf/extend.conf
user daemon;
daemon off;
master_process off;
worker_processes 1;
error_log stderr;
events {
worker_connections 1024;
}
http {
upstream backend {
server app:3000;
}
server {
listen 80;
listen 443 ssl;
ssl_certificate /etc/ssl/certs/myssl.crt;
ssl_certificate_key /etc/ssl/private/myssl.key;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
require "docker"
module DockerBuilder
def build(context:, tag:, intermediates:, debug:, dockerfile: nil)
print_output =
if debug
-> (chunk) {
json = JSON.parse(chunk)
puts json["stream"]
}
else
-> (chunk) { nil }
end
options = {
't' => tag,
'rm' => !intermediates,
}
options["dockerfile"] = dockerfile
Docker::Image.build_from_dir(context, options, &print_output)
end
end
......@@ -7,6 +7,10 @@ module PathHelper
__build_path("../../", *path)
end
def docker_path(*path)
__build_path("/docker", *path)
end
private
def __build_path(name, *path)
Pathname.new(File.join(File.dirname(__FILE__), name, *path))
......
require_relative "docker_builder"
require_relative "path_helper"
class ProxyBuilder
include DockerBuilder
include PathHelper
TAG = "hone/static-proxy:latest"
def initialize(debug = false, intermediates = false)
@build = build(
context: docker_path("proxy").to_s,
debug: debug,
tag: TAG,
intermediates: intermediates
)
end
end
require_relative "proxy_builder"
require_relative "container_runner"
class ProxyRunner < ContainerRunner
def initialize(config_ru = nil)
options = {
"name" => "proxy",
"Image" => ProxyBuilder::TAG
}
options["HostConfig"] = { "Binds" => ["#{config_ru}:/app/config/"] } if config_ru
super(options)
end
end
require_relative "path_helper"
require_relative "docker_builder"
class RouterBuilder
include PathHelper
include DockerBuilder
TAG = "hone/static-router:latest"
def initialize(debug = false, intermediates = false)
@image = build(
context: docker_path("/router").to_s,
tag: TAG,
intermediates: intermediates,
debug: debug
)
end
end
require_relative "router_builder"
require_relative "container_runner"
class RouterRunner < ContainerRunner
def self.boot2docker_ip
%x(boot2docker ip).match(/([0-9]{1,3}\.){3}[0-9]{1,3}/)[0]
rescue Errno::ENOENT
end
CONTAINER_PORT = "80"
HOST_PORT = "80"
HOST_IP = boot2docker_ip || "127.0.0.1"
def initialize
super({
"name" => "router",
"Image" => RouterBuilder::TAG,
"HostConfig" => {
"Links" => ["app:app"],
"PortBindings" => {
"#{CONTAINER_PORT}/tcp" => [{
"HostIp" => HOST_IP,
"HostPort" => HOST_PORT,
}]
}
}
})
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment