Refactored funko module

This commit is contained in:
Roberto Alsina 2024-07-02 19:08:14 -03:00
parent 2a6f64a53e
commit 6ca518ae32
5 changed files with 210 additions and 190 deletions

View File

@ -2,7 +2,9 @@ require "docr"
require "kemal" require "kemal"
require "../funko.cr" require "../funko.cr"
module Funkos module Funko
extend self
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 }
@ -20,8 +22,9 @@ module Funkos
end end
result << { result << {
"name" => funko.name, "name" => funko.name,
"state" => state, "state" => state,
"status" => funko.status,
} }
end end

View File

@ -1,4 +1,4 @@
require "./funkos.cr" require "./funko.cr"
require "./proxyconf.cr" require "./proxyconf.cr"
require "./secrets.cr" require "./secrets.cr"
require "compress/gzip" require "compress/gzip"

View File

@ -38,7 +38,7 @@ module Faaso
end end
def run def run
funkos = Funko.from_paths(@arguments) funkos = Funko::Funko.from_paths(@arguments)
local = @options.@bool["local"] local = @options.@bool["local"]
if local if local
@ -120,7 +120,7 @@ module Faaso
end end
def run def run
funkos = Funko.from_names(@arguments) funkos = Funko::Funko.from_names(@arguments)
funkos.each do |funko| funkos.each do |funko|
local = @options.@bool["local"] local = @options.@bool["local"]
@ -188,7 +188,7 @@ module Faaso
end end
def run def run
funkos = Funko.from_paths(@arguments) funkos = Funko::Funko.from_paths(@arguments)
funkos.each do |funko| funkos.each do |funko|
# Create temporary build location # Create temporary build location
dst_path = Path.new("export", funko.name) dst_path = Path.new("export", funko.name)

View File

