General rewrite to support scales different than 1

This commit is contained in:
Roberto Alsina 2024-07-03 14:53:33 -03:00
parent 11d7cf1f9f
commit cec745039b
11 changed files with 363 additions and 246 deletions

View File

@ -1,19 +1,17 @@
# This configuration file was generated by `ameba --gen-config` # 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 14:31:04 UTC using Ameba version 1.6.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the reported problems are removed from the code base. # one by one as the reported problems are removed from the code base.
# Problems found: 12 # Problems found: 5
# Run `ameba --only Documentation/DocumentationAdmonition` for details # Run `ameba --only Documentation/DocumentationAdmonition` for details
Documentation/DocumentationAdmonition: Documentation/DocumentationAdmonition:
Description: Reports documentation admonitions Description: Reports documentation admonitions
Timezone: UTC Timezone: UTC
Excluded: Excluded:
- src/faaso.cr
- src/secrets.cr - src/secrets.cr
- src/daemon/main.cr - src/daemon/main.cr
- src/daemon/secrets.cr - src/daemon/secrets.cr
- src/funko.cr
- spec/faaso_spec.cr - spec/faaso_spec.cr
Admonitions: Admonitions:
- TODO - TODO
@ -29,8 +27,8 @@ Naming/BlockParameterName:
MinNameLength: 3 MinNameLength: 3
AllowNamesEndingInNumbers: true AllowNamesEndingInNumbers: true
Excluded: Excluded:
- src/faaso.cr
- src/daemon/funko.cr - src/daemon/funko.cr
- src/commands/build.cr
AllowedNames: AllowedNames:
- _ - _
- e - 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

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

@ -0,0 +1,31 @@
module Faaso
module Commands
struct Status
def local(options, name)
funko = Funko::Funko.from_names([name])[0]
Log.info { "Name: #{funko.name}" }
Log.info { "Scale: #{funko.scale}" }
containers = funko.containers
Log.info { "Containers: #{funko.containers.size}" }
containers.each do |container|
Log.info { " #{container.@names[0]} #{container.status}" }
end
images = funko.images
Log.info { "Images: #{images.size}" }
images.each do |image|
Log.info { " #{image.repo_tags} #{image.created}" }
end
end
def remote(options, name)
end
def run(options, name)
if options["--local"]
return local(options, name)
end
remote(options, name)
end
end
end
end

View File

@ -5,6 +5,32 @@ require "../funko.cr"
module Funko module Funko
extend self extend self
# 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
# 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/pause/" do |env| get "/funkos/:name/pause/" do |env|
funko = Funko.from_names([env.params.url["name"]])[0] funko = Funko.from_names([env.params.url["name"]])[0]
funko.pause funko.pause
@ -33,6 +59,36 @@ module Funko
end end
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 "/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
get "/funkos/" do |env| get "/funkos/" do |env|
funkos = Funko.from_docker funkos = Funko.from_docker
funkos.sort! { |a, b| a.name <=> b.name } funkos.sort! { |a, b| a.name <=> b.name }
@ -62,4 +118,20 @@ module Funko
result.to_json result.to_json
end end
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 end

View File

@ -11,62 +11,4 @@ require "uuid"
# FIXME: make configurable # FIXME: make configurable
basic_auth "admin", "admin" 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 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 "./funko.cr"
require "crest" require "crest"
require "docr" require "docr"
@ -26,161 +30,5 @@ module Faaso
end end
module Commands 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
end end

View File

