Compare commits

..

4 Commits

Author SHA1 Message Date
6ca518ae32 Refactored funko module 2024-07-02 19:08:14 -03:00
2a6f64a53e Reorg secrets code 2024-07-02 19:03:29 -03:00
1648f2f99e Fix HTML 2024-07-02 19:02:05 -03:00
227c770b61 Some more frontend 2024-07-02 17:39:18 -03:00
10 changed files with 317 additions and 254 deletions

View File

@ -3,7 +3,7 @@ build: shard.yml $(wildcard src/**/*cr)
proxy: build
docker build . -t faaso-proxy --no-cache
start-proxy:
docker run --network=faaso-net -v /var/run/docker.sock:/var/run/docker.sock -v secrets:/home/app/secrets -p 8888:8888 faaso-proxy
docker run --name faaso_proxy --rm --network=faaso-net -v /var/run/docker.sock:/var/run/docker.sock -v secrets:/home/app/secrets -p 8888:8888 faaso-proxy
.PHONY: build proxy-image start-proxy

View File

@ -1,15 +1,14 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://matcha.mizu.sh/matcha.css" />
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<body>
<script>
async function getFunkos() {
const response = await fetch("funkos/");
funkos = await response.json();
console.log(funkos);
}
getFunkos();
</script>
<header class="container">
<h1>Your Funko Collection</h1>
</header>
<main class=container>
<button hx-get="funkos/?format=html" hx-target="#funko-list">Update</button>
<table>
<thead>
@ -20,6 +19,7 @@
</tr>
<tbody id="funko-list">
</tbody>
</thead>
</thead>
</main>
</body>
</head>

37
src/daemon/funko.cr Normal file
View File

@ -0,0 +1,37 @@
require "docr"
require "kemal"
require "../funko.cr"
module Funko
extend self
get "/funkos/" do |env|
funkos = Funko.from_docker
funkos.sort! { |a, b| a.name <=> b.name }
result = [] of Hash(String, String)
funkos.each do |funko|
state = ""
case funko
when .running?
state = "running"
when .paused?
state = "paused"
else
state = "stopped"
end
result << {
"name" => funko.name,
"state" => state,
"status" => funko.status,
}
end
if env.params.query.fetch("format", "json") == "html"
render "src/views/funkos.ecr"
else
result.to_json
end
end
end

View File

@ -1,17 +0,0 @@
require "docr"
require "kemal"
require "../funko.cr"
module Funkos
get "/funkos/" do |env|
funkos : Array(Funko) = Funko.from_docker
funkos.sort! { |a, b| a.name <=> b.name }
if env.params.query.fetch("format", "json") == "html"
render "src/views/funkos.ecr"
else
funkos.to_json
end
end
end

View File

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

View File

@ -1,44 +1,10 @@
require "kemal"
require "../secrets.cr"
module Secrets
SECRETS = Hash(String, String).new
SECRET_PATH = "./secrets/"
extend self
# TODO: sanitize all inputs
# Store secrets in a tree of files
def self.update_secrets
# Save new secrets
SECRETS.map do |_name, value|
funko, name = _name.split("-", 2)
funko_dir = Path.new(SECRET_PATH, funko)
Dir.mkdir_p(funko_dir)
File.write(Path.new(funko_dir, name), value)
end
# Delete secrets not in the hash
Dir.glob(Path.new(SECRET_PATH, "*")).each do |funko_dir|
funko = File.basename(funko_dir)
Dir.glob(Path.new(funko_dir, "*")).each do |secret_file|
name = File.basename(secret_file)
unless SECRETS.has_key?("#{funko}-#{name}")
File.delete(secret_file)
end
end
end
end
# Load secrets from the disk
def self.load_secrets
Dir.glob(Path.new(SECRET_PATH, "*")).each do |funko_dir|
funko = File.basename(funko_dir)
Dir.glob(Path.new(funko_dir, "*")).each do |secret_file|
name = File.basename(secret_file)
value = File.read(secret_file)
SECRETS["#{funko}-#{name}"] = value
end
end
end
# Gets a secret in form {"name": "funko_name-secret_name", "value": "secret_value"}
post "/secrets/" do |env|
name = env.params.json["name"].as(String)

View File

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

View File

