Compare commits
5 Commits
11d7cf1f9f
...
64778bba19
Author | SHA1 | Date | |
---|---|---|---|
64778bba19 | |||
e618f7c9e6 | |||
126cae6c18 | |||
e17f421b5e | |||
cec745039b |
@ -1,18 +1,18 @@
|
|||||||
# 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 18:18:32 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: 6
|
||||||
# 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/daemon/funko.cr
|
||||||
- src/funko.cr
|
- src/funko.cr
|
||||||
- spec/faaso_spec.cr
|
- spec/faaso_spec.cr
|
||||||
Admonitions:
|
Admonitions:
|
||||||
@ -29,8 +29,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
68
src/commands/build.cr
Normal 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
18
src/commands/export.cr
Normal 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
85
src/commands/scale.cr
Normal 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
43
src/commands/status.cr
Normal 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
|
@ -5,31 +5,71 @@ require "../funko.cr"
|
|||||||
module Funko
|
module Funko
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
get "/funkos/:name/pause/" do |env|
|
# Get the funko's status
|
||||||
funko = Funko.from_names([env.params.url["name"]])[0]
|
get "/funkos/:name/status/" do |env|
|
||||||
funko.pause
|
name = env.params.url["name"]
|
||||||
funko.wait_for("paused", 5)
|
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
|
end
|
||||||
|
|
||||||
get "/funkos/:name/unpause/" do |env|
|
# Get the funko's scale
|
||||||
funko = Funko.from_names([env.params.url["name"]])[0]
|
get "/funkos/:name/scale/" do |env|
|
||||||
funko.unpause
|
name = env.params.url["name"]
|
||||||
funko.wait_for("running", 5)
|
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
|
end
|
||||||
|
|
||||||
get "/funkos/:name/start/" do |env|
|
# Set the funko's scale
|
||||||
funko = Funko.from_names([env.params.url["name"]])[0]
|
post "/funkos/:name/scale/" do |env|
|
||||||
funko.start
|
name = env.params.url["name"]
|
||||||
funko.wait_for("running", 5)
|
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
|
end
|
||||||
|
|
||||||
get "/funkos/:name/stop/" do |env|
|
# Build image for funko received as "funko.tgz"
|
||||||
funko = Funko.from_names([env.params.url["name"]])[0]
|
# TODO: This may take a while, consider using something like
|
||||||
begin
|
# mosquito-cr/mosquito to make it a job queue
|
||||||
funko.stop
|
post "/funkos/build/" do |env|
|
||||||
funko.wait_for("exited", 5)
|
# Create place to build funko
|
||||||
rescue ex : Docr::Errors::DockerAPIError
|
tmp_dir = Path.new("tmp", UUID.random.to_s)
|
||||||
halt env, status_code: 500, response: "Failed to stop container"
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -39,20 +79,12 @@ module Funko
|
|||||||
result = [] of Hash(String, String)
|
result = [] of Hash(String, String)
|
||||||
|
|
||||||
funkos.each do |funko|
|
funkos.each do |funko|
|
||||||
state = ""
|
state = "FIXME"
|
||||||
case funko
|
|
||||||
when .running?
|
|
||||||
state = "running"
|
|
||||||
when .paused?
|
|
||||||
state = "paused"
|
|
||||||
else
|
|
||||||
state = "stopped"
|
|
||||||
end
|
|
||||||
|
|
||||||
result << {
|
result << {
|
||||||
"name" => funko.name,
|
"name" => funko.name,
|
||||||
"state" => state,
|
"state" => state,
|
||||||
"status" => funko.status,
|
"status" => "FIXME",
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -62,4 +94,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
|
||||||
|
@ -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
|
||||||
|
160
src/faaso.cr
160
src/faaso.cr
@ -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
|
||||||
|
126
src/funko.cr
126
src/funko.cr
@ -6,6 +6,20 @@ require "yaml"
|
|||||||
module Funko
|
module Funko
|
||||||
extend self
|
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
|
class Funko
|
||||||
include YAML::Serializable
|
include YAML::Serializable
|
||||||
|
|
||||||
@ -67,8 +81,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,17 +94,29 @@ 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
|
||||||
|
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
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# Setup the target directory `path` with all the files needed
|
# Setup the target directory `path` with all the files needed
|
||||||
@ -127,7 +155,16 @@ 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
|
||||||
|
# FIXME: use self.images and add filters
|
||||||
def image_history
|
def image_history
|
||||||
docker_api = Docr::API.new(Docr::Client.new)
|
docker_api = Docr::API.new(Docr::Client.new)
|
||||||
begin
|
begin
|
||||||
@ -143,63 +180,29 @@ 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
|
||||||
|
|
||||||
|
# A comprehensive status for the funko:
|
||||||
|
def docker_status
|
||||||
|
Status.new(
|
||||||
|
name: name,
|
||||||
|
containers: containers,
|
||||||
|
images: images,
|
||||||
|
scale: scale,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Descriptive status for the funko
|
# Start container with given id
|
||||||
def status
|
def start(id : String)
|
||||||
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
|
|
||||||
docker_api = Docr::API.new(Docr::Client.new)
|
docker_api = Docr::API.new(Docr::Client.new)
|
||||||
images = self.image_history
|
begin
|
||||||
running = self.containers.select { |container|
|
docker_api.containers.start(id)
|
||||||
container.@state == "running"
|
rescue ex : Docr::Errors::DockerAPIError
|
||||||
}.sort! { |i, j|
|
Log.error { "#{ex}" } unless ex.status_code == 304 # This just happens
|
||||||
(images.index(j.@image_id) || 9999) <=> (images.index(i.@image_id) || 9999)
|
end
|
||||||
}
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Start exited container with the newer image
|
# Start exited container with the newer image
|
||||||
@ -258,7 +261,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 +276,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 +316,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
|
||||||
|
37
src/main.cr
37
src/main.cr
@ -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
|
||||||
|
@ -8,4 +8,24 @@
|
|||||||
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-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/"
|
Loading…
Reference in New Issue
Block a user