@ -67,8 +67,10 @@ module Funko
# Get the number of running instances of this funko # Get the number of running instances of this funko
def scale def scale
docker_api = Docr::API.new(Docr::Client.new) docker_api = Docr::API.new(Docr::Client.new)
docker_api.containers.list.count { |container| docker_api.containers.list.select { |container|
container.@name.starts_with? "faaso-#{name}-" && container.@state == "running" container.@state == "running"
}.count { |container|
container.@names.any?(&.starts_with?("/faaso-#{name}-"))
} }
end end
@ -78,12 +80,19 @@ module Funko
current_scale = self.scale current_scale = self.scale
return if current_scale == new_scale return if current_scale == new_scale
Log.info { "Scaling #{name} from #{current_scale} to #{new_scale}" }
if new_scale > current_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
sleep 0.2.seconds
start(id)
}
else else
containers.select { |contatiner| container.@state == "running" }.sort! { |i, j| containers.select { |container| container.@state == "running" }.sort! { |i, j|
i.@created <=> j.@created i.@created <=> j.@created
}.each { |container| }.each { |container|
Log.info { "Removing instance" }
docker_api.containers.stop(container.@id) docker_api.containers.stop(container.@id)
current_scale -= 1 current_scale -= 1
break if current_scale == new_scale break if current_scale == new_scale
@ -127,6 +136,14 @@ module Funko
tags: ["faaso-#{name}:latest"]) { |x| Log.info { x } } tags: ["faaso-#{name}:latest"]) { |x| Log.info { x } }
end 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 # Return a list of image IDs for this funko, most recent first
def image_history def image_history
docker_api = Docr::API.new(Docr::Client.new) docker_api = Docr::API.new(Docr::Client.new)
@ -143,10 +160,9 @@ module Funko
# Get all containers related to this funko # Get all containers related to this funko
def containers def containers
docker_api = Docr::API.new(Docr::Client.new) docker_api = Docr::API.new(Docr::Client.new)
docker_api.containers.list( docker_api.containers.list(all: true).select { |container|
all: true, container.@names.any?(&.starts_with?("/faaso-#{name}-"))
filters: {"name" => ["faaso-#{name}"]} }
)
end end
# Descriptive status for the funko # Descriptive status for the funko
@ -202,6 +218,16 @@ module Funko
} }
end end
# Start container with given id
def start(id : String)
docker_api = Docr::API.new(Docr::Client.new)
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 # Start exited container with the newer image
# or unpause paused container # or unpause paused container
def start def start
@ -258,7 +284,7 @@ module Funko
Dir.mkdir_p(secrets_mount) Dir.mkdir_p(secrets_mount)
conf = Docr::Types::CreateContainerConfig.new( conf = Docr::Types::CreateContainerConfig.new(
image: "faaso-#{name}:latest", image: "faaso-#{name}:latest",
hostname: name, hostname: "#{name}",
# Port in the container side # Port in the container side
host_config: Docr::Types::HostConfig.new( host_config: Docr::Types::HostConfig.new(
network_mode: "faaso-net", network_mode: "faaso-net",
@ -273,7 +299,7 @@ module Funko
) )
docker_api = Docr::API.new(Docr::Client.new) 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 } } response.@warnings.each { |msg| Log.warn { msg } }
docker_api.containers.start(response.@id) if autostart docker_api.containers.start(response.@id) if autostart
response.@id response.@id
@ -313,3 +339,8 @@ module Funko
end end
end 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 "colorize"
require "docopt" 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 # Log formatter for
struct LogFormat < Log::StaticFormatter struct LogFormat < Log::StaticFormatter
@@colors = { @@colors = {
@ -29,7 +14,7 @@ struct LogFormat < Log::StaticFormatter
} }
def run def run
string "[#{Time.local}] #{@entry.severity.label}: #{@entry.message}".colorize(@@colors[@entry.severity.label]) string "#{@entry.message}".colorize(@@colors[@entry.severity.label])
end end
def self.setup(verbosity) def self.setup(verbosity)
@ -48,6 +33,22 @@ struct LogFormat < Log::StaticFormatter
end end
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) ans = Docopt.docopt(doc, ARGV)
LogFormat.setup(ans["-v"].to_s.to_i) 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))) Faaso::Commands::Build.new.run(ans, ans["FOLDER"].as(Array(String)))
when .fetch("export", false) when .fetch("export", false)
Faaso::Commands::Export.new.run(ans, ans["SOURCE"].as(String), ans["DESTINATION"].as(String)) 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 end

View File

@ -8,4 +8,23 @@
ReverseOnly Yes ReverseOnly Yes
ReverseMagic Yes ReverseMagic Yes
ReversePath "/admin/" "http://127.0.0.1:3000/" 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-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/"