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

@@ -2,31 +2,60 @@ package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"sync"
"github.com/google/uuid"
"github.com/juls0730/flux/pkg"
"go.uber.org/zap"
)
type App struct {
ID int64 `json:"id,omitempty"`
Deployment *Deployment `json:"-"`
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 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{
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
if err != nil {
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 {
c, err := CreateContainer(ctx, &container, projectConfig.Name, false, deployment)
c, err := flux.CreateContainer(ctx, &container, false, deployment, container.Name)
if err != nil {
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)
}
headContainer := &pkg.Container{
Name: projectConfig.Name,
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 = CreateContainer(ctx, headContainer, projectConfig.Name, true, deployment)
_, 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
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 {
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.Name, app)
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("name", app.Name))
logger.Debugw("Upgrading deployment", zap.String("id", app.Id.String()))
// if deploy is not started, start it
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)
if err != nil {
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.
func (app *App) Remove(ctx context.Context) error {
Flux.appManager.RemoveApp(app.Name)
app.flux.appManager.RemoveApp(app.Id)
err := app.Deployment.Remove(ctx)
if err != nil {
@@ -105,13 +140,13 @@ func (app *App) Remove(ctx context.Context) error {
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 {
logger.Errorw("Failed to delete app", zap.Error(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)
if err != nil {
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 {
sync.Map
pkg.TypedMap[uuid.UUID, *App]
nameIndex pkg.TypedMap[string, uuid.UUID]
}
func (am *AppManager) GetApp(name string) *App {
app, exists := am.Load(name)
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.(*App)
return app
}
func (am *AppManager) GetAllApps() []*App {
var apps []*App
am.Range(func(key, value interface{}) bool {
if app, ok := value.(*App); ok {
apps = append(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(name string) {
am.Delete(name)
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(name string, app *App) {
if app.Deployment.Containers == nil || app.Deployment.Head == nil || len(app.Deployment.Containers) == 0 {
panic("nil containers")
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.Store(name, app)
am.nameIndex.Store(app.Name, id)
am.Store(id, app)
}
// nukes an app completely
func (am *AppManager) DeleteApp(name string) error {
app := am.GetApp(name)
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
}
am.Delete(name)
return nil
}
@@ -193,10 +242,13 @@ func (am *AppManager) Init() {
var apps []App
for rows.Next() {
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))
return
}
app.Id = uuid.Must(uuid.FromBytes(appIdBlob))
app.flux = Flux
apps = append(apps, app)
}
@@ -250,7 +302,7 @@ func (am *AppManager) Init() {
deployment.Head = headContainer
app.Deployment = deployment
am.AddApp(app.Name, &app)
am.AddApp(app.Id, &app)
status, err := deployment.Status(context.Background())
if err != nil {

View File

@@ -6,13 +6,13 @@ import (
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/pkg/namesgenerator"
"github.com/juls0730/flux/pkg"
"go.uber.org/zap"
)
@@ -30,8 +30,9 @@ type Volume struct {
type Container struct {
ID int64 `json:"id"`
Head bool `json:"head"` // if the container is the head of the deployment
Name string `json:"name"`
Head bool `json:"head"` // if the container is the head of the deployment
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:"-"`
Volumes []*Volume `json:"volumes"`
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
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 {
if host == ":" {
return nil, fmt.Errorf("invalid host %s", host)
}
}
safeImageName := strings.ReplaceAll(imageName, "/", "_")
containerName := fmt.Sprintf("flux_%s-%s-%s", safeImageName, projectName, time.Now().Format("20060102-150405"))
containerName := fmt.Sprintf("flux-%s", namesgenerator.GetRandomName(0))
logger.Debugw("Creating container", zap.String("container_id", containerName))
mounts := make([]mount.Mount, len(vols))
volumes := make(map[string]struct{}, len(vols))
@@ -86,6 +85,9 @@ func CreateDockerContainer(ctx context.Context, imageName string, projectName st
Image: imageName,
Env: environment,
Volumes: volumes,
Labels: map[string]string{
"managed-by": "flux",
},
},
&container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyUnlessStopped},
@@ -104,6 +106,7 @@ func CreateDockerContainer(ctx context.Context, imageName string, projectName st
c := &Container{
ContainerID: [64]byte([]byte(resp.ID)),
Volumes: vols,
Name: containerName,
}
return c, nil
@@ -113,9 +116,9 @@ func CreateDockerContainer(ctx context.Context, imageName string, projectName st
// 1. Create the container in the docker daemon
// 2. Create the volumes for the container
// 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) {
if container.Name == "" {
return nil, fmt.Errorf("container name is empty")
func (flux *FluxServer) CreateContainer(ctx context.Context, container *pkg.Container, head bool, deployment *Deployment, friendlyName string) (c *Container, err error) {
if friendlyName == "" {
return nil, fmt.Errorf("container friendly name is empty")
}
if container.ImageName == "" {
@@ -166,7 +169,7 @@ func CreateContainer(ctx context.Context, container *pkg.Container, projectName
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)
}
c, err = CreateDockerContainer(ctx, container.ImageName, projectName, volumes, container.Environment, hosts)
c, err = CreateDockerContainer(ctx, container.ImageName, volumes, container.Environment, hosts)
if err != nil {
return nil, err
}
c.Name = container.Name
c.FriendlyName = friendlyName
var containerIDString string
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)
tx, err := Flux.db.Begin()
tx, err := flux.db.Begin()
if err != nil {
return nil, err
}
@@ -231,24 +234,21 @@ func CreateContainer(ctx context.Context, container *pkg.Container, projectName
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
logger.Debugw("Upgrading container", zap.ByteString("container_id", c.ContainerID[:12]))
if c.Volumes == nil {
return nil, fmt.Errorf("no volumes found for container %s", c.ContainerID[:12])
}
var hosts []string
for _, container := range c.Deployment.Containers {
containerJSON, err := Flux.dockerClient.ContainerInspect(context.Background(), string(container.ContainerID[:]))
if err != nil {
return nil, err
}
hosts = containerJSON.HostConfig.ExtraHosts
containerJSON, err := Flux.dockerClient.ContainerInspect(context.Background(), string(c.ContainerID[:]))
if err != nil {
return nil, err
}
newContainer, err := CreateDockerContainer(ctx, imageName, projectConfig.Name, c.Volumes, projectConfig.Environment, hosts)
hosts := containerJSON.HostConfig.ExtraHosts
newContainer, err := CreateDockerContainer(ctx, imageName, c.Volumes, emvironment, hosts)
if err != nil {
return nil, err
}
@@ -294,7 +294,7 @@ func (c *Container) Upgrade(ctx context.Context, imageName, projectPath string,
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
func (c *Container) Start(ctx context.Context, initial bool) error {
if !initial && c.Head {
@@ -331,14 +331,18 @@ func (c *Container) Start(ctx context.Context, initial bool) error {
return err
}
hosts = append(hosts, fmt.Sprintf("%s:%s", supplementalContainer.Name, ip))
hosts = append(hosts, fmt.Sprintf("%s:%s", supplementalContainer.FriendlyName, ip))
}
// recreate yourself
// TODO: pull this out so it stays in sync with CreateDockerContainer
resp, err := Flux.dockerClient.ContainerCreate(ctx, &container.Config{
Image: containerJSON.Image,
Env: containerJSON.Config.Env,
Volumes: volumes,
Labels: map[string]string{
"managed-by": "flux",
},
},
&container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyUnlessStopped},
@@ -521,21 +525,3 @@ func RemoveVolume(ctx context.Context, volumeID string) error {
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/exec"
"path/filepath"
"strings"
"sync"
"github.com/docker/docker/pkg/namesgenerator"
"github.com/google/uuid"
"github.com/joho/godotenv"
"github.com/juls0730/flux/pkg"
"go.uber.org/zap"
@@ -24,55 +27,52 @@ var (
)
type DeployRequest struct {
Config multipart.File `form:"config"`
Code multipart.File `form:"code"`
}
type DeployResponse struct {
App App `json:"app"`
Id uuid.UUID `form:"id"`
Config pkg.ProjectConfig `form:"config"`
Code multipart.File `form:"code"`
}
type DeploymentLock struct {
mu sync.Mutex
deployed map[string]context.CancelFunc
deployed map[uuid.UUID]context.CancelFunc
}
func NewDeploymentLock() *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
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()
defer dt.mu.Unlock()
// Check if the app is already being deployed
if _, exists := dt.deployed[appName]; exists {
return nil, fmt.Errorf("app %s is already being deployed", appName)
if _, exists := dt.deployed[appId]; exists {
return nil, fmt.Errorf("app is already being deployed")
}
// Create a context that can be cancelled
ctx, cancel := context.WithCancel(ctx)
// Store the cancel function
dt.deployed[appName] = cancel
dt.deployed[appId] = cancel
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)
func (dt *DeploymentLock) UnlockDeployment(appName string) {
func (dt *DeploymentLock) UnlockDeployment(appId uuid.UUID) {
dt.mu.Lock()
defer dt.mu.Unlock()
// Remove the app from deployed tracking
if cancel, exists := dt.deployed[appName]; exists {
if cancel, exists := dt.deployed[appId]; exists {
// Cancel the context
cancel()
// Remove from map
delete(dt.deployed, appName)
delete(dt.deployed, appId)
}
}
@@ -84,8 +84,8 @@ type DeploymentEvent struct {
StatusCode int `json:"status,omitempty"`
}
func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
if Flux.appManager == nil {
func (flux *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
if flux.appManager == nil {
panic("App manager is nil")
}
@@ -101,22 +101,36 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
}
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)
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))
http.Error(w, "Invalid flux.json", http.StatusBadRequest)
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 {
// This will happen if the app is already being deployed
http.Error(w, err.Error(), http.StatusConflict)
@@ -125,7 +139,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
go func() {
<-ctx.Done()
deploymentLock.UnlockDeployment(projectConfig.Name)
deploymentLock.UnlockDeployment(deployRequest.Id)
}()
flusher, ok := w.(http.Flusher)
@@ -213,9 +227,9 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
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 {
logger.Infow("Failed to upload code", zap.Error(err))
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,
// and place all of it's content into the environment field so that docker can find it later
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 {
logger.Errorw("Failed to open env file", zap.Error(err))
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{
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
@@ -331,8 +368,8 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
reader, writer = io.Pipe()
logger.Debugw("Building image for project", zap.String("name", projectConfig.Name))
imageName := fmt.Sprintf("flux_%s-image", projectConfig.Name)
buildCmd := exec.Command("pack", "build", imageName, "--builder", s.config.Builder)
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
@@ -365,7 +402,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
return
}
app := Flux.appManager.GetApp(projectConfig.Name)
app := flux.appManager.GetApp(deployRequest.Id)
eventChannel <- DeploymentEvent{
Stage: "creating",
@@ -373,7 +410,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
}
if app == nil {
app, err = CreateApp(ctx, imageName, projectPath, projectConfig)
app, err = flux.CreateApp(ctx, imageName, projectPath, projectConfig, deployRequest.Id)
} else {
err = app.Upgrade(ctx, imageName, projectPath, projectConfig)
}
@@ -389,18 +426,27 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
return
}
var extApp pkg.App
extApp.Id = app.Id
extApp.Name = app.Name
extApp.DeploymentID = app.DeploymentID
eventChannel <- DeploymentEvent{
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) {
name := r.PathValue("name")
func (flux *FluxServer) StartDeployHandler(w http.ResponseWriter, r *http.Request) {
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 {
http.Error(w, "App not found", http.StatusNotFound)
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) {
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 {
http.Error(w, "App not found", http.StatusNotFound)
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) {
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 {
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) {
if s.config.DisableDeleteAll {
http.Error(w, "Delete all is disabled", http.StatusForbidden)
return
}
for _, app := range Flux.appManager.GetAllApps() {
err := Flux.appManager.DeleteApp(app.Name)
err := Flux.appManager.DeleteApp(app.Id)
if err != nil {
logger.Errorw("Failed to remove app", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -500,7 +559,7 @@ func (s *FluxServer) ListAppsHandler(w http.ResponseWriter, r *http.Request) {
return
}
extApp.ID = app.ID
extApp.Id = app.Id
extApp.Name = app.Name
extApp.DeploymentID = app.DeploymentID
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)
// 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
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
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.
container, err := deployment.Head.Upgrade(ctx, imageName, projectPath, projectConfig)
newHeadContainer, err := deployment.Head.Upgrade(ctx, imageName, projectPath, projectConfig.Environment)
if err != nil {
logger.Errorw("Failed to upgrade container", zap.Error(err))
return err
}
// copy(container.ContainerID[:], containerIDString)
deployment.Head = container
deployment.Containers = append(deployment.Containers, container)
oldHeadContainer := deployment.Head
Flux.db.Exec("DELETE FROM containers WHERE id = ?", oldHeadContainer.ID)
logger.Debugw("Starting container", zap.ByteString("container_id", container.ContainerID[:12]))
err = container.Start(ctx, true)
var containers []*Container
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 {
logger.Errorw("Failed to start container", zap.Error(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))
return err
}
@@ -79,48 +83,13 @@ func (deployment *Deployment) Upgrade(ctx context.Context, projectConfig *pkg.Pr
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
if oldProxy != nil {
go oldProxy.GracefulShutdown(oldContainers)
go oldProxy.GracefulShutdown([]*Container{oldHeadContainer})
} else {
for _, container := range oldContainers {
err := RemoveDockerContainer(context.Background(), string(container.ContainerID[:]))
if err != nil {
logger.Errorw("Failed to remove container", zap.Error(err))
}
err := RemoveDockerContainer(context.Background(), string(oldHeadContainer.ContainerID[:]))
if err != nil {
logger.Errorw("Failed to remove container", zap.Error(err))
}
}

View File

@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS deployments (
);
CREATE TABLE IF NOT EXISTS apps (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
id BLOB PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
deployment_id INTEGER,
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/client"
"github.com/google/uuid"
"github.com/juls0730/flux/pkg"
_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
@@ -38,8 +39,9 @@ var (
)
type FluxServerConfig struct {
Builder string `json:"builder"`
Compression pkg.Compression `json:"compression"`
Builder string `json:"builder"`
DisableDeleteAll bool `json:"disable_delete_all"`
Compression pkg.Compression `json:"compression"`
}
type FluxServer struct {
@@ -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
// 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
projectPath := filepath.Join(s.rootDir, "apps", projectConfig.Name)
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
@@ -259,10 +261,10 @@ func (s *FluxServer) UploadAppCode(code io.Reader, projectConfig *pkg.ProjectCon
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 {
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 {
return fmt.Errorf("failed to prepare statement: %v", err)
}