Compare commits
61 Commits
53986899c1
...
main
Author | SHA1 | Date | |
---|---|---|---|
21893fe612 | |||
665b4f9ab7 | |||
3b2297e954 | |||
4f4daf5943 | |||
2d333c3df1 | |||
f015afe7f0 | |||
81ec077928 | |||
afb6e8df0c | |||
50e8ff7e56 | |||
6489ec0dc2 | |||
56ff326098 | |||
80ea5d4fde | |||
67c37a49e1 | |||
ef8f5c357f | |||
a1a141c77e | |||
77b1539776 | |||
eea98ff8f6 | |||
a2d65d4b5e | |||
0358744b46 | |||
3378899612 | |||
a896f2e032 | |||
fe52566872 | |||
62d66a5286 | |||
2face37b19 | |||
c59bf87bbd | |||
8e6ec620aa | |||
5e7764ca9f | |||
d93c8518da | |||
379b4e2472 | |||
7104403585 | |||
47fcdda2d4 | |||
68c9960a58 | |||
52e461ec80 | |||
bcbf74bbaa | |||
bbe934f2b8 | |||
8228ea3233 | |||
f14a8d1c39 | |||
4aa307c65c | |||
2ddbda5a4f | |||
10775aeb11 | |||
6bd98d6792 | |||
7dd5248a6e | |||
31ab54478a | |||
3e4c940479 | |||
30e447dc8a | |||
3b45b6a28f | |||
495e5c350e | |||
c7188b87d7 | |||
824c94bebc | |||
889a5a2955 | |||
de46e9864b | |||
46ff8fc584 | |||
125870d0a8 | |||
6ff67e0190 | |||
b611ed199b | |||
86e2db39fb | |||
31509df8f7 | |||
c28239295f | |||
eedad69f72 | |||
bf8d86de9b | |||
43e1bb44b1 |
16
.ameba.yml
16
.ameba.yml
@ -1,19 +1,17 @@
|
|||||||
# This configuration file was generated by `ameba --gen-config`
|
# This configuration file was generated by `ameba --gen-config`
|
||||||
# on 2024-07-03 18:18:32 UTC using Ameba version 1.6.1.
|
# on 2024-07-07 14:44:11 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: 6
|
# Problems found: 5
|
||||||
# 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/secrets.cr
|
- src/secrets.cr
|
||||||
- src/daemon/main.cr
|
|
||||||
- src/daemon/secrets.cr
|
- src/daemon/secrets.cr
|
||||||
- src/daemon/funko.cr
|
- src/daemon/funko.cr
|
||||||
- src/funko.cr
|
|
||||||
- spec/faaso_spec.cr
|
- spec/faaso_spec.cr
|
||||||
Admonitions:
|
Admonitions:
|
||||||
- TODO
|
- TODO
|
||||||
@ -22,6 +20,16 @@ Documentation/DocumentationAdmonition:
|
|||||||
Enabled: true
|
Enabled: true
|
||||||
Severity: Warning
|
Severity: Warning
|
||||||
|
|
||||||
|
# Problems found: 1
|
||||||
|
# Run `ameba --only Lint/UselessAssign` for details
|
||||||
|
Lint/UselessAssign:
|
||||||
|
Description: Disallows useless variable assignments
|
||||||
|
ExcludeTypeDeclarations: false
|
||||||
|
Excluded:
|
||||||
|
- src/daemon/config.cr
|
||||||
|
Enabled: true
|
||||||
|
Severity: Warning
|
||||||
|
|
||||||
# Problems found: 3
|
# Problems found: 3
|
||||||
# Run `ameba --only Naming/BlockParameterName` for details
|
# Run `ameba --only Naming/BlockParameterName` for details
|
||||||
Naming/BlockParameterName:
|
Naming/BlockParameterName:
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ export/
|
|||||||
secrets/
|
secrets/
|
||||||
.rucksack
|
.rucksack
|
||||||
.rucksack.toc
|
.rucksack.toc
|
||||||
|
.faaso.yml
|
||||||
|
3
.hadolint.yml
Normal file
3
.hadolint.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ignored:
|
||||||
|
- DL3018
|
||||||
|
- DL3059
|
38
.pre-commit-config.yaml
Normal file
38
.pre-commit-config.yaml
Normal 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$'
|
33
Dockerfile
33
Dockerfile
@ -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"]
|
||||||
|
14
Makefile
14
Makefile
@ -2,17 +2,25 @@ 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 \
|
||||||
--env-file=proxy.env \
|
|
||||||
-e FAASO_SECRET_PATH=${PWD}/secrets \
|
-e FAASO_SECRET_PATH=${PWD}/secrets \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-v ${PWD}/secrets:/home/app/secrets \
|
-v ${PWD}/secrets:/home/app/secrets \
|
||||||
-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
|
||||||
|
37
README.md
37
README.md
@ -50,22 +50,25 @@ 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
|
||||||
|
|
||||||
### Funkos
|
### Funkos
|
||||||
|
|
||||||
In FaaSO you (the user) can create Funkos. Funkos are the moral equivalent of AWS
|
In FaaSO you (the user) can create Funkos. Funkos are the moral equivalent of AWS
|
||||||
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,24 +85,26 @@ 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.
|
||||||
|
|
||||||
But the funko has *the interesting bits* of the app.
|
But the funko has *the interesting bits* of the app.
|
||||||
|
|
||||||
The full details of how to write funkos are still in flux, so not documenting
|
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
|
||||||
@ -127,10 +132,10 @@ The proxy has a few goals:
|
|||||||
2) It automatically reverse-proxies to all funkos.
|
2) It automatically reverse-proxies to all funkos.
|
||||||
|
|
||||||
If you deployed a funko called `hello` and your faaso proxy is at
|
If you deployed a funko called `hello` and your faaso proxy is at
|
||||||
`http://myserver:8888` then the `/` path in your funko is at
|
`http://myserver:8888` then the `/` path in your funko is at
|
||||||
`http://myserver:8888/funko/hello/`
|
`http://myserver:8888/funko/hello/`
|
||||||
|
|
||||||
This proxying is automatic, you don't need to do anything. As long as you
|
This proxying is automatic, you don't need to do anything. As long as you
|
||||||
build the image for your funko in the server and then start the funko in the
|
build the image for your funko in the server and then start the funko in the
|
||||||
server? It should work.
|
server? It should work.
|
||||||
|
|
||||||
@ -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
|
||||||
|
52
TODO.md
52
TODO.md
@ -1,21 +1,43 @@
|
|||||||
# 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
|
||||||
* Setting up hostname for Caddy's automatic HTTPS
|
* Setting up hostname for Caddy's automatic HTTPS
|
||||||
* Config UI in frontend?
|
* Config UI in frontend?
|
||||||
* Polish frontend UI **A LOT**
|
* Polish frontend UI **A LOT**
|
||||||
* Version checks for consistency between client/server
|
* ✅ Version checks for consistency between client/server
|
||||||
* Have 3 runtimes:
|
* ✅ Have 3 runtimes:
|
||||||
* Crystal + Kemal ✅
|
* ✅ Crystal + Kemal
|
||||||
* Python + Flask [WIP]
|
* ✅ 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
|
||||||
* APIs
|
* FaaSO server setup
|
||||||
|
* APIs
|
||||||
* Sanitize all inputs
|
* Sanitize all inputs
|
||||||
* Streaming responses in slow operations
|
* ✅ Streaming responses in slow operations like scaling down
|
||||||
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 environment
|
||||||
|
* ✅ Make server password configurable
|
||||||
|
* ✅ admin/admin auth client side
|
||||||
|
* ✅ `faaso login` is not working properly yet with proxy
|
||||||
|
* CD for binaries and images for at least arm64/x86
|
||||||
|
* Multi-container docker logs [faaso logs -f FUNKO]
|
||||||
|
* ✅ Configurable verbosity, support stderr/stdout split
|
||||||
|
* ✅ Fix proxy reload / Make it reload on file changes
|
||||||
|
* Implement `faaso help command`
|
||||||
|
* ✅ Fix `export examples/hello_crystal` it has a `template/`
|
||||||
|
* ✅ Implement zero-downtime rollout (`faaso deploy`)
|
||||||
|
* ✅ Cleanup `tmp/whatever` after use
|
||||||
|
* ✅ `faaso scale` remote is broken
|
||||||
|
* ✅ Setup linters/pre-commit/etc
|
||||||
|
|
||||||
|
## Things to do but not before release
|
||||||
|
|
||||||
|
* Propagate errors from `run_faaso` to the remote client
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
http_port 8888
|
http_port 8888
|
||||||
https_port 8887
|
https_port 8887
|
||||||
local_certs
|
local_certs
|
||||||
}
|
}
|
||||||
|
|
||||||
http://*:8888 {
|
http://*:8888 {
|
||||||
basicauth /admin/* {
|
forward_auth /admin/* http://127.0.0.1:3000 {
|
||||||
admin {$HTTP_BASIC_AUTH_PASSWORD}
|
uri /auth
|
||||||
|
copy_headers {
|
||||||
|
Authorization
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_path /admin/terminal/* {
|
handle_path /admin/terminal/* {
|
||||||
reverse_proxy /* http://127.0.0.1:7681
|
reverse_proxy /* http://127.0.0.1:7681
|
||||||
}
|
}
|
||||||
handle_path /admin/* {
|
handle_path /admin/* {
|
||||||
reverse_proxy /* http://127.0.0.1:3000
|
reverse_proxy /* http://127.0.0.1:3000
|
||||||
}
|
}
|
||||||
|
import funkos
|
||||||
}
|
}
|
||||||
|
1
config/faaso.yml
Normal file
1
config/faaso.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
password: adminfoo
|
0
config/funkos
Normal file
0
config/funkos
Normal 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)
|
||||||
|
@ -15,34 +15,31 @@ 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:
|
||||||
|
|
||||||
* If we have two containers for one funko, we need to consider the "state" of
|
* If we have two containers for one funko, we need to consider the "state" of
|
||||||
the funko differently.
|
the funko differently.
|
||||||
* What does it mean to start/pause/stop a funko with two instances
|
* What does it mean to start/pause/stop a funko with two instances
|
||||||
* Do we want to enable two-instance funkos? With round-robin proxy?
|
* Do we want to enable two-instance funkos? With round-robin proxy?
|
||||||
* 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
# README
|
# Readme for Hello_crystal
|
||||||
|
|
||||||
This is the readme for people trying to *use* this runtime.
|
This is a funko using the Crystal runtime for [FaaSO](https://git.ralsina.me/ralsina/faaso)
|
||||||
|
|
||||||
|
## What is Hello_crystal
|
||||||
|
|
||||||
|
Write here what it is
|
||||||
|
|
||||||
|
## How to use Hello_crystal
|
||||||
|
|
||||||
|
And so on.
|
||||||
|
14
examples/hello_crystal/shard.yml
Normal file
14
examples/hello_crystal/shard.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
name: hello_crystal
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
targets:
|
||||||
|
funko:
|
||||||
|
main: main.cr
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
kemal:
|
||||||
|
github: kemalcr/kemal
|
||||||
|
|
||||||
|
# development_dependencies:
|
||||||
|
# webmock:
|
||||||
|
# github: manastech/webmock.cr
|
@ -1 +0,0 @@
|
|||||||
FAASO_PASSWORD=$2a$14$C35905PxPzICAZKc/O9jYOS7ipZNPBtrndja8Yu3bvs/UujckryHS
|
|
@ -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>
|
||||||
|
@ -10,7 +10,7 @@ RUN shards build --release
|
|||||||
RUN strip bin/*
|
RUN strip bin/*
|
||||||
|
|
||||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as ship
|
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as ship
|
||||||
RUN apk update && apk upgrade && apk add openssl pcre2 libgcc gc libevent curl {{ ship_packages | join " " }} && apk cache clean
|
RUN apk update && apk upgrade && apk add pcre2 libgcc gc libevent curl {{ ship_packages | join " " }} && apk cache clean
|
||||||
RUN addgroup -S app && adduser app -S -G app
|
RUN addgroup -S app && adduser app -S -G app
|
||||||
|
|
||||||
WORKDIR /home/app
|
WORKDIR /home/app
|
||||||
@ -19,4 +19,4 @@ USER app
|
|||||||
COPY --from=build /home/app/bin/funko .
|
COPY --from=build /home/app/bin/funko .
|
||||||
|
|
||||||
CMD ["./funko"]
|
CMD ["./funko"]
|
||||||
HEALTHCHECK {{ healthcheck_options }} CMD {{ healthcheck_command }}
|
HEALTHCHECK {{ healthcheck_options }} CMD {{ healthcheck_command }}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# README
|
# README
|
||||||
|
|
||||||
This is the readme for people wanting to change this runtime,
|
This is the readme for people wanting to change this runtime,
|
||||||
not for people trying to use it
|
not for people trying to use it
|
||||||
|
@ -12,4 +12,3 @@ dependencies:
|
|||||||
# development_dependencies:
|
# development_dependencies:
|
||||||
# webmock:
|
# webmock:
|
||||||
# github: manastech/webmock.cr
|
# github: manastech/webmock.cr
|
||||||
|
|
||||||
|
20
runtimes/express/Dockerfile.j2
Normal file
20
runtimes/express/Dockerfile.j2
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as build
|
||||||
|
|
||||||
|
RUN apk update && apk upgrade && apk add nodejs npm {{ ship_packages | join(" ") }} {{ devel_packages | join(" ") }} && apk cache clean
|
||||||
|
|
||||||
|
WORKDIR /home/app
|
||||||
|
|
||||||
|
COPY ./ ./
|
||||||
|
RUN npm i
|
||||||
|
|
||||||
|
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine as ship
|
||||||
|
RUN apk update && apk upgrade && apk add nodejs curl {{ ship_packages | join " " }} && apk cache clean
|
||||||
|
RUN addgroup -S app && adduser app -S -G app
|
||||||
|
|
||||||
|
WORKDIR /home/app
|
||||||
|
USER app
|
||||||
|
|
||||||
|
COPY --from=build /home/app/ .
|
||||||
|
|
||||||
|
CMD ["node", "funko.js"]
|
||||||
|
HEALTHCHECK {{ healthcheck_options }} CMD {{ healthcheck_command }}
|
15
runtimes/express/template/funko.js
Normal file
15
runtimes/express/template/funko.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const app = express()
|
||||||
|
const port = 3000
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Hello World!')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/ping', (req, res) => {
|
||||||
|
res.send('OK')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Example funko listening on port ${port}`)
|
||||||
|
})
|
2
runtimes/express/template/funko.yml.j2
Normal file
2
runtimes/express/template/funko.yml.j2
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
name: {{ name }}
|
||||||
|
runtime: {{ runtime }}
|
11
runtimes/express/template/package.json.j2
Normal file
11
runtimes/express/template/package.json.j2
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "{{name}}",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "funko.js",
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"description": "Example Funko",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.19.2"
|
||||||
|
}
|
||||||
|
}
|
@ -18,5 +18,5 @@ USER app
|
|||||||
|
|
||||||
COPY --from=build /home/app/ .
|
COPY --from=build /home/app/ .
|
||||||
|
|
||||||
CMD ["venv/bin/uwsgi", "--http", "0.0.0.0:3000", "--master", "-p", "4", "-w", "funko:app"]
|
CMD ["venv/bin/uwsgi", "--http", "0.0.0.0:3000", "--master", "-p", "1", "-w", "funko:app"]
|
||||||
HEALTHCHECK {{ healthcheck_options }} CMD {{ healthcheck_command }}
|
HEALTHCHECK {{ healthcheck_options }} CMD {{ healthcheck_command }}
|
||||||
|
@ -5,3 +5,7 @@ app = Flask("{{name}}")
|
|||||||
@app.route('/')
|
@app.route('/')
|
||||||
def handle(req):
|
def handle(req):
|
||||||
return "Hello World from Flask!"
|
return "Hello World from Flask!"
|
||||||
|
|
||||||
|
@app.route('/ping')
|
||||||
|
def handle(req):
|
||||||
|
return "OK"
|
||||||
|
@ -1 +1 @@
|
|||||||
flask
|
flask
|
||||||
|
19
shard.lock
19
shard.lock
@ -4,6 +4,14 @@ 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:
|
||||||
|
git: https://github.com/crystal-community/cr-config.git
|
||||||
|
version: 5.1.0+git.commit.5eae3dfbf97da7dfa7c6e64a2a508069948518d3
|
||||||
|
|
||||||
crest:
|
crest:
|
||||||
git: https://github.com/mamantoha/crest.git
|
git: https://github.com/mamantoha/crest.git
|
||||||
version: 1.3.13
|
version: 1.3.13
|
||||||
@ -22,7 +30,7 @@ shards:
|
|||||||
|
|
||||||
docr:
|
docr:
|
||||||
git: https://github.com/ralsina/docr.git
|
git: https://github.com/ralsina/docr.git
|
||||||
version: 0.1.1+git.commit.18f15cc7111b1d0c63347c7cca07aee9ec87a7a8
|
version: 0.1.1+git.commit.98a20178d5ae1391f1cd56e372530de6aa2b1ebc
|
||||||
|
|
||||||
exception_page:
|
exception_page:
|
||||||
git: https://github.com/crystal-loot/exception_page.git
|
git: https://github.com/crystal-loot/exception_page.git
|
||||||
@ -40,6 +48,14 @@ shards:
|
|||||||
git: https://github.com/kemalcr/kemal.git
|
git: https://github.com/kemalcr/kemal.git
|
||||||
version: 1.5.0
|
version: 1.5.0
|
||||||
|
|
||||||
|
kemal-basic-auth:
|
||||||
|
git: https://github.com/kemalcr/kemal-basic-auth.git
|
||||||
|
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
|
||||||
@ -47,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
|
||||||
|
|
||||||
|
12
shard.yml
12
shard.yml
@ -15,10 +15,14 @@ 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:
|
||||||
github: straight-shoota/crinja
|
github: straight-shoota/crinja
|
||||||
|
cr-config:
|
||||||
|
github: crystal-community/cr-config
|
||||||
crystar:
|
crystar:
|
||||||
github: naqvis/crystar
|
github: naqvis/crystar
|
||||||
docopt:
|
docopt:
|
||||||
@ -28,8 +32,12 @@ dependencies:
|
|||||||
branch: add_exposed_ports
|
branch: add_exposed_ports
|
||||||
kemal:
|
kemal:
|
||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
|
kemal-basic-auth:
|
||||||
|
github: kemalcr/kemal-basic-auth
|
||||||
|
oplog:
|
||||||
|
github: ralsina/oplog
|
||||||
rucksack:
|
rucksack:
|
||||||
github: busyloop/rucksack
|
github: busyloop/rucksack
|
||||||
|
|
||||||
scripts:
|
scripts:
|
||||||
postinstall: cat .rucksack >> bin/faaso
|
postinstall: cat .rucksack >> bin/faaso
|
||||||
|
@ -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,50 +18,52 @@ 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)
|
||||||
# Create a tarball for the funko
|
next
|
||||||
buf = IO::Memory.new
|
end
|
||||||
Compress::Gzip::Writer.open(buf) do |gzip|
|
Faaso.check_version
|
||||||
Crystar::Writer.open(gzip) do |tw|
|
# Create a tarball for the funko
|
||||||
Log.debug { "Adding files to tarball" }
|
buf = IO::Memory.new
|
||||||
Dir.glob("#{tmp_dir}/**/*").each do |path|
|
Compress::Gzip::Writer.open(buf) do |gzip|
|
||||||
next unless File.file? path
|
Crystar::Writer.open(gzip) do |tw|
|
||||||
rel_path = Path[path].relative_to tmp_dir
|
Log.debug { "Adding files to tarball" }
|
||||||
Log.debug { "Adding #{rel_path}" }
|
Dir.glob("#{tmp_dir}/**/*").each do |path|
|
||||||
file_info = File.info(path)
|
next unless File.file? path
|
||||||
hdr = Crystar::Header.new(
|
rel_path = Path[path].relative_to tmp_dir
|
||||||
name: rel_path.to_s,
|
Log.debug { "Adding #{rel_path}" }
|
||||||
mode: file_info.permissions.to_u32,
|
file_info = File.info(path)
|
||||||
size: file_info.size,
|
hdr = Crystar::Header.new(
|
||||||
)
|
name: rel_path.to_s,
|
||||||
tw.write_header(hdr)
|
mode: file_info.permissions.to_u32,
|
||||||
tw.write(File.read(path).to_slice)
|
size: file_info.size,
|
||||||
end
|
)
|
||||||
|
tw.write_header(hdr)
|
||||||
|
tw.write(File.read(path).to_slice)
|
||||||
end
|
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["output"] }
|
|
||||||
rescue ex : Crest::InternalServerError
|
|
||||||
Log.error(exception: ex) { "Error building funko #{funko.name} from #{funko.path}" }
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
FileUtils.rm_rf(tmp_dir)
|
||||||
|
tmp = File.tempname
|
||||||
|
File.open(tmp, "w") do |outf|
|
||||||
|
outf << buf
|
||||||
|
end
|
||||||
|
|
||||||
|
url = "#{Config.server}funkos/build/"
|
||||||
|
|
||||||
|
user, password = Config.auth
|
||||||
|
Log.info { "Uploading funko to #{Config.server}" }
|
||||||
|
Log.info { "Starting remote build:" }
|
||||||
|
Crest.post(
|
||||||
|
url,
|
||||||
|
{"funko.tgz" => File.open(tmp), "name" => "funko.tgz"},
|
||||||
|
user: user, password: password
|
||||||
|
) do |response|
|
||||||
|
IO.copy(response.body_io, STDOUT)
|
||||||
|
end
|
||||||
|
Log.info { "Build finished successfully." }
|
||||||
|
rescue ex : Crest::InternalServerError
|
||||||
|
Log.error(exception: ex) { "Error building funko #{funko.name} from #{funko.path}" }
|
||||||
|
return 1
|
||||||
end
|
end
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
|
59
src/commands/deploy.cr
Normal file
59
src/commands/deploy.cr
Normal 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
|
47
src/commands/login.cr
Normal file
47
src/commands/login.cr
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
module Faaso
|
||||||
|
module Commands
|
||||||
|
struct Login
|
||||||
|
def run(options) : Int32
|
||||||
|
server = Config.server
|
||||||
|
Log.info { "Enter password for #{server}" }
|
||||||
|
if STDIN.tty?
|
||||||
|
password = (STDIN.noecho &.gets.try &.chomp).to_s
|
||||||
|
else
|
||||||
|
password = STDIN.gets.to_s
|
||||||
|
end
|
||||||
|
# This is tricky. If the service is running behind a reverse proxy
|
||||||
|
# then /version is locked, but if it's not, only /auth is locked.
|
||||||
|
# So we try /version first without a password, and if it succeeds
|
||||||
|
# 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
|
||||||
|
CONFIG.hosts[server] = {"admin", password}
|
||||||
|
Config.save
|
||||||
|
0
|
||||||
|
rescue ex : Crest::Unauthorized
|
||||||
|
Log.error { "Wrong password" }
|
||||||
|
1
|
||||||
|
rescue ex : Socket::ConnectError
|
||||||
|
Log.error { "Connection refused" }
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -10,43 +10,46 @@ 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 !scale
|
if funko.image_history.empty?
|
||||||
|
Log.error { "Unknown funko #{funko.name}" }
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
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
|
||||||
if funko.image_history.empty?
|
funko.scale(scale)
|
||||||
Log.error { "Error: no images available for #{funko.name}:latest" }
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
funko.scale(scale.as(String).to_i)
|
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote(options, name, scale) : Int32
|
def remote(options, name : String, scale : Int | Nil) : Int32
|
||||||
if !scale
|
user, password = Config.auth
|
||||||
response = Crest.get(
|
Faaso.check_version
|
||||||
"#{FAASO_SERVER}funkos/#{name}/scale/", \
|
if scale.nil?
|
||||||
user: "admin", password: "admin")
|
Crest.get(
|
||||||
else
|
"#{Config.server}funkos/#{name}/scale/", \
|
||||||
response = Crest.post(
|
user: user, password: password) do |response|
|
||||||
"#{FAASO_SERVER}funkos/#{name}/scale/",
|
IO.copy(response.body_io, STDOUT)
|
||||||
{"scale" => scale}, user: "admin", password: "admin")
|
end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
Crest.post(
|
||||||
|
"#{Config.server}funkos/#{name}/scale/",
|
||||||
|
{"scale" => scale}, user: user, password: password) do |response|
|
||||||
|
IO.copy(response.body_io, STDOUT)
|
||||||
end
|
end
|
||||||
body = JSON.parse(response.body)
|
|
||||||
Log.info { body["output"] }
|
|
||||||
0
|
0
|
||||||
rescue ex : Crest::InternalServerError
|
rescue ex : Crest::InternalServerError
|
||||||
Log.error { "Error scaling funko #{name}" }
|
Log.error(exception: ex) { "Error scaling funko #{name}" }
|
||||||
body = JSON.parse(ex.response.body)
|
|
||||||
Log.info { body["output"] }
|
|
||||||
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
|
||||||
|
@ -13,19 +13,21 @@ module Faaso
|
|||||||
end
|
end
|
||||||
|
|
||||||
def remote(options, funko, name, secret) : Int32
|
def remote(options, funko, name, secret) : Int32
|
||||||
|
Faaso.check_version
|
||||||
|
user, password = Config.auth
|
||||||
if options["--add"]
|
if options["--add"]
|
||||||
Crest.post(
|
Crest.post(
|
||||||
"#{FAASO_SERVER}secrets/",
|
"#{Config.server}secrets/",
|
||||||
{
|
{
|
||||||
"funko" => funko,
|
"funko" => funko,
|
||||||
"name" => name,
|
"name" => name,
|
||||||
"value" => secret,
|
"value" => secret,
|
||||||
}, user: "admin", password: "admin")
|
}, user: user, password: password)
|
||||||
Log.info { "Secret created" }
|
Log.info { "Secret created" }
|
||||||
elsif options["--delete"]
|
elsif options["--delete"]
|
||||||
Crest.delete(
|
Crest.delete(
|
||||||
"#{FAASO_SERVER}secrets/#{funko}/#{name}",
|
"#{Config.server}secrets/#{funko}/#{name}",
|
||||||
user: "admin", password: "admin")
|
user: user, password: password)
|
||||||
end
|
end
|
||||||
0
|
0
|
||||||
rescue ex : Crest::RequestFailed
|
rescue ex : Crest::RequestFailed
|
||||||
|
@ -5,6 +5,11 @@ module Faaso
|
|||||||
funko = Funko::Funko.from_names([name])[0]
|
funko = Funko::Funko.from_names([name])[0]
|
||||||
status = funko.docker_status
|
status = funko.docker_status
|
||||||
|
|
||||||
|
if status.images.size == 0
|
||||||
|
Log.error { "Unkown funko: #{name}" }
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
Log.info { "Name: #{status.@name}" }
|
Log.info { "Name: #{status.@name}" }
|
||||||
Log.info { "Scale: #{status.scale}" }
|
Log.info { "Scale: #{status.scale}" }
|
||||||
|
|
||||||
@ -21,16 +26,16 @@ module Faaso
|
|||||||
end
|
end
|
||||||
|
|
||||||
def remote(options, name) : Int32
|
def remote(options, name) : Int32
|
||||||
response = Crest.get(
|
Faaso.check_version
|
||||||
"#{FAASO_SERVER}funkos/#{name}/status/", \
|
user, password = Config.auth
|
||||||
user: "admin", password: "admin")
|
Crest.get(
|
||||||
body = JSON.parse(response.body)
|
"#{Config.server}funkos/#{name}/status/", \
|
||||||
Log.info { body["output"] }
|
user: user, password: password) do |response|
|
||||||
|
IO.copy(response.body_io, STDOUT)
|
||||||
|
end
|
||||||
0
|
0
|
||||||
rescue ex : Crest::InternalServerError
|
rescue ex : Crest::InternalServerError
|
||||||
Log.error { "Error scaling funko #{name}" }
|
Log.error(exception: ex) { "Error scaling funko #{name}" }
|
||||||
body = JSON.parse(ex.response.body)
|
|
||||||
Log.info { body["output"] }
|
|
||||||
1
|
1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
44
src/config.cr
Normal file
44
src/config.cr
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
require "yaml"
|
||||||
|
|
||||||
|
CONFIG = Config.load
|
||||||
|
|
||||||
|
class Config
|
||||||
|
include YAML::Serializable
|
||||||
|
|
||||||
|
property hosts : Hash(String, {String, String}) = Hash(String, {String, String}).new
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@hosts = {} of String => {String, String}
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load : Config
|
||||||
|
if File.file? ".faaso.yml"
|
||||||
|
return Config.from_yaml(File.read(".faaso.yml"))
|
||||||
|
end
|
||||||
|
Config.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.save
|
||||||
|
File.open(".faaso.yml", "w") do |outf|
|
||||||
|
outf << CONFIG.to_yaml
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@@already_warned = false
|
||||||
|
|
||||||
|
def self.server : String
|
||||||
|
@@already_warned = true
|
||||||
|
url = ENV.fetch("FAASO_SERVER", nil)
|
||||||
|
if url.nil?
|
||||||
|
Log.warn { "FAASO_SERVER not set" } unless @@already_warned
|
||||||
|
url = "http://localhost:3000/"
|
||||||
|
end
|
||||||
|
url += "/" unless url.ends_with? "/"
|
||||||
|
Log.info { "Using server #{url}" } unless @@already_warned
|
||||||
|
url
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.auth : {String, String}
|
||||||
|
CONFIG.hosts.fetch(server, {"admin", ""})
|
||||||
|
end
|
||||||
|
end
|
47
src/daemon/config.cr
Normal file
47
src/daemon/config.cr
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
require "cr-config"
|
||||||
|
require "kemal-basic-auth"
|
||||||
|
|
||||||
|
class Config
|
||||||
|
include CrConfig
|
||||||
|
|
||||||
|
option password : String, default: "admin"
|
||||||
|
|
||||||
|
def self.load
|
||||||
|
builder = Config.new_builder
|
||||||
|
builder.providers do
|
||||||
|
[
|
||||||
|
CrConfig::Providers::SimpleFileProvider.new("config/faaso.yml"),
|
||||||
|
CrConfig::Providers::EnvVarProvider.new,
|
||||||
|
]
|
||||||
|
end
|
||||||
|
config = builder.build
|
||||||
|
Config.set_instance config
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ConfigAuthHandler < Kemal::BasicAuth::Handler
|
||||||
|
only ["/auth", "/auth/*"]
|
||||||
|
|
||||||
|
def call(context)
|
||||||
|
return call_next(context) unless only_match?(context)
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
# Ignored, just make the compiler happy
|
||||||
|
@credentials = Kemal::BasicAuth::Credentials.new({"foo" => "bar"})
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize?(value) : String?
|
||||||
|
username, password = Base64.decode_string(value[BASIC.size + 1..-1]).split(":")
|
||||||
|
if username == "admin" && password == Config.instance.password
|
||||||
|
username
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tie auth to config
|
||||||
|
|
||||||
|
add_handler ConfigAuthHandler.new
|
@ -1,3 +1,4 @@
|
|||||||
|
require "base58"
|
||||||
require "docr"
|
require "docr"
|
||||||
require "kemal"
|
require "kemal"
|
||||||
require "../funko.cr"
|
require "../funko.cr"
|
||||||
@ -8,39 +9,25 @@ module Funko
|
|||||||
# Get the funko's status
|
# Get the funko's status
|
||||||
get "/funkos/:name/status/" do |env|
|
get "/funkos/:name/status/" do |env|
|
||||||
name = env.params.url["name"]
|
name = env.params.url["name"]
|
||||||
response = run_faaso(["status", name])
|
run_faaso(["status", name], env)
|
||||||
|
|
||||||
if response["exit_code"] != 0
|
|
||||||
halt env, status_code: 500, response: response.to_json
|
|
||||||
else
|
|
||||||
response.to_json
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get the funko's scale
|
# Get the funko's scale
|
||||||
get "/funkos/:name/scale/" do |env|
|
get "/funkos/:name/scale/" do |env|
|
||||||
name = env.params.url["name"]
|
name = env.params.url["name"]
|
||||||
response = run_faaso(["scale", name])
|
run_faaso(["scale", name], env)
|
||||||
|
|
||||||
if response["exit_code"] != 0
|
|
||||||
halt env, status_code: 500, response: response.to_json
|
|
||||||
else
|
|
||||||
response.to_json
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set the funko's scale
|
# Set the funko's scale
|
||||||
post "/funkos/:name/scale/" do |env|
|
post "/funkos/:name/scale/" do |env|
|
||||||
name = env.params.url["name"]
|
name = env.params.url["name"]
|
||||||
scale = env.params.body["scale"].as(String)
|
scale = env.params.body["scale"].as(String)
|
||||||
response = run_faaso(["scale", name, scale])
|
run_faaso(["scale", name, scale], env)
|
||||||
if response["exit_code"] != 0
|
end
|
||||||
Log.error { response }
|
|
||||||
halt env, status_code: 500, response: response.to_json
|
get "/funkos/:name/deploy" do |env|
|
||||||
else
|
name = env.params.url["name"]
|
||||||
Log.info { response }
|
run_faaso(["deploy", name], env)
|
||||||
response.to_json
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Build image for funko received as "funko.tgz"
|
# Build image for funko received as "funko.tgz"
|
||||||
@ -48,7 +35,7 @@ module Funko
|
|||||||
# 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
|
||||||
@ -66,15 +53,10 @@ module Funko
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Build the thing
|
# Build the thing
|
||||||
response = run_faaso(["build", tmp_dir.to_s, "--no-runtime"])
|
run_faaso(["build", tmp_dir.to_s, "--no-runtime"], env)
|
||||||
|
ensure
|
||||||
if response["exit_code"] != 0
|
FileUtils.rm_rf(tmp_dir) unless tmp_dir.nil?
|
||||||
halt env, status_code: 500, response: response.to_json
|
|
||||||
else
|
|
||||||
response.to_json
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Endpoints for the web frontend
|
# Endpoints for the web frontend
|
||||||
|
|
||||||
# General status for the front page
|
# General status for the front page
|
||||||
@ -150,21 +132,24 @@ module Funko
|
|||||||
"<iframe src='terminal/' width='100%' height='100%'></iframe>"
|
"<iframe src='terminal/' width='100%' height='100%'></iframe>"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to run faaso locally and get a response back
|
# Helper to run faaso locally and respond via env
|
||||||
def run_faaso(args : Array(String))
|
def run_faaso(args : Array(String), env)
|
||||||
Log.info { "Running faaso [#{args.join(", ")}, -l]" }
|
args << "-l" # Always local in the server
|
||||||
output = IO::Memory.new
|
Log.info { "Running faaso [#{args}" }
|
||||||
status = Process.run(
|
Process.run(
|
||||||
command: "faaso",
|
command: "faaso",
|
||||||
args: args + ["-l"], # Always local in the server
|
args: args,
|
||||||
output: output,
|
env: {"FAASO_SERVER_SIDE" => "true"},
|
||||||
error: output,
|
) do |process|
|
||||||
)
|
loop do
|
||||||
Log.debug { "faaso output: #{output}" }
|
data = process.output.gets(chomp: false)
|
||||||
result = {
|
env.response.print data
|
||||||
"exit_code" => status.exit_code,
|
env.response.flush
|
||||||
"output" => output.to_s,
|
Fiber.yield # Without this the process never ends
|
||||||
}
|
break if process.terminated?
|
||||||
result
|
end
|
||||||
|
end
|
||||||
|
# FIXME: find a way to raise an exception on failure
|
||||||
|
# of the faaso process
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
require "./config.cr"
|
||||||
require "./funko.cr"
|
require "./funko.cr"
|
||||||
require "./proxy.cr"
|
require "./proxy.cr"
|
||||||
require "./secrets.cr"
|
require "./secrets.cr"
|
||||||
@ -8,8 +9,27 @@ require "docr"
|
|||||||
require "kemal"
|
require "kemal"
|
||||||
require "uuid"
|
require "uuid"
|
||||||
|
|
||||||
|
Config.load
|
||||||
|
|
||||||
|
macro version
|
||||||
|
"{{ `grep version shard.yml | cut -d: -f2` }}".strip()
|
||||||
|
end
|
||||||
|
|
||||||
get "/" do |env|
|
get "/" do |env|
|
||||||
env.redirect "/index.html"
|
env.redirect "/index.html"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/version" do
|
||||||
|
"#{version}"
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/auth" do
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/reload" do
|
||||||
|
Log.info { "Reloading configuration" }
|
||||||
|
Config.load
|
||||||
|
"Config reloaded"
|
||||||
|
end
|
||||||
|
|
||||||
Kemal.run
|
Kemal.run
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
require "docr"
|
|
||||||
require "./funko.cr"
|
require "./funko.cr"
|
||||||
|
require "docr"
|
||||||
require "kemal"
|
require "kemal"
|
||||||
|
|
||||||
|
|
||||||
module Proxy
|
module Proxy
|
||||||
CADDY_CONFIG_PATH = "config/Caddyfile"
|
CADDY_CONFIG_PATH = "config/Caddyfile"
|
||||||
@@current_config = File.read(CADDY_CONFIG_PATH)
|
CADDY_CONFIG_FUNKOS = "config/funkos"
|
||||||
|
@@current_config = File.read(CADDY_CONFIG_FUNKOS)
|
||||||
|
|
||||||
# Get current proxy config
|
# Get current proxy config
|
||||||
get "/proxy/" do
|
get "/proxy/" do
|
||||||
@ -20,31 +20,11 @@ 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)
|
||||||
config = <<-CONFIG
|
|
||||||
{
|
|
||||||
http_port 8888
|
|
||||||
https_port 8887
|
|
||||||
local_certs
|
|
||||||
}
|
|
||||||
|
|
||||||
http://*:8888 {
|
|
||||||
basicauth /admin/* {
|
|
||||||
admin {$HTTP_BASIC_AUTH_PASSWORD}
|
|
||||||
}
|
|
||||||
|
|
||||||
handle_path /admin/terminal/* {
|
|
||||||
reverse_proxy /* http://127.0.0.1:7681
|
|
||||||
}
|
|
||||||
handle_path /admin/* {
|
|
||||||
reverse_proxy /* http://127.0.0.1:3000
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG
|
|
||||||
|
|
||||||
|
config = ""
|
||||||
funkos = Funko::Funko.from_docker
|
funkos = Funko::Funko.from_docker
|
||||||
funkos.each do |funko|
|
funkos.each do |funko|
|
||||||
next if funko.name == "proxy"
|
next if funko.name == "proxy"
|
||||||
@ -62,22 +42,20 @@ CONFIG
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
config += "\n}"
|
|
||||||
|
|
||||||
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
|
||||||
Process.run(command: "caddy", args: ["reload", "--config", "Caddyfile"])
|
|
||||||
@@current_config = config
|
@@current_config = config
|
||||||
|
Process.run(command: "caddy", args: ["reload", "--config", CADDY_CONFIG_PATH])
|
||||||
end
|
end
|
||||||
config
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update proxy config once a second
|
# Update proxy config every 1 second (if changed)
|
||||||
spawn do
|
spawn do
|
||||||
loop do
|
loop do
|
||||||
Proxy.update_proxy_config
|
Proxy.update_proxy_config
|
||||||
|
@ -4,7 +4,7 @@ module Terminal
|
|||||||
@@terminal_process : Process | Nil = nil
|
@@terminal_process : Process | Nil = nil
|
||||||
|
|
||||||
def start_terminal(_args = ["sh"], readonly = true)
|
def start_terminal(_args = ["sh"], readonly = true)
|
||||||
args = ["-p", "7681", "-c", "admin:admin", "-o"]
|
args = ["-p", "7681", "-o"]
|
||||||
args += ["-W"] unless readonly
|
args += ["-W"] unless readonly
|
||||||
args += _args
|
args += _args
|
||||||
# We have a process there, kill it
|
# We have a process there, kill it
|
||||||
|
19
src/faaso.cr
19
src/faaso.cr
@ -1,5 +1,7 @@
|
|||||||
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/new.cr"
|
require "./commands/new.cr"
|
||||||
require "./commands/scale.cr"
|
require "./commands/scale.cr"
|
||||||
require "./commands/secret.cr"
|
require "./commands/secret.cr"
|
||||||
@ -11,9 +13,6 @@ require "docr/utils.cr"
|
|||||||
require "json"
|
require "json"
|
||||||
require "uuid"
|
require "uuid"
|
||||||
|
|
||||||
# API if you just ran faaso-daemon
|
|
||||||
FAASO_SERVER = ENV.fetch("FAASO_SERVER", "http://localhost:3000/")
|
|
||||||
|
|
||||||
# Functions as a Service, Ops!
|
# Functions as a Service, Ops!
|
||||||
module Faaso
|
module Faaso
|
||||||
VERSION = "0.1.0"
|
VERSION = "0.1.0"
|
||||||
@ -28,9 +27,19 @@ module Faaso
|
|||||||
))
|
))
|
||||||
rescue ex : Docr::Errors::DockerAPIError
|
rescue ex : Docr::Errors::DockerAPIError
|
||||||
raise ex if ex.status_code != 409 # Network already exists
|
raise ex if ex.status_code != 409 # Network already exists
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
module Commands
|
# Compare version with server's
|
||||||
|
def self.check_version
|
||||||
|
user, password = Config.auth
|
||||||
|
server_version = Crest.get(
|
||||||
|
"#{Config.server}version/", \
|
||||||
|
user: user, password: password).body
|
||||||
|
|
||||||
|
local_version = "#{version}"
|
||||||
|
|
||||||
|
if server_version != local_version
|
||||||
|
Log.warn { "Server is version #{server_version} and client is #{local_version}" }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
59
src/funko.cr
59
src/funko.cr
@ -85,25 +85,33 @@ 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
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
containers.select { |container| container.@state == "running" }.sort! { |i, j|
|
# Sort them older to newer, so we stop the oldest
|
||||||
|
containers.sort! { |i, j|
|
||||||
i.@created <=> j.@created
|
i.@created <=> j.@created
|
||||||
}.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
|
||||||
@ -115,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
|
||||||
@ -183,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:
|
||||||
@ -206,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
|
||||||
channel.send(nil) if scale == new_scale
|
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
|
||||||
|
end
|
||||||
sleep 0.2.seconds
|
sleep 0.2.seconds
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -220,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
|
||||||
|
|
||||||
@ -279,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
|
||||||
@ -319,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
|
|
||||||
|
69
src/main.cr
69
src/main.cr
@ -1,45 +1,12 @@
|
|||||||
|
require "./config.cr"
|
||||||
require "./faaso.cr"
|
require "./faaso.cr"
|
||||||
require "colorize"
|
require "colorize"
|
||||||
require "docopt"
|
require "docopt"
|
||||||
|
require "oplog"
|
||||||
require "rucksack"
|
require "rucksack"
|
||||||
|
|
||||||
struct LogFormat < Log::StaticFormatter
|
macro version
|
||||||
@@colors = {
|
"{{ `grep version shard.yml | cut -d: -f2` }}".strip()
|
||||||
"FATAL" => :red,
|
|
||||||
"ERROR" => :red,
|
|
||||||
"WARN" => :yellow,
|
|
||||||
"INFO" => :green,
|
|
||||||
"DEBUG" => :blue,
|
|
||||||
"TRACE" => :light_blue,
|
|
||||||
}
|
|
||||||
|
|
||||||
def run
|
|
||||||
string "#{@entry.message}".colorize(@@colors[@entry.severity.label])
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.setup(verbosity)
|
|
||||||
Colorize.on_tty_only!
|
|
||||||
if verbosity < 3
|
|
||||||
_verbosity = [
|
|
||||||
Log::Severity::Fatal,
|
|
||||||
Log::Severity::Error,
|
|
||||||
Log::Severity::Warn,
|
|
||||||
][[verbosity, 2].min]
|
|
||||||
Log.setup(
|
|
||||||
_verbosity,
|
|
||||||
Log::IOBackend.new(io: STDERR, formatter: LogFormat)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
_verbosity = [Log::Severity::Info,
|
|
||||||
Log::Severity::Debug,
|
|
||||||
Log::Severity::Trace,
|
|
||||||
][[verbosity - 3, 3].min]
|
|
||||||
Log.setup(
|
|
||||||
_verbosity,
|
|
||||||
Log::IOBackend.new(io: STDOUT, formatter: LogFormat)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
doc = <<-DOC
|
doc = <<-DOC
|
||||||
@ -47,12 +14,15 @@ 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 new -r runtime FOLDER [-v <level>]
|
faaso new -r runtime FOLDER [-v <level>]
|
||||||
faaso scale FUNKO [SCALE] [-v <level>] [-l]
|
faaso scale FUNKO [SCALE] [-v <level>] [-l]
|
||||||
faaso secret (-d|-a) FUNKO SECRET [-v <level>] [-l]
|
faaso secret (-d|-a) FUNKO SECRET [-v <level>] [-l]
|
||||||
faaso status FUNKO [-v <level>] [-l]
|
faaso status FUNKO [-v <level>] [-l]
|
||||||
faaso version
|
faaso version
|
||||||
|
faaso help COMMAND
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-a --add Add
|
-a --add Add
|
||||||
@ -61,27 +31,32 @@ Options:
|
|||||||
-l --local Run commands locally instead of against a FaaSO server
|
-l --local Run commands locally instead of against a FaaSO server
|
||||||
--no-runtime Don't merge a runtime into the funko
|
--no-runtime Don't merge a runtime into the funko
|
||||||
-r runtime Runtime for the new funko (use -r list for examples)
|
-r runtime Runtime for the new funko (use -r list for examples)
|
||||||
-v level Control the logging verbosity, 0 to 5 [default: 3]
|
-v level Control the logging verbosity, 0 to 6 [default: 4]
|
||||||
DOC
|
DOC
|
||||||
|
|
||||||
ans = Docopt.docopt(doc, ARGV)
|
ans = Docopt.docopt(doc, ARGV)
|
||||||
LogFormat.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)
|
||||||
|
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)
|
||||||
|
Log.info { "#{version}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
exit(status)
|
exit 0
|
||||||
|
@ -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}"
|
||||||
@ -77,6 +79,8 @@ module Runtime
|
|||||||
# file is like "#{base}/foo"
|
# file is like "#{base}/foo"
|
||||||
# dst is like #{dst_path}/foo
|
# dst is like #{dst_path}/foo
|
||||||
dst = Path[dst_path] / Path[file].relative_to(base_path)
|
dst = Path[dst_path] / Path[file].relative_to(base_path)
|
||||||
|
# Make sure we have dest dir
|
||||||
|
Dir.mkdir_p dst.dirname unless File.directory? dst.dirname
|
||||||
# Render templated files
|
# Render templated files
|
||||||
if file.ends_with? ".j2"
|
if file.ends_with? ".j2"
|
||||||
dst = dst.sibling(dst.stem)
|
dst = dst.sibling(dst.stem)
|
||||||
|
@ -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>
|
<%- f["containers"].as(Array(Docr::Types::ContainerSummary)).each do |c| -%>
|
||||||
<thead>
|
<div class="grid">
|
||||||
<th>ID</th>
|
<div>
|
||||||
<th>Current?</th>
|
<tt><%= c.@names[0].split("-")[-1] %></tt>
|
||||||
<th>Actions</th>
|
</div>
|
||||||
</thead>
|
<div>
|
||||||
<tbody>
|
|
||||||
<%- f["containers"].as(Array(Docr::Types::ContainerSummary)).each do |c| -%>
|
|
||||||
<tr>
|
|
||||||
<td><tt><%= c.@names[0].split("-")[-1] %></tt></td>
|
|
||||||
<td>
|
|
||||||
<%- 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 -%>
|
||||||
|
Reference in New Issue
Block a user