@ -3,209 +3,226 @@ require "file_utils"
require "yaml" require "yaml"
# A funko, built from its source metadata # A funko, built from its source metadata
class Funko module Funko
include YAML::Serializable extend self
# Required, the name of the funko. Must be unique across FaaSO class Funko
property name : String include YAML::Serializable
# if Nil, it has no template whatsoever # Required, the name of the funko. Must be unique across FaaSO
property runtime : (String | Nil)? = nil property name : String
# Extra operating system packages shipped with the runtime's Docker image # if Nil, it has no template whatsoever
property ship_packages : Array(String) = [] of String property runtime : (String | Nil)? = nil
# Extra operating system packages used only when *building* the funko # Extra operating system packages shipped with the runtime's Docker image
property devel_packages : Array(String) = [] of String property ship_packages : Array(String) = [] of String
# Where this is located in the filesystem # Extra operating system packages used only when *building* the funko
@[YAML::Field(ignore: true)] property devel_packages : Array(String) = [] of String
property path : String = ""
# Healthcheck properties # Where this is located in the filesystem
property healthcheck_options : String = "--interval=1m --timeout=2s --start-period=2s --retries=3" @[YAML::Field(ignore: true)]
property healthcheck_command : String = "curl --fail http://localhost:3000/ping || exit 1" property path : String = ""
def _to_context # Healthcheck properties
{ property healthcheck_options : String = "--interval=1m --timeout=2s --start-period=2s --retries=3"
"name" => name, property healthcheck_command : String = "curl --fail http://localhost:3000/ping || exit 1"
"ship_packages" => ship_packages,
"devel_packages" => devel_packages,
"healthcheck_options" => healthcheck_options,
"healthcheck_command" => healthcheck_command,
}
end
def to_json(json : JSON::Builder) def _to_context
json.object do {
json.field("name", name) "name" => name,
json.field("ship_packages", ship_packages) "ship_packages" => ship_packages,
json.field("devel_packages", devel_packages) "devel_packages" => devel_packages,
json.field("healthcheck_options", healthcheck_options) "healthcheck_options" => healthcheck_options,
json.field("healthcheck_command", healthcheck_command) "healthcheck_command" => healthcheck_command,
}
end end
end
# Create an Array of funkos from an Array of folders containing definitions def to_json(json : JSON::Builder)
def self.from_paths(paths : Array(String | Path)) : Array(Funko) json.object do
paths.map { |path| Path.new(path, "funko.yml") } json.field("name", name)
.select { |path| File.exists?(path) } json.field("ship_packages", ship_packages)
.map { |path| json.field("devel_packages", devel_packages)
f = Funko.from_yaml(File.read(path.to_s)) json.field("healthcheck_options", healthcheck_options)
f.path = path.parent.to_s json.field("healthcheck_command", healthcheck_command)
f end
end
# Create an Array of funkos from an Array of folders containing definitions
def self.from_paths(paths : Array(String | Path)) : Array(Funko)
paths.map { |path| Path.new(path, "funko.yml") }
.select { |path| File.exists?(path) }
.map { |path|
f = Funko.from_yaml(File.read(path.to_s))
f.path = path.parent.to_s
f
}
end
# Create an array of funkos just from names. These are limited in function
# and can't call `prepare_build` or some other functionality
def self.from_names(names : Array(String)) : Array(Funko)
names.map { |name|
Funko.from_yaml("name: #{name}")
} }
end end
# Create an array of funkos just from names. These are limited in function # Get all the funkos docker knows about.
# and can't call `prepare_build` or some other functionality def self.from_docker : Array(Funko)
def self.from_names(names : Array(String)) : Array(Funko) docker_api = Docr::API.new(Docr::Client.new)
names.map { |name| names = [] of String
Funko.from_yaml("name: #{name}") funko_containers = docker_api.containers.list(
} all: true,
end ).each { |container|
p! container.@names
# Get all the funkos docker knows about. container.@names.each { |name|
def self.from_docker : Array(Funko) names << name.split("-", 2)[1].lstrip("/") if name.starts_with?("/faaso-")
docker_api = Docr::API.new(Docr::Client.new) }
names = Set(String).new
docker_api.images.list(all: true).select { |i|
next if i.@repo_tags.nil?
i.@repo_tags.as(Array(String)).each { |tag|
names << tag.split(":", 2)[0].split("-", 2)[1] if tag.starts_with?("faaso-")
} }
}
from_names(names.to_a)
end
# Setup the target directory `path` with all the files needed pp! names
# to build a docker image
def prepare_build(path : Path) from_names(names.to_a.sort!)
# Copy runtime if requested end
if !runtime.nil?
runtime_dir = Path.new("runtimes", runtime.as(String)) # Setup the target directory `path` with all the files needed
raise Exception.new("Error: runtime #{runtime} not found for funko #{name} in #{path}") unless File.exists?(runtime_dir) # to build a docker image
Dir.glob("#{runtime_dir}/*").each { |src| def prepare_build(path : Path)
# Copy runtime if requested
if !runtime.nil?
runtime_dir = Path.new("runtimes", runtime.as(String))
raise Exception.new("Error: runtime #{runtime} not found for funko #{name} in #{path}") unless File.exists?(runtime_dir)
Dir.glob("#{runtime_dir}/*").each { |src|
FileUtils.cp_r(src, path)
}
# Replace templates with processed files
context = _to_context
Dir.glob("#{path}/**/*.j2").each { |template|
dst = template[..-4]
File.open(dst, "w") do |file|
file << Crinja.render(File.read(template), context)
end
File.delete template
}
end
# Copy funko
raise Exception.new("Internal error: empty funko path for #{name}") if self.path.empty?
Dir.glob("#{self.path}/*").each { |src|
FileUtils.cp_r(src, path) FileUtils.cp_r(src, path)
} }
# Replace templates with processed files end
context = _to_context
Dir.glob("#{path}/**/*.j2").each { |template| # Build image using docker in path previously prepared using `prepare_build`
dst = template[..-4] def build(path : Path)
File.open(dst, "w") do |file| docker_api = Docr::API.new(Docr::Client.new)
file << Crinja.render(File.read(template), context) docker_api.images.build(
end context: path.to_s,
File.delete template tags: ["faaso-#{name}:latest"]) { |x| Log.info { x } }
end
# Return a list of image IDs for this funko, most recent first
def image_history
docker_api = Docr::API.new(Docr::Client.new)
begin
docker_api.images.history(
name: "faaso-#{name}"
).sort { |i, j| j.@created <=> i.@created }.map(&.@id)
rescue ex : Docr::Errors::DockerAPIError
Log.error { "#{ex}" }
[] of String
end
end
# 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}"]}
)
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 end
# Copy funko # Is any instance of this funko paused?
raise Exception.new("Internal error: empty funko path for #{name}") if self.path.empty? def paused?
Dir.glob("#{self.path}/*").each { |src| self.containers.any? { |container|
FileUtils.cp_r(src, path) container.@state == "paused"
} }
end end
# Build image using docker in path previously prepared using `prepare_build` # Unpause paused container with the newer image
def build(path : Path) def unpause
docker_api = Docr::API.new(Docr::Client.new) docker_api = Docr::API.new(Docr::Client.new)
docker_api.images.build( images = self.image_history
context: path.to_s, paused = self.containers.select { |container|
tags: ["faaso-#{name}:latest"]) { |x| Log.info { x } } container.@state == "paused"
end }.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
# Return a list of image IDs for this funko, most recent first # Is any instance of this funko exited?
def image_history def exited?
docker_api = Docr::API.new(Docr::Client.new) self.containers.any? { |container|
begin container.@state == "exited"
docker_api.images.history( }
name: "faaso-#{name}" end
).sort { |i, j| j.@created <=> i.@created }.map(&.@id)
rescue ex : Docr::Errors::DockerAPIError # Restart exited container with the newer image
Log.error { "#{ex}" } def start
[] of String # FIXME refactor DRY with unpause
docker_api = Docr::API.new(Docr::Client.new)
images = self.image_history
exited = self.containers.select { |container|
container.@state == "exited"
}.sort! { |i, j|
(images.index(j.@image_id) || 9999) <=> (images.index(i.@image_id) || 9999)
}
docker_api.containers.restart(exited[0].@id) unless exited.empty?
end
# Create a container for this funko
def create_container(autostart : Bool = true) : String
secrets_mount = "#{Dir.current}/secrets/#{name}"
Dir.mkdir_p(secrets_mount)
conf = Docr::Types::CreateContainerConfig.new(
image: "faaso-#{name}:latest",
hostname: name,
# Port in the container side
host_config: Docr::Types::HostConfig.new(
network_mode: "faaso-net",
mounts: [
Docr::Types::Mount.new(
source: secrets_mount,
target: "/secrets",
type: "bind"
),
]
)
)
docker_api = Docr::API.new(Docr::Client.new)
response = docker_api.containers.create(name: "faaso-#{name}", config: conf)
response.@warnings.each { |msg| Log.warn { msg } }
docker_api.containers.start(response.@id) if autostart
response.@id
end end
end end
# 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}"]}
)
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
# 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
# Restart exited container with the newer image
def start
# FIXME refactor DRY with unpause
docker_api = Docr::API.new(Docr::Client.new)
images = self.image_history
exited = self.containers.select { |container|
container.@state == "exited"
}.sort! { |i, j|
(images.index(j.@image_id) || 9999) <=> (images.index(i.@image_id) || 9999)
}
docker_api.containers.restart(exited[0].@id) unless exited.empty?
end
# Create a container for this funko
def create_container(autostart : Bool = true) : String
secrets_mount = "#{Dir.current}/secrets/#{name}"
Dir.mkdir_p(secrets_mount)
conf = Docr::Types::CreateContainerConfig.new(
image: "faaso-#{name}:latest",
hostname: name,
# Port in the container side
host_config: Docr::Types::HostConfig.new(
network_mode: "faaso-net",
mounts: [
Docr::Types::Mount.new(
source: secrets_mount,
target: "/secrets",
type: "bind"
),
]
)
)
docker_api = Docr::API.new(Docr::Client.new)
response = docker_api.containers.create(name: "faaso-#{name}", config: conf)
response.@warnings.each { |msg| Log.warn { msg } }
docker_api.containers.start(response.@id) if autostart
response.@id
end
end end

View File

@ -1,7 +1,7 @@
<%- result.each do |f| -%> <%- result.each do |f| -%>
<tr> <tr>
<td><%= f["name"] %></td> <td><%= f["name"] %></td>
<td><%= f["state"] %></td> <td><%= f["status"] %></td>
<td> <td>
<%- if f["state"] == "running" -%> <%- if f["state"] == "running" -%>
<button state="disabled">Start</button> <button state="disabled">Start</button>