From 238d045691e3a9e21086ccab79d353d7e0c49210 Mon Sep 17 00:00:00 2001 From: Roberto Alsina Date: Fri, 28 Jun 2024 14:09:58 -0300 Subject: [PATCH] Skeleton of basic functionality, all as TODOs --- shard.lock | 14 +++++ shard.yml | 9 ++- src/faaso.cr | 67 +++++++++++++++++++++- src/main.cr | 36 ++++++++++++ templates/crystal-http/Dockerfile | 39 +++++++++++++ templates/crystal-http/function/handler.cr | 12 ++++ templates/crystal-http/function/shard.yml | 2 + templates/crystal-http/main.cr | 41 +++++++++++++ templates/crystal-http/template.yml | 6 ++ 9 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 shard.lock create mode 100644 src/main.cr create mode 100644 templates/crystal-http/Dockerfile create mode 100644 templates/crystal-http/function/handler.cr create mode 100644 templates/crystal-http/function/shard.yml create mode 100644 templates/crystal-http/main.cr create mode 100644 templates/crystal-http/template.yml diff --git a/shard.lock b/shard.lock new file mode 100644 index 0000000..045111e --- /dev/null +++ b/shard.lock @@ -0,0 +1,14 @@ +version: 2.0 +shards: + commander: + git: https://github.com/mrrooijen/commander.git + version: 0.4.0 + + crystar: + git: https://github.com/naqvis/crystar.git + version: 0.4.0 + + docr: + git: https://github.com/marghidanu/docr.git + version: 0.1.1+git.commit.59fb466b748d5d29a8f2de88a54b71dbfe0285aa + diff --git a/shard.yml b/shard.yml index f1510bf..119f639 100644 --- a/shard.yml +++ b/shard.yml @@ -6,8 +6,15 @@ authors: targets: faaso: - main: src/faaso.cr + main: src/main.cr crystal: '>= 1.12.2' license: MIT + + +dependencies: + docr: + github: marghidanu/docr + commander: + github: mrrooijen/commander \ No newline at end of file diff --git a/src/faaso.cr b/src/faaso.cr index 14a4e31..93bac94 100644 --- a/src/faaso.cr +++ b/src/faaso.cr @@ -1,6 +1,71 @@ +require "commander" + # TODO: Write documentation for `Faaso` module Faaso VERSION = "0.1.0" - # TODO: Put your code here + module Commands + class Build + @arguments : Array(String) = [] of String + @options : Commander::Options + + def initialize(options, arguments) + @options = options + @arguments = arguments + end + + def run + @arguments.each do |arg| + puts "Building function... #{arg}" + # A function is a folder with stuff in it + # TODO: decide template based on file extensions or other metadata + # TODO: copy template and add function files to it + # TODO: build Docker image + # TODO: push Docker image to registry + # TODO: return image name for testing + end + end + end + + class Up + @arguments : Array(String) = [] of String + @options : Commander::Options + + def initialize(options, arguments) + @options = options + @arguments = arguments + end + + def run + @arguments.each do |arg| + puts "Starting function... #{arg}" + # TODO: Check that we have an image for the function + # TODO: Start a container with the image + # TODO: Run test for healthcheck + # TODO: Map route in reverse proxy to function + # TODO: Return function URL for testing + end + end + end + + class Down + @arguments : Array(String) = [] of String + @options : Commander::Options + + def initialize(options, arguments) + @options = options + @arguments = arguments + end + + def run + @arguments.each do |arg| + puts "Stopping function... #{arg}" + # TODO: check if function is running + # TODO: stop function container + # TODO: delete function container + # TODO: remove route from reverse proxy + end + end + end + end end diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..55cb8f6 --- /dev/null +++ b/src/main.cr @@ -0,0 +1,36 @@ +require "commander" +require "./faaso.cr" + +cli = Commander::Command.new do |cmd| + cmd.use = "faaso" + cmd.long = "Functions as a Service, Open" + + cmd.commands.add do |command| + command.use = "build" + command.short = "Build a function" + command.long = "Build a function's Docker image and optionally upload it to registry" + command.run do |options, arguments| + Faaso::Commands::Build.new(options, arguments).run + end + end + + cmd.commands.add do |command| + command.use = "up" + command.short = "Start a function" + command.long = "Start a function in a container" + command.run do |options, arguments| + Faaso::Commands::Up.new(options, arguments).run + end + end + + cmd.commands.add do |command| + command.use = "down" + command.short = "Stop a function" + command.long = "Stop a function in a container" + command.run do |options, arguments| + Faaso::Commands::Down.new(options, arguments).run + end + end +end + +Commander.run(cli, ARGV) diff --git a/templates/crystal-http/Dockerfile b/templates/crystal-http/Dockerfile new file mode 100644 index 0000000..d2357bc --- /dev/null +++ b/templates/crystal-http/Dockerfile @@ -0,0 +1,39 @@ +FROM --platform=${TARGETPLATFORM:-linux/amd64} ghcr.io/openfaas/of-watchdog:0.9.10 as watchdog +FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as build +ARG ADDITIONAL_PACKAGE + +COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog + +RUN apk update && apk upgrade && apk add crystal shards openssl-dev ${ADDITIONAL_PACKAGE} && apk cache clean + +WORKDIR /home/app + +COPY function/shard.yml shard.yml +RUN shards install +COPY . . +RUN crystal build main.cr -o handler --error-trace -p && rm -rf /root/.cache + +FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as ship +ARG ADDITIONAL_PACKAGE +RUN apk update && apk upgrade && apk add openssl pcre2 libgcc gc libevent ${ADDITIONAL_PACKAGE} && apk cache clean +# Add non root user +# Add non root user +RUN addgroup -S app && adduser app -S -G app + +WORKDIR /home/app +USER app + +COPY --from=build /home/app/function/ . +COPY --from=build /home/app/handler . +COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog + +ENV fprocess="./handler" +ENV exec_timeout=30 +EXPOSE 8080 + +HEALTHCHECK --interval=2s CMD [ -e /tmp/.lock ] || exit 1 + +ENV upstream_url="http://127.0.0.1:5000" +ENV mode="http" + +CMD ["fwatchdog"] diff --git a/templates/crystal-http/function/handler.cr b/templates/crystal-http/function/handler.cr new file mode 100644 index 0000000..4ebe178 --- /dev/null +++ b/templates/crystal-http/function/handler.cr @@ -0,0 +1,12 @@ +require "http/request" +require "http/headers" + +class Handler + def run(request : HTTP::Request) + { + body: "Hello, Crystal. You said: #{request.body.try(&.gets_to_end)}", + status_code: 200, + headers: HTTP::Headers{"Content-Type" => "text/plain"}, + } + end +end diff --git a/templates/crystal-http/function/shard.yml b/templates/crystal-http/function/shard.yml new file mode 100644 index 0000000..b65c156 --- /dev/null +++ b/templates/crystal-http/function/shard.yml @@ -0,0 +1,2 @@ +name: crystal-http-template +version: 0.1.0 diff --git a/templates/crystal-http/main.cr b/templates/crystal-http/main.cr new file mode 100644 index 0000000..90acc5f --- /dev/null +++ b/templates/crystal-http/main.cr @@ -0,0 +1,41 @@ +require "http/server" +require "./function/handler" + +server = HTTP::Server.new do |context| + response_triple : NamedTuple(body: String, headers: HTTP::Headers, status_code: Int32) | + NamedTuple(body: String, headers: HTTP::Headers) | + NamedTuple(body: String, status_code: Int32) | + NamedTuple(body: String) | + NamedTuple(headers: HTTP::Headers, status_code: Int32) | + NamedTuple(headers: HTTP::Headers) | + NamedTuple(status_code: Int32) + + handler = Handler.new + response_triple = handler.run(context.request) + + if response_triple.is_a?(NamedTuple(body: String, headers: HTTP::Headers, status_code: Int32) | + NamedTuple(body: String, status_code: Int32) | + NamedTuple(headers: HTTP::Headers, status_code: Int32) | + NamedTuple(status_code: Int32)) + context.response.status_code = response_triple[:status_code] + end + + if response_triple.is_a?(NamedTuple(body: String, headers: HTTP::Headers, status_code: Int32) | + NamedTuple(body: String, headers: HTTP::Headers) | + NamedTuple(headers: HTTP::Headers, status_code: Int32) | + NamedTuple(headers: HTTP::Headers)) + response_triple[:headers].each do |key, value| + context.response.headers[key] = value + end + end + + if response_triple.is_a?(NamedTuple(body: String, headers: HTTP::Headers, status_code: Int32) | + NamedTuple(body: String, headers: HTTP::Headers) | + NamedTuple(body: String, status_code: Int32) | + NamedTuple(body: String)) + context.response.print(response_triple[:body]) + end +end + +server.bind_tcp "0.0.0.0", 5000 +server.listen diff --git a/templates/crystal-http/template.yml b/templates/crystal-http/template.yml new file mode 100644 index 0000000..66d437b --- /dev/null +++ b/templates/crystal-http/template.yml @@ -0,0 +1,6 @@ +language: crystal +fprocess: ./handler +welcome_message: | + You have created a new function which uses crystal 1.0.0. + To include third-party dependencies, use a vendoring tool like shards: + shards documentation: https://github.com/crystal-lang/shards