Compare commits

..

28 Commits

Author SHA1 Message Date
21893fe612 Beginnings of making frontend pretty, switched to pico css 2024-07-09 20:37:43 -03:00
665b4f9ab7 Fix proxy config confusion 2024-07-09 16:40:15 -03:00
3b2297e954 Housekeeping 2024-07-09 13:23:21 -03:00
4f4daf5943 Types tweak 2024-07-09 13:23:16 -03:00
2d333c3df1 * More robust stderr/stdout mixing when
running commandsserver side
* More robust streaming responses client side
2024-07-09 13:01:37 -03:00
f015afe7f0 Ensure network exists for start-proxy 2024-07-09 10:57:36 -03:00
81ec077928 Use base58 2024-07-08 22:34:38 -03:00
afb6e8df0c Dockerfile linter 2024-07-08 22:34:27 -03:00
50e8ff7e56 add check to ensure config/funkos is empty 2024-07-08 22:34:07 -03:00
6489ec0dc2 Use base58 for random strings 2024-07-08 22:33:52 -03:00
56ff326098 Lint 2024-07-08 22:02:47 -03:00
80ea5d4fde add check to ensure config/funkos is empty 2024-07-08 21:53:57 -03:00
67c37a49e1 Refactored log setup onto separate project 2024-07-08 21:27:40 -03:00
ef8f5c357f Some type 2024-07-08 17:42:33 -03:00
a1a141c77e ignore faaso config 2024-07-08 17:41:37 -03:00
77b1539776 Some types 2024-07-08 17:41:11 -03:00
eea98ff8f6 todo management 2024-07-08 15:20:12 -03:00
a2d65d4b5e Cleanup tmp directory after build 2024-07-08 15:18:06 -03:00
0358744b46 Remove template correctly in Runtime.runtime_files 2024-07-08 13:51:39 -03:00
3378899612 todo management 2024-07-08 13:39:37 -03:00
a896f2e032 Rethought login 2024-07-08 13:34:54 -03:00
fe52566872 Faaso deploy command 2024-07-08 11:46:40 -03:00
62d66a5286 Faaso deploy command 2024-07-08 11:46:33 -03:00
2face37b19 todo management 2024-07-08 11:24:55 -03:00
c59bf87bbd oops 2024-07-08 11:16:26 -03:00
8e6ec620aa Faaso deploy command 2024-07-08 11:16:03 -03:00
5e7764ca9f todo management 2024-07-08 07:41:06 -03:00
d93c8518da Minor refactor 2024-07-08 07:37:01 -03:00
37 changed files with 434 additions and 295 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ export/
secrets/ secrets/
.rucksack .rucksack
.rucksack.toc .rucksack.toc
.faaso.yml

3
.hadolint.yml Normal file
View File

@ -0,0 +1,3 @@
ignored:
- DL3018
- DL3059

38
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,38 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shellcheck
- id: markdownlint
exclude: '^content'
- repo: https://github.com/mrtazz/checkmake
rev: 0.2.2
hooks:
- id: checkmake
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.23.2
hooks:
- id: check-github-workflows
- repo: local
hooks:
- id: empty-funkos
name: empty-funkos
entry: test ! -s config/funkos
language: system
pass_filenames: false
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint-docker
exclude: 'j2$'

View File

