Massive architectural rework
This commit massively overhauls the project's structure to simplify development. Most parts are now correctly compartmentalized and dependencies are passed in a sane way rather than global variables galore xd.
This commit is contained in:
576
internal/handlers/app.go
Normal file
576
internal/handlers/app.go
Normal file
@@ -0,0 +1,576 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/pkg/namesgenerator"
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
proxyManagerService "github.com/juls0730/flux/internal/services/proxy"
|
||||
"github.com/juls0730/flux/internal/util"
|
||||
"github.com/juls0730/flux/pkg"
|
||||
"github.com/juls0730/flux/pkg/API"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var deploymentLock *util.MutexLock[uuid.UUID] = util.NewMutexLock[uuid.UUID]()
|
||||
|
||||
func (flux *FluxServer) DeployNewApp(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "test/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
err := r.ParseMultipartForm(10 << 32) // 10 GiB
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to parse multipart form", zap.Error(err))
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var deployRequest API.DeployRequest
|
||||
projectConfig := new(pkg.ProjectConfig)
|
||||
if err := json.Unmarshal([]byte(r.FormValue("config")), &projectConfig); err != nil {
|
||||
flux.logger.Errorw("Failed to decode config", zap.Error(err))
|
||||
|
||||
http.Error(w, "Invalid flux.json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deployRequest.Config = *projectConfig
|
||||
idStr := r.FormValue("id")
|
||||
|
||||
if idStr == "" {
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to generate uuid", zap.Error(err))
|
||||
http.Error(w, "Failed to generate uuid", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
deployRequest.Id = id
|
||||
} else {
|
||||
deployRequest.Id, err = uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to parse uuid", zap.Error(err))
|
||||
http.Error(w, "Failed to parse uuid", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// make sure the id exists in the database
|
||||
app := flux.appManager.GetApp(deployRequest.Id)
|
||||
if app == nil {
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx, err := deploymentLock.Lock(deployRequest.Id, r.Context())
|
||||
if err != nil && err == util.ErrLocked {
|
||||
// This will happen if the app is already being deployed
|
||||
http.Error(w, "Cannot deploy app, it's already being deployed", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
deploymentLock.Unlock(deployRequest.Id)
|
||||
}()
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
|
||||
eventChannel := make(chan API.DeploymentEvent, 10)
|
||||
defer close(eventChannel)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// make sure the connection doesnt close while there are SSE events being sent
|
||||
defer wg.Wait()
|
||||
|
||||
wg.Add(1)
|
||||
go func(w http.ResponseWriter, flusher http.Flusher) {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event, ok := <-eventChannel:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ev := API.DeploymentEvent{
|
||||
Message: event.Message,
|
||||
}
|
||||
|
||||
eventJSON, err := json.Marshal(ev)
|
||||
if err != nil {
|
||||
// Write error directly to ResponseWriter
|
||||
jsonErr := json.NewEncoder(w).Encode(err)
|
||||
if jsonErr != nil {
|
||||
fmt.Fprint(w, "data: {\"message\": \"Error encoding error\"}\n\n")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "data: %s\n\n", err.Error())
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "event: %s\n", event.Stage)
|
||||
fmt.Fprintf(w, "data: %s\n\n", eventJSON)
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
if event.Stage == "error" || event.Stage == "complete" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}(w, flusher)
|
||||
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "start",
|
||||
Message: "Uploading code",
|
||||
}
|
||||
|
||||
deployRequest.Code, _, err = r.FormFile("code")
|
||||
if err != nil {
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: "No code archive found",
|
||||
}
|
||||
return
|
||||
}
|
||||
defer deployRequest.Code.Close()
|
||||
|
||||
if projectConfig.Name == "" || projectConfig.Url == "" || projectConfig.Port == 0 {
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: "Invalid flux.json, a name, url, and port must be specified",
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if projectConfig.Name == "all" {
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: "Reserved name 'all' is not allowed",
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
flux.logger.Infow("Deploying project", zap.String("name", projectConfig.Name), zap.String("url", projectConfig.Url), zap.String("id", deployRequest.Id.String()))
|
||||
|
||||
projectPath, err := flux.UploadAppCode(deployRequest.Code, deployRequest.Id)
|
||||
if err != nil {
|
||||
flux.logger.Infow("Failed to upload code", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to upload code: %s", err),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if projectConfig.EnvFile != "" {
|
||||
envPath := filepath.Join(projectPath, projectConfig.EnvFile)
|
||||
// prevent path traversal
|
||||
realEnvPath, err := filepath.EvalSymlinks(envPath)
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to eval symlinks", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to eval symlinks: %s", err),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(realEnvPath, projectPath) {
|
||||
flux.logger.Errorw("Env file is not in project directory", zap.String("env_file", projectConfig.EnvFile))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Env file is not in project directory: %s", projectConfig.EnvFile),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
envBytes, err := os.Open(realEnvPath)
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to open env file", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to open env file: %v", err),
|
||||
}
|
||||
return
|
||||
}
|
||||
defer envBytes.Close()
|
||||
|
||||
envVars, err := godotenv.Parse(envBytes)
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to parse env file", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to parse env file: %v", err),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for key, value := range envVars {
|
||||
projectConfig.Environment = append(projectConfig.Environment, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
// pipe the output of the build process to the event channel
|
||||
pipeGroup := sync.WaitGroup{}
|
||||
streamPipe := func(pipe io.ReadCloser) {
|
||||
pipeGroup.Add(1)
|
||||
defer pipeGroup.Done()
|
||||
defer pipe.Close()
|
||||
|
||||
scanner := bufio.NewScanner(pipe)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "cmd_output",
|
||||
Message: line,
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to read pipe: %s", err),
|
||||
}
|
||||
flux.logger.Errorw("Error reading pipe", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
flux.logger.Debugw("Preparing project", zap.String("name", projectConfig.Name), zap.String("id", deployRequest.Id.String()))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "preparing",
|
||||
Message: "Preparing project",
|
||||
}
|
||||
|
||||
// redirect stdout and stderr to the event channel
|
||||
reader, writer := io.Pipe()
|
||||
prepareCmd := exec.Command("go", "generate")
|
||||
prepareCmd.Dir = projectPath
|
||||
prepareCmd.Stdout = writer
|
||||
prepareCmd.Stderr = writer
|
||||
|
||||
err = prepareCmd.Start()
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to prepare project", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to prepare project: %s", err),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
go streamPipe(reader)
|
||||
|
||||
pipeGroup.Wait()
|
||||
|
||||
err = prepareCmd.Wait()
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to prepare project", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to prepare project: %s", err),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Close()
|
||||
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "building",
|
||||
Message: "Building project image",
|
||||
}
|
||||
|
||||
reader, writer = io.Pipe()
|
||||
flux.logger.Debugw("Building image for project", zap.String("name", projectConfig.Name))
|
||||
imageName := fmt.Sprintf("fluxi-%s", namesgenerator.GetRandomName(0))
|
||||
buildCmd := exec.Command("pack", "build", imageName, "--builder", flux.config.Builder)
|
||||
buildCmd.Dir = projectPath
|
||||
buildCmd.Stdout = writer
|
||||
buildCmd.Stderr = writer
|
||||
|
||||
err = buildCmd.Start()
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to build image", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to build image: %s", err),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
go streamPipe(reader)
|
||||
|
||||
pipeGroup.Wait()
|
||||
|
||||
err = buildCmd.Wait()
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to build image", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to build image: %s", err),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
app := flux.appManager.GetApp(deployRequest.Id)
|
||||
|
||||
if app == nil {
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "creating",
|
||||
Message: "Creating app, this might take a while...",
|
||||
}
|
||||
|
||||
app, err = flux.appManager.CreateApp(r.Context(), imageName, projectConfig, deployRequest.Id)
|
||||
} else {
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "upgrading",
|
||||
Message: "Upgrading app, this might take a while...",
|
||||
}
|
||||
|
||||
// we dont need to change `app` since this upgrade will use the same app and update it in place
|
||||
err = flux.appManager.Upgrade(r.Context(), app.Id, imageName, projectConfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to deploy app", zap.Error(err))
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to upgrade app: %s", err),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var extApp API.App
|
||||
extApp.Id = app.Id
|
||||
extApp.Name = app.Name
|
||||
extApp.DeploymentID = app.DeploymentID
|
||||
|
||||
eventChannel <- API.DeploymentEvent{
|
||||
Stage: "complete",
|
||||
Message: extApp,
|
||||
}
|
||||
|
||||
flux.logger.Infow("App deployed successfully", zap.String("id", app.Id.String()))
|
||||
}
|
||||
|
||||
func (flux *FluxServer) GetAllApps(w http.ResponseWriter, r *http.Request) {
|
||||
var apps []API.App
|
||||
for _, app := range flux.appManager.GetAllApps() {
|
||||
var extApp API.App
|
||||
deploymentStatus, err := app.Deployment.Status(r.Context(), flux.docker, flux.logger)
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to get deployment status", zap.Error(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
extApp.Id = app.Id
|
||||
extApp.Name = app.Name
|
||||
extApp.DeploymentID = app.DeploymentID
|
||||
extApp.DeploymentStatus = deploymentStatus
|
||||
apps = append(apps, extApp)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(apps)
|
||||
}
|
||||
|
||||
func (flux *FluxServer) GetAppById(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid app id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
app := flux.appManager.GetApp(id)
|
||||
if app == nil {
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var extApp API.App
|
||||
deploymentStatus, err := app.Deployment.Status(r.Context(), flux.docker, flux.logger)
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to get deployment status", zap.Error(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
extApp.Id = app.Id
|
||||
extApp.Name = app.Name
|
||||
extApp.DeploymentID = app.DeploymentID
|
||||
extApp.DeploymentStatus = deploymentStatus
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(extApp)
|
||||
}
|
||||
|
||||
func (flux *FluxServer) GetAppByName(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
app := flux.appManager.GetAppByName(name)
|
||||
if app == nil {
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var extApp API.App
|
||||
deploymentStatus, err := app.Deployment.Status(r.Context(), flux.docker, flux.logger)
|
||||
if err != nil {
|
||||
flux.logger.Errorw("Failed to get deployment status", zap.Error(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
extApp.Id = app.Id
|
||||
extApp.Name = app.Name
|
||||
extApp.DeploymentID = app.DeploymentID
|
||||
extApp.DeploymentStatus = deploymentStatus
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(extApp)
|
||||
}
|
||||
|
||||
func (flux *FluxServer) StartApp(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid app id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
app := flux.appManager.GetApp(id)
|
||||
if app == nil {
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := app.Deployment.Status(r.Context(), flux.docker, flux.logger)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if status == "running" {
|
||||
http.Error(w, "App is already running", http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
err = app.Deployment.Start(r.Context(), flux.docker)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
deploymentInternalUrl, err := app.Deployment.GetInternalUrl(flux.docker)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
newProxy, err := proxyManagerService.NewDeploymentProxy(*deploymentInternalUrl)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
flux.proxy.AddProxy(app.Deployment.URL, newProxy)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (flux *FluxServer) StopApp(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid app id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
app := flux.appManager.GetApp(id)
|
||||
if app == nil {
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := app.Deployment.Status(r.Context(), flux.docker, flux.logger)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if status == "stopped" || status == "failed" {
|
||||
http.Error(w, "App is already stopped", http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
err = app.Deployment.Stop(r.Context(), flux.docker)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
flux.proxy.RemoveDeployment(app.Deployment.URL)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (flux *FluxServer) DeleteAllDeploymentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
apps := flux.appManager.GetAllApps()
|
||||
for _, app := range apps {
|
||||
err := flux.appManager.DeleteApp(app.Id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (flux *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid app id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
app := flux.appManager.GetApp(id)
|
||||
if app == nil {
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
err = flux.appManager.DeleteApp(id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
28
internal/handlers/schema.sql
Normal file
28
internal/handlers/schema.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE IF NOT EXISTS deployments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
url TEXT NOT NULL UNIQUE,
|
||||
port INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS apps (
|
||||
id BLOB PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
deployment_id INTEGER,
|
||||
FOREIGN KEY(deployment_id) REFERENCES deployments(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS containers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
container_id TEXT NOT NULL,
|
||||
head BOOLEAN NOT NULL,
|
||||
deployment_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(deployment_id) REFERENCES deployments(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS volumes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
volume_id TEXT NOT NULL,
|
||||
mountpoint TEXT NOT NULL,
|
||||
container_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(container_id) REFERENCES containers(id)
|
||||
);
|
||||
247
internal/handlers/server.go
Normal file
247
internal/handlers/server.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/google/uuid"
|
||||
"github.com/juls0730/flux/internal/docker"
|
||||
"github.com/juls0730/flux/internal/services/appManagerService"
|
||||
proxyManagerService "github.com/juls0730/flux/internal/services/proxy"
|
||||
|
||||
"github.com/juls0730/flux/pkg"
|
||||
"github.com/juls0730/flux/pkg/API"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed schema.sql
|
||||
schema string
|
||||
DefaultConfig = pkg.DaemonConfig{
|
||||
Builder: "paketobuildpacks/builder-jammy-tiny",
|
||||
CompressionLevel: 0,
|
||||
DaemonHost: "0.0.0.0:5647",
|
||||
ProxyHost: "0.0.0.0:7465",
|
||||
}
|
||||
)
|
||||
|
||||
type FluxServer struct {
|
||||
db *sql.DB
|
||||
|
||||
docker *docker.DockerClient
|
||||
// TODO: implement
|
||||
proxy *proxyManagerService.ProxyManager
|
||||
appManager *appManagerService.AppManager
|
||||
|
||||
rootDir string
|
||||
config pkg.DaemonConfig
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewServer() *FluxServer {
|
||||
flux := &FluxServer{}
|
||||
|
||||
verbosity, err := strconv.Atoi(os.Getenv("FLUXD_VERBOSITY"))
|
||||
if err != nil {
|
||||
verbosity = 0
|
||||
}
|
||||
|
||||
config := zap.NewProductionConfig()
|
||||
|
||||
if os.Getenv("DEBUG") == "true" {
|
||||
config = zap.NewDevelopmentConfig()
|
||||
verbosity = -1
|
||||
}
|
||||
|
||||
config.Level = zap.NewAtomicLevelAt(zapcore.Level(verbosity))
|
||||
|
||||
lameLogger, err := config.Build()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create logger: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flux.logger = lameLogger.Sugar()
|
||||
|
||||
rootDir := os.Getenv("FLUXD_ROOT_DIR")
|
||||
if rootDir == "" {
|
||||
rootDir = "/var/fluxd"
|
||||
}
|
||||
|
||||
flux.rootDir = rootDir
|
||||
|
||||
configPath := filepath.Join(flux.rootDir, "config.json")
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
if err := os.MkdirAll(rootDir, 0755); err != nil {
|
||||
flux.logger.Fatalw("Failed to create fluxd directory", zap.Error(err))
|
||||
}
|
||||
|
||||
configBytes, err := json.Marshal(DefaultConfig)
|
||||
if err != nil {
|
||||
flux.logger.Fatalw("Failed to marshal default config", zap.Error(err))
|
||||
}
|
||||
|
||||
fmt.Printf("Config file not found creating default config file at %s\n", configPath)
|
||||
if err := os.WriteFile(configPath, configBytes, 0644); err != nil {
|
||||
flux.logger.Fatalw("Failed to write config file", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
configFile, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
flux.logger.Fatalw("Failed to read config file", zap.Error(err))
|
||||
}
|
||||
|
||||
// apply the config file over the default config, this way if we have missing fields, they will be filled in with
|
||||
// the default values
|
||||
flux.config = DefaultConfig
|
||||
if err := json.Unmarshal(configFile, &flux.config); err != nil {
|
||||
flux.logger.Fatalw("Failed to parse config file", zap.Error(err))
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(flux.rootDir, "fluxd.db")
|
||||
|
||||
flux.db, err = sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
flux.logger.Fatalw("Failed to open database", zap.Error(err))
|
||||
}
|
||||
|
||||
err = flux.db.Ping()
|
||||
if err != nil {
|
||||
flux.logger.Fatalw("Failed to ping database", zap.Error(err))
|
||||
}
|
||||
|
||||
_, err = flux.db.Exec(schema)
|
||||
if err != nil {
|
||||
flux.logger.Fatalw("Failed to create database schema", zap.Error(err))
|
||||
}
|
||||
|
||||
flux.docker = docker.NewDocker(nil, flux.logger)
|
||||
if err != nil {
|
||||
flux.logger.Fatalw("Failed to create docker client", zap.Error(err))
|
||||
}
|
||||
|
||||
flux.logger.Infof("Pulling builder image %s this may take a while...", flux.config.Builder)
|
||||
events, err := flux.docker.ImagePull(context.Background(), fmt.Sprintf("%s:latest", flux.config.Builder), image.PullOptions{})
|
||||
if err != nil {
|
||||
flux.logger.Fatalw("Failed to pull builder image", zap.Error(err))
|
||||
}
|
||||
|
||||
// blocking until the iamge is pulled
|
||||
io.Copy(io.Discard, events)
|
||||
|
||||
flux.proxy = proxyManagerService.NewProxyManager(flux.logger)
|
||||
|
||||
flux.appManager = appManagerService.NewAppManager(flux.db, flux.docker, flux.proxy, flux.logger)
|
||||
flux.appManager.Init()
|
||||
|
||||
return flux
|
||||
}
|
||||
|
||||
func (s *FluxServer) Stop() {
|
||||
s.logger.Sync()
|
||||
}
|
||||
|
||||
func (s *FluxServer) ListenAndServe() error {
|
||||
s.logger.Infow("Starting server", zap.String("daemon_host", s.config.DaemonHost), zap.String("proxy_host", s.config.ProxyHost))
|
||||
|
||||
go s.proxy.ListenAndServe(s.config.ProxyHost)
|
||||
return http.ListenAndServe(s.config.DaemonHost, nil)
|
||||
}
|
||||
|
||||
func (s *FluxServer) DaemonInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(API.Info{
|
||||
CompressionLevel: s.config.CompressionLevel,
|
||||
Version: pkg.Version,
|
||||
})
|
||||
}
|
||||
|
||||
// This extracts and uploads a tar file to a temporary directory, and returns the path to the directory
|
||||
func (s *FluxServer) UploadAppCode(code io.Reader, appId uuid.UUID) (string, error) {
|
||||
var err error
|
||||
outputPath, err := os.MkdirTemp(os.TempDir(), appId.String())
|
||||
if err != nil {
|
||||
s.logger.Errorw("Failed to create project directory", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
var gzReader *gzip.Reader
|
||||
defer func() {
|
||||
if gzReader != nil {
|
||||
gzReader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if s.config.CompressionLevel > 0 {
|
||||
gzReader, err = gzip.NewReader(code)
|
||||
if err != nil {
|
||||
s.logger.Infow("Failed to create gzip reader", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
var tarReader *tar.Reader
|
||||
|
||||
if gzReader != nil {
|
||||
tarReader = tar.NewReader(gzReader)
|
||||
} else {
|
||||
tarReader = tar.NewReader(code)
|
||||
}
|
||||
|
||||
s.logger.Infow("Extracting files for project", zap.String("project", outputPath))
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Debugw("Failed to read tar header", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Construct full path
|
||||
path := filepath.Join(outputPath, header.Name)
|
||||
|
||||
// Handle different file types
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err = os.MkdirAll(path, 0755); err != nil {
|
||||
s.logger.Debugw("Failed to extract directory", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
s.logger.Debugw("Failed to extract directory", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
outFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
s.logger.Debugw("Failed to extract file", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
if _, err = io.Copy(outFile, tarReader); err != nil {
|
||||
s.logger.Debugw("Failed to copy file during extraction", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outputPath, nil
|
||||
}
|
||||
Reference in New Issue
Block a user