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.
This commit is contained in:
Zoe
2025-04-13 05:37:39 -05:00
parent 79322c4c5e
commit f4bf2ff5a1
17 changed files with 401 additions and 206 deletions

View File

@@ -4,10 +4,14 @@ Flux is a lightweight self-hosted pseudo-PaaS for hosting Golang web apps with e
**Goals**: **Goals**:
- Automatic deployment of Golang web apps, simply run `flux init`, chnage the app name, and run `flux deploy` and you're done! - Automatic deployment of Golang web apps, simply run `flux init`, and run `flux deploy` to deploy your app!
- Zero-downtime deployments with blue-green deployments - Zero-downtime deployments with blue-green deployments
- Simple but powerful configuration, flux should be able to handle most use cases, from a micro web app to a fullstack app with databases, caching layers, full text search, etc. - Simple but powerful configuration, flux should be able to handle most use cases, from a micro web app to a fullstack app with databases, caching layers, full text search, etc.
**What is flux not?**
- Flux is not meant to be used as a multi-tenant PaaS, it is meant to be used by trusted individuals, while flux will still have security in mind, certain things are not secure. For example, anyone can delete all your apps, so be careful, anyone who has access to your flux server can do a lot of damage.
**Limitations**: **Limitations**:
- Theoretically flux is likely limited by the amount of containers can fit in the bridge network, but I haven't tested this - Theoretically flux is likely limited by the amount of containers can fit in the bridge network, but I haven't tested this
- Containers are not particularly isolated, if one malicious container wanted to scan all containers, or interact with other containers it tectically shouldnt, it totally just can (todo?) - Containers are not particularly isolated, if one malicious container wanted to scan all containers, or interact with other containers it tectically shouldnt, it totally just can (todo?)
@@ -102,11 +106,19 @@ Flux daemon looks for a confgiuration file in `/var/fluxd/config.json` but can b
```json ```json
{ {
"builder": "paketobuildpacks/builder-jammy-tiny" "builder": "paketobuildpacks/builder-jammy-tiny",
"disable_delete_all": false,
"compression": {
"enabled": false
}
} }
``` ```
- `builder`: The buildpack builder to use (default: `paketobuildpacks/builder-jammy-tiny`) - `builder`: The buildpack builder to use (default: `paketobuildpacks/builder-jammy-tiny`)
- `disable_delete_all`: Disable the delete all deployments endpoint (default: `false`)
- `compression`: Compression settings
- `enabled`: Enable compression (default: `false`)
- `level`: Compression level
#### Daemon Settings #### Daemon Settings

View File

@@ -98,7 +98,7 @@ func DeleteCommand(ctx models.CommandCtx, args []string) error {
return deleteAll(ctx, noConfirm) return deleteAll(ctx, noConfirm)
} }
projectName, err := GetProjectName("delete", args) projectName, err := GetProjectId("delete", args, ctx.Config)
if err != nil { if err != nil {
return fmt.Errorf("\tfailed to get project name: %v.\n\tSee flux delete --help for more information", err) return fmt.Errorf("\tfailed to get project name: %v.\n\tSee flux delete --help for more information", err)
} }
@@ -136,6 +136,11 @@ func DeleteCommand(ctx models.CommandCtx, args []string) error {
return fmt.Errorf("delete failed: %s", responseBody) return fmt.Errorf("delete failed: %s", responseBody)
} }
if len(args) == 0 {
// remove the .fluxid file if it exists
os.Remove(".fluxid")
}
fmt.Printf("Successfully deleted %s\n", projectName) fmt.Printf("Successfully deleted %s\n", projectName)
return nil return nil

View File

