feature: Adds backend system and basic API.

This adds the backend API, as well as backend infrastructure, including
autostarting and basic communication between the Goroutine + Application.
This commit is contained in:
greysoh 2024-12-23 15:52:16 -05:00
parent 611d7f24f8
commit 0b73b4aa47
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
12 changed files with 777 additions and 44 deletions

View file

@ -1,44 +1,34 @@
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/api/controllers/v1/backends"
"git.terah.dev/imterah/hermes/api/controllers/v1/users"
"git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log"
"github.com/gin-gonic/gin"
"github.com/urfave/cli/v2"
"gorm.io/gorm"
)
func main() {
logLevel := os.Getenv("HERMES_LOG_LEVEL")
func entrypoint(cCtx *cli.Context) error {
developmentMode := false
if os.Getenv("HERMES_DEVELOPMENT_MODE") != "" {
log.Warn("You have development mode enabled. This may weaken security.")
developmentMode = true
}
if logLevel != "" {
switch logLevel {
case "debug":
log.SetLevel(log.DebugLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "fatal":
log.SetLevel(log.FatalLevel)
}
}
log.Info("Hermes is initializing...")
log.Debug("Initializing database and opening it...")
@ -51,13 +41,117 @@ func main() {
log.Debug("Running database migrations...")
if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil {
log.Fatalf("Failed to run database migrations: %s", err)
return fmt.Errorf("Failed to run database migrations: %s", err)
}
log.Debug("Initializing the JWT subsystem...")
if err := jwtcore.SetupJWT(); err != nil {
log.Fatalf("Failed to initialize the JWT subsystem: %s", err.Error())
return fmt.Errorf("Failed to initialize the JWT subsystem: %s", err.Error())
}
log.Debug("Initializing the backend subsystem...")
backendMetadataPath := cCtx.String("backends-path")
backendMetadata, err := os.ReadFile(backendMetadataPath)
if err != nil {
return fmt.Errorf("Failed to read backends: %s", err.Error())
}
availableBackends := []*backendruntime.Backend{}
err = json.Unmarshal(backendMetadata, &availableBackends)
if err != nil {
return fmt.Errorf("Failed to parse backends: %s", err.Error())
}
for _, backend := range availableBackends {
backend.Path = path.Join(filepath.Dir(backendMetadataPath), backend.Path)
}
backendruntime.Init(availableBackends)
log.Debug("Enumerating backends...")
backendList := []dbcore.Backend{}
if err := dbcore.DB.Find(&backendList).Error; err != nil {
return fmt.Errorf("Failed to enumerate backends: %s", err.Error())
}
for _, backend := range backendList {
log.Infof("Starting up backend #%d: %s", backend.ID, backend.Name)
var backendRuntimeFilePath string
for _, runtime := range backendruntime.AvailableBackends {
if runtime.Name == backend.Backend {
backendRuntimeFilePath = runtime.Path
}
}
if backendRuntimeFilePath == "" {
log.Errorf("Unsupported backend recieved for ID %d: %s", backend.ID, backend.Backend)
continue
}
backendInstance := backendruntime.NewBackend(backendRuntimeFilePath)
err = backendInstance.Start()
if err != nil {
log.Errorf("Failed to start backend #%d: %s", backend.ID, err.Error())
continue
}
backendParameters, err := base64.StdEncoding.DecodeString(backend.BackendParameters)
if err != nil {
log.Errorf("Failed to decode backend parameters for backend #%d: %s", backend.ID, err.Error())
continue
}
backendInstance.RuntimeCommands <- &commonbackend.Start{
Type: "start",
Arguments: backendParameters,
}
backendStartResponse := <-backendInstance.RuntimeCommands
switch responseMessage := backendStartResponse.(type) {
case error:
log.Warnf("Failed to get response for backend #%d: %s", backend.ID, responseMessage.Error())
err = backendInstance.Stop()
if err != nil {
log.Warnf("Failed to stop backend: %s", err.Error())
}
continue
case *commonbackend.BackendStatusResponse:
if !responseMessage.IsRunning {
err = backendInstance.Stop()
if err != nil {
log.Warnf("Failed to start backend: %s", err.Error())
}
if responseMessage.Message == "" {
log.Errorf("Unkown error while trying to start the backend #%d", backend.ID)
} else {
log.Errorf("Failed to start backend: %s", responseMessage.Message)
}
continue
}
default:
log.Errorf("Got illegal response type for backend #%d: %T", backend.ID, responseMessage)
continue
}
backendruntime.RunningBackends[backend.ID] = backendInstance
log.Infof("Successfully started backend #%d", backend.ID)
}
log.Debug("Initializing API...")
@ -97,10 +191,55 @@ func main() {
engine.POST("/api/v1/users/remove", users.RemoveUser)
engine.POST("/api/v1/users/lookup", users.LookupUser)
log.Infof("Listening on: %s", listeningAddress)
engine.POST("/api/v1/backends/create", backends.CreateBackend)
log.Infof("Listening on '%s'", listeningAddress)
err = engine.Run(listeningAddress)
if err != nil {
log.Fatalf("Error running web server: %s", err.Error())
return fmt.Errorf("Error running web server: %s", err.Error())
}
return nil
}
func main() {
logLevel := os.Getenv("HERMES_LOG_LEVEL")
if logLevel != "" {
switch logLevel {
case "debug":
log.SetLevel(log.DebugLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "fatal":
log.SetLevel(log.FatalLevel)
}
}
app := &cli.App{
Name: "hermes",
Usage: "port forwarding across boundaries",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "backends-path",
Aliases: []string{"b"},
Usage: "path to the backend manifest file",
Required: true,
},
},
Action: entrypoint,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}