small cleanput and switch to sentinel
This commit is contained in:
@@ -1,3 +1,2 @@
|
|||||||
DISABLE_GLOOMI=false
|
PRELOAD_PLUGINS='[{"file": "gloomi.so", "domains": ["localhost"]}]' # make sure gloomi.so is in the $PLUGINS_DIR
|
||||||
GLOOMI_HOSTNAME=localhost
|
|
||||||
PLUGINS_DIR=plugs
|
PLUGINS_DIR=plugs
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ type PluginUpload struct {
|
|||||||
|
|
||||||
## GLoomI
|
## GLoomI
|
||||||
|
|
||||||
GLoomI is the included plugin management interface for GLoom, it utilizes the GLoom RPC, much like you would if you wanted to make your own management interface. By default, GLoomI is configured to use 127.0.0.1 as the hostname, but you con configure it to use a different hostname by setting the `GLOOMI_HOSTNAME` environment variable. The endpoints for GLoomI are as follows:
|
GLoomI is the included plugin management interface for GLoom, it utilizes the GLoom RPC, much like you would if you wanted to make your own management interface. By default, GLoomI is configured to use localhost as the hostname, but you con configure it to use a different hostname by changing the `PRELOADED_PLUGINS` environment variable, as describe in the [README](README.md). The endpoints for GLoomI are as follows:
|
||||||
|
|
||||||
- `GET /api/plugins` - This endpoint returns a list of all registered plugins and their domains.
|
- `GET /api/plugins` - This endpoint returns a list of all registered plugins and their domains.
|
||||||
- `POST /api/plugins` - This endpoint uploads a plugin to GLoom. it takes a multipart/form-data request with the following fields:
|
- `POST /api/plugins` - This endpoint uploads a plugin to GLoom. it takes a multipart/form-data request with the following fields:
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -1,6 +1,6 @@
|
|||||||
# GLoom
|
# GLoom
|
||||||
|
|
||||||
GLoom is a plugin-based web app manager written in Go (perhaps a pico-paas). GLoom's focus is to provide and simple and efficient way to host micro-web apps easily. Currently, GLoom is a fun little proof of concept, and now even supports unloading plugins, and gracefully handles plugins that crash, but it is not yet ready for production use, and may not ever be. GLoom is still in early development, so expect some rough edges and bugs and at its heart, GLoom is just a proof of concept, fun to write, and fun to use, but not production ready.
|
GLoom is a plugin-based web app manager written in Go (perhaps a pico-paas). GLoom's focus is to provide and simple and efficient way to host micro-web apps easily. Currently, GLoom is a fun little proof of concept, and now even supports unloading plugins, and gracefully handles plugins that crash, but it is not yet ready for production use, and may not ever be. GLoom is still in early development, so expect some rough edges and bugs and at its heart, GLoom is just a proof of concept, fun to write, and fun to use, but not production ready. If you want a more stable and production ready web app manager, check out my other project, [Flux](https://github.com/juls0730/flux) ;).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ GLoom is a plugin-based web app manager written in Go (perhaps a pico-paas). GLo
|
|||||||
- Go 1.20 or higher
|
- Go 1.20 or higher
|
||||||
- [zqdgr](https://github.com/juls0730/zqdgr)
|
- [zqdgr](https://github.com/juls0730/zqdgr)
|
||||||
|
|
||||||
|
This project is primarily written for Linux, and has only been tested on Linux, you might have luck with other operating systems, but it is not guaranteed to work, feel free to open an issue if you encounter any problems.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
@@ -32,9 +34,9 @@ GLoom is a plugin-based web app manager written in Go (perhaps a pico-paas). GLo
|
|||||||
zqdgr build
|
zqdgr build
|
||||||
```
|
```
|
||||||
|
|
||||||
and if you want to build the project without the GLoom management Interface (you will not be able to manage plugins wunless you have another interface like GLoomI):
|
and if you want to build the project without the GLoom management Interface (you will not be able to manage plugins wunless you have another interface like GLoomI or mark all the plugins you want to use as preloaded):
|
||||||
```bash
|
```bash
|
||||||
zqdgr build:no-gloomi
|
zqdgr build:gloom
|
||||||
```
|
```
|
||||||
|
|
||||||
and make sure to clear the `PRELOAD_PLUGINS` environment variable and set it to your preferred management interface, or nothing at all if you don't want to use a management interface.
|
and make sure to clear the `PRELOAD_PLUGINS` environment variable and set it to your preferred management interface, or nothing at all if you don't want to use a management interface.
|
||||||
@@ -48,6 +50,7 @@ GLoom is configured using environment variables. The following environment varia
|
|||||||
```json
|
```json
|
||||||
[{"file": "gloomi.so", "domains": ["localhost"]}]
|
[{"file": "gloomi.so", "domains": ["localhost"]}]
|
||||||
```
|
```
|
||||||
|
It is not recommended to preload any plugin other than a management interface, this is because preloaded plugins are protected and cannot be updated in an green-blue fashion.
|
||||||
- `PLUGINS_DIR` - The directory where plugins are stored. This is a string value, so you can set it to any directory path you want. The default value is `plugs`.
|
- `PLUGINS_DIR` - The directory where plugins are stored. This is a string value, so you can set it to any directory path you want. The default value is `plugs`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -60,4 +63,4 @@ Contributions are welcome!
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GLoom is licensed under the MIT License.
|
GLoom is licensed under the MIT License and ever file is licensed under it unless otherwise specified.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "Go Project",
|
"name": "GLoomI",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Example description",
|
"description": "GLoomI is the included plugin management interface for GLoom",
|
||||||
"author": "you",
|
"author": "juls0730",
|
||||||
"license": "BSL-1.0",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "go build -buildmode=plugin -o ../plugs/gloomi.so main.go"
|
"build": "go build -buildmode=plugin -o ../plugs/gloomi.so main.go"
|
||||||
},
|
},
|
||||||
|
|||||||
10
go.mod
10
go.mod
@@ -1,15 +1,11 @@
|
|||||||
module github.com/juls0730/gloom
|
module github.com/juls0730/gloom
|
||||||
|
|
||||||
go 1.23.4
|
go 1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/juls0730/sentinel v0.0.0-20250515154110-2e7e6586cacd
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require golang.org/x/sync v0.14.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
|
||||||
)
|
|
||||||
|
|||||||
13
go.sum
13
go.sum
@@ -1,13 +1,8 @@
|
|||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/juls0730/sentinel v0.0.0-20250515154110-2e7e6586cacd h1:JNazPdlAs307Gtaqmb+wfCjcOzO3MRXxg9nf0bAKAnc=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/juls0730/sentinel v0.0.0-20250515154110-2e7e6586cacd/go.mod h1:CnRvcleiS2kvK1N2PeQmeoRP5EXpBDpHPkg72vAUaSg=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
// Copyright (c) 2024 @tarampampam
|
||||||
// https://gist.github.com/tarampampam/f96538257ff125ab71785710d48b3118
|
// https://gist.github.com/tarampampam/f96538257ff125ab71785710d48b3118
|
||||||
|
|
||||||
package libs
|
package libs
|
||||||
|
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|||||||
221
main.go
221
main.go
@@ -11,9 +11,7 @@ import (
|
|||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
"net/rpc"
|
"net/rpc"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
@@ -21,11 +19,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/juls0730/gloom/libs"
|
"github.com/juls0730/gloom/libs"
|
||||||
|
"github.com/juls0730/sentinel"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,10 +52,10 @@ type GLoom struct {
|
|||||||
hostMap libs.SyncMap[string, bool]
|
hostMap libs.SyncMap[string, bool]
|
||||||
|
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
ProxyManager *ProxyManager
|
ProxyManager *sentinel.ProxyManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGloom(proxyManager *ProxyManager) (*GLoom, error) {
|
func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
|
||||||
pluginsDir := os.Getenv("PLUGINS_DIR")
|
pluginsDir := os.Getenv("PLUGINS_DIR")
|
||||||
if pluginsDir == "" {
|
if pluginsDir == "" {
|
||||||
pluginsDir = "plugs"
|
pluginsDir = "plugs"
|
||||||
@@ -132,11 +130,11 @@ func NewGloom(proxyManager *ProxyManager) (*GLoom, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gloom *GLoom) LoadInitialPlugins() error {
|
func (gloom *GLoom) LoadInitialPlugins() error {
|
||||||
slog.Debug("Loading initial plugins")
|
slog.Info("Loading initial plugins")
|
||||||
|
|
||||||
for _, plugin := range gloom.preloadPlugins {
|
for _, plugin := range gloom.preloadPlugins {
|
||||||
if err := gloom.RegisterPlugin(filepath.Join(gloom.pluginDir, plugin.File), plugin.File, plugin.Domains); err != nil {
|
if err := gloom.RegisterPlugin(filepath.Join(gloom.pluginDir, plugin.File), plugin.File, plugin.Domains); err != nil {
|
||||||
slog.Warn("Failed to register plugin", "pluginPath", plugin.File, "error", err)
|
panic(fmt.Errorf("failed to load preload plugin %s: %w (make sure its in %s)", plugin.File, err, gloom.pluginDir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +162,8 @@ func (gloom *GLoom) LoadInitialPlugins() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.Info("Loaded initial plugins")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +233,15 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str
|
|||||||
}
|
}
|
||||||
process := cmd.Process
|
process := cmd.Process
|
||||||
|
|
||||||
|
timeout := time.After(5 * time.Second)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
_ = process.Signal(os.Interrupt)
|
||||||
|
return fmt.Errorf("timed out waiting for pluginHost to start (this is likely a GLoom bug)")
|
||||||
|
default:
|
||||||
|
}
|
||||||
_, err := os.Stat(controlPath)
|
_, err := os.Stat(controlPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
@@ -276,16 +284,22 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy, err := NewDeploymentProxy(socketPath)
|
proxy, err := sentinel.NewDeploymentProxy(socketPath, NewUnixSocketTransport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldProxy *Proxy
|
var oldProxy *sentinel.Proxy
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
var ok bool
|
var ok bool
|
||||||
oldProxy, ok = gloom.ProxyManager.Load(domain)
|
if value, exists := gloom.ProxyManager.Load(domain); exists {
|
||||||
// there can only be one in a set of domains. If a is the domains already attached to the proxy, and b is
|
oldProxy = value.(*sentinel.Proxy)
|
||||||
|
ok = true
|
||||||
|
} else {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// there can only be one proxy in a set of domains. If a is the domains already attached to the proxy, and b is
|
||||||
// a superset of a, but the new members of b are not in any other set, then we can be sure there is just one
|
// a superset of a, but the new members of b are not in any other set, then we can be sure there is just one
|
||||||
if ok {
|
if ok {
|
||||||
break
|
break
|
||||||
@@ -307,7 +321,11 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str
|
|||||||
|
|
||||||
if oldProxy != nil {
|
if oldProxy != nil {
|
||||||
go func() {
|
go func() {
|
||||||
oldProxy.GracefulShutdown(nil)
|
slog.Debug("Gracefully shutting down old proxy")
|
||||||
|
err := oldProxy.GracefulShutdown(nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Failed to gracefully shutdown old proxy", "error", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +370,7 @@ func (gloom *GLoom) StartRPCServer() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("RPC server running on port 7143\n")
|
slog.Info("RPC server running on port 7143\n")
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
@@ -439,7 +457,6 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var plugExists bool
|
var plugExists bool
|
||||||
// TODO: make name a consistent identifier
|
|
||||||
slog.Debug("Checking if plugin exists", "pluginPath", pluginPath, "pluginName", plugin.Name)
|
slog.Debug("Checking if plugin exists", "pluginPath", pluginPath, "pluginName", plugin.Name)
|
||||||
rpc.gloom.DB.QueryRow("SELECT 1 FROM plugins WHERE name = ?", plugin.Name).Scan(&plugExists)
|
rpc.gloom.DB.QueryRow("SELECT 1 FROM plugins WHERE name = ?", plugin.Name).Scan(&plugExists)
|
||||||
slog.Debug("Plugin exists", "pluginExists", plugExists)
|
slog.Debug("Plugin exists", "pluginExists", plugExists)
|
||||||
@@ -515,7 +532,7 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Plugin uploaded successfully")
|
slog.Debug("Plugin uploaded successfully")
|
||||||
|
|
||||||
if err := rpc.gloom.RegisterPlugin(pluginPath, plugin.Name, domains); err != nil {
|
if err := rpc.gloom.RegisterPlugin(pluginPath, plugin.Name, domains); err != nil {
|
||||||
os.Remove(pluginPath)
|
os.Remove(pluginPath)
|
||||||
@@ -524,26 +541,23 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !plugExists {
|
if plugExists {
|
||||||
_, err = rpc.gloom.DB.Exec("INSERT INTO plugins (path, name, domains) VALUES (?, ?, ?)", pluginPath, plugin.Name, strings.Join(plugin.Domains, ","))
|
|
||||||
if err != nil {
|
|
||||||
*reply = fmt.Sprintf("Plugin upload failed: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err = rpc.gloom.DB.Exec("UPDATE plugins SET domains = ?, path = ? WHERE name = ?", strings.Join(plugin.Domains, ","), pluginPath, plugin.Name)
|
_, err = rpc.gloom.DB.Exec("UPDATE plugins SET domains = ?, path = ? WHERE name = ?", strings.Join(plugin.Domains, ","), pluginPath, plugin.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
*reply = fmt.Sprintf("Plugin upload failed: %v", err)
|
*reply = fmt.Sprintf("Plugin upload failed: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if plugExists {
|
|
||||||
// exit out early otherwise we risk creating multiple of the same plugin and causing undefined behavior
|
|
||||||
*reply = "Plugin updated successfully"
|
*reply = "Plugin updated successfully"
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = rpc.gloom.DB.Exec("INSERT INTO plugins (path, name, domains) VALUES (?, ?, ?)", pluginPath, plugin.Name, strings.Join(plugin.Domains, ","))
|
||||||
|
if err != nil {
|
||||||
|
*reply = fmt.Sprintf("Plugin upload failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
*reply = "Plugin uploaded successfully"
|
*reply = "Plugin uploaded successfully"
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -594,7 +608,7 @@ func main() {
|
|||||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
proxyManager := NewProxyManager()
|
proxyManager := sentinel.NewProxyManager(RequestLogger{})
|
||||||
|
|
||||||
gloom, err := NewGloom(proxyManager)
|
gloom, err := NewGloom(proxyManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -607,89 +621,15 @@ func main() {
|
|||||||
|
|
||||||
gloom.LoadInitialPlugins()
|
gloom.LoadInitialPlugins()
|
||||||
|
|
||||||
fmt.Println("Server running at http://localhost:3000")
|
slog.Info("Server running at http://localhost:3000")
|
||||||
if err := gloom.ProxyManager.ListenAndServe("127.0.0.1:3000"); err != nil {
|
if err := gloom.ProxyManager.ListenAndServe("127.0.0.1:3000"); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is the object that oversees the proxying of requests to the correct deployment
|
type RequestLogger struct{}
|
||||||
type ProxyManager struct {
|
|
||||||
libs.SyncMap[string, *Proxy]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProxyManager() *ProxyManager {
|
func (RequestLogger) LogRequest(app string, status int, latency time.Duration, ip, method, path string) {
|
||||||
return &ProxyManager{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (proxyManager *ProxyManager) ListenAndServe(host string) error {
|
|
||||||
slog.Info("Proxy server starting", "url", host)
|
|
||||||
if err := http.ListenAndServe(host, proxyManager); err != nil && err != http.ErrServerClosed {
|
|
||||||
return fmt.Errorf("failed to start proxy server: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stops forwarding traffic to a deployment
|
|
||||||
func (proxyManager *ProxyManager) RemoveDeployment(host string) {
|
|
||||||
slog.Info("Removing proxy", "host", host)
|
|
||||||
proxyManager.Delete(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Starts forwarding traffic to a deployment. The deployment must be ready to recieve requests before this is called.
|
|
||||||
func (proxyManager *ProxyManager) AddProxy(host string, proxy *Proxy) {
|
|
||||||
slog.Debug("Adding proxy", "host", host)
|
|
||||||
proxyManager.Store(host, proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function is responsible for taking an http request and forwarding it to the correct deployment
|
|
||||||
func (proxyManager *ProxyManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
start := time.Now()
|
|
||||||
host := r.Host
|
|
||||||
path := r.URL.Path
|
|
||||||
method := r.Method
|
|
||||||
ip := getClientIP(r)
|
|
||||||
|
|
||||||
slog.Debug("Proxying request", "host", host, "path", path, "method", method, "ip", ip)
|
|
||||||
proxy, ok := proxyManager.Load(host)
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "Not found", http.StatusNotFound)
|
|
||||||
logRequest(host, http.StatusNotFound, time.Since(start), ip, method, path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a custom ResponseWriter to capture the status code
|
|
||||||
rw := &ResponseWriterInterceptor{ResponseWriter: w, statusCode: http.StatusOK}
|
|
||||||
|
|
||||||
proxy.proxyFunc.ServeHTTP(rw, r)
|
|
||||||
|
|
||||||
latency := time.Since(start)
|
|
||||||
statusCode := rw.statusCode
|
|
||||||
|
|
||||||
logRequest(host, statusCode, latency, ip, method, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getClientIP retrieves the client's IP address from the request.
|
|
||||||
// It handles cases where the IP might be forwarded by proxies.
|
|
||||||
func getClientIP(r *http.Request) string {
|
|
||||||
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
|
||||||
return forwarded
|
|
||||||
}
|
|
||||||
return r.RemoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseWriterInterceptor is a custom http.ResponseWriter that captures the status code.
|
|
||||||
type ResponseWriterInterceptor struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *ResponseWriterInterceptor) WriteHeader(code int) {
|
|
||||||
rw.statusCode = code
|
|
||||||
rw.ResponseWriter.WriteHeader(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logRequest(app string, status int, latency time.Duration, ip, method, path string) {
|
|
||||||
slog.Info("Proxy Request",
|
slog.Info("Proxy Request",
|
||||||
slog.String("time", time.Now().Format(time.RFC3339)),
|
slog.String("time", time.Now().Format(time.RFC3339)),
|
||||||
slog.Int("status", status),
|
slog.Int("status", status),
|
||||||
@@ -710,85 +650,12 @@ func (d *unixDialer) DialContext(ctx context.Context, network, address string) (
|
|||||||
return net.Dial("unix", d.socketPath)
|
return net.Dial("unix", d.socketPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUnixSocketTransport(socketPath string) *http.Transport {
|
func NewUnixSocketTransport(socket string) *http.Transport {
|
||||||
return &http.Transport{
|
return &http.Transport{
|
||||||
DialContext: (&unixDialer{socketPath: socketPath}).DialContext,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Proxy struct {
|
|
||||||
socket string
|
|
||||||
proxyFunc *httputil.ReverseProxy
|
|
||||||
shutdownTimeout time.Duration
|
|
||||||
activeRequests int64
|
|
||||||
}
|
|
||||||
|
|
||||||
const PROXY_SHUTDOWN_TIMEOUT = 30 * time.Second
|
|
||||||
|
|
||||||
// Creates a proxy for a given deployment
|
|
||||||
func NewDeploymentProxy(socket string) (*Proxy, error) {
|
|
||||||
proxy := &Proxy{
|
|
||||||
socket: socket,
|
|
||||||
shutdownTimeout: PROXY_SHUTDOWN_TIMEOUT,
|
|
||||||
activeRequests: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
transport := &http.Transport{
|
|
||||||
DialContext: (&unixDialer{socketPath: socket}).DialContext,
|
DialContext: (&unixDialer{socketPath: socket}).DialContext,
|
||||||
MaxIdleConns: 100,
|
MaxIdleConns: 100,
|
||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
MaxIdleConnsPerHost: 100,
|
MaxIdleConnsPerHost: 100,
|
||||||
ForceAttemptHTTP2: false,
|
ForceAttemptHTTP2: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy.proxyFunc = &httputil.ReverseProxy{
|
|
||||||
Director: func(req *http.Request) {
|
|
||||||
req.URL = &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: req.Host,
|
|
||||||
Path: req.URL.Path,
|
|
||||||
}
|
|
||||||
atomic.AddInt64(&proxy.activeRequests, 1)
|
|
||||||
},
|
|
||||||
Transport: transport,
|
|
||||||
ModifyResponse: func(resp *http.Response) error {
|
|
||||||
atomic.AddInt64(&proxy.activeRequests, -1)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
slog.Error("Proxy error", "error", err)
|
|
||||||
atomic.AddInt64(&proxy.activeRequests, -1)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Proxy) GracefulShutdown(shutdownFunc func()) {
|
|
||||||
slog.Debug("Shutting down proxy", "socket", p.socket)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), p.shutdownTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
done := false
|
|
||||||
for !done {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
slog.Debug("Proxy shutdown timed out", "socket", p.socket)
|
|
||||||
|
|
||||||
done = true
|
|
||||||
default:
|
|
||||||
if atomic.LoadInt64(&p.activeRequests) == 0 {
|
|
||||||
slog.Debug("Proxy shutdown completed successfully", "socket", p.socket)
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shutdownFunc != nil {
|
|
||||||
shutdownFunc()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ func init() {
|
|||||||
type Plugin interface {
|
type Plugin interface {
|
||||||
Init() (*fiber.Config, error)
|
Init() (*fiber.Config, error)
|
||||||
RegisterRoutes(app fiber.Router)
|
RegisterRoutes(app fiber.Router)
|
||||||
// Name() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginInstance struct {
|
type PluginInstance struct {
|
||||||
@@ -43,12 +42,6 @@ type PluginInstance struct {
|
|||||||
Router *fiber.App
|
Router *fiber.App
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init is the entry point for a container process
|
|
||||||
func (p *PluginInstance) Run(pluginName string) {
|
|
||||||
log.Printf("Starting container with plugin %s", pluginName)
|
|
||||||
// Load and initialize the plugin here
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
signalChan := make(chan os.Signal, 1)
|
signalChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(signalChan, os.Interrupt)
|
signal.Notify(signalChan, os.Interrupt)
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ CREATE TABLE IF NOT EXISTS plugins (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
path TEXT NOT NULL UNIQUE,
|
path TEXT NOT NULL UNIQUE,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
/* domains is a comma-separated list of domains */
|
||||||
domains TEXT NOT NULL
|
domains TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"author": "juls0730",
|
"author": "juls0730",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "zqdgr build:gloomi && zqdgr build:pluginHost && go build",
|
"build": "zqdgr build:gloomi && zqdgr build:gloom",
|
||||||
"build:pluginHost": "sh -c \"cd pluginHost; go build -o ../host main.go\"",
|
"build:pluginHost": "sh -c \"cd pluginHost; go build -o ../host main.go\"",
|
||||||
"build:gloomi": "sh -c \"cd gloomi; zqdgr build\"",
|
"build:gloomi": "sh -c \"cd gloomi; zqdgr build\"",
|
||||||
"build:no-gloomi": "go build",
|
"build:gloom": "zqdgr build:pluginHost && go build",
|
||||||
"clean": "rm -rf plugs && rm -rf host && rm -rf gloom.db && rm -rf plugin/plugin.so && rm -rf gloom",
|
"clean": "rm -rf plugs && rm -rf host && rm -rf gloom.db && rm -rf plugin/plugin.so && rm -rf gloom",
|
||||||
"dev": "zqdgr build && ./gloom"
|
"dev": "zqdgr build && ./gloom"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user