Compare commits

..

5 Commits

11 changed files with 408 additions and 323 deletions

View File

@ -1,18 +1,18 @@
# This configuration file was generated by `ameba --gen-config`
# on 2024-07-02 22:36:34 UTC using Ameba version 1.6.1.
# on 2024-07-03 18:18:32 UTC using Ameba version 1.6.1.
# The point is for the user to remove these configuration records
# one by one as the reported problems are removed from the code base.
# Problems found: 12
# Problems found: 6
# Run `ameba --only Documentation/DocumentationAdmonition` for details
Documentation/DocumentationAdmonition:
Description: Reports documentation admonitions
Timezone: UTC
Excluded:
- src/faaso.cr
- src/secrets.cr
- src/daemon/main.cr
- src/daemon/secrets.cr
- src/daemon/funko.cr
- src/funko.cr
- spec/faaso_spec.cr
Admonitions:
@ -29,8 +29,8 @@ Naming/BlockParameterName:
MinNameLength: 3
AllowNamesEndingInNumbers: true
Excluded:
- src/faaso.cr
- src/daemon/funko.cr
- src/commands/build.cr
AllowedNames:
- _
- e

68
src/commands/build.cr Normal file
View File

@ -0,0 +1,68 @@
module Faaso
module Commands
# Build images for one or more funkos from source
struct Build
def run(options, folders : Array(String))
funkos = Funko::Funko.from_paths(folders)
if options["--local"]
funkos.each do |funko|
# Create temporary build location
tmp_dir = Path.new("tmp", UUID.random.to_s)
Dir.mkdir_p(tmp_dir) unless File.exists? tmp_dir
funko.prepare_build tmp_dir
Log.info { "Building function... #{funko.name} in #{tmp_dir}" }
funko.build tmp_dir
end
else # Running against a server
funkos.each do |funko|
# Create a tarball for the funko
buf = IO::Memory.new
Compress::Gzip::Writer.open(buf) do |gzip|
Crystar::Writer.open(gzip) do |tw|
Dir.glob("#{funko.path}/**/*").each do |path|
next unless File.file? path
rel_path = Path[path].relative_to funko.path
file_info = File.info(path)
hdr = Crystar::Header.new(
name: rel_path.to_s,
mode: file_info.permissions.to_u32,
size: file_info.size,
)
tw.write_header(hdr)
tw.write(File.read(path).to_slice)
end
end
end
tmp = File.tempname
File.open(tmp, "w") do |outf|
outf << buf
end
url = "#{FAASO_SERVER}funkos/build/"
begin
Log.info { "Uploading funko to #{FAASO_SERVER}" }
response = Crest.post(
url,
{"funko.tgz" => File.open(tmp), "name" => "funko.tgz"},
user: "admin", password: "admin"
)
Log.info { "Build finished successfully." }
body = JSON.parse(response.body)
Log.info { body["stdout"] }
rescue ex : Crest::InternalServerError
Log.error { "Error building funko #{funko.name} from #{funko.path}" }
body = JSON.parse(ex.response.body)
Log.info { body["stdout"] }
Log.error { body["stderr"] }
exit 1
end
end
end
end
end
end
end

18
src/commands/export.cr Normal file
View File

@ -0,0 +1,18 @@
module Faaso
module Commands
struct Export
def run(options, source : String, destination : String)
funko = Funko::Funko.from_paths([source])[0]
# Create temporary build location
dst_path = destination
if File.exists? dst_path
Log.error { "#{dst_path} already exists, not exporting #{funko.path}" }
return 1
end
Log.info { "Exporting #{funko.path} to #{dst_path}" }
Dir.mkdir_p(dst_path)
funko.prepare_build Path[dst_path]
end
end
end
end

85
src/commands/scale.cr Normal file
View File