@@ -18,6 +18,7 @@ import (
"time" "time"
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/google/uuid"
"github.com/juls0730/flux/cmd/flux/models" "github.com/juls0730/flux/cmd/flux/models"
"github.com/juls0730/flux/pkg" "github.com/juls0730/flux/pkg"
) )
@@ -188,7 +189,32 @@ func DeployCommand(ctx models.CommandCtx, args []string) error {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
configPart, err := writer.CreateFormFile("config", "flux.json")
if _, err := os.Stat(".fluxid"); err == nil {
idPart, err := writer.CreateFormField("id")
if err != nil {
return fmt.Errorf("failed to create id part: %v", err)
}
idFile, err := os.Open(".fluxid")
if err != nil {
return fmt.Errorf("failed to open .fluxid: %v", err)
}
defer idFile.Close()
var idBytes []byte
if idBytes, err = io.ReadAll(idFile); err != nil {
return fmt.Errorf("failed to read .fluxid: %v", err)
}
if _, err := uuid.Parse(string(idBytes)); err != nil {
return fmt.Errorf(".fluxid does not contain a valid uuid")
}
idPart.Write(idBytes)
}
configPart, err := writer.CreateFormField("config")
if err != nil { if err != nil {
return fmt.Errorf("failed to create config part: %v", err) return fmt.Errorf("failed to create config part: %v", err)
@@ -246,7 +272,19 @@ func DeployCommand(ctx models.CommandCtx, args []string) error {
switch event { switch event {
case "complete": case "complete":
loadingSpinner.Stop() loadingSpinner.Stop()
fmt.Printf("App %s deployed successfully!\n", data.Message.(map[string]interface{})["name"]) fmt.Printf("App %s deployed successfully!\n", data.Message.(map[string]any)["name"])
if _, err := os.Stat(".fluxid"); os.IsNotExist(err) {
idFile, err := os.Create(".fluxid")
if err != nil {
return fmt.Errorf("failed to create .fluxid: %v", err)
}
defer idFile.Close()
id := data.Message.(map[string]any)["id"].(string)
if _, err := idFile.Write([]byte(id)); err != nil {
return fmt.Errorf("failed to write .fluxid: %v", err)
}
}
return nil return nil
case "cmd_output": case "cmd_output":
customWriter.Printf("... %s\n", data.Message) customWriter.Printf("... %s\n", data.Message)

View File

@@ -3,14 +3,27 @@ package commands
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http"
"os" "os"
"strings"
"github.com/juls0730/flux/cmd/flux/models"
"github.com/juls0730/flux/pkg" "github.com/juls0730/flux/pkg"
) )
func GetProjectName(command string, args []string) (string, error) { func GetProjectId(command string, args []string, config models.Config) (string, error) {
var projectName string var projectName string
if _, err := os.Stat(".fluxid"); err == nil {
id, err := os.ReadFile(".fluxid")
if err != nil {
return "", fmt.Errorf("failed to read .fluxid: %v", err)
}
return string(id), nil
}
if len(args) == 0 { if len(args) == 0 {
if _, err := os.Stat("flux.json"); err != nil { if _, err := os.Stat("flux.json"); err != nil {
return "", fmt.Errorf("the current directory is not a flux project, please run flux %[1]s in the project directory", command) return "", fmt.Errorf("the current directory is not a flux project, please run flux %[1]s in the project directory", command)
@@ -32,5 +45,28 @@ func GetProjectName(command string, args []string) (string, error) {
projectName = args[0] projectName = args[0]
} }
return projectName, nil // make an http get request to the daemon to get the project name
resp, err := http.Get(config.DeamonURL + "/apps/by-name/" + projectName)
if err != nil {
return "", fmt.Errorf("failed to get project name: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response body: %v", err)
}
responseBody = []byte(strings.TrimSuffix(string(responseBody), "\n"))
return "", fmt.Errorf("get project name failed: %s", responseBody)
}
var app pkg.App
if err := json.NewDecoder(resp.Body).Decode(&app); err != nil {
return "", fmt.Errorf("failed to decode app: %v", err)
}
return app.Id.String(), nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func StartCommand(ctx models.CommandCtx, args []string) error { func StartCommand(ctx models.CommandCtx, args []string) error {
projectName, err := GetProjectName("start", args) projectName, err := GetProjectId("start", args, ctx.Config)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func StopCommand(ctx models.CommandCtx, args []string) error { func StopCommand(ctx models.CommandCtx, args []string) error {
projectName, err := GetProjectName("stop", args) projectName, err := GetProjectId("stop", args, ctx.Config)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -14,10 +14,11 @@ func main() {
http.HandleFunc("POST /deploy", fluxServer.DeployHandler) http.HandleFunc("POST /deploy", fluxServer.DeployHandler)
http.HandleFunc("DELETE /deployments", fluxServer.DeleteAllDeploymentsHandler) http.HandleFunc("DELETE /deployments", fluxServer.DeleteAllDeploymentsHandler)
http.HandleFunc("DELETE /deployments/{name}", fluxServer.DeleteDeployHandler) http.HandleFunc("DELETE /deployments/{id}", fluxServer.DeleteDeployHandler)
http.HandleFunc("POST /start/{name}", fluxServer.StartDeployHandler) http.HandleFunc("POST /start/{id}", fluxServer.StartDeployHandler)
http.HandleFunc("POST /stop/{name}", fluxServer.StopDeployHandler) http.HandleFunc("POST /stop/{id}", fluxServer.StopDeployHandler)
http.HandleFunc("GET /apps", fluxServer.ListAppsHandler) http.HandleFunc("GET /apps", fluxServer.ListAppsHandler)
http.HandleFunc("GET /apps/by-name/{name}", fluxServer.GetAppByNameHandler)
http.HandleFunc("GET /heartbeat", fluxServer.DaemonInfoHandler) http.HandleFunc("GET /heartbeat", fluxServer.DaemonInfoHandler)
fluxServer.Logger.Info("Fluxd started on http://127.0.0.1:5647") fluxServer.Logger.Info("Fluxd started on http://127.0.0.1:5647")

5
go.mod
View File

@@ -9,7 +9,10 @@ require (
github.com/mattn/go-sqlite3 v1.14.24 github.com/mattn/go-sqlite3 v1.14.24
) )
require go.uber.org/multierr v1.10.0 // indirect require (
github.com/google/uuid v1.6.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
)
require ( require (
github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect

View File

@@ -2,31 +2,60 @@ package server
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"github.com/google/uuid"
"github.com/juls0730/flux/pkg" "github.com/juls0730/flux/pkg"
"go.uber.org/zap" "go.uber.org/zap"
) )
type App struct { type App struct {
ID int64 `json:"id,omitempty"` Id uuid.UUID `json:"id,omitempty"`
Deployment *Deployment `json:"-"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Deployment *Deployment `json:"-"`
DeploymentID int64 `json:"deployment_id,omitempty"` 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 // 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 // structure that contains all of the data for a project
func CreateApp(ctx context.Context, imageName string, projectPath string, projectConfig *pkg.ProjectConfig) (*App, error) { func (flux *FluxServer) CreateApp(ctx context.Context, imageName string, projectPath string, projectConfig *pkg.ProjectConfig, id uuid.UUID) (*App, error) {
app := &App{ app := &App{
Name: projectConfig.Name, Id: id,
flux: flux,
} }
logger.Debugw("Creating deployment", zap.String("name", app.Name)) logger.Debugw("Creating deployment", zap.String("id", app.Id.String()))
deployment, err := CreateDeployment(projectConfig.Port, projectConfig.Url, Flux.db) deployment, err := flux.CreateDeployment(projectConfig.Port, projectConfig.Url)
app.Deployment = deployment app.Deployment = deployment
if err != nil { if err != nil {
logger.Errorw("Failed to create deployment", zap.Error(err)) logger.Errorw("Failed to create deployment", zap.Error(err))
@@ -34,7 +63,7 @@ func CreateApp(ctx context.Context, imageName string, projectPath string, projec
} }
for _, container := range projectConfig.Containers { for _, container := range projectConfig.Containers {
c, err := CreateContainer(ctx, &container, projectConfig.Name, false, deployment) c, err := flux.CreateContainer(ctx, &container, false, deployment, container.Name)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create container: %v", err) return nil, fmt.Errorf("failed to create container: %v", err)
} }
@@ -42,37 +71,41 @@ func CreateApp(ctx context.Context, imageName string, projectPath string, projec
c.Start(ctx, true) c.Start(ctx, true)
} }
headContainer := &pkg.Container{ headContainer := pkg.Container{
Name: projectConfig.Name,
ImageName: imageName, ImageName: imageName,
Volumes: projectConfig.Volumes, Volumes: projectConfig.Volumes,
Environment: projectConfig.Environment, Environment: projectConfig.Environment,
} }
// this call does a lot for us, see it's documentation for more info // this call does a lot for us, see it's documentation for more info
_, err = CreateContainer(ctx, headContainer, projectConfig.Name, true, deployment) _, err = flux.CreateContainer(ctx, &headContainer, true, deployment, projectConfig.Name)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create container: %v", err) return nil, fmt.Errorf("failed to create container: %v", err)
} }
// create app in the database // create app in the database
err = appInsertStmt.QueryRow(projectConfig.Name, deployment.ID).Scan(&app.ID, &app.Name, &app.DeploymentID) var appIdBlob []byte
err = appInsertStmt.QueryRow(id[:], projectConfig.Name, deployment.ID).Scan(&appIdBlob, &app.Name, &app.DeploymentID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to insert app: %v", err) 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) err = deployment.Start(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to start deployment: %v", err) return nil, fmt.Errorf("failed to start deployment: %v", err)
} }
Flux.appManager.AddApp(app.Name, app) flux.appManager.AddApp(app.Id, app)
return app, nil return app, nil
} }
func (app *App) Upgrade(ctx context.Context, imageName string, projectPath string, projectConfig *pkg.ProjectConfig) error { func (app *App) Upgrade(ctx context.Context, imageName string, projectPath string, projectConfig *pkg.ProjectConfig) error {
logger.Debugw("Upgrading deployment", zap.String("name", app.Name)) logger.Debugw("Upgrading deployment", zap.String("id", app.Id.String()))
// if deploy is not started, start it // if deploy is not started, start it
deploymentStatus, err := app.Deployment.Status(ctx) deploymentStatus, err := app.Deployment.Status(ctx)
@@ -87,6 +120,8 @@ func (app *App) Upgrade(ctx context.Context, imageName string, projectPath strin
} }
} }
app.flux.db.Exec("UPDATE apps SET name = ? WHERE id = ?", projectConfig.Name, app.Id[:])
err = app.Deployment.Upgrade(ctx, projectConfig, imageName, projectPath) err = app.Deployment.Upgrade(ctx, projectConfig, imageName, projectPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to upgrade deployment: %v", err) return fmt.Errorf("failed to upgrade deployment: %v", err)
@@ -97,7 +132,7 @@ func (app *App) Upgrade(ctx context.Context, imageName string, projectPath strin
// delete an app and deployment from the database, and its project files from disk. // delete an app and deployment from the database, and its project files from disk.
func (app *App) Remove(ctx context.Context) error { func (app *App) Remove(ctx context.Context) error {
Flux.appManager.RemoveApp(app.Name) app.flux.appManager.RemoveApp(app.Id)
err := app.Deployment.Remove(ctx) err := app.Deployment.Remove(ctx)
if err != nil { if err != nil {
@@ -105,13 +140,13 @@ func (app *App) Remove(ctx context.Context) error {
return err return err
} }
_, err = Flux.db.Exec("DELETE FROM apps WHERE id = ?", app.ID) _, err = app.flux.db.Exec("DELETE FROM apps WHERE id = ?", app.Id[:])
if err != nil { if err != nil {
logger.Errorw("Failed to delete app", zap.Error(err)) logger.Errorw("Failed to delete app", zap.Error(err))
return err return err
} }
projectPath := filepath.Join(Flux.rootDir, "apps", app.Name) projectPath := filepath.Join(app.flux.rootDir, "apps", app.Id.String())
err = os.RemoveAll(projectPath) err = os.RemoveAll(projectPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to remove project directory: %v", err) return fmt.Errorf("failed to remove project directory: %v", err)
@@ -121,57 +156,71 @@ func (app *App) Remove(ctx context.Context) error {
} }
type AppManager struct { type AppManager struct {
sync.Map pkg.TypedMap[uuid.UUID, *App]
nameIndex pkg.TypedMap[string, uuid.UUID]
} }
func (am *AppManager) GetApp(name string) *App { func (am *AppManager) GetAppByName(name string) *App {
app, exists := am.Load(name) 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 { if !exists {
return nil return nil
} }
return app.(*App) return app
} }
func (am *AppManager) GetAllApps() []*App { func (am *AppManager) GetAllApps() []*App {
var apps []*App var apps []*App
am.Range(func(key, value interface{}) bool { am.Range(func(key uuid.UUID, app *App) bool {
if app, ok := value.(*App); ok {
apps = append(apps, app) apps = append(apps, app)
}
return true return true
}) })
return apps return apps
} }
// removes an app from the app manager // removes an app from the app manager
func (am *AppManager) RemoveApp(name string) { func (am *AppManager) RemoveApp(id uuid.UUID) {
am.Delete(name) app, ok := am.Load(id)
if !ok {
return
}
am.nameIndex.Delete(app.Name)
am.Delete(id)
} }
// add a given app to the app manager // add a given app to the app manager
func (am *AppManager) AddApp(name string, app *App) { func (am *AppManager) AddApp(id uuid.UUID, app *App) {
if app.Deployment.Containers == nil || app.Deployment.Head == nil || len(app.Deployment.Containers) == 0 { if app.Deployment.Containers == nil || app.Deployment.Head == nil || len(app.Deployment.Containers) == 0 || app.Name == "" {
panic("nil containers") panic("invalid app")
} }
am.Store(name, app) am.nameIndex.Store(app.Name, id)
am.Store(id, app)
} }
// nukes an app completely // nukes an app completely
func (am *AppManager) DeleteApp(name string) error { func (am *AppManager) DeleteApp(id uuid.UUID) error {
app := am.GetApp(name) app := am.GetApp(id)
if app == nil { if app == nil {
return fmt.Errorf("app not found") return fmt.Errorf("app not found")
} }
// calls RemoveApp
err := app.Remove(context.Background()) err := app.Remove(context.Background())
if err != nil { if err != nil {
return err return err
} }
am.Delete(name)
return nil return nil
} }
@@ -193,10 +242,13 @@ func (am *AppManager) Init() {
var apps []App var apps []App
for rows.Next() { for rows.Next() {
var app App var app App
if err := rows.Scan(&app.ID, &app.Name, &app.DeploymentID); err != nil { var appIdBlob []byte
if err := rows.Scan(&appIdBlob, &app.Name, &app.DeploymentID); err != nil {
logger.Warnw("Failed to scan app", zap.Error(err)) logger.Warnw("Failed to scan app", zap.Error(err))
return return
} }
app.Id = uuid.Must(uuid.FromBytes(appIdBlob))
app.flux = Flux
apps = append(apps, app) apps = append(apps, app)
} }
@@ -250,7 +302,7 @@ func (am *AppManager) Init() {
deployment.Head = headContainer deployment.Head = headContainer
app.Deployment = deployment app.Deployment = deployment
am.AddApp(app.Name, &app) am.AddApp(app.Id, &app)
status, err := deployment.Status(context.Background()) status, err := deployment.Status(context.Background())
if err != nil { if err != nil {

View File

@@ -6,13 +6,13 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
"github.com/docker/docker/pkg/namesgenerator"
"github.com/juls0730/flux/pkg" "github.com/juls0730/flux/pkg"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -31,7 +31,8 @@ type Volume struct {
type Container struct { type Container struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Head bool `json:"head"` // if the container is the head of the deployment Head bool `json:"head"` // if the container is the head of the deployment
Name string `json:"name"` FriendlyName string `json:"friendly_name"` // name used by other containers to reach this container
Name string `json:"name"` // name of the container in the docker daemon
Deployment *Deployment `json:"-"` Deployment *Deployment `json:"-"`
Volumes []*Volume `json:"volumes"` Volumes []*Volume `json:"volumes"`
ContainerID [64]byte `json:"container_id"` ContainerID [64]byte `json:"container_id"`
@@ -58,16 +59,14 @@ func CreateDockerVolume(ctx context.Context) (vol *Volume, err error) {
} }
// Creates a container in the docker daemon and returns the descriptor for the container // Creates a container in the docker daemon and returns the descriptor for the container
func CreateDockerContainer(ctx context.Context, imageName string, projectName string, vols []*Volume, environment []string, hosts []string) (*Container, error) { func CreateDockerContainer(ctx context.Context, imageName string, vols []*Volume, environment []string, hosts []string) (*Container, error) {
for _, host := range hosts { for _, host := range hosts {
if host == ":" { if host == ":" {
return nil, fmt.Errorf("invalid host %s", host) return nil, fmt.Errorf("invalid host %s", host)
} }
} }
safeImageName := strings.ReplaceAll(imageName, "/", "_") containerName := fmt.Sprintf("flux-%s", namesgenerator.GetRandomName(0))
containerName := fmt.Sprintf("flux_%s-%s-%s", safeImageName, projectName, time.Now().Format("20060102-150405"))
logger.Debugw("Creating container", zap.String("container_id", containerName)) logger.Debugw("Creating container", zap.String("container_id", containerName))
mounts := make([]mount.Mount, len(vols)) mounts := make([]mount.Mount, len(vols))
volumes := make(map[string]struct{}, len(vols)) volumes := make(map[string]struct{}, len(vols))
@@ -86,6 +85,9 @@ func CreateDockerContainer(ctx context.Context, imageName string, projectName st
Image: imageName, Image: imageName,
Env: environment, Env: environment,
Volumes: volumes, Volumes: volumes,
Labels: map[string]string{
"managed-by": "flux",
},
}, },
&container.HostConfig{ &container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyUnlessStopped}, RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyUnlessStopped},
@@ -104,6 +106,7 @@ func CreateDockerContainer(ctx context.Context, imageName string, projectName st
c := &Container{ c := &Container{
ContainerID: [64]byte([]byte(resp.ID)), ContainerID: [64]byte([]byte(resp.ID)),
Volumes: vols, Volumes: vols,
Name: containerName,
} }
return c, nil return c, nil
@@ -113,9 +116,9 @@ func CreateDockerContainer(ctx context.Context, imageName string, projectName st
// 1. Create the container in the docker daemon // 1. Create the container in the docker daemon
// 2. Create the volumes for the container // 2. Create the volumes for the container
// 3. Insert the container and volumes into the database // 3. Insert the container and volumes into the database
func CreateContainer(ctx context.Context, container *pkg.Container, projectName string, head bool, deployment *Deployment) (c *Container, err error) { func (flux *FluxServer) CreateContainer(ctx context.Context, container *pkg.Container, head bool, deployment *Deployment, friendlyName string) (c *Container, err error) {
if container.Name == "" { if friendlyName == "" {
return nil, fmt.Errorf("container name is empty") return nil, fmt.Errorf("container friendly name is empty")
} }
if container.ImageName == "" { if container.ImageName == "" {
@@ -166,7 +169,7 @@ func CreateContainer(ctx context.Context, container *pkg.Container, projectName
return nil, err return nil, err
} }
hosts = append(hosts, fmt.Sprintf("%s:%s", container.Name, containerName)) hosts = append(hosts, fmt.Sprintf("%s:%s", container.FriendlyName, containerName))
} }
} }
@@ -182,12 +185,12 @@ func CreateContainer(ctx context.Context, container *pkg.Container, projectName
io.Copy(io.Discard, image) io.Copy(io.Discard, image)
} }
c, err = CreateDockerContainer(ctx, container.ImageName, projectName, volumes, container.Environment, hosts) c, err = CreateDockerContainer(ctx, container.ImageName, volumes, container.Environment, hosts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.Name = container.Name c.FriendlyName = friendlyName
var containerIDString string var containerIDString string
err = containerInsertStmt.QueryRow(c.ContainerID[:], head, deployment.ID).Scan(&c.ID, &containerIDString, &c.Head, &c.DeploymentID) err = containerInsertStmt.QueryRow(c.ContainerID[:], head, deployment.ID).Scan(&c.ID, &containerIDString, &c.Head, &c.DeploymentID)
@@ -196,7 +199,7 @@ func CreateContainer(ctx context.Context, container *pkg.Container, projectName
} }
copy(c.ContainerID[:], containerIDString) copy(c.ContainerID[:], containerIDString)
tx, err := Flux.db.Begin() tx, err := flux.db.Begin()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -231,24 +234,21 @@ func CreateContainer(ctx context.Context, container *pkg.Container, projectName
return c, nil return c, nil
} }
func (c *Container) Upgrade(ctx context.Context, imageName, projectPath string, projectConfig *pkg.ProjectConfig) (*Container, error) { func (c *Container) Upgrade(ctx context.Context, imageName, projectPath string, emvironment []string) (*Container, error) {
// Create new container with new image // Create new container with new image
logger.Debugw("Upgrading container", zap.ByteString("container_id", c.ContainerID[:12])) logger.Debugw("Upgrading container", zap.ByteString("container_id", c.ContainerID[:12]))
if c.Volumes == nil { if c.Volumes == nil {
return nil, fmt.Errorf("no volumes found for container %s", c.ContainerID[:12]) return nil, fmt.Errorf("no volumes found for container %s", c.ContainerID[:12])
} }
var hosts []string containerJSON, err := Flux.dockerClient.ContainerInspect(context.Background(), string(c.ContainerID[:]))
for _, container := range c.Deployment.Containers {
containerJSON, err := Flux.dockerClient.ContainerInspect(context.Background(), string(container.ContainerID[:]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
hosts = containerJSON.HostConfig.ExtraHosts hosts := containerJSON.HostConfig.ExtraHosts
}
newContainer, err := CreateDockerContainer(ctx, imageName, projectConfig.Name, c.Volumes, projectConfig.Environment, hosts) newContainer, err := CreateDockerContainer(ctx, imageName, c.Volumes, emvironment, hosts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -294,7 +294,7 @@ func (c *Container) Upgrade(ctx context.Context, imageName, projectPath string,
return newContainer, nil return newContainer, nil
} }
// initial indicates if the container was just created, because if not, we need to fix the extra hsots since it's not guaranteed that the supplemental containers have the same ip // initial indicates if the container was just created, because if not, we need to fix the extra hosts field since it's not guaranteed that the supplemental containers have the same ip
// as they had when the deployment was previously on // as they had when the deployment was previously on
func (c *Container) Start(ctx context.Context, initial bool) error { func (c *Container) Start(ctx context.Context, initial bool) error {
if !initial && c.Head { if !initial && c.Head {
@@ -331,14 +331,18 @@ func (c *Container) Start(ctx context.Context, initial bool) error {
return err return err
} }
hosts = append(hosts, fmt.Sprintf("%s:%s", supplementalContainer.Name, ip)) hosts = append(hosts, fmt.Sprintf("%s:%s", supplementalContainer.FriendlyName, ip))
} }
// recreate yourself // recreate yourself
// TODO: pull this out so it stays in sync with CreateDockerContainer
resp, err := Flux.dockerClient.ContainerCreate(ctx, &container.Config{ resp, err := Flux.dockerClient.ContainerCreate(ctx, &container.Config{
Image: containerJSON.Image, Image: containerJSON.Image,
Env: containerJSON.Config.Env, Env: containerJSON.Config.Env,
Volumes: volumes, Volumes: volumes,
Labels: map[string]string{
"managed-by": "flux",
},
}, },
&container.HostConfig{ &container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyUnlessStopped}, RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyUnlessStopped},
@@ -521,21 +525,3 @@ func RemoveVolume(ctx context.Context, volumeID string) error {
return nil return nil
} }
func findExistingDockerContainers(ctx context.Context, containerPrefix string) (map[string]bool, error) {
containers, err := Flux.dockerClient.ContainerList(ctx, container.ListOptions{
All: true,
})
if err != nil {
return nil, err
}
var existingContainers map[string]bool = make(map[string]bool)
for _, container := range containers {
if strings.HasPrefix(container.Names[0], fmt.Sprintf("/%s-", containerPrefix)) {
existingContainers[container.ID] = true
}
}
return existingContainers, nil
}

View File

@@ -12,8 +12,11 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"github.com/docker/docker/pkg/namesgenerator"
"github.com/google/uuid"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/juls0730/flux/pkg" "github.com/juls0730/flux/pkg"
"go.uber.org/zap" "go.uber.org/zap"
@@ -24,55 +27,52 @@ var (
) )
type DeployRequest struct { type DeployRequest struct {
Config multipart.File `form:"config"` Id uuid.UUID `form:"id"`
Config pkg.ProjectConfig `form:"config"`
Code multipart.File `form:"code"` Code multipart.File `form:"code"`
} }
type DeployResponse struct {
App App `json:"app"`
}
type DeploymentLock struct { type DeploymentLock struct {
mu sync.Mutex mu sync.Mutex
deployed map[string]context.CancelFunc deployed map[uuid.UUID]context.CancelFunc
} }
func NewDeploymentLock() *DeploymentLock { func NewDeploymentLock() *DeploymentLock {
return &DeploymentLock{ return &DeploymentLock{
deployed: make(map[string]context.CancelFunc), deployed: make(map[uuid.UUID]context.CancelFunc),
} }
} }
// This function will lock a deployment based on an app name so that the same app cannot be deployed twice simultaneously // This function will lock a deployment based on an app name so that the same app cannot be deployed twice simultaneously
func (dt *DeploymentLock) LockDeployment(appName string, ctx context.Context) (context.Context, error) { func (dt *DeploymentLock) LockDeployment(appId uuid.UUID, ctx context.Context) (context.Context, error) {
dt.mu.Lock() dt.mu.Lock()
defer dt.mu.Unlock() defer dt.mu.Unlock()
// Check if the app is already being deployed // Check if the app is already being deployed
if _, exists := dt.deployed[appName]; exists { if _, exists := dt.deployed[appId]; exists {
return nil, fmt.Errorf("app %s is already being deployed", appName) return nil, fmt.Errorf("app is already being deployed")
} }
// Create a context that can be cancelled // Create a context that can be cancelled
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
// Store the cancel function // Store the cancel function
dt.deployed[appName] = cancel dt.deployed[appId] = cancel
return ctx, nil return ctx, nil
} }
// This function will unlock a deployment based on an app name so that the same app can be deployed again (you would call this after a deployment has completed) // This function will unlock a deployment based on an app name so that the same app can be deployed again (you would call this after a deployment has completed)
func (dt *DeploymentLock) UnlockDeployment(appName string) { func (dt *DeploymentLock) UnlockDeployment(appId uuid.UUID) {
dt.mu.Lock() dt.mu.Lock()
defer dt.mu.Unlock() defer dt.mu.Unlock()
// Remove the app from deployed tracking // Remove the app from deployed tracking
if cancel, exists := dt.deployed[appName]; exists { if cancel, exists := dt.deployed[appId]; exists {
// Cancel the context // Cancel the context
cancel() cancel()
// Remove from map // Remove from map
delete(dt.deployed, appName) delete(dt.deployed, appId)
} }
} }
@@ -84,8 +84,8 @@ type DeploymentEvent struct {
StatusCode int `json:"status,omitempty"` StatusCode int `json:"status,omitempty"`
} }
func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) { func (flux *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
if Flux.appManager == nil { if flux.appManager == nil {
panic("App manager is nil") panic("App manager is nil")
} }
@@ -101,22 +101,36 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
} }
var deployRequest DeployRequest var deployRequest DeployRequest
deployRequest.Config, _, err = r.FormFile("config")
if err != nil {
http.Error(w, "No flux.json found", http.StatusBadRequest)
return
}
defer deployRequest.Config.Close()
projectConfig := new(pkg.ProjectConfig) projectConfig := new(pkg.ProjectConfig)
if err := json.NewDecoder(deployRequest.Config).Decode(&projectConfig); err != nil { if err := json.Unmarshal([]byte(r.FormValue("config")), &projectConfig); err != nil {
logger.Errorw("Failed to decode config", zap.Error(err)) logger.Errorw("Failed to decode config", zap.Error(err))
http.Error(w, "Invalid flux.json", http.StatusBadRequest) http.Error(w, "Invalid flux.json", http.StatusBadRequest)
return return
} }
ctx, err := deploymentLock.LockDeployment(projectConfig.Name, r.Context()) deployRequest.Config = *projectConfig
idStr := r.FormValue("id")
if idStr == "" {
id, err := uuid.NewRandom()
if err != nil {
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 {
logger.Errorw("Failed to parse uuid", zap.Error(err))
http.Error(w, "Failed to parse uuid", http.StatusBadRequest)
return
}
}
ctx, err := deploymentLock.LockDeployment(deployRequest.Id, r.Context())
if err != nil { if err != nil {
// This will happen if the app is already being deployed // This will happen if the app is already being deployed
http.Error(w, err.Error(), http.StatusConflict) http.Error(w, err.Error(), http.StatusConflict)
@@ -125,7 +139,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
go func() { go func() {
<-ctx.Done() <-ctx.Done()
deploymentLock.UnlockDeployment(projectConfig.Name) deploymentLock.UnlockDeployment(deployRequest.Id)
}() }()
flusher, ok := w.(http.Flusher) flusher, ok := w.(http.Flusher)
@@ -213,9 +227,9 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Infow("Deploying project", zap.String("name", projectConfig.Name), zap.String("url", projectConfig.Url)) logger.Infow("Deploying project", zap.String("name", projectConfig.Name), zap.String("url", projectConfig.Url), zap.String("id", deployRequest.Id.String()))
projectPath, err := s.UploadAppCode(deployRequest.Code, projectConfig) projectPath, err := flux.UploadAppCode(deployRequest.Code, deployRequest.Id)
if err != nil { if err != nil {
logger.Infow("Failed to upload code", zap.Error(err)) logger.Infow("Failed to upload code", zap.Error(err))
eventChannel <- DeploymentEvent{ eventChannel <- DeploymentEvent{
@@ -229,7 +243,30 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
// We need to pre-process EnvFile since docker has no concept of where the file is, or anything like that, so we have to read from it, // We need to pre-process EnvFile since docker has no concept of where the file is, or anything like that, so we have to read from it,
// and place all of it's content into the environment field so that docker can find it later // and place all of it's content into the environment field so that docker can find it later
if projectConfig.EnvFile != "" { if projectConfig.EnvFile != "" {
envBytes, err := os.Open(filepath.Join(projectPath, projectConfig.EnvFile)) envPath := filepath.Join(projectPath, projectConfig.EnvFile)
// prevent path traversal
realEnvPath, err := filepath.EvalSymlinks(envPath)
if err != nil {
logger.Errorw("Failed to eval symlinks", zap.Error(err))
eventChannel <- DeploymentEvent{
Stage: "error",
Message: fmt.Sprintf("Failed to eval symlinks: %s", err),
StatusCode: http.StatusInternalServerError,
}
return
}
if !strings.HasPrefix(realEnvPath, projectPath) {
logger.Errorw("Env file is not in project directory", zap.String("env_file", projectConfig.EnvFile))
eventChannel <- DeploymentEvent{
Stage: "error",
Message: fmt.Sprintf("Env file is not in project directory: %s", projectConfig.EnvFile),
StatusCode: http.StatusBadRequest,
}
return
}
envBytes, err := os.Open(realEnvPath)
if err != nil { if err != nil {
logger.Errorw("Failed to open env file", zap.Error(err)) logger.Errorw("Failed to open env file", zap.Error(err))
eventChannel <- DeploymentEvent{ eventChannel <- DeploymentEvent{
@@ -281,14 +318,14 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
logger.Debugw("Preparing project", zap.String("name", projectConfig.Name)) logger.Debugw("Preparing project", zap.String("name", projectConfig.Name), zap.String("id", deployRequest.Id.String()))
eventChannel <- DeploymentEvent{ eventChannel <- DeploymentEvent{
Stage: "preparing", Stage: "preparing",
Message: "Preparing project", Message: "Preparing project",
} }
// redirect stdout and stderr to the event channel
reader, writer := io.Pipe() reader, writer := io.Pipe()
prepareCmd := exec.Command("go", "generate") prepareCmd := exec.Command("go", "generate")
prepareCmd.Dir = projectPath prepareCmd.Dir = projectPath
prepareCmd.Stdout = writer prepareCmd.Stdout = writer
@@ -331,8 +368,8 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
reader, writer = io.Pipe() reader, writer = io.Pipe()
logger.Debugw("Building image for project", zap.String("name", projectConfig.Name)) logger.Debugw("Building image for project", zap.String("name", projectConfig.Name))
imageName := fmt.Sprintf("flux_%s-image", projectConfig.Name) imageName := fmt.Sprintf("fluxi-%s", namesgenerator.GetRandomName(0))
buildCmd := exec.Command("pack", "build", imageName, "--builder", s.config.Builder) buildCmd := exec.Command("pack", "build", imageName, "--builder", flux.config.Builder)
buildCmd.Dir = projectPath buildCmd.Dir = projectPath
buildCmd.Stdout = writer buildCmd.Stdout = writer
buildCmd.Stderr = writer buildCmd.Stderr = writer
@@ -365,7 +402,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
app := Flux.appManager.GetApp(projectConfig.Name) app := flux.appManager.GetApp(deployRequest.Id)
eventChannel <- DeploymentEvent{ eventChannel <- DeploymentEvent{
Stage: "creating", Stage: "creating",
@@ -373,7 +410,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
} }
if app == nil { if app == nil {
app, err = CreateApp(ctx, imageName, projectPath, projectConfig) app, err = flux.CreateApp(ctx, imageName, projectPath, projectConfig, deployRequest.Id)
} else { } else {
err = app.Upgrade(ctx, imageName, projectPath, projectConfig) err = app.Upgrade(ctx, imageName, projectPath, projectConfig)
} }
@@ -389,18 +426,27 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
var extApp pkg.App
extApp.Id = app.Id
extApp.Name = app.Name
extApp.DeploymentID = app.DeploymentID
eventChannel <- DeploymentEvent{ eventChannel <- DeploymentEvent{
Stage: "complete", Stage: "complete",
Message: app, Message: extApp,
} }
logger.Infow("App deployed successfully", zap.String("name", app.Name)) logger.Infow("App deployed successfully", zap.String("id", app.Id.String()))
} }
func (s *FluxServer) StartDeployHandler(w http.ResponseWriter, r *http.Request) { func (flux *FluxServer) StartDeployHandler(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name") appId, err := uuid.Parse(r.PathValue("id"))
if err != nil {
http.Error(w, "Invalid app id", http.StatusBadRequest)
return
}
app := Flux.appManager.GetApp(name) app := flux.appManager.GetApp(appId)
if app == nil { if app == nil {
http.Error(w, "App not found", http.StatusNotFound) http.Error(w, "App not found", http.StatusNotFound)
return return
@@ -431,9 +477,13 @@ func (s *FluxServer) StartDeployHandler(w http.ResponseWriter, r *http.Request)
} }
func (s *FluxServer) StopDeployHandler(w http.ResponseWriter, r *http.Request) { func (s *FluxServer) StopDeployHandler(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name") appId, err := uuid.Parse(r.PathValue("id"))
if err != nil {
http.Error(w, "Invalid app id", http.StatusBadRequest)
return
}
app := Flux.appManager.GetApp(name) app := Flux.appManager.GetApp(appId)
if app == nil { if app == nil {
http.Error(w, "App not found", http.StatusNotFound) http.Error(w, "App not found", http.StatusNotFound)
return return
@@ -460,11 +510,15 @@ func (s *FluxServer) StopDeployHandler(w http.ResponseWriter, r *http.Request) {
} }
func (s *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request) { func (s *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name") appId, err := uuid.Parse(r.PathValue("id"))
if err != nil {
http.Error(w, "Invalid app id", http.StatusBadRequest)
return
}
logger.Debugw("Deleting deployment", zap.String("name", name)) logger.Debugw("Deleting deployment", zap.String("id", appId.String()))
err := Flux.appManager.DeleteApp(name) err = Flux.appManager.DeleteApp(appId)
if err != nil { if err != nil {
logger.Errorw("Failed to delete app", zap.Error(err)) logger.Errorw("Failed to delete app", zap.Error(err))
@@ -476,8 +530,13 @@ func (s *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request)
} }
func (s *FluxServer) DeleteAllDeploymentsHandler(w http.ResponseWriter, r *http.Request) { func (s *FluxServer) DeleteAllDeploymentsHandler(w http.ResponseWriter, r *http.Request) {
if s.config.DisableDeleteAll {
http.Error(w, "Delete all is disabled", http.StatusForbidden)
return
}
for _, app := range Flux.appManager.GetAllApps() { for _, app := range Flux.appManager.GetAllApps() {
err := Flux.appManager.DeleteApp(app.Name) err := Flux.appManager.DeleteApp(app.Id)
if err != nil { if err != nil {
logger.Errorw("Failed to remove app", zap.Error(err)) logger.Errorw("Failed to remove app", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -500,7 +559,7 @@ func (s *FluxServer) ListAppsHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
extApp.ID = app.ID extApp.Id = app.Id
extApp.Name = app.Name extApp.Name = app.Name
extApp.DeploymentID = app.DeploymentID extApp.DeploymentID = app.DeploymentID
extApp.DeploymentStatus = deploymentStatus extApp.DeploymentStatus = deploymentStatus

View File

@@ -24,7 +24,7 @@ type Deployment struct {
// Creates a deployment row in the database, containting the URL the app should be hosted on (it's public hostname) // Creates a deployment row in the database, containting the URL the app should be hosted on (it's public hostname)
// and the port that the web server is listening on // and the port that the web server is listening on
func CreateDeployment(port uint16, appUrl string, db *sql.DB) (*Deployment, error) { func (flux *FluxServer) CreateDeployment(port uint16, appUrl string) (*Deployment, error) {
var deployment Deployment var deployment Deployment
err := deploymentInsertStmt.QueryRow(appUrl, port).Scan(&deployment.ID, &deployment.URL, &deployment.Port) err := deploymentInsertStmt.QueryRow(appUrl, port).Scan(&deployment.ID, &deployment.URL, &deployment.Port)
@@ -38,30 +38,34 @@ func CreateDeployment(port uint16, appUrl string, db *sql.DB) (*Deployment, erro
// Takes an existing deployment, and gracefully upgrades the app to a new image // Takes an existing deployment, and gracefully upgrades the app to a new image
func (deployment *Deployment) Upgrade(ctx context.Context, projectConfig *pkg.ProjectConfig, imageName string, projectPath string) error { func (deployment *Deployment) Upgrade(ctx context.Context, projectConfig *pkg.ProjectConfig, imageName string, projectPath string) error {
existingContainers, err := findExistingDockerContainers(ctx, projectConfig.Name)
if err != nil {
return fmt.Errorf("failed to find existing containers: %v", err)
}
// we only upgrade the head container, in the future we might want to allow upgrading supplemental containers, but this should work just fine for now. // we only upgrade the head container, in the future we might want to allow upgrading supplemental containers, but this should work just fine for now.
container, err := deployment.Head.Upgrade(ctx, imageName, projectPath, projectConfig) newHeadContainer, err := deployment.Head.Upgrade(ctx, imageName, projectPath, projectConfig.Environment)
if err != nil { if err != nil {
logger.Errorw("Failed to upgrade container", zap.Error(err)) logger.Errorw("Failed to upgrade container", zap.Error(err))
return err return err
} }
// copy(container.ContainerID[:], containerIDString) oldHeadContainer := deployment.Head
deployment.Head = container Flux.db.Exec("DELETE FROM containers WHERE id = ?", oldHeadContainer.ID)
deployment.Containers = append(deployment.Containers, container)
logger.Debugw("Starting container", zap.ByteString("container_id", container.ContainerID[:12])) var containers []*Container
err = container.Start(ctx, true) for _, container := range deployment.Containers {
if !container.Head {
containers = append(containers, container)
}
}
deployment.Head = newHeadContainer
deployment.Containers = append(containers, newHeadContainer)
logger.Debugw("Starting container", zap.ByteString("container_id", newHeadContainer.ContainerID[:12]))
err = newHeadContainer.Start(ctx, true)
if err != nil { if err != nil {
logger.Errorw("Failed to start container", zap.Error(err)) logger.Errorw("Failed to start container", zap.Error(err))
return err return err
} }
if err := container.Wait(ctx, projectConfig.Port); err != nil { if err := newHeadContainer.Wait(ctx, projectConfig.Port); err != nil {
logger.Errorw("Failed to wait for container", zap.Error(err)) logger.Errorw("Failed to wait for container", zap.Error(err))
return err return err
} }
@@ -79,50 +83,15 @@ func (deployment *Deployment) Upgrade(ctx context.Context, projectConfig *pkg.Pr
return err return err
} }
tx, err := Flux.db.Begin()
if err != nil {
logger.Errorw("Failed to begin transaction", zap.Error(err))
return err
}
var containers []*Container
var oldContainers []*Container
// delete the old head container from the database, and update the deployment's container list
for _, container := range deployment.Containers {
if existingContainers[string(container.ContainerID[:])] {
logger.Debugw("Deleting container from db", zap.ByteString("container_id", container.ContainerID[:12]))
_, err = tx.Exec("DELETE FROM containers WHERE id = ?", container.ID)
oldContainers = append(oldContainers, container)
if err != nil {
logger.Errorw("Failed to delete container", zap.Error(err))
tx.Rollback()
return err
}
continue
}
containers = append(containers, container)
}
if err := tx.Commit(); err != nil {
logger.Errorw("Failed to commit transaction", zap.Error(err))
return err
}
// gracefully shutdown the old proxy, or if it doesnt exist, just remove the containers // gracefully shutdown the old proxy, or if it doesnt exist, just remove the containers
if oldProxy != nil { if oldProxy != nil {
go oldProxy.GracefulShutdown(oldContainers) go oldProxy.GracefulShutdown([]*Container{oldHeadContainer})
} else { } else {
for _, container := range oldContainers { err := RemoveDockerContainer(context.Background(), string(oldHeadContainer.ContainerID[:]))
err := RemoveDockerContainer(context.Background(), string(container.ContainerID[:]))
if err != nil { if err != nil {
logger.Errorw("Failed to remove container", zap.Error(err)) logger.Errorw("Failed to remove container", zap.Error(err))
} }
} }
}
deployment.Containers = containers deployment.Containers = containers
return nil return nil

View File

@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS deployments (
); );
CREATE TABLE IF NOT EXISTS apps ( CREATE TABLE IF NOT EXISTS apps (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, id BLOB PRIMARY KEY,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
deployment_id INTEGER, deployment_id INTEGER,
FOREIGN KEY(deployment_id) REFERENCES deployments(id) FOREIGN KEY(deployment_id) REFERENCES deployments(id)

View File

@@ -17,6 +17,7 @@ import (
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/google/uuid"
"github.com/juls0730/flux/pkg" "github.com/juls0730/flux/pkg"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"go.uber.org/zap" "go.uber.org/zap"
@@ -39,6 +40,7 @@ var (
type FluxServerConfig struct { type FluxServerConfig struct {
Builder string `json:"builder"` Builder string `json:"builder"`
DisableDeleteAll bool `json:"disable_delete_all"`
Compression pkg.Compression `json:"compression"` Compression pkg.Compression `json:"compression"`
} }
@@ -185,9 +187,9 @@ func NewServer() *FluxServer {
// Handler for uploading a project to the server. We have to upload the entire project since we need to build the // 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 // project ourselves to work with the buildpacks
func (s *FluxServer) UploadAppCode(code io.Reader, projectConfig *pkg.ProjectConfig) (string, error) { func (s *FluxServer) UploadAppCode(code io.Reader, appId uuid.UUID) (string, error) {
var err error var err error
projectPath := filepath.Join(s.rootDir, "apps", projectConfig.Name) projectPath := filepath.Join(s.rootDir, "apps", appId.String())
if err = os.MkdirAll(projectPath, 0755); err != nil { if err = os.MkdirAll(projectPath, 0755); err != nil {
logger.Errorw("Failed to create project directory", zap.Error(err)) logger.Errorw("Failed to create project directory", zap.Error(err))
return "", err return "", err
@@ -259,10 +261,10 @@ func (s *FluxServer) UploadAppCode(code io.Reader, projectConfig *pkg.ProjectCon
return projectPath, nil return projectPath, nil
} }
// TODO: split each prepare statement into its coresponding module so the statememnts are easier to fine // TODO: split each prepare statement into its coresponding module so the statememnts are easier to find
func PrepareDBStatements(db *sql.DB) error { func PrepareDBStatements(db *sql.DB) error {
var err error var err error
appInsertStmt, err = db.Prepare("INSERT INTO apps (name, deployment_id) VALUES ($1, $2) RETURNING id, name, deployment_id") appInsertStmt, err = db.Prepare("INSERT INTO apps (id, name, deployment_id) VALUES ($1, $2, $3) RETURNING id, name, deployment_id")
if err != nil { if err != nil {
return fmt.Errorf("failed to prepare statement: %v", err) return fmt.Errorf("failed to prepare statement: %v", err)
} }

View File

@@ -1,7 +1,9 @@
package pkg package pkg
import "github.com/google/uuid"
type App struct { type App struct {
ID int64 `json:"id,omitempty"` Id uuid.UUID `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
DeploymentID int64 `json:"deployment_id,omitempty"` DeploymentID int64 `json:"deployment_id,omitempty"`
DeploymentStatus string `json:"deployment_status,omitempty"` DeploymentStatus string `json:"deployment_status,omitempty"`

30
pkg/typedmap.go Normal file
View File

@@ -0,0 +1,30 @@
package pkg
import "sync"
type TypedMap[K comparable, V any] struct {
internal sync.Map
}
func (m *TypedMap[K, V]) Load(key K) (V, bool) {
val, ok := m.internal.Load(key)
if !ok {
var zero V
return zero, false
}
return val.(V), true
}
func (m *TypedMap[K, V]) Store(key K, value V) {
m.internal.Store(key, value)
}
func (m *TypedMap[K, V]) Delete(key K) {
m.internal.Delete(key)
}
func (m *TypedMap[K, V]) Range(f func(key K, value V) bool) {
m.internal.Range(func(k, v any) bool {
return f(k.(K), v.(V))
})
}

View File

@@ -1,3 +1,3 @@
package pkg package pkg
const Version = "2025.04.13-05" const Version = "2025.04.13-10"