Files
flux/internal/server/app.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

321 lines
8.2 KiB
Go

package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/juls0730/flux/pkg"
"go.uber.org/zap"
)
type App struct {
Id uuid.UUID `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Deployment *Deployment `json:"-"`
DeploymentID int64 `json:"deployment_id,omitempty"`
flux *FluxServer
}
func (flux *FluxServer) GetAppByNameHandler(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 pkg.App
deploymentStatus, err := app.Deployment.Status(r.Context())
if err != nil {
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)
}
// Create the initial app row in the database and create and start the deployment. The app is the overarching data
// structure that contains all of the data for a project
func (flux *FluxServer) CreateApp(ctx context.Context, imageName string, projectPath string, projectConfig *pkg.ProjectConfig, id uuid.UUID) (*App, error) {
app := &App{
Id: id,
flux: flux,
}
logger.Debugw("Creating deployment", zap.String("id", app.Id.String()))
deployment, err := flux.CreateDeployment(projectConfig.Port, projectConfig.Url)
app.Deployment = deployment
if err != nil {
logger.Errorw("Failed to create deployment", zap.Error(err))
return nil, err
}
for _, container := range projectConfig.Containers {
c, err := flux.CreateContainer(ctx, &container, false, deployment, container.Name)
if err != nil {
return nil, fmt.Errorf("failed to create container: %v", err)
}
c.Start(ctx, true)
}
headContainer := pkg.Container{
ImageName: imageName,
Volumes: projectConfig.Volumes,
Environment: projectConfig.Environment,
}
// this call does a lot for us, see it's documentation for more info
_, err = flux.CreateContainer(ctx, &headContainer, true, deployment, projectConfig.Name)
if err != nil {
return nil, fmt.Errorf("failed to create container: %v", err)
}
// create app in the database
var appIdBlob []byte
err = appInsertStmt.QueryRow(id[:], projectConfig.Name, deployment.ID).Scan(&appIdBlob, &app.Name, &app.DeploymentID)
if err != nil {
return nil, fmt.Errorf("failed to insert app: %v", err)
}
app.Id, err = uuid.FromBytes(appIdBlob)
if err != nil {
return nil, fmt.Errorf("failed to parse app id: %v", err)
}
err = deployment.Start(ctx)
if err != nil {
return nil, fmt.Errorf("failed to start deployment: %v", err)
}
flux.appManager.AddApp(app.Id, app)
return app, nil
}
func (app *App) Upgrade(ctx context.Context, imageName string, projectPath string, projectConfig *pkg.ProjectConfig) error {
logger.Debugw("Upgrading deployment", zap.String("id", app.Id.String()))
// if deploy is not started, start it
deploymentStatus, err := app.Deployment.Status(ctx)
if err != nil {
return fmt.Errorf("failed to get deployment status: %v", err)
}
if deploymentStatus != "running" {
err = app.Deployment.Start(ctx)
if err != nil {
return fmt.Errorf("failed to start deployment: %v", err)
}
}
app.flux.db.Exec("UPDATE apps SET name = ? WHERE id = ?", projectConfig.Name, app.Id[:])
err = app.Deployment.Upgrade(ctx, projectConfig, imageName, projectPath)
if err != nil {
return fmt.Errorf("failed to upgrade deployment: %v", err)
}
return nil
}
// delete an app and deployment from the database, and its project files from disk.
func (app *App) Remove(ctx context.Context) error {
app.flux.appManager.RemoveApp(app.Id)
err := app.Deployment.Remove(ctx)
if err != nil {
logger.Errorw("Failed to remove deployment", zap.Error(err))
return err
}
_, err = app.flux.db.Exec("DELETE FROM apps WHERE id = ?", app.Id[:])
if err != nil {
logger.Errorw("Failed to delete app", zap.Error(err))
return err
}
projectPath := filepath.Join(app.flux.rootDir, "apps", app.Id.String())
err = os.RemoveAll(projectPath)
if err != nil {
return fmt.Errorf("failed to remove project directory: %v", err)
}
return nil
}
type AppManager struct {
pkg.TypedMap[uuid.UUID, *App]
nameIndex pkg.TypedMap[string, uuid.UUID]
}
func (am *AppManager) GetAppByName(name string) *App {
id, ok := am.nameIndex.Load(name)
if !ok {
return nil
}
return am.GetApp(id)
}
func (am *AppManager) GetApp(id uuid.UUID) *App {
app, exists := am.Load(id)
if !exists {
return nil
}
return app
}
func (am *AppManager) GetAllApps() []*App {
var apps []*App
am.Range(func(key uuid.UUID, app *App) bool {
apps = append(apps, app)
return true
})
return apps
}
// removes an app from the app manager
func (am *AppManager) RemoveApp(id uuid.UUID) {
app, ok := am.Load(id)
if !ok {
return
}
am.nameIndex.Delete(app.Name)
am.Delete(id)
}
// add a given app to the app manager
func (am *AppManager) AddApp(id uuid.UUID, app *App) {
if app.Deployment.Containers == nil || app.Deployment.Head == nil || len(app.Deployment.Containers) == 0 || app.Name == "" {
panic("invalid app")
}
am.nameIndex.Store(app.Name, id)
am.Store(id, app)
}
// nukes an app completely
func (am *AppManager) DeleteApp(id uuid.UUID) error {
app := am.GetApp(id)
if app == nil {
return fmt.Errorf("app not found")
}
// calls RemoveApp
err := app.Remove(context.Background())
if err != nil {
return err
}
return nil
}
// Scan every app in the database, and create in memory structures if the deployment is already running
func (am *AppManager) Init() {
logger.Info("Initializing deployments")
if Flux.db == nil {
logger.Panic("DB is nil")
}
rows, err := Flux.db.Query("SELECT id, name, deployment_id FROM apps")
if err != nil {
logger.Warnw("Failed to query apps", zap.Error(err))
return
}
defer rows.Close()
var apps []App
for rows.Next() {
var app App
var appIdBlob []byte
if err := rows.Scan(&appIdBlob, &app.Name, &app.DeploymentID); err != nil {
logger.Warnw("Failed to scan app", zap.Error(err))
return
}
app.Id = uuid.Must(uuid.FromBytes(appIdBlob))
app.flux = Flux
apps = append(apps, app)
}
for _, app := range apps {
deployment := &Deployment{}
var headContainer *Container
Flux.db.QueryRow("SELECT id, url, port FROM deployments WHERE id = ?", app.DeploymentID).Scan(&deployment.ID, &deployment.URL, &deployment.Port)
deployment.Containers = make([]*Container, 0)
rows, err = Flux.db.Query("SELECT id, container_id, deployment_id, head FROM containers WHERE deployment_id = ?", app.DeploymentID)
if err != nil {
logger.Warnw("Failed to query containers", zap.Error(err))
return
}
defer rows.Close()
for rows.Next() {
var container Container
var containerIDString string
rows.Scan(&container.ID, &containerIDString, &container.DeploymentID, &container.Head)
container.Deployment = deployment
copy(container.ContainerID[:], containerIDString)
if container.Head {
if headContainer != nil {
logger.Fatal("Several containers are marked as head")
}
headContainer = &container
}
rows, err := Flux.db.Query("SELECT id, volume_id, container_id, mountpoint FROM volumes WHERE container_id = ?", container.ContainerID[:])
if err != nil {
logger.Warnw("Failed to query volumes", zap.Error(err))
return
}
defer rows.Close()
for rows.Next() {
volume := new(Volume)
rows.Scan(&volume.ID, &volume.VolumeID, &volume.ContainerID, &volume.Mountpoint)
container.Volumes = append(container.Volumes, volume)
}
deployment.Containers = append(deployment.Containers, &container)
}
if headContainer == nil {
logger.Fatal("head container is nil!")
}
deployment.Head = headContainer
app.Deployment = deployment
am.AddApp(app.Id, &app)
status, err := deployment.Status(context.Background())
if err != nil {
logger.Warnw("Failed to get deployment status", zap.Error(err))
continue
}
if status != "running" {
continue
}
deployment.Proxy, _ = deployment.NewDeploymentProxy()
Flux.proxy.AddDeployment(deployment)
}
}