@ -0,0 +1,85 @@
module Faaso
module Commands
# Bring up one or more funkos by name.
#
# This doesn't guarantee that they will be running the latest
# version, and it will try to recicle paused and exited containers.
#
# If there is no other way, it will create a brand new container with
# the latest known image and start it.
#
# If there are no images for the funko, it will fail to bring it up.
struct Scale
def local(options, name, scale)
funko = Funko::Funko.from_names([name])[0]
# Asked about scale
if !scale
Log.info { "Funko #{name} has a scale of #{funko.scale}" }
return 0
end
# Asked to set scale
if funko.image_history.empty?
Log.error { "Error: no images available for #{funko.name}:latest" }
exit 1
end
funko.scale(scale.as(String).to_i)
end
def remote(options, name, scale)
if !scale
response = Crest.get(
"#{FAASO_SERVER}funkos/#{name}/scale/", \
user: "admin", password: "admin")
else
response = Crest.post(
"#{FAASO_SERVER}funkos/#{name}/scale/",
{"scale" => scale}, user: "admin", password: "admin")
end
body = JSON.parse(response.body)
Log.info { body["output"] }
rescue ex : Crest::InternalServerError
Log.error { "Error scaling funko #{name}" }
body = JSON.parse(ex.response.body)
Log.info { body["output"] }
exit 1
end
def run(options, name, scale)
if options["--local"]
return local(options, name, scale)
end
remote(options, name, scale)
# case self
# when .running?
# # If it's already up, do nothing
# # FIXME: bring back out-of-date warning
# Log.info { "#{funko.name} is already up" }
# when .paused?
# # If it is paused, unpause it
# Log.info { "Resuming existing paused container" }
# funko.unpause
# when .exited?
# Log.info { "Starting function #{funko.name}" }
# Log.info { "Restarting existing exited container" }
# funko.start
# else
# # Only have an image, deploy from scratch
# Faaso.setup_network # We need it
# Log.info { "Creating and starting new container" }
# funko.create_container(autostart: true)
# (1..5).each { |_|
# break if funko.running?
# sleep 0.1.seconds
# }
# if !funko.running?
# Log.warn { "Container for #{funko.name} is not running yet" }
# next
# end
# Log.info { "Container for #{funko.name} is running" }
# end
end
end
end
end

43
src/commands/status.cr Normal file
View File

@ -0,0 +1,43 @@
module Faaso
module Commands
struct Status
def local(options, name)
funko = Funko::Funko.from_names([name])[0]
status = funko.docker_status
Log.info { "Name: #{status.@name}" }
Log.info { "Scale: #{status.scale}" }
Log.info { "Containers: #{status.containers.size}" }
status.containers.each do |container|
Log.info { " #{container.@names[0]} #{container.status}" }
end
Log.info { "Images: #{status.images.size}" }
status.images.each do |image|
Log.info { " #{image.repo_tags} #{Time.unix(image.created)}" }
end
end
def remote(options, name)
response = Crest.get(
"#{FAASO_SERVER}funkos/#{name}/status/", \
user: "admin", password: "admin")
body = JSON.parse(response.body)
Log.info { body["output"] }
rescue ex : Crest::InternalServerError
Log.error { "Error scaling funko #{name}" }
body = JSON.parse(ex.response.body)
Log.info { body["output"] }
exit 1
end
def run(options, name)
if options["--local"]
return local(options, name)
end
remote(options, name)
end
end
end
end

View File

@ -5,31 +5,71 @@ require "../funko.cr"
module Funko
extend self
get "/funkos/:name/pause/" do |env|
funko = Funko.from_names([env.params.url["name"]])[0]
funko.pause
funko.wait_for("paused", 5)
# Get the funko's status
get "/funkos/:name/status/" do |env|
name = env.params.url["name"]
response = run_faaso(["status", name])
if response["exit_code"] != 0
halt env, status_code: 500, response: response.to_json
else
response.to_json
end
end
get "/funkos/:name/unpause/" do |env|
funko = Funko.from_names([env.params.url["name"]])[0]
funko.unpause
funko.wait_for("running", 5)
# Get the funko's scale
get "/funkos/:name/scale/" do |env|
name = env.params.url["name"]
response = run_faaso(["scale", name])
if response["exit_code"] != 0
halt env, status_code: 500, response: response.to_json
else
response.to_json
end
end
get "/funkos/:name/start/" do |env|
funko = Funko.from_names([env.params.url["name"]])[0]
funko.start
funko.wait_for("running", 5)
# Set the funko's scale
post "/funkos/:name/scale/" do |env|
name = env.params.url["name"]
scale = env.params.body["scale"].as(String)
response = run_faaso(["scale", name, scale])
if response["exit_code"] != 0
Log.error { response }
halt env, status_code: 500, response: response.to_json
else
Log.info { response }
response.to_json
end
end
get "/funkos/:name/stop/" do |env|
funko = Funko.from_names([env.params.url["name"]])[0]
begin
funko.stop
funko.wait_for("exited", 5)
rescue ex : Docr::Errors::DockerAPIError
halt env, status_code: 500, response: "Failed to stop container"
# Build image for funko received as "funko.tgz"
# TODO: This may take a while, consider using something like
# mosquito-cr/mosquito to make it a job queue
post "/funkos/build/" do |env|
# Create place to build funko
tmp_dir = Path.new("tmp", UUID.random.to_s)
Dir.mkdir_p(tmp_dir) unless File.exists? tmp_dir
# Expand tarball in there
file = env.params.files["funko.tgz"].tempfile
Compress::Gzip::Reader.open(file) do |gzip|
Crystar::Reader.open(gzip) do |tar|
tar.each_entry do |entry|
File.open(Path.new(tmp_dir, entry.name), "w") do |dst|
IO.copy entry.io, dst
end
end
end
end
# Build the thing
response = run_faaso(["build", tmp_dir.to_s])
if response["exit_code"] != 0
halt env, status_code: 500, response: response.to_json
else
response.to_json
end
end
@ -39,20 +79,12 @@ module Funko
result = [] of Hash(String, String)
funkos.each do |funko|
state = ""
case funko
when .running?
state = "running"
when .paused?
state = "paused"
else
state = "stopped"
end
state = "FIXME"
result << {
"name" => funko.name,
"state" => state,
"status" => funko.status,
"status" => "FIXME",
}
end
@ -62,4 +94,20 @@ module Funko
result.to_json
end
end
def run_faaso(args : Array(String))
Log.info { "Running faaso [#{args.join(", ")}, -l]" }
output = IO::Memory.new
status = Process.run(
command: "faaso",
args: args + ["-l"], # Always local in the server
output: output,
error: output,
)
result = {
"exit_code" => status.exit_code,
"output" => output.to_s,
}
result
end
end

