Files
flux/internal/server/server.go
Zoe f4bf2ff5a1 Cleanup, bug fixes, and improvements
This commit changes how projects are handled internally so that projects
can be renamed. This commit also fixes some bugs, and removes redundant
code.
2025-04-13 05:37:39 -05:00

285 lines
7.2 KiB
Go

package server
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/docker/docker/client"
"github.com/google/uuid"
"github.com/juls0730/flux/pkg"
_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var (
//go:embed schema.sql
schemaBytes []byte
DefaultConfig = FluxServerConfig{
Builder: "paketobuildpacks/builder-jammy-tiny",
Compression: pkg.Compression{
Enabled: false,
Level: 0,
},
}
Flux *FluxServer
logger *zap.SugaredLogger
)
type FluxServerConfig struct {
Builder string `json:"builder"`
DisableDeleteAll bool `json:"disable_delete_all"`
Compression pkg.Compression `json:"compression"`
}
type FluxServer struct {
config FluxServerConfig
db *sql.DB
proxy *Proxy
rootDir string
appManager *AppManager
dockerClient *client.Client
Logger *zap.SugaredLogger
}
func NewFluxServer() *FluxServer {
dockerClient, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
logger.Fatalw("Failed to create docker client", zap.Error(err))
}
rootDir := os.Getenv("FLUXD_ROOT_DIR")
if rootDir == "" {
rootDir = "/var/fluxd"
}
if err := os.MkdirAll(rootDir, 0755); err != nil {
logger.Fatalw("Failed to create fluxd directory", zap.Error(err))
}
db, err := sql.Open("sqlite3", filepath.Join(rootDir, "fluxd.db"))
if err != nil {
logger.Fatalw("Failed to open database", zap.Error(err))
}
_, err = db.Exec(string(schemaBytes))
if err != nil {
logger.Fatalw("Failed to create database schema", zap.Error(err))
}
err = PrepareDBStatements(db)
if err != nil {
logger.Fatalw("Failed to prepare database statements", zap.Error(err))
}
return &FluxServer{
db: db,
proxy: &Proxy{},
appManager: new(AppManager),
rootDir: rootDir,
dockerClient: dockerClient,
}
}
func (s *FluxServer) Stop() {
s.Logger.Sync()
}
func NewServer() *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 {
logger.Fatalw("Failed to create logger", zap.Error(err))
}
logger = lameLogger.Sugar()
Flux = NewFluxServer()
Flux.Logger = logger
var serverConfig FluxServerConfig
// parse config, if it doesnt exist, create it and use the default config
configPath := filepath.Join(Flux.rootDir, "config.json")
if _, err := os.Stat(configPath); err != nil {
if err := os.MkdirAll(Flux.rootDir, 0755); err != nil {
logger.Fatalw("Failed to create fluxd directory", zap.Error(err))
}
configBytes, err := json.Marshal(DefaultConfig)
if err != nil {
logger.Fatalw("Failed to marshal default config", zap.Error(err))
}
logger.Debugw("Config file not found creating default config file at", zap.String("path", configPath))
if err := os.WriteFile(configPath, configBytes, 0644); err != nil {
logger.Fatalw("Failed to write config file", zap.Error(err))
}
}
configFile, err := os.ReadFile(configPath)
if err != nil {
logger.Fatalw("Failed to read config file", zap.Error(err))
}
if err := json.Unmarshal(configFile, &serverConfig); err != nil {
logger.Fatalw("Failed to parse config file", zap.Error(err))
}
Flux.config = serverConfig
logger.Infof("Pulling builder image %s this may take a while...", serverConfig.Builder)
events, err := Flux.dockerClient.ImagePull(context.Background(), fmt.Sprintf("%s:latest", serverConfig.Builder), image.PullOptions{})
if err != nil {
logger.Fatalw("Failed to pull builder image", zap.Error(err))
}
// blocking until the iamge is pulled
io.Copy(io.Discard, events)
logger.Infow("Successfully pulled builder image", zap.String("image", serverConfig.Builder))
if err := os.MkdirAll(filepath.Join(Flux.rootDir, "apps"), 0755); err != nil {
logger.Fatalw("Failed to create apps directory", zap.Error(err))
}
Flux.appManager.Init()
port := os.Getenv("FLUXD_PROXY_PORT")
if port == "" {
port = "7465"
}
go func() {
logger.Infof("Proxy server starting on http://127.0.0.1:%s", port)
if err := http.ListenAndServe(fmt.Sprintf(":%s", port), Flux.proxy); err != nil {
logger.Fatalw("Failed to start proxy server", zap.Error(err))
}
}()
return Flux
}
// Handler for uploading a project to the server. We have to upload the entire project since we need to build the
// project ourselves to work with the buildpacks
func (s *FluxServer) UploadAppCode(code io.Reader, appId uuid.UUID) (string, error) {
var err error
projectPath := filepath.Join(s.rootDir, "apps", appId.String())
if err = os.MkdirAll(projectPath, 0755); err != nil {
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.Compression.Enabled {
gzReader, err = gzip.NewReader(code)
if err != nil {
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)
}
logger.Infow("Extracting files for project", zap.String("project", projectPath))
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
logger.Debugw("Failed to read tar header", zap.Error(err))
return "", err
}
// Construct full path
path := filepath.Join(projectPath, header.Name)
// Handle different file types
switch header.Typeflag {
case tar.TypeDir:
if err = os.MkdirAll(path, 0755); err != nil {
logger.Debugw("Failed to extract directory", zap.Error(err))
return "", err
}
case tar.TypeReg:
if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
logger.Debugw("Failed to extract directory", zap.Error(err))
return "", err
}
outFile, err := os.Create(path)
if err != nil {
logger.Debugw("Failed to extract file", zap.Error(err))
return "", err
}
defer outFile.Close()
if _, err = io.Copy(outFile, tarReader); err != nil {
logger.Debugw("Failed to copy file during extraction", zap.Error(err))
return "", err
}
}
}
return projectPath, nil
}
// TODO: split each prepare statement into its coresponding module so the statememnts are easier to find
func PrepareDBStatements(db *sql.DB) error {
var err error
appInsertStmt, err = db.Prepare("INSERT INTO apps (id, name, deployment_id) VALUES ($1, $2, $3) RETURNING id, name, deployment_id")
if err != nil {
return fmt.Errorf("failed to prepare statement: %v", err)
}
containerInsertStmt, err = db.Prepare("INSERT INTO containers (container_id, head, deployment_id) VALUES (?, ?, ?) RETURNING id, container_id, head, deployment_id")
if err != nil {
return err
}
deploymentInsertStmt, err = db.Prepare("INSERT INTO deployments (url, port) VALUES ($1, $2) RETURNING id, url, port")
if err != nil {
logger.Errorw("Failed to prepare statement", zap.Error(err))
return err
}
return nil
}