@ -3,210 +3,226 @@ require "file_utils"
require "yaml"
# A funko, built from its source metadata
class Funko
include YAML::Serializable
module Funko
extend self
# Required, the name of the funko. Must be unique across FaaSO
property name : String
class Funko
include YAML::Serializable
# if Nil, it has no template whatsoever
property runtime : (String | Nil)? = nil
# Required, the name of the funko. Must be unique across FaaSO
property name : String
# Extra operating system packages shipped with the runtime's Docker image
property ship_packages : Array(String) = [] of String
# if Nil, it has no template whatsoever
property runtime : (String | Nil)? = nil
# Extra operating system packages used only when *building* the funko
property devel_packages : Array(String) = [] of String
# Extra operating system packages shipped with the runtime's Docker image
property ship_packages : Array(String) = [] of String
# Where this is located in the filesystem
@[YAML::Field(ignore: true)]
property path : String = ""
# Extra operating system packages used only when *building* the funko
property devel_packages : Array(String) = [] of String
# Healthcheck properties
property healthcheck_options : String = "--interval=1m --timeout=2s --start-period=2s --retries=3"
property healthcheck_command : String = "curl --fail http://localhost:3000/ping || exit 1"
# Where this is located in the filesystem
@[YAML::Field(ignore: true)]
property path : String = ""
def _to_context
{
"name" => name,
"ship_packages" => ship_packages,
"devel_packages" => devel_packages,
"healthcheck_options" => healthcheck_options,
"healthcheck_command" => healthcheck_command,
}
end
# Healthcheck properties
property healthcheck_options : String = "--interval=1m --timeout=2s --start-period=2s --retries=3"
property healthcheck_command : String = "curl --fail http://localhost:3000/ping || exit 1"
def to_json(json : JSON::Builder)
json.object do
json.field("name", name)
json.field("ship_packages", ship_packages)
json.field("devel_packages", devel_packages)
json.field("healthcheck_options", healthcheck_options)
json.field("healthcheck_command", healthcheck_command)
def _to_context
{
"name" => name,
"ship_packages" => ship_packages,
"devel_packages" => devel_packages,
"healthcheck_options" => healthcheck_options,
"healthcheck_command" => healthcheck_command,
}
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
def to_json(json : JSON::Builder)
json.object do
json.field("name", name)
json.field("ship_packages", ship_packages)
json.field("devel_packages", devel_packages)
json.field("healthcheck_options", healthcheck_options)
json.field("healthcheck_command", healthcheck_command)
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
# 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
# Get all the funkos docker knows about.
def self.from_docker : Array(Funko)
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-")
# Get all the funkos docker knows about.
def self.from_docker : Array(Funko)
docker_api = Docr::API.new(Docr::Client.new)
names = [] of String
funko_containers = docker_api.containers.list(
all: true,
).each { |container|
p! container.@names
container.@names.each { |name|
names << name.split("-", 2)[1].lstrip("/") if name.starts_with?("/faaso-")
}
}
}
pp! names
from_names(names.to_a)
end
# Setup the target directory `path` with all the files needed
# to build a docker image
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|
pp! names
from_names(names.to_a.sort!)
end
# Setup the target directory `path` with all the files needed
# to build a docker image
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)
}
# 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
# Build image using docker in path previously prepared using `prepare_build`
def build(path : Path)
docker_api = Docr::API.new(Docr::Client.new)
docker_api.images.build(
context: path.to_s,
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
# 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)
}
end
# Is any instance of this funko paused?
def paused?
self.containers.any? { |container|
container.@state == "paused"
}
end
# Build image using docker in path previously prepared using `prepare_build`
def build(path : Path)
docker_api = Docr::API.new(Docr::Client.new)
docker_api.images.build(
context: path.to_s,
tags: ["faaso-#{name}:latest"]) { |x| Log.info { x } }
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
# 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
# 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
# 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

42
src/secrets.cr Normal file
View File

@ -0,0 +1,42 @@
module Secrets
extend self
SECRETS = Hash(String, String).new
SECRET_PATH = "./secrets/"
# TODO: sanitize all inputs
# Store secrets in a tree of files
def update_secrets
# Save new secrets
SECRETS.map do |_name, value|
funko, name = _name.split("-", 2)
funko_dir = Path.new(SECRET_PATH, funko)
Dir.mkdir_p(funko_dir)
File.write(Path.new(funko_dir, name), value)
end
# Delete secrets not in the hash
Dir.glob(Path.new(SECRET_PATH, "*")).each do |funko_dir|
funko = File.basename(funko_dir)
Dir.glob(Path.new(funko_dir, "*")).each do |secret_file|
name = File.basename(secret_file)
unless SECRETS.has_key?("#{funko}-#{name}")
File.delete(secret_file)
end
end
end
end
# Load secrets from the disk
def load_secrets
Dir.glob(Path.new(SECRET_PATH, "*")).each do |funko_dir|
funko = File.basename(funko_dir)
Dir.glob(Path.new(funko_dir, "*")).each do |secret_file|
name = File.basename(secret_file)
value = File.read(secret_file)
SECRETS["#{funko}-#{name}"] = value
end
end
end
end
Secrets.load_secrets

View File

@ -1,7 +1,26 @@
<%- funkos.each do |funko| -%>
<%- result.each do |f| -%>
<tr>
<td><%= funko.name %></td>
<td>sarasa</td>
<td><button>DOSTUFF</button></td>
<td><%= f["name"] %></td>
<td><%= f["status"] %></td>
<td>
<%- if f["state"] == "running" -%>
<button state="disabled">Start</button>
<button>Pause</button>
<button>Stop</button>
<button>Restart</button>
<%- end -%>
<%- if f["state"] == "paused" -%>
<button>Start</button>
<button disabled>Pause</button>
<button>Stop</button>
<button>Restart</button>
<%- end -%>
<%- if f["state"] == "stopped" -%>
<button>Start</button>
<button disabled>Pause</button>
<button disabled>Stop</button>
<button disabled>Restart</button>
<%- end -%>
</td>
</tr>
<%- end -%>