View File

@ -11,62 +11,4 @@ require "uuid"
# FIXME: make configurable
basic_auth "admin", "admin"
# Bring up the funko
get "/funko/:name/up/" do |env|
name = env.params.url["name"]
response = run_faaso(["up", name])
if response["exit_code"] != 0
halt env, status_code: 500, response: response.to_json
else
response.to_json
end
end
# Build image for funko received as "funko.tgz"
# TODO: This may take a while, consider using something like
# mosquito-cr/mosquito to make it a job queue
post "/funko/build/" do |env|
# Create place to build funko
tmp_dir = Path.new("tmp", UUID.random.to_s)
Dir.mkdir_p(tmp_dir) unless File.exists? tmp_dir
# Expand tarball in there
file = env.params.files["funko.tgz"].tempfile
Compress::Gzip::Reader.open(file) do |gzip|
Crystar::Reader.open(gzip) do |tar|
tar.each_entry do |entry|
File.open(Path.new(tmp_dir, entry.name), "w") do |dst|
IO.copy entry.io, dst
end
end
end
end
# Build the thing
response = run_faaso(["build", tmp_dir.to_s])
if response["exit_code"] != 0
halt env, status_code: 500, response: response.to_json
else
response.to_json
end
end
def run_faaso(args : Array(String))
stderr = IO::Memory.new
stdout = IO::Memory.new
status = Process.run(
command: "faaso",
args: args + ["-l"], # Always local in the server
output: stdout,
error: stderr,
)
{
"exit_code" => status.exit_code,
"stdout" => stdout.to_s,
"stderr" => stderr.to_s,
}
end
Kemal.run

View File

