diff --git a/environments/ruby/Dockerfile b/environments/ruby/Dockerfile index 778816a1..28bf0dce 100644 --- a/environments/ruby/Dockerfile +++ b/environments/ruby/Dockerfile @@ -1,16 +1,11 @@ -FROM ruby:2.4.1 +FROM ruby:2.6.1-alpine3.9 -RUN apt-get update -qq && apt-get install -y build-essential - -RUN mkdir /app -WORKDIR /app -ADD Gemfile /app/Gemfile -ADD Gemfile.lock /app/Gemfile.lock -RUN bundle install +RUN apk update +RUN apk add --no-cache build-base COPY . /app - -EXPOSE 8888 +WORKDIR /app +RUN bundle install ENTRYPOINT ["ruby"] CMD ["server.rb"] diff --git a/environments/ruby/Gemfile b/environments/ruby/Gemfile index 4b94f42d..068569a8 100644 --- a/environments/ruby/Gemfile +++ b/environments/ruby/Gemfile @@ -2,3 +2,4 @@ source "https://rubygems.org" gem "rack" +gem "thin" diff --git a/environments/ruby/README.md b/environments/ruby/README.md index 0e4ea5e9..e5020569 100644 --- a/environments/ruby/README.md +++ b/environments/ruby/README.md @@ -2,7 +2,7 @@ This is the Ruby environment for Fission. -It's a Docker image containing a Ruby 2.4.1 runtime. The image uses +It's a Docker image containing a Ruby 2.6.1 runtime. The image uses Rack with WEBrick to host the internal web server. The environment works via convention where you create a Ruby method diff --git a/environments/ruby/builder/Dockerfile b/environments/ruby/builder/Dockerfile new file mode 100644 index 00000000..9f73afa7 --- /dev/null +++ b/environments/ruby/builder/Dockerfile @@ -0,0 +1,12 @@ +ARG BUILDER_IMAGE=fission/builder:latest +FROM ${BUILDER_IMAGE} + +FROM ruby:2.6.1-alpine3.9 +COPY --from=0 /builder /builder + +RUN apk update +RUN apk add --no-cache ruby ruby-dev ruby-bundler build-base + +ADD defaultBuildCmd /usr/local/bin/build + +EXPOSE 8001 diff --git a/environments/ruby/builder/defaultBuildCmd b/environments/ruby/builder/defaultBuildCmd new file mode 100755 index 00000000..e6a2637a --- /dev/null +++ b/environments/ruby/builder/defaultBuildCmd @@ -0,0 +1,9 @@ +#!/bin/sh +set -euxo pipefail + +if [ -f ${SRC_PKG}/Gemfile.lock ] +then + cd $SRC_PKG + bundle install --deployment +fi +cp -r ${SRC_PKG} ${DEPLOY_PKG} \ No newline at end of file diff --git a/environments/ruby/fission/handler.rb b/environments/ruby/fission/handler.rb index c11d36ad..a594f5e2 100644 --- a/environments/ruby/fission/handler.rb +++ b/environments/ruby/fission/handler.rb @@ -7,10 +7,10 @@ module Fission def self.call(env) context = Context.new(env) - response = if method(:handler).arity > 0 - handler(context) + response = if $handler.arity > 0 + $handler.call(context) else - handler + $handler.call end response.is_a?(Array) ? response : Rack::Response.new([response]).finish diff --git a/environments/ruby/fission/specializer.rb b/environments/ruby/fission/specializer.rb index a6b1dce8..1b85e532 100644 --- a/environments/ruby/fission/specializer.rb +++ b/environments/ruby/fission/specializer.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true + require 'benchmark' +require 'json' module Fission - CODE_PATH = '/userfunc/user' - module Specializer + + CODE_PATH = '/userfunc/user' + def self.call(env) request = Request.new(env) @@ -12,11 +15,70 @@ module Fission time = Benchmark.measure { load CODE_PATH } request.logger.info("User code loaded in #{(time.real * 1000).round(3)}ms") - Rack::Response.new([], 201).finish + # set to "handler" for v1 specialization + $handler = method(:handler) + Rack::Response.new([], 201).finish rescue => e request.logger.error(%(Specialization failed - #{e}\n#{e.backtrace.join("\n")})) Rack::Response.new(['500 Internal Server Error'], 500, {}).finish end end + + module V2 + module Specializer + + def self.load_vendor(path) + gems = Dir[File.join(path, 'vendor/bundle/ruby/*/gems/*/lib')] + exts = Dir[File.join(path, 'vendor/bundle/ruby/*/extensions/x86_64-linux/*/*')] + + $LOAD_PATH.unshift(*gems) + $LOAD_PATH.unshift(*exts) + end + + def self.call(env) + request = Request.new(env) + + body = JSON.parse(request.body.read) + path = body['filepath'] + func = body['functionName'] + + time = Benchmark.measure do + if File.file?(path) + # If pointing to just a single file, all we need to do is load it + request.logger.debug("Loading file #{path}") + + load path + elsif File.directory?(path) + # First we want to load all the vendor files + load_vendor(path) + + # We then want to get all the .rb files in this src + rb_files = File.join(path, "**/*.rb") + + # But we don't want to include the vendor files that we loaded above + vendor_dir = File.join(path, 'vendor') + src_files = Dir[rb_files].reject {|d| d.start_with?(vendor_dir) } + + request.logger.debug("Loading sources #{src_files}") + + src_files.each do |file| + load file + end + else + request.logger.error(%(Specialization failed - could not find src at #{path}})) + Rack::Response.new(['500 Internal Server Error'], 500, {}).finish + + return + end + end + request.logger.info("User code loaded in #{(time.real * 1000).round(3)}ms") + + # set global handler for this specialization + $handler = method(func) + + Rack::Response.new([], 201).finish + end + end + end end diff --git a/environments/ruby/server.rb b/environments/ruby/server.rb index 6232f750..5b087ab8 100644 --- a/environments/ruby/server.rb +++ b/environments/ruby/server.rb @@ -1,21 +1,33 @@ # frozen_string_literal: true require 'rack' +require 'thin' require 'logger' require_relative 'fission/specializer' require_relative 'fission/handler' +$handler = nil + app = Rack::Builder.new do use Rack::Logger, Logger::DEBUG + use Rack::CommonLogger map "/specialize" do run Fission::Specializer end - map "/" do + map '/v2/specialize' do + run Fission::V2::Specializer + end + + map "/healthz" do + run ->(env) { [ 200, {}, [] ] } + end + + map '/' do run Fission::Handler end end -Rack::Handler::WEBrick.run app, Host: '0.0.0.0', Port: 8888 +Rack::Handler::Thin.run app, Host: '0.0.0.0', Port: 8888 diff --git a/examples/ruby/README.md b/examples/ruby/README.md index e4140c5b..741b9a04 100644 --- a/examples/ruby/README.md +++ b/examples/ruby/README.md @@ -105,3 +105,11 @@ $ curl http://$FISSION_ROUTER/request/123?key=abc --BODY-- ``` + +## V2 Specification Example (with builder support) + +``` +$ fission function create --name parse --env ruby --src "parse/*" --entrypoint handler +$ fission fn test --name parse --body 'This is my message' + This is my message +``` diff --git a/examples/ruby/parse/Gemfile b/examples/ruby/parse/Gemfile new file mode 100644 index 00000000..b24e9c93 --- /dev/null +++ b/examples/ruby/parse/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +gem "nokogiri" \ No newline at end of file diff --git a/examples/ruby/parse/Gemfile.lock b/examples/ruby/parse/Gemfile.lock new file mode 100644 index 00000000..f8a7a823 --- /dev/null +++ b/examples/ruby/parse/Gemfile.lock @@ -0,0 +1,15 @@ +GEM + remote: https://rubygems.org/ + specs: + mini_portile2 (2.4.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) + +PLATFORMS + ruby + +DEPENDENCIES + nokogiri + +BUNDLED WITH + 1.16.1 diff --git a/examples/ruby/parse/parse.rb b/examples/ruby/parse/parse.rb new file mode 100644 index 00000000..8f999b85 --- /dev/null +++ b/examples/ruby/parse/parse.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'nokogiri' + +def handler(context) + context.logger.info("Received request") + + doc = Nokogiri::XML(context.request.body.read) + ele = doc.at_xpath('//message') + msg = ele.nil? ? 'No Message' : ele.content + + Rack::Response.new([msg, "\n"]).finish +end \ No newline at end of file