@ -1,5 +1,13 @@
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as build FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3.20 AS build
RUN apk update && apk add crystal shards yaml-dev openssl-dev zlib-dev libxml2-dev make && apk cache clean RUN apk add --no-cache \
crystal \
shards \
yaml-dev \
openssl-dev \
zlib-dev \
libxml2-dev \
make
RUN rm -rf /var/cache/apk/*
RUN addgroup -S app && adduser app -S -G app RUN addgroup -S app && adduser app -S -G app
WORKDIR /home/app WORKDIR /home/app
COPY shard.yml Makefile ./ COPY shard.yml Makefile ./
@ -9,8 +17,22 @@ COPY runtimes/ runtimes/
RUN make RUN make
# RUN strip bin/* # RUN strip bin/*
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as ship FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3.20 AS ship
RUN apk update && apk add caddy nss-tools multirun docker openssl zlib yaml pcre2 gc libevent libgcc libxml2 ttyd && apk cache clean RUN apk add --no-cache \
caddy \
nss-tools \
multirun \
docker \
openssl \
zlib \
yaml \
pcre2 \
gc \
libevent \
libgcc \
libxml2 \
ttyd
RUN rm -rf /var/cache/apk/*
# Unprivileged user # Unprivileged user
RUN addgroup -S app && adduser app -S -G app RUN addgroup -S app && adduser app -S -G app
@ -21,7 +43,6 @@ COPY public/ public/
COPY --from=build /home/app/bin/faaso-daemon /home/app/bin/faaso /usr/bin/ COPY --from=build /home/app/bin/faaso-daemon /home/app/bin/faaso /usr/bin/
# Mount points for persistent data # Mount points for persistent data
RUN mkdir /secrets RUN mkdir /secrets /config
RUN mkdir /config
CMD ["/usr/bin/multirun", "-v", "faaso-daemon", "caddy run --config config/Caddyfile"] CMD ["/usr/bin/multirun", "-v", "faaso-daemon", "caddy run --config config/Caddyfile"]

View File

@ -2,9 +2,13 @@ build: shard.yml $(wildcard src/**/*) $(runtimes/**/*)
shards build -d --error-trace shards build -d --error-trace
cat .rucksack >> bin/faaso cat .rucksack >> bin/faaso
cat .rucksack >> bin/faaso-daemon cat .rucksack >> bin/faaso-daemon
proxy: build proxy:
docker build . -t faaso-proxy docker build . -t faaso-proxy
all: build proxy
start-proxy: start-proxy:
docker network create faaso-net || true
docker run --name faaso-proxy-one \ docker run --name faaso-proxy-one \
--rm --network=faaso-net \ --rm --network=faaso-net \
-e FAASO_SECRET_PATH=${PWD}/secrets \ -e FAASO_SECRET_PATH=${PWD}/secrets \
@ -13,5 +17,10 @@ start-proxy:
-v ${PWD}/config:/home/app/config \ -v ${PWD}/config:/home/app/config \
-p 8888:8888 faaso-proxy -p 8888:8888 faaso-proxy
test:
crystal spec
.PHONY: build proxy-image start-proxy clean:
rm bin/*
.PHONY: all build proxy-image start-proxy test clean

View File

@ -50,12 +50,14 @@ This will give you:
You need a server, with docker. In that server, build it as explained above. You need a server, with docker. In that server, build it as explained above.
You can run the `faaso-proxy` with something like this: You can run the `faaso-proxy` with something like this:
``` ```shell
docker run --network=faaso-net -v /var/run/docker.sock:/var/run/docker.sock -p 8888:8888 faaso-proxy docker run --network=faaso-net \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8888:8888 faaso-proxy
``` ```
That will give `faaso-proxy` access to your docker, and expose the functionality in That will give `faaso-proxy` access to your docker, and expose the functionality
port 8888. in port 8888.
## What it Does ## What it Does
@ -65,7 +67,8 @@ In FaaSO you (the user) can create Funkos. Funkos are the moral equivalent of AW
lambdas and whatever they are called in other systems. In short, they are simple lambdas and whatever they are called in other systems. In short, they are simple
programs that handle web requests. programs that handle web requests.
For example, here is a `hello world` level funko written using Crystal, a file called `funko.cr`: For example, here is a `hello world` level funko written using Crystal,
a file called `funko.cr`:
```crystal ```crystal
get "/" do get "/" do
@ -82,7 +85,8 @@ runtime: crystal
``` ```
If you have those two files in a folder, that folder is a funko, which is called If you have those two files in a folder, that folder is a funko, which is called
`hello` and FaaSO knows it's written in Crystal. In fact, it knows (because the crystal runtime explains that, don't worry about it yet) that it's part of an `hello` and FaaSO knows it's written in Crystal. In fact, it knows (because the
crystal runtime explains that, don't worry about it yet) that it's part of an
application written in the [Kemal framework](https://kemalcr.com/) and it knows application written in the [Kemal framework](https://kemalcr.com/) and it knows
how to create a whole container which runs the app, and how to check its health, how to create a whole container which runs the app, and how to check its health,
and so on. and so on.
@ -93,13 +97,14 @@ The full details of how to write funkos are still in flux, so not documenting
it for now. Eventually, you will be able to just write the parts you it for now. Eventually, you will be able to just write the parts you
need to write to create funkos in different languages. It's easy! need to write to create funkos in different languages. It's easy!
### So what can a funko do? ### So what can a funko do
Once you have a funko, you can *build* it, which will give you a docker image. Once you have a funko, you can *build* it, which will give you a docker image.
```faaso build --local myfunko/``` ```faaso build --local myfunko/```
Or you can export it and get rid of all the mistery of how your funko **really** works: Or you can export it and get rid of all the mistery of how your funko
**really** works:
```faaso export myfunko/ myfuko-exported``` ```faaso export myfunko/ myfuko-exported```
@ -116,9 +121,9 @@ than one, although currently only one is used by the proxy.
The proxy has a few goals: The proxy has a few goals:
1) You can connect to it using `faaso` and have it build/run/etc your funkos. 1) You can connect to it using `faaso` and have it build/run/etc your funkos.
* This builds the funko in your machine: `faaso build -l myfunko/` * This builds the funko in your machine: `faaso build -l myfunko/`
* This builds the funko in the server pointed at by FAASO_SERVER: `faaso build myfunko/` * This builds the funko in the server pointed at by FAASO_SERVER:
`faaso build myfunko/`
Yes, they are exactly the same thing. In fact, if you don't use the `-l` flag, Yes, they are exactly the same thing. In fact, if you don't use the `-l` flag,
faaso just tells the proxy "hey proxy, run *your* copy of faaso over there and faaso just tells the proxy "hey proxy, run *your* copy of faaso over there and
@ -145,4 +150,4 @@ beyond bugfixes, since I am redesigning things all the time.
## Contributors ## Contributors
- [Roberto Alsina](https://github.com/ralsina) - creator and maintainer * [Roberto Alsina](https://github.com/ralsina) - creator and maintainer

32
TODO.md
View File

@ -1,4 +1,6 @@
# Things that need doing before first release # TODO LIST
## Things that need doing before first release
* User flow for initial proxy setup * User flow for initial proxy setup
* ✅ Setting up password * ✅ Setting up password
@ -10,30 +12,32 @@
* ✅ Crystal + Kemal * ✅ Crystal + Kemal
* ✅ Python + Flask * ✅ Python + Flask
* ✅ Nodejs + Express * ✅ Nodejs + Express
* Document * Create a site
* How to create a runtime * Document
* How to create a funko * FaaSO for app developers
* How to setup the proxy * FaaSO for runtime developers
* FaaSO server setup
* APIs * APIs
* Sanitize all inputs * Sanitize all inputs
* ✅ Streaming responses in slow operations like scaling down * ✅ Streaming responses in slow operations like scaling down
or building or building
* Make more things configurable / remove hardcoded stuff * Make more things configurable / remove hardcoded stuff
* ✅ Make server take options from file * ✅ Make server take options from file
* ✅ Make server take options from environment * ✅ Make server take options from environment
* ✅ Make server password configurable * ✅ Make server password configurable
* admin/admin auth client side [WIP, broke everything] * admin/admin auth client side
* `faaso login` is not working properly yet with proxy * `faaso login` is not working properly yet with proxy
* CD for binaries and images for at least arm64/x86 * CD for binaries and images for at least arm64/x86
* Multi-container docker logs [faaso logs -f FUNKO] * Multi-container docker logs [faaso logs -f FUNKO]
*Direct error and above to stderr, others to stdout, *Configurable verbosity, support stderr/stdout split
while keeping logging level configurable
* ✅ Fix proxy reload / Make it reload on file changes * ✅ Fix proxy reload / Make it reload on file changes
* Implement `faaso help command` * Implement `faaso help command`
* Fix `export examples/hello_crystal` it has a `template/` * Fix `export examples/hello_crystal` it has a `template/`
* Implement zero-downtime rollout * Implement zero-downtime rollout (`faaso deploy`)
* Cleanup `tmp/` after use unless `DEBUG` is set * Cleanup `tmp/whatever` after use
*`faaso scale` remote is broken
* ✅ Setup linters/pre-commit/etc
# Things to do but not before release ## Things to do but not before release
* Propagate errors from `run_faaso` to the remote client * Propagate errors from `run_faaso` to the remote client

View File

@ -1,8 +0,0 @@
handle_path /faaso/hello/* {
reverse_proxy /* http://faaso-hello-9zth8p:3000 {
health_uri /ping
fail_duration 30s
}
}

View File

@ -38,7 +38,7 @@ up/downscaling, no multiple versions routed by header.
Specifically: no downscaling to zero. It makes everything MUCH Specifically: no downscaling to zero. It makes everything MUCH
more complicated. more complicated.
# Function structure ## Function structure
Example using crystal, but it could be anything. Any function has Example using crystal, but it could be anything. Any function has
an associated runtime, for example "crystal" or "python". an associated runtime, for example "crystal" or "python".
@ -69,7 +69,9 @@ Probably some `metadata.yml` that is *not* in the template.
* Files that should be copied along the function * Files that should be copied along the function
* Whatever * Whatever
# Implementation Ideas ## Implementation Ideas
* caddy for proxy? It's simple, fast, API-configurable. * caddy for proxy? It's simple, fast, API-configurable.
* Local docker registry for images? See https://www.docker.com/blog/how-to-use-your-own-registry-2/ (maybe later) * Local docker registry for images? See
[use own registry](https://www.docker.com/blog/how-to-use-your-own-registry-2/)
(maybe later)

View File

@ -15,21 +15,24 @@ Solution: start the funko on the server. Done. It's implemented.
## Variant 3: Deploy to the server and it's already running ## Variant 3: Deploy to the server and it's already running
1. If it's already running and it's running the latest image, then nothing to be done. 1. If it's already running and it's running the latest image, then nothing
2. It it's running and is not the latest, we can stop it and start with the latest image. to be done.
2. It it's running and is not the latest, we can stop it and start with the
latest image.
* Action 2 causes downtime. Usually it will not be significant, but it's there. * Action 2 causes downtime. Usually it will not be significant, but it's there.
* In the future it may be important to have zero downtime. * In the future it may be important to have zero downtime.
* We need to figure out what is implied by doing "zero downtime" to see if * We need to figure out what is implied by doing "zero downtime" to see if
not doing it now would make it impossible. not doing it now would make it impossible.
For zero downtime, we want to have two instances running, switch the proxy to the new For zero downtime, we want to have two instances running, switch the proxy
one, then stop the old one. to the new one, then stop the old one.
Currently it's impossible to run two instances because the container name is Currently it's impossible to run two instances because the container name is
faaso-funkoname, and we can't have 2 of those. faaso-funkoname, and we can't have 2 of those.
So: we could instead have faaso-funkoname-1, faaso-funkoname-2, etc. with some sort of suffix So: we could instead have faaso-funkoname-1, faaso-funkoname-2, etc.
with some sort of suffix
Changes implied in the faaso code: Changes implied in the faaso code:
@ -40,9 +43,3 @@ Changes implied in the faaso code:
* What happens if we have two instances with different images? * What happens if we have two instances with different images?
Answers coming up. Answers coming up.

View File

@ -49,10 +49,12 @@ faaso-net.
The proxy is the only container exposed to the host network, everything The proxy is the only container exposed to the host network, everything
else needs to be accessed through it. else needs to be accessed through it.
The faaso-proxy container will automatically proxy all requests so if you access the URL `http://faaso-proxy:8888/funko/hello/foo` that will be The faaso-proxy container will automatically proxy all requests so if
you access the URL `http://faaso-proxy:8888/funko/hello/foo` that will be
proxied to `/foo` in the `hello` funko. proxied to `/foo` in the `hello` funko.
This is all done via naming conventions. You can create your own `faaso-whatever` container, add it to the `faaso-net` and faaso will This is all done via naming conventions. You can create your own
`faaso-whatever` container, add it to the `faaso-net` and faaso will
happily consider it a funko. happily consider it a funko.
In the same way all funkos are simply docker containers running in that In the same way all funkos are simply docker containers running in that
@ -78,4 +80,3 @@ faaso-proxy -- GET /bar --> faaso-funko1
The dynamic proxying is achieved by reading the current state of The dynamic proxying is achieved by reading the current state of
Docker and just adapt to it using the naming conventions mentioned Docker and just adapt to it using the naming conventions mentioned
above. above.

View File

@ -31,7 +31,7 @@ So, the proxy can periodically examine its secret store and populate a folder
If on starting a funko we always do a bind mount of `/secrets/foo` to `/secrets` If on starting a funko we always do a bind mount of `/secrets/foo` to `/secrets`
then it will always have its secrets in place. then it will always have its secrets in place.
## Problem 2: how can the proxy know the secrets without keeping them in the image? ## Problem 2: how can the proxy know the secrets without keeping them in the image
They can't be shipped via the image, so they need to be injected via the admin API. They can't be shipped via the image, so they need to be injected via the admin API.

View File

@ -2,8 +2,8 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="stylesheet" href="https://matcha.mizu.sh/matcha.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css" />
<script src="https://unpkg.com/htmx.org@2.0.0"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css" /> <script src="https://unpkg.com/htmx.org@2.0.0"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
@ -11,15 +11,20 @@
<header class="container"> <header class="container">
<h1>FaaSO Admin Interface</h1> <h1>FaaSO Admin Interface</h1>
</header> </header>
<main class=container> <article>
<h2>Your Funko Collection <nav>
<button id="update-funkos" style="float:right; display:inline;" hx-trigger="load, click, every 60s" <ul>
<li><strong style="font-size: 200%;">Your Funko Collection</strong></li>
</ul>
<ul>
<li><button id="update-funkos" style="float:right; display:inline;" hx-trigger="load, click, every 60s"
hx-get="funkos/?format=html" hx-target="#funko-list"> hx-get="funkos/?format=html" hx-target="#funko-list">
Refresh Refresh
</button> </button>
</h2> </ul>
</nav>
<span id="message"></span> <span id="message"></span>
<table hx-target="#message"> <table hx-target="#message" class="striped">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@ -31,23 +36,27 @@
</tbody> </tbody>
</table> </table>
<div id="terminal" style="resize: vertical; overflow: auto;"></div> <div id="terminal" style="resize: vertical; overflow: auto;"></div>
</main> </article>
<script> <script>
update_funkos = function () { update_funkos = function () {
document.getElementById("update-funkos").click() document.getElementById("update-funkos").click()
} }
</script> </script>
<main class=container> <article>
<h2> <nav>
Your Secrets <ul>
<button id="update-secrets" style="float:right; display:inline;" hx-trigger="load, click, every 60s" <li><strong style="font-size: 200%;">Your Secrets</strong>
</ul>
<ul>
<li><button id="update-secrets" style="float:right; display:inline;" hx-trigger="load, click, every 60s"
hx-get="secrets/?format=html" hx-target="#secret-list"> hx-get="secrets/?format=html" hx-target="#secret-list">
Refresh Refresh
</button> </button>
<button style="float:right; display:inline;" onclick="show_new_secret()"> <li><button style="float:right; display:inline;" onclick="show_new_secret()">
Add Add
</button> </button>
</h2> </ul>
</nav>
<span id="message"></span> <span id="message"></span>
<table hx-target="#message"> <table hx-target="#message">
<thead> <thead>
@ -60,14 +69,20 @@
<tbody id="secret-list"> <tbody id="secret-list">
</tbody> </tbody>
<dialog id="add-secret"> <dialog id="add-secret">
<topic>New Secret</topic> <article>
<header>
New Secret
</header>
<form hx-post="secrets/"> <form hx-post="secrets/">
<input placeholder="funko name" id="new-secret-funko" name="funko"> <input placeholder="funko name" id="new-secret-funko" name="funko">
<input placeholder="secret name" id="new-secret-name" name="name"> <input placeholder="secret name" id="new-secret-name" name="name">
<input placeholder="secret value" type="password" id="new-secret-password" name="value"> <input placeholder="secret value" type="password" id="new-secret-password" name="value">
<button type="submit" hx-on:htmx:after-request="hide_new_secret()">CREATE</button> <fieldset role="group" style="text-align: right;">
<button style="width:9em; display: inline;" type="submit" hx-on:htmx:after-request="hide_new_secret()">CREATE</button>
<button style="width:9em; display: inline;" onclick="hide_new_secret(); close();" aria-label="Close" rel="prev">CLOSE</button>
</fieldset>
</form> </form>
<button onclick="hide_new_secret(); close();">CLOSE</button> </article>
</dialog> </dialog>
<script> <script>
update_secrets = function() { update_secrets = function() {
@ -87,5 +102,5 @@
update_secrets() update_secrets()
} }
</script> </script>
</main> </article>
</body> </body>

View File

@ -12,4 +12,3 @@ dependencies:
# development_dependencies: # development_dependencies:
# webmock: # webmock:
# github: manastech/webmock.cr # github: manastech/webmock.cr

View File

@ -4,6 +4,10 @@ shards:
git: https://github.com/sija/backtracer.cr.git git: https://github.com/sija/backtracer.cr.git
version: 1.2.2 version: 1.2.2
base58:
git: https://github.com/crystal-china/base58.cr.git
version: 0.1.0+git.commit.d1150d4a6f086013a475640ad00e561a2fe1082a
cr-config: cr-config:
git: https://github.com/crystal-community/cr-config.git git: https://github.com/crystal-community/cr-config.git
version: 5.1.0+git.commit.5eae3dfbf97da7dfa7c6e64a2a508069948518d3 version: 5.1.0+git.commit.5eae3dfbf97da7dfa7c6e64a2a508069948518d3
@ -40,10 +44,6 @@ shards:
git: https://github.com/mamantoha/http_proxy.git git: https://github.com/mamantoha/http_proxy.git
version: 0.10.3 version: 0.10.3
inotify:
git: https://github.com/petoem/inotify.cr.git
version: 1.0.3
kemal: kemal:
git: https://github.com/kemalcr/kemal.git git: https://github.com/kemalcr/kemal.git
version: 1.5.0 version: 1.5.0
@ -52,6 +52,10 @@ shards:
git: https://github.com/kemalcr/kemal-basic-auth.git git: https://github.com/kemalcr/kemal-basic-auth.git
version: 1.0.0 version: 1.0.0
oplog:
git: https://github.com/ralsina/oplog.git
version: 0.1.0+git.commit.70e3a7bbc2f1f4d75cf4e142244b263ee2844ba1
radix: radix:
git: https://github.com/luislavena/radix.git git: https://github.com/luislavena/radix.git
version: 0.4.1 version: 0.4.1
@ -59,4 +63,3 @@ shards:
rucksack: rucksack:
git: https://github.com/busyloop/rucksack.git git: https://github.com/busyloop/rucksack.git
version: 2.0.0 version: 2.0.0

View File

@ -15,6 +15,8 @@ crystal: ">= 1.12.2"
license: MIT license: MIT
dependencies: dependencies:
base58:
github: crystal-china/base58.cr
crest: crest:
github: mamantoha/crest github: mamantoha/crest
crinja: crinja:
@ -28,12 +30,12 @@ dependencies:
docr: docr:
github: ralsina/docr github: ralsina/docr
branch: add_exposed_ports branch: add_exposed_ports
inotify:
github: petoem/inotify.cr
kemal: kemal:
github: kemalcr/kemal github: kemalcr/kemal
kemal-basic-auth: kemal-basic-auth:
github: kemalcr/kemal-basic-auth github: kemalcr/kemal-basic-auth
oplog:
github: ralsina/oplog
rucksack: rucksack:
github: busyloop/rucksack github: busyloop/rucksack

View File

@ -1,3 +1,5 @@
require "base58"
module Faaso module Faaso
module Commands module Commands
# Build images for one or more funkos from source # Build images for one or more funkos from source
@ -7,7 +9,7 @@ module Faaso
# Create temporary build location # Create temporary build location
funkos.each do |funko| funkos.each do |funko|
tmp_dir = Path.new("tmp", UUID.random.to_s) tmp_dir = Path.new("tmp", Random.base58(8))
Dir.mkdir_p(tmp_dir) unless File.exists? tmp_dir Dir.mkdir_p(tmp_dir) unless File.exists? tmp_dir
funko.runtime = nil if options["--no-runtime"] funko.runtime = nil if options["--no-runtime"]
@ -16,7 +18,9 @@ module Faaso
if options["--local"] if options["--local"]
Log.info { "Building function... #{funko.name} in #{tmp_dir}" } Log.info { "Building function... #{funko.name} in #{tmp_dir}" }
funko.build tmp_dir funko.build tmp_dir
else # Running against a server FileUtils.rm_rf(tmp_dir)
next
end
Faaso.check_version Faaso.check_version
# Create a tarball for the funko # Create a tarball for the funko
buf = IO::Memory.new buf = IO::Memory.new
@ -38,7 +42,7 @@ module Faaso
end end
end end
end end
FileUtils.rm_rf(tmp_dir)
tmp = File.tempname tmp = File.tempname
File.open(tmp, "w") do |outf| File.open(tmp, "w") do |outf|
outf << buf outf << buf
@ -46,7 +50,6 @@ module Faaso
url = "#{Config.server}funkos/build/" url = "#{Config.server}funkos/build/"
begin
user, password = Config.auth user, password = Config.auth
Log.info { "Uploading funko to #{Config.server}" } Log.info { "Uploading funko to #{Config.server}" }
Log.info { "Starting remote build:" } Log.info { "Starting remote build:" }
@ -55,18 +58,13 @@ module Faaso
{"funko.tgz" => File.open(tmp), "name" => "funko.tgz"}, {"funko.tgz" => File.open(tmp), "name" => "funko.tgz"},
user: user, password: password user: user, password: password
) do |response| ) do |response|
loop do IO.copy(response.body_io, STDOUT)
Log.info { response.body_io.gets }
break if response.body_io.closed?
end
end end
Log.info { "Build finished successfully." } Log.info { "Build finished successfully." }
rescue ex : Crest::InternalServerError rescue ex : Crest::InternalServerError
Log.error(exception: ex) { "Error building funko #{funko.name} from #{funko.path}" } Log.error(exception: ex) { "Error building funko #{funko.name} from #{funko.path}" }
return 1 return 1
end end
end
end
0 0
end end
end end

59
src/commands/deploy.cr Normal file
View File

@ -0,0 +1,59 @@
module Faaso
module Commands
struct Deploy
def local(options, funko_name : String) : Int32
funko = Funko::Funko.from_names([funko_name])[0]
# Get scale, check for out-of-date containers
current_scale = funko.scale
latest_image = funko.latest_image
containers = funko.containers
out_of_date = containers.count { |container| container.image_id != latest_image }
Log.info { "Need to update #{out_of_date} containers" }
Log.info { "Scaling from #{current_scale} to #{current_scale + out_of_date}" }
# Increase scale to get enough up-to-date containers
new_containers = funko.scale(current_scale + out_of_date)
# Wait for them to be healthy
begin
funko.wait_for(current_scale + out_of_date, 120, healthy: true)
rescue ex : Exception
# Failed to start, rollback
Log.error(exception: ex) { "Failed to scale, rolling back" }
docker_api = Docr::API.new(Docr::Client.new)
new_containers.each do |container_id|
docker_api.containers.stop(container_id)
docker_api.containers.delete(container_id)
end
return 1
end
Log.info { "Scaling down to #{current_scale}" }
# Decrease scale to the desired amount
funko.scale(current_scale)
funko.wait_for(current_scale, 30)
Log.info { "Deployed #{funko_name}" }
0
end
def remote(options, funko_name : String) : Int32
user, password = Config.auth
Faaso.check_version
Crest.get(
"#{Config.server}funkos/#{funko_name}/deploy/", \
user: user, password: password) do |response|
IO.copy(response.body_io, STDOUT)
end
0
end
def run(options, funko_name : String) : Int32
Log.info { "Deploying #{funko_name}" }
if options["--local"]
local(options, funko_name)
else
remote(options, funko_name)
end
end
end
end
end

View File

@ -9,10 +9,28 @@ module Faaso
else else
password = STDIN.gets.to_s password = STDIN.gets.to_s
end end
# Testing with auth/ which is guaranteed locked # This is tricky. If the service is running behind a reverse proxy
Crest.get( # then /version is locked, but if it's not, only /auth is locked.
"#{server}auth/", \ # So we try /version first without a password, and if it succeeds
user: "admin", password: password).body # we try /auth with the password. If /version fails, we try /version
# with the password
#
begin
# Version without password.
Crest.get("#{server}version/")
# Auth with password
begin
Crest.get("#{server}auth/", user: "admin", password: password)
rescue ex : Crest::Unauthorized
# Failed with auth/
Log.error { "Wrong password" }
return 1
end
rescue ex : Crest::Unauthorized
# Version with password
Crest.get("#{server}version/", user: "admin", password: password)
end
# If we got here the password is ok # If we got here the password is ok
CONFIG.hosts[server] = {"admin", password} CONFIG.hosts[server] = {"admin", password}
Config.save Config.save

View File

@ -10,43 +10,37 @@ module Faaso
# In both cases stopped instances after the required # In both cases stopped instances after the required
# scale is reached are deleted. # scale is reached are deleted.
struct Scale struct Scale
def local(options, name, scale) : Int32 def local(options, name : String, scale : Int | Nil) : Int32
funko = Funko::Funko.from_names([name])[0] funko = Funko::Funko.from_names([name])[0]
# Asked about scale # Asked about scale
if funko.image_history.empty? if funko.image_history.empty?
Log.error { "Unknown funko #{funko.name}" } Log.error { "Unknown funko #{funko.name}" }
return 1 return 1
end end
if !scale if scale.nil?
Log.info { "Funko #{name} has a scale of #{funko.scale}" } Log.info { "Funko #{name} has a scale of #{funko.scale}" }
return 0 return 0
end end
# Asked to set scale # Asked to set scale
funko.scale(scale.as(String).to_i) funko.scale(scale)
0 0
end end
def remote(options, name, scale) : Int32 def remote(options, name : String, scale : Int | Nil) : Int32
user, password = Config.auth user, password = Config.auth
Faaso.check_version Faaso.check_version
if !scale if scale.nil?
Crest.get( Crest.get(
"#{Config.server}funkos/#{name}/scale/", \ "#{Config.server}funkos/#{name}/scale/", \
user: user, password: password) do |response| user: user, password: password) do |response|
loop do IO.copy(response.body_io, STDOUT)
Log.info { response.body_io.gets }
break if response.body_io.closed?
end end
return 0
end end
else
Crest.post( Crest.post(
"#{Config.server}funkos/#{name}/scale/", "#{Config.server}funkos/#{name}/scale/",
{"scale" => scale}, user: user, password: password) do |response| {"scale" => scale}, user: user, password: password) do |response|
loop do IO.copy(response.body_io, STDOUT)
Log.info { response.body_io.gets }
break if response.body_io.closed?
end
end
end end
0 0
rescue ex : Crest::InternalServerError rescue ex : Crest::InternalServerError
@ -54,7 +48,8 @@ module Faaso
1 1
end end
def run(options, name, scale) : Int32 def run(options, name : String, scale) : Int32
scale = scale.try &.to_s.to_i
if options["--local"] if options["--local"]
return local(options, name, scale) return local(options, name, scale)
end end

View File

@ -31,10 +31,7 @@ module Faaso
Crest.get( Crest.get(
"#{Config.server}funkos/#{name}/status/", \ "#{Config.server}funkos/#{name}/status/", \
user: user, password: password) do |response| user: user, password: password) do |response|
loop do IO.copy(response.body_io, STDOUT)
Log.info { response.body_io.gets }
break if response.body_io.closed?
end
end end
0 0
rescue ex : Crest::InternalServerError rescue ex : Crest::InternalServerError

View File

@ -20,7 +20,7 @@ class Config
end end
class ConfigAuthHandler < Kemal::BasicAuth::Handler class ConfigAuthHandler < Kemal::BasicAuth::Handler
only ["/auth"] only ["/auth", "/auth/*"]
def call(context) def call(context)
return call_next(context) unless only_match?(context) return call_next(context) unless only_match?(context)

View File

@ -1,3 +1,4 @@
require "base58"
require "docr" require "docr"
require "kemal" require "kemal"
require "../funko.cr" require "../funko.cr"
@ -24,12 +25,17 @@ module Funko
run_faaso(["scale", name, scale], env) run_faaso(["scale", name, scale], env)
end end
get "/funkos/:name/deploy" do |env|
name = env.params.url["name"]
run_faaso(["deploy", name], env)
end
# Build image for funko received as "funko.tgz" # Build image for funko received as "funko.tgz"
# TODO: This may take a while, consider using something like # TODO: This may take a while, consider using something like
# mosquito-cr/mosquito to make it a job queue # mosquito-cr/mosquito to make it a job queue
post "/funkos/build/" do |env| post "/funkos/build/" do |env|
# Create place to build funko # Create place to build funko
tmp_dir = Path.new("tmp", UUID.random.to_s) tmp_dir = Path.new("tmp", Random.base58(8))
Dir.mkdir_p(tmp_dir) unless File.exists? tmp_dir Dir.mkdir_p(tmp_dir) unless File.exists? tmp_dir
# Expand tarball in there # Expand tarball in there
@ -48,8 +54,9 @@ module Funko
# Build the thing # Build the thing
run_faaso(["build", tmp_dir.to_s, "--no-runtime"], env) run_faaso(["build", tmp_dir.to_s, "--no-runtime"], env)
ensure
FileUtils.rm_rf(tmp_dir) unless tmp_dir.nil?
end end
# Endpoints for the web frontend # Endpoints for the web frontend
# General status for the front page # General status for the front page
@ -126,20 +133,21 @@ module Funko
end end
# Helper to run faaso locally and respond via env # Helper to run faaso locally and respond via env
def run_faaso(args : Array(String), env) : Bool def run_faaso(args : Array(String), env)
Log.info { "Running faaso [#{args.join(", ")}, -l, 2>&1]" } args << "-l" # Always local in the server
Log.info { "Running faaso [#{args}" }
Process.run( Process.run(
command: "faaso", command: "faaso",
args: args + ["-l", "2>&1"], # Always local in the server args: args,
shell: true, env: {"FAASO_SERVER_SIDE" => "true"},
) do |process| ) do |process|
loop do loop do
env.response.print process.output.gets(chomp: false) data = process.output.gets(chomp: false)
env.response.print data
env.response.flush env.response.flush
Fiber.yield Fiber.yield # Without this the process never ends
break if process.terminated? break if process.terminated?
end end
true
end end
# FIXME: find a way to raise an exception on failure # FIXME: find a way to raise an exception on failure
# of the faaso process # of the faaso process

View File

@ -1,16 +1,11 @@
require "./funko.cr" require "./funko.cr"
require "docr" require "docr"
require "inotify"
require "kemal" require "kemal"
module Proxy module Proxy
CADDY_CONFIG_PATH = "config/funkos" CADDY_CONFIG_PATH = "config/Caddyfile"
@@current_config = File.read(CADDY_CONFIG_PATH) CADDY_CONFIG_FUNKOS = "config/funkos"
@@current_config = File.read(CADDY_CONFIG_FUNKOS)
@@watcher = Inotify.watch(CADDY_CONFIG_PATH) do |_|
Log.info { "Reloading caddy config" }
Process.run(command: "caddy", args: ["reload", "--config", CADDY_CONFIG_PATH])
end
# Get current proxy config # Get current proxy config
get "/proxy/" do get "/proxy/" do
@ -25,7 +20,7 @@ module Proxy
update_proxy_config update_proxy_config
end end
def self.update_proxy_config def self.update_proxy_config : Nil
docker_api = Docr::API.new(Docr::Client.new) docker_api = Docr::API.new(Docr::Client.new)
containers = docker_api.containers.list(all: true) containers = docker_api.containers.list(all: true)
@ -50,13 +45,13 @@ module Proxy
if @@current_config != config if @@current_config != config
Log.info { "Updating proxy config" } Log.info { "Updating proxy config" }
File.open(CADDY_CONFIG_PATH, "w") do |file| File.open(CADDY_CONFIG_FUNKOS, "w") do |file|
file << config file << config
end end
# Reload config # Reload config
@@current_config = config @@current_config = config
Process.run(command: "caddy", args: ["reload", "--config", CADDY_CONFIG_PATH])
end end
config
end end
end end

View File

@ -1,4 +1,5 @@
require "./commands/build.cr" require "./commands/build.cr"
require "./commands/deploy.cr"
require "./commands/export.cr" require "./commands/export.cr"
require "./commands/login.cr" require "./commands/login.cr"
require "./commands/new.cr" require "./commands/new.cr"

View File

@ -85,16 +85,22 @@ module Funko
end end
# Set the number of running instances of this funko # Set the number of running instances of this funko
def scale(new_scale : Int) # Returns the list of IDs started or stopped
def scale(new_scale : Int) : Array(String)
docker_api = Docr::API.new(Docr::Client.new) docker_api = Docr::API.new(Docr::Client.new)
current_scale = self.scale current_scale = self.scale
return if current_scale == new_scale result = [] of String
if current_scale == new_scale
Log.info { "Funko #{name} already at scale #{new_scale}" }
return result
end
Log.info { "Scaling #{name} from #{current_scale} to #{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 { (current_scale...new_scale).each {
Log.info { "Adding instance" } Log.info { "Adding instance" }
id = create_container result << (id = create_container)
start(id) start(id)
sleep 0.1.seconds sleep 0.1.seconds
} }
@ -105,6 +111,7 @@ module Funko
}.each { |container| }.each { |container|
Log.info { "Removing instance" } Log.info { "Removing instance" }
docker_api.containers.stop(container.@id) docker_api.containers.stop(container.@id)
result << container.@id
current_scale -= 1 current_scale -= 1
break if current_scale == new_scale break if current_scale == new_scale
sleep 0.1.seconds sleep 0.1.seconds
@ -116,6 +123,8 @@ module Funko
Log.info { "Pruning dead instance" } Log.info { "Pruning dead instance" }
docker_api.containers.delete(container.@id) docker_api.containers.delete(container.@id)
} }
result
end end
# Setup the target directory `path` with all the files needed # Setup the target directory `path` with all the files needed
@ -184,7 +193,7 @@ module Funko
docker_api.containers.list(all: true).select { |container| docker_api.containers.list(all: true).select { |container|
container.@names.any?(&.starts_with?("/faaso-#{name}-")) && container.@names.any?(&.starts_with?("/faaso-#{name}-")) &&
container.@state == "running" container.@state == "running"
} } || [] of Docr::Types::ContainerSummary
end end
# A comprehensive status for the funko: # A comprehensive status for the funko:
@ -207,12 +216,36 @@ module Funko
end end
end end
# Wait up to `t` seconds for the funko to reach the requested `state` # Wait up to `t` seconds for the funko to reach the desired scale
def wait_for(new_scale : Int, t) # If `healthy` is true, it will wait for the container to be declared
# healthy by the healthcheck
def wait_for(new_scale : Int, t : Int, healthy : Bool = false)
docker_api = Docr::API.new(Docr::Client.new)
channel = Channel(Nil).new channel = Channel(Nil).new
spawn do spawn do
loop do loop do
if healthy
channel.send(nil) if containers.select { |container|
begin
details = docker_api.containers.inspect(container.@id)
if details.nil?
false
elsif details.state.nil?
false
elsif details.state.as(Docr::Types::ContainerState).health.nil?
false
elsif details.state.as(Docr::Types::ContainerState).health.as(Docr::Types::Health).status == "healthy"
true
end
false
rescue ex : Docr::Errors::DockerAPIError
Log.error { "#{ex}" } unless ex.status_code == 304 # This just happens
false
end
} == new_scale
else
channel.send(nil) if scale == new_scale channel.send(nil) if scale == new_scale
end
sleep 0.2.seconds sleep 0.2.seconds
end end
end end
@ -221,7 +254,7 @@ module Funko
when channel.receive when channel.receive
Log.info { "Funko #{name} reached scale #{new_scale}" } Log.info { "Funko #{name} reached scale #{new_scale}" }
when timeout(t.seconds) when timeout(t.seconds)
Log.error { "Funko #{name} did not reach scale #{new_scale} in #{t} seconds" } raise Exception.new("Funko #{name} did not reach scale #{new_scale} in #{t} seconds")
end end
end end
@ -280,7 +313,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}-#{randstr}", config: conf) response = docker_api.containers.create(name: "faaso-#{name}-#{Random.base58(6)}", 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
@ -320,8 +353,3 @@ 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

@ -1,50 +0,0 @@
module Logging
extend self
class LogBackend < Log::IOBackend
@stdout = Log::IOBackend.new(io: STDOUT, formatter: LogFormat)
@stderr = Log::IOBackend.new(io: STDERR, formatter: LogFormat)
def write(entry : Log::Entry)
if entry.severity >= Log::Severity::Error
@stderr.write entry
else
@stdout.write entry
end
end
end
struct LogFormat < Log::StaticFormatter
@@colors = {
"FATAL" => :red,
"ERROR" => :red,
"WARN" => :yellow,
"NOTICE" => :yellow,
"INFO" => :green,
"DEBUG" => :blue,
"TRACE" => :light_blue,
}
def run
string "#{@entry.message}".colorize(@@colors[@entry.severity.label])
end
end
def self.setup(verbosity)
Colorize.on_tty_only!
verbosity = [0, verbosity].max
verbosity = [6, verbosity].min
severity = [
Log::Severity::Fatal,
Log::Severity::Error,
Log::Severity::Warn,
Log::Severity::Notice,
Log::Severity::Info,
Log::Severity::Debug,
Log::Severity::Trace,
][verbosity]
Log.setup(
severity,
LogBackend.new)
end
end

View File

@ -1,8 +1,8 @@
require "./config.cr" require "./config.cr"
require "./faaso.cr" require "./faaso.cr"
require "./log.cr"
require "colorize" require "colorize"
require "docopt" require "docopt"
require "oplog"
require "rucksack" require "rucksack"
macro version macro version
@ -14,6 +14,7 @@ FaaSO CLI tool.
Usage: Usage:
faaso build FOLDER ... [-v <level>] [-l] [--no-runtime] faaso build FOLDER ... [-v <level>] [-l] [--no-runtime]
faaso deploy FUNKO [-v <level>] [-l]
faaso export SOURCE DESTINATION [-v <level>] faaso export SOURCE DESTINATION [-v <level>]
faaso login [-v <level>] faaso login [-v <level>]
faaso new -r runtime FOLDER [-v <level>] faaso new -r runtime FOLDER [-v <level>]
@ -34,27 +35,28 @@ Options:
DOC DOC
ans = Docopt.docopt(doc, ARGV) ans = Docopt.docopt(doc, ARGV)
Logging.setup(ans["-v"].to_s.to_i) Oplog.setup(ans["-v"].to_s.to_i) unless ENV.fetch("FAASO_SERVER_SIDE", nil)
Log.debug { ans } Log.debug { ans }
status : Int32 = 0
case ans case ans
when .fetch("build", false) when .fetch("build", false)
status = Faaso::Commands::Build.new.run(ans, ans["FOLDER"].as(Array(String))) exit Faaso::Commands::Build.new.run(ans, ans["FOLDER"].as(Array(String)))
when .fetch("deploy", false)
exit Faaso::Commands::Deploy.new.run(ans, ans["FUNKO"].as(String))
when .fetch("export", false) when .fetch("export", false)
status = Faaso::Commands::Export.new.run(ans, ans["SOURCE"].as(String), ans["DESTINATION"].as(String)) exit Faaso::Commands::Export.new.run(ans, ans["SOURCE"].as(String), ans["DESTINATION"].as(String))
when .fetch("login", false) when .fetch("login", false)
status = Faaso::Commands::Login.new.run(ans) exit Faaso::Commands::Login.new.run(ans)
when .fetch("new", false) when .fetch("new", false)
status = Faaso::Commands::New.new.run(ans, ans["FOLDER"].as(Array(String))[0]) exit Faaso::Commands::New.new.run(ans, ans["FOLDER"].as(Array(String))[0])
when .fetch("scale", false) when .fetch("scale", false)
status = Faaso::Commands::Scale.new.run(ans, ans["FUNKO"].as(String), ans["SCALE"]) exit Faaso::Commands::Scale.new.run(ans, ans["FUNKO"].as(String), ans["SCALE"])
when .fetch("secret", false) when .fetch("secret", false)
status = Faaso::Commands::Secret.new.run(ans, ans["FUNKO"].as(String), ans["SECRET"].as(String)) exit Faaso::Commands::Secret.new.run(ans, ans["FUNKO"].as(String), ans["SECRET"].as(String))
when .fetch("status", false) when .fetch("status", false)
status = Faaso::Commands::Status.new.run(ans, ans["FUNKO"].as(String)) exit Faaso::Commands::Status.new.run(ans, ans["FUNKO"].as(String))
when .fetch("version", false) when .fetch("version", false)
Log.info { "#{version}" } Log.info { "#{version}" }
end end
exit(status) exit 0

View File

@ -23,7 +23,9 @@ module Runtime
if @@known.includes? "./runtimes/#{runtime}" if @@known.includes? "./runtimes/#{runtime}"
Log.info { "Using known runtime #{runtime}" } Log.info { "Using known runtime #{runtime}" }
runtime_base = "./runtimes/#{runtime}/" runtime_base = "./runtimes/#{runtime}/"
runtime_files = @@filelist.select(&.starts_with?(runtime_base)) runtime_files = @@filelist.select(&.starts_with?(runtime_base)).map { |path|
Path[path].normalize.to_s
}
elsif File.exists? runtime elsif File.exists? runtime
Log.info { "Using directory #{runtime} as runtime" } Log.info { "Using directory #{runtime} as runtime" }
runtime_base = "#{runtime}" runtime_base = "#{runtime}"

View File

@ -1,39 +1,32 @@
<%- result.each do |f| -%> <%- result.each do |f| -%>
<tr hx-indicator="#spinner-<%= f["name"] %>"> <tr hx-indicator="#spinner-<%= f["name"] %>">
<td> <td style="vertical-align: top;">
<%= f["name"] %> <%= f["name"] %>
<img id="spinner-<%= f["name"] %>" src="bars.svg" class="htmx-indicator"> <img id="spinner-<%= f["name"] %>" src="bars.svg" class="htmx-indicator">
</td> </td>
<td> <td style="vertical-align: top;">
<table>
<thead>
<th>ID</th>
<th>Current?</th>
<th>Actions</th>
</thead>
<tbody>
<%- f["containers"].as(Array(Docr::Types::ContainerSummary)).each do |c| -%> <%- f["containers"].as(Array(Docr::Types::ContainerSummary)).each do |c| -%>
<tr> <div class="grid">
<td><tt><%= c.@names[0].split("-")[-1] %></tt></td> <div>
<td> <tt><%= c.@names[0].split("-")[-1] %></tt>
</div>
<div>
<%- if c.image_id == f["latest_image"] -%> <%- if c.image_id == f["latest_image"] -%>
<span style="color:green;""> 🟢</span> <span style="color:green;""> 🟢</span>
<%- else -%> <%- else -%>
<span style="color:red;""> 🟢</span> <span style="color:red;""> 🟢</span>
<%- end -%> <%- end -%>
</td> </div>
<td> <div role="group">
<button hx-target="#terminal" hx-get="funkos/terminal/logs/<%= c.@names[0].lstrip("/") %>/">Logs</button> <button hx-target="#terminal" hx-get="funkos/terminal/logs/<%= c.@names[0].lstrip("/") %>/">Logs</button>
<button hx-target="#terminal" hx-get="funkos/terminal/shell/<%= c.@names[0].lstrip("/") %>/">Shell</button> <button hx-target="#terminal" hx-get="funkos/terminal/shell/<%= c.@names[0].lstrip("/") %>/">Shell</button>
</td> </div>
</tr> </div>
<%- end -%> <%- end -%>
</tbody>
</p>
</td> </td>
</table>
<td> <td>
<div role="group">
<%- if f["name"] == "proxy" -%> <%- if f["name"] == "proxy" -%>
<%- else -%> <%- else -%>
<%- if f["scale"].as(String).to_i > 0 -%> <%- if f["scale"].as(String).to_i > 0 -%>
@ -47,6 +40,7 @@
<%- end -%> <%- end -%>
<button hx-get="funkos/<%= f["name"] %>/restart" hx-on:htmx:after-request="update_funkos()">Restart</button> <button hx-get="funkos/<%= f["name"] %>/restart" hx-on:htmx:after-request="update_funkos()">Restart</button>
<%- end -%> <%- end -%>
</div>
</td> </td>
</tr> </tr>
<%- end -%> <%- end -%>