@ -1,3 +1,7 @@
require "./commands/build.cr"
require "./commands/export.cr"
require "./commands/scale.cr"
require "./commands/status.cr"
require "./funko.cr"
require "crest"
require "docr"
@ -26,161 +30,5 @@ module Faaso
end
module Commands
# Build images for one or more funkos
class Build
def run(options, folders : Array(String))
funkos = Funko::Funko.from_paths(folders)
if options["--local"]
funkos.each do |funko|
# Create temporary build location
tmp_dir = Path.new("tmp", UUID.random.to_s)
Dir.mkdir_p(tmp_dir) unless File.exists? tmp_dir
funko.prepare_build tmp_dir
Log.info { "Building function... #{funko.name} in #{tmp_dir}" }
funko.build tmp_dir
end
else # Running against a server
funkos.each do |funko|
# Create a tarball for the funko
buf = IO::Memory.new
Compress::Gzip::Writer.open(buf) do |gzip|
Crystar::Writer.open(gzip) do |tw|
Dir.glob("#{funko.path}/**/*").each do |path|
next unless File.file? path
rel_path = Path[path].relative_to funko.path
file_info = File.info(path)
hdr = Crystar::Header.new(
name: rel_path.to_s,
mode: file_info.permissions.to_u32,
size: file_info.size,
)
tw.write_header(hdr)
tw.write(File.read(path).to_slice)
end
end
end
tmp = File.tempname
File.open(tmp, "w") do |outf|
outf << buf
end
url = "#{FAASO_SERVER}funko/build/"
begin
Log.info { "Uploading funko to #{FAASO_SERVER}" }
response = Crest.post(
url,
{"funko.tgz" => File.open(tmp), "name" => "funko.tgz"},
user: "admin", password: "admin"
)
Log.info { "Build finished successfully." }
body = JSON.parse(response.body)
Log.info { body["stdout"] }
rescue ex : Crest::InternalServerError
Log.error { "Error building funko #{funko.name} from #{funko.path}" }
body = JSON.parse(ex.response.body)
Log.info { body["stdout"] }
Log.error { body["stderr"] }
exit 1
end
end
end
end
end
# Bring up one or more funkos by name.
#
# This doesn't guarantee that they will be running the latest
# version, and it will try to recicle paused and exited containers.
#
# If there is no other way, it will create a brand new container with
# the latest known image and start it.
#
# If there are no images for the funko, it will fail to bring it up.
class Up
@arguments : Array(String) = [] of String
@options : Hash(String, Bool)
def initialize(options, arguments)
@options = options
@arguments = arguments
end
def run
funkos = Funko::Funko.from_names(@arguments)
funkos.each do |funko|
local = @options.@bool["local"]
if !local
begin
response = Crest.get("#{FAASO_SERVER}funko/#{funko.name}/up/",
user: "admin", password: "admin")
body = JSON.parse(response.body)
Log.info { body["stdout"] }
next
rescue ex : Crest::InternalServerError
Log.error { "Error bringing up #{funko.name}" }
body = JSON.parse(ex.response.body)
Log.info { body["stdout"] }
Log.error { body["stderr"] }
exit 1
end
end
if funko.image_history.empty?
Log.error { "Error: no images available for #{funko.name}:latest" }
exit 1
end
case funko
when .running?
# If it's already up, do nothing
# FIXME: bring back out-of-date warning
Log.info { "#{funko.name} is already up" }
when .paused?
# If it is paused, unpause it
Log.info { "Resuming existing paused container" }
funko.unpause
when .exited?
Log.info { "Starting function #{funko.name}" }
Log.info { "Restarting existing exited container" }
funko.start
else
# Only have an image, deploy from scratch
Faaso.setup_network # We need it
Log.info { "Creating and starting new container" }
funko.create_container(autostart: true)
(1..5).each { |_|
break if funko.running?
sleep 0.1.seconds
}
if !funko.running?
Log.warn { "Container for #{funko.name} is not running yet" }
next
end
Log.info { "Container for #{funko.name} is running" }
end
end
end
end
class Export
def run(options, source : String, destination : String)
funko = Funko::Funko.from_paths([source])[0]
# Create temporary build location
dst_path = destination
if File.exists? dst_path
Log.error { "#{dst_path} already exists, not exporting #{funko.path}" }
return 1
end
Log.info { "Exporting #{funko.path} to #{dst_path}" }
Dir.mkdir_p(dst_path)
funko.prepare_build Path[dst_path]
end
end
end
end

View File

