2024-06-29 17:36:11 +00:00
|
|
|
require "./funko.cr"
|
2024-06-28 17:09:58 +00:00
|
|
|
require "commander"
|
2024-06-28 20:42:10 +00:00
|
|
|
require "docr"
|
|
|
|
require "docr/utils.cr"
|
2024-06-28 19:24:52 +00:00
|
|
|
require "file_utils"
|
2024-06-29 17:36:11 +00:00
|
|
|
require "kiwi/file_store"
|
2024-06-28 19:24:52 +00:00
|
|
|
require "uuid"
|
2024-06-28 17:09:58 +00:00
|
|
|
|
2024-06-29 15:29:53 +00:00
|
|
|
# FIXME make it configurable
|
|
|
|
REPO = "localhost:5000"
|
|
|
|
|
|
|
|
# Functions as a Service, Ops!
|
2024-06-28 15:41:21 +00:00
|
|
|
module Faaso
|
|
|
|
VERSION = "0.1.0"
|
|
|
|
|
2024-06-29 17:36:11 +00:00
|
|
|
# A simple persistent k/v store
|
|
|
|
store = Kiwi::FileStore.new(".kiwi")
|
|
|
|
|
2024-06-29 21:58:49 +00:00
|
|
|
# Ensure the faaso-net network exists
|
|
|
|
def self.setup_network
|
|
|
|
begin
|
|
|
|
docker_api = Docr::API.new(Docr::Client.new)
|
|
|
|
docker_api.networks.create(Docr::Types::NetworkConfig.new(
|
|
|
|
name: "faaso-net",
|
|
|
|
check_duplicate: false,
|
|
|
|
driver: "bridge"
|
|
|
|
))
|
|
|
|
rescue ex : Docr::Errors::DockerAPIError
|
|
|
|
raise ex if ex.status_code != 409 # Network already exists
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-06-28 17:09:58 +00:00
|
|
|
module Commands
|
|
|
|
class Build
|
|
|
|
@arguments : Array(String) = [] of String
|
|
|
|
@options : Commander::Options
|
|
|
|
|
|
|
|
def initialize(options, arguments)
|
|
|
|
@options = options
|
|
|
|
@arguments = arguments
|
|
|
|
end
|
|
|
|
|
|
|
|
def run
|
2024-06-29 15:22:59 +00:00
|
|
|
funkos = Funko.from_paths(@arguments)
|
|
|
|
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
|
|
|
|
|
|
|
|
# Copy runtime if requested
|
|
|
|
if !funko.runtime.nil?
|
|
|
|
runtime_dir = Path.new("runtimes", funko.runtime.to_s)
|
|
|
|
if !File.exists? runtime_dir
|
|
|
|
puts "Error: runtime #{funko.runtime} not found"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
Dir.glob("#{runtime_dir}/*").each { |src|
|
|
|
|
FileUtils.cp_r(src, tmp_dir)
|
|
|
|
}
|
2024-06-28 19:24:52 +00:00
|
|
|
end
|
2024-06-29 15:22:59 +00:00
|
|
|
|
|
|
|
# Copy funko
|
|
|
|
if funko.path.empty?
|
|
|
|
puts "Internal error: empty funko path for #{funko.name}"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
Dir.glob("#{funko.path}/*").each { |src|
|
|
|
|
FileUtils.cp_r(src, tmp_dir)
|
|
|
|
}
|
|
|
|
|
|
|
|
puts "Building function... #{funko.name} in #{tmp_dir}"
|
|
|
|
|
|
|
|
slug = funko.name
|
|
|
|
|
|
|
|
# FIXME: this should be configurable
|
2024-06-29 15:29:53 +00:00
|
|
|
repo = REPO
|
2024-06-29 15:22:59 +00:00
|
|
|
|
2024-06-28 20:42:10 +00:00
|
|
|
docker_api = Docr::API.new(Docr::Client.new)
|
2024-06-29 15:22:59 +00:00
|
|
|
docker_api.images.build(
|
|
|
|
context: tmp_dir.to_s,
|
2024-06-29 17:36:11 +00:00
|
|
|
tags: ["#{funko.name}:latest"]) { }
|
2024-06-28 17:09:58 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Up
|
|
|
|
@arguments : Array(String) = [] of String
|
|
|
|
@options : Commander::Options
|
|
|
|
|
|
|
|
def initialize(options, arguments)
|
|
|
|
@options = options
|
|
|
|
@arguments = arguments
|
|
|
|
end
|
|
|
|
|
|
|
|
def run
|
2024-06-29 15:29:53 +00:00
|
|
|
funkos = Funko.from_paths(@arguments)
|
|
|
|
funkos.each do |funko|
|
|
|
|
repo = REPO
|
2024-06-29 17:36:11 +00:00
|
|
|
container_name = "faaso-#{funko.name}"
|
2024-06-28 23:07:27 +00:00
|
|
|
docker_api = Docr::API.new(Docr::Client.new)
|
|
|
|
# Pull image from registry
|
2024-06-29 17:36:11 +00:00
|
|
|
# docker_api.images.create(image: tag)
|
|
|
|
|
|
|
|
# Get image history, sorted newer image first
|
|
|
|
begin
|
|
|
|
images = docker_api.images.history(
|
|
|
|
name: funko.name
|
|
|
|
).sort { |a, b| b.@created <=> a.@created }.map(&.@id)
|
|
|
|
rescue ex : Docr::Errors::DockerAPIError
|
|
|
|
puts "Error: no images available for #{funko.name}:latest"
|
|
|
|
puts ex
|
|
|
|
next
|
|
|
|
end
|
2024-06-29 15:29:53 +00:00
|
|
|
|
2024-06-29 17:36:11 +00:00
|
|
|
latest_image = images[0]
|
|
|
|
|
|
|
|
# Filter by name so only faaso-thisfunko are affected from now on
|
|
|
|
# sorted newer image first
|
|
|
|
containers = docker_api.containers.list(
|
|
|
|
all: true,
|
|
|
|
filters: {"name" => [container_name]}
|
|
|
|
).sort { |a, b| (images.index(b.@image_id) || 9999) <=> (images.index(a.@image_id) || 9999) }
|
|
|
|
|
|
|
|
# If it's already up, do nothing
|
2024-06-29 15:29:53 +00:00
|
|
|
if containers.any? { |container|
|
2024-06-29 17:36:11 +00:00
|
|
|
is_running = container.@state == "running"
|
|
|
|
is_old = container.@image_id != latest_image
|
|
|
|
p! container.@image_id
|
|
|
|
puts "Warning: running outdated version" if is_running && is_old
|
|
|
|
is_running
|
2024-06-28 23:57:58 +00:00
|
|
|
}
|
2024-06-29 17:36:11 +00:00
|
|
|
puts "#{funko.name} is already up"
|
2024-06-29 15:29:53 +00:00
|
|
|
next
|
2024-06-28 23:07:27 +00:00
|
|
|
end
|
|
|
|
|
2024-06-28 23:57:04 +00:00
|
|
|
# If it is paused, unpause it
|
2024-06-29 15:29:53 +00:00
|
|
|
paused = containers.select { |container|
|
2024-06-29 17:36:11 +00:00
|
|
|
container.@state == "paused"
|
2024-06-28 23:57:04 +00:00
|
|
|
}
|
2024-06-28 23:57:58 +00:00
|
|
|
if paused.size > 0
|
2024-06-28 23:57:04 +00:00
|
|
|
puts "Resuming existing paused container"
|
|
|
|
docker_api.containers.unpause(paused[0].@id)
|
2024-06-29 15:29:53 +00:00
|
|
|
next
|
2024-06-28 23:57:04 +00:00
|
|
|
end
|
|
|
|
|
2024-06-28 23:07:27 +00:00
|
|
|
# If it is exited, start it
|
2024-06-29 15:29:53 +00:00
|
|
|
existing = containers.select { |container|
|
2024-06-29 17:36:11 +00:00
|
|
|
container.@state == "exited"
|
2024-06-28 23:07:27 +00:00
|
|
|
}
|
|
|
|
|
2024-06-29 15:29:53 +00:00
|
|
|
puts "Starting function #{funko.name}"
|
2024-06-28 23:57:58 +00:00
|
|
|
if existing.size > 0
|
2024-06-28 23:07:27 +00:00
|
|
|
puts "Restarting existing exited container"
|
|
|
|
docker_api.containers.start(existing[0].@id)
|
2024-06-29 15:29:53 +00:00
|
|
|
next
|
2024-06-28 23:07:27 +00:00
|
|
|
end
|
2024-06-28 23:57:04 +00:00
|
|
|
|
2024-06-29 18:19:56 +00:00
|
|
|
# Deploy from scratch
|
2024-06-29 21:58:49 +00:00
|
|
|
Faaso.setup_network # We need it
|
2024-06-28 23:57:04 +00:00
|
|
|
puts "Creating new container"
|
|
|
|
conf = Docr::Types::CreateContainerConfig.new(
|
2024-06-29 17:36:11 +00:00
|
|
|
image: "#{funko.name}:latest",
|
2024-06-29 15:29:53 +00:00
|
|
|
hostname: funko.name,
|
2024-06-29 01:10:35 +00:00
|
|
|
# Port in the container side
|
2024-06-29 15:29:53 +00:00
|
|
|
exposed_ports: {"#{funko.port}/tcp" => {} of String => String},
|
2024-06-29 01:10:35 +00:00
|
|
|
host_config: Docr::Types::HostConfig.new(
|
2024-06-29 21:58:49 +00:00
|
|
|
network_mode: "faaso-net",
|
2024-06-29 15:29:53 +00:00
|
|
|
port_bindings: {"#{funko.port}/tcp" => [Docr::Types::PortBinding.new(
|
|
|
|
host_port: "", # Host port, empty means random
|
2024-06-29 01:10:35 +00:00
|
|
|
host_ip: "127.0.0.1", # Host IP
|
|
|
|
)]}
|
|
|
|
)
|
2024-06-28 23:57:04 +00:00
|
|
|
)
|
2024-06-29 15:29:53 +00:00
|
|
|
|
2024-06-29 17:36:11 +00:00
|
|
|
response = docker_api.containers.create(name: container_name, config: conf)
|
2024-06-29 15:35:03 +00:00
|
|
|
response.@warnings.each { |msg| puts "Warning: #{msg}" }
|
2024-06-28 23:57:04 +00:00
|
|
|
docker_api.containers.start(response.@id)
|
2024-06-29 18:19:56 +00:00
|
|
|
containers = docker_api.containers.list(
|
|
|
|
all: true,
|
|
|
|
filters: {"name" => [container_name]}
|
|
|
|
)
|
|
|
|
|
|
|
|
(1..5).each { |i|
|
|
|
|
break if containers[0].state == "running"
|
|
|
|
sleep 0.1.seconds
|
|
|
|
}
|
|
|
|
if containers[0].state != "running"
|
|
|
|
puts "Container for #{funko.name} is not running yet"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
puts "Container for #{funko.name} is running"
|
|
|
|
public_port = containers[0].@ports[0].@public_port
|
|
|
|
# TODO: Map route in reverse proxy to function
|
2024-06-28 17:09:58 +00:00
|
|
|
end
|
2024-06-28 23:57:04 +00:00
|
|
|
# TODO: Run test for healthcheck
|
|
|
|
# TODO: Return function URL for testing
|
2024-06-28 17:09:58 +00:00
|
|
|
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
|
2024-06-29 17:36:11 +00:00
|
|
|
|
|
|
|
class Deploy
|
|
|
|
@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: Everything
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2024-06-28 17:09:58 +00:00
|
|
|
end
|
2024-06-28 15:41:21 +00:00
|
|
|
end
|