@ -6,6 +6,20 @@ require "yaml"
module Funko
extend self
struct Status
property name : String = ""
property scale : Int32 = 0
property containers : Array(Docr::Types::ContainerSummary) = [] of Docr::Types::ContainerSummary
property images : Array(Docr::Types::ImageSummary) = [] of Docr::Types::ImageSummary
def initialize(name, scale, containers, images)
@name = name
@scale = scale
@containers = containers
@images = images
end
end
class Funko
include YAML::Serializable
@ -67,8 +81,10 @@ module Funko
# Get the number of running instances of this funko
def scale
docker_api = Docr::API.new(Docr::Client.new)
docker_api.containers.list.count { |container|
container.@name.starts_with? "faaso-#{name}-" && container.@state == "running"
docker_api.containers.list.select { |container|
container.@state == "running"
}.count { |container|
container.@names.any?(&.starts_with?("/faaso-#{name}-"))
}
end
@ -78,17 +94,29 @@ module Funko
current_scale = self.scale
return if current_scale == new_scale
Log.info { "Scaling #{name} from #{current_scale} to #{new_scale}" }
if new_scale > current_scale
(current_scale...new_scale).each { start(create_container) }
Log.info { "Adding instance" }
(current_scale...new_scale).each {
id = create_container
start(id)
}
else
containers.select { |contatiner| container.@state == "running" }.sort! { |i, j|
containers.select { |container| container.@state == "running" }.sort! { |i, j|
i.@created <=> j.@created
}.each { |container|
Log.info { "Removing instance" }
docker_api.containers.stop(container.@id)
current_scale -= 1
break if current_scale == new_scale
}
end
# And now, let's kill all the containers that are NOT running
containers.select { |container| container.@state != "running" }.each { |container|
Log.info { "Pruning dead instance" }
docker_api.containers.delete(container.@id)
}
end
# Setup the target directory `path` with all the files needed
@ -127,7 +155,16 @@ module Funko
tags: ["faaso-#{name}:latest"]) { |x| Log.info { x } }
end
def images
docker_api = Docr::API.new(Docr::Client.new)
docker_api.images.list.select { |image|
false if image.@repo_tags.nil?
true if image.@repo_tags.as(Array(String)).any?(&.starts_with?("faaso-#{name}:"))
}
end
# Return a list of image IDs for this funko, most recent first
# FIXME: use self.images and add filters
def image_history
docker_api = Docr::API.new(Docr::Client.new)
begin
@ -143,63 +180,29 @@ module Funko
# Get all containers related to this funko
def containers
docker_api = Docr::API.new(Docr::Client.new)
docker_api.containers.list(
all: true,
filters: {"name" => ["faaso-#{name}"]}
docker_api.containers.list(all: true).select { |container|
container.@names.any?(&.starts_with?("/faaso-#{name}-"))
}
end
# A comprehensive status for the funko:
def docker_status
Status.new(
name: name,
containers: containers,
images: images,
scale: scale,
)
end
# Descriptive status for the funko
def status
status = self.containers.map { |container|
container.@status
}.join(", ")
status.empty? ? "Stopped" : status
end
# Is any instance of this funko running?
def running?
self.containers.any? { |container|
container.@state == "running"
}
end
# Is any instance of this funko paused?
def paused?
self.containers.any? { |container|
container.@state == "paused"
}
end
# Pause running container
def pause
# Start container with given id
def start(id : String)
docker_api = Docr::API.new(Docr::Client.new)
images = self.image_history
running = self.containers.select { |container|
container.@state == "running"
}.sort! { |i, j|
(images.index(j.@image_id) || 9999) <=> (images.index(i.@image_id) || 9999)
}
docker_api.containers.pause(running[0].@id) unless running.empty?
end
# Unpause paused container with the newer image
def unpause
docker_api = Docr::API.new(Docr::Client.new)
images = self.image_history
paused = self.containers.select { |container|
container.@state == "paused"
}.sort! { |i, j|
(images.index(j.@image_id) || 9999) <=> (images.index(i.@image_id) || 9999)
}
docker_api.containers.unpause(paused[0].@id) unless paused.empty?
end
# Is any instance of this funko exited?
def exited?
self.containers.any? { |container|
container.@state == "exited"
}
begin
docker_api.containers.start(id)
rescue ex : Docr::Errors::DockerAPIError
Log.error { "#{ex}" } unless ex.status_code == 304 # This just happens
end
end
# Start exited container with the newer image
@ -258,7 +261,7 @@ module Funko
Dir.mkdir_p(secrets_mount)
conf = Docr::Types::CreateContainerConfig.new(
image: "faaso-#{name}:latest",
hostname: name,
hostname: "#{name}",
# Port in the container side
host_config: Docr::Types::HostConfig.new(
network_mode: "faaso-net",
@ -273,7 +276,7 @@ module Funko
)
docker_api = Docr::API.new(Docr::Client.new)
response = docker_api.containers.create(name: "faaso-#{name}", config: conf)
response = docker_api.containers.create(name: "faaso-#{name}-#{randstr}", config: conf)
response.@warnings.each { |msg| Log.warn { msg } }
docker_api.containers.start(response.@id) if autostart
response.@id
@ -313,3 +316,8 @@ module Funko
end
end
end
def randstr(length = 6) : String
chars = "abcdefghijklmnopqrstuvwxyz0123456789"
String.new(Bytes.new(chars.to_slice.sample(length).to_unsafe, length))
end

View File

@ -2,21 +2,6 @@ require "./faaso.cr"
require "colorize"
require "docopt"
doc = <<-DOC
FaaSO CLI tool.
Usage:
faaso build FOLDER ... [-l] [-v=<level>]
faaso scale FUNKO_NAME SCALE [-l] [-v=<level>]
faaso export SOURCE DESTINATION [-v=<level>]
Options:
-h --help Show this screen.
--version Show version.
-l --local Run commands locally instead of against a FaaSO server.
-v=level Control the logging verbosity, 0 to 5 [default: 3]
DOC
# Log formatter for
struct LogFormat < Log::StaticFormatter
@@colors = {
@ -29,7 +14,7 @@ struct LogFormat < Log::StaticFormatter
}
def run
string "[#{Time.local}] #{@entry.severity.label}: #{@entry.message}".colorize(@@colors[@entry.severity.label])
string "#{@entry.message}".colorize(@@colors[@entry.severity.label])
end
def self.setup(verbosity)
@ -48,6 +33,22 @@ struct LogFormat < Log::StaticFormatter
end
end
doc = <<-DOC
FaaSO CLI tool.
Usage:
faaso build FOLDER ... [-l] [-v=<level>]
faaso scale FUNKO_NAME [SCALE] [-l] [-v=<level>]
faaso status FUNKO_NAME [-l] [-v=<level>]
faaso export SOURCE DESTINATION [-v=<level>]
Options:
-l --local Run commands locally instead of against a FaaSO server.
-h --help Show this screen.
--version Show version.
-v=level Control the logging verbosity, 0 to 5 [default: 3]
DOC
ans = Docopt.docopt(doc, ARGV)
LogFormat.setup(ans["-v"].to_s.to_i)
@ -56,4 +57,8 @@ when .fetch("build", false)
Faaso::Commands::Build.new.run(ans, ans["FOLDER"].as(Array(String)))
when .fetch("export", false)
Faaso::Commands::Export.new.run(ans, ans["SOURCE"].as(String), ans["DESTINATION"].as(String))
when .fetch("scale", false)
Faaso::Commands::Scale.new.run(ans, ans["FUNKO_NAME"].as(String), ans["SCALE"])
when .fetch("status", false)
Faaso::Commands::Status.new.run(ans, ans["FUNKO_NAME"].as(String))
end

View File

@ -8,4 +8,24 @@
ReverseOnly Yes
ReverseMagic Yes
ReversePath "/admin/" "http://127.0.0.1:3000/"
ReversePath "/faaso/hello/" "http://hello:3000/"
ReversePath "/faaso/hello-101807275100116/" "http://hello-101807275100116:3000/"
ReversePath "/faaso/hello-109717610410253/" "http://hello-109717610410253:3000/"
ReversePath "/faaso/hello-115791188481102/" "http://hello-115791188481102:3000/"
ReversePath "/faaso/hello-489850679780/" "http://hello-489850679780:3000/"
ReversePath "/faaso/hello-52122101707950/" "http://hello-52122101707950:3000/"
ReversePath "/faaso/hello-53828586108120/" "http://hello-53828586108120:3000/"
ReversePath "/faaso/hello-5412177121100122/" "http://hello-5412177121100122:3000/"
ReversePath "/faaso/hello-679912011112183/" "http://hello-679912011112183:3000/"
ReversePath "/faaso/hello-687849738368/" "http://hello-687849738368:3000/"
ReversePath "/faaso/hello-689880877456/" "http://hello-689880877456:3000/"
ReversePath "/faaso/hello-6987113768753/" "http://hello-6987113768753:3000/"
ReversePath "/faaso/hello-7273704811390/" "http://hello-7273704811390:3000/"
ReversePath "/faaso/hello-761221081008155/" "http://hello-761221081008155:3000/"
ReversePath "/faaso/hello-9798100678476/" "http://hello-9798100678476:3000/"
ReversePath "/faaso/hello-98103104100103100/" "http://hello-98103104100103100:3000/"
ReversePath "/faaso/hello-e24ojr/" "http://hello-e24ojr:3000/"
ReversePath "/faaso/hello-foo/" "http://hello-foo:3000/"
ReversePath "/faaso/hello-gfvij3/" "http://hello-gfvij3:3000/"
ReversePath "/faaso/hello-ngisvh/" "http://hello-ngisvh:3000/"
ReversePath "/faaso/hello-rqp8o3/" "http://hello-rqp8o3:3000/"
ReversePath "/faaso/hello-xtpu69/" "http://hello-xtpu69:3000/"