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:
16
README.md
16
README.md
@@ -4,10 +4,14 @@ Flux is a lightweight self-hosted pseudo-PaaS for hosting Golang web apps with e
|
||||
|
||||
**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
|
||||
- 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**:
|
||||
- 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?)
|
||||
@@ -102,11 +106,19 @@ Flux daemon looks for a confgiuration file in `/var/fluxd/config.json` but can b
|
||||
|
||||
```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`)
|
||||
- `disable_delete_all`: Disable the delete all deployments endpoint (default: `false`)
|
||||
- `compression`: Compression settings
|
||||
- `enabled`: Enable compression (default: `false`)
|
||||
- `level`: Compression level
|
||||
|
||||
#### Daemon Settings
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ func DeleteCommand(ctx models.CommandCtx, args []string) error {
|
||||
return deleteAll(ctx, noConfirm)
|
||||
}
|
||||
|
||||
projectName, err := GetProjectName("delete", args)
|
||||
projectName, err := GetProjectId("delete", args, ctx.Config)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
// remove the .fluxid file if it exists
|
||||
os.Remove(".fluxid")
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully deleted %s\n", projectName)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/google/uuid"
|
||||
"github.com/juls0730/flux/cmd/flux/models"
|
||||
"github.com/juls0730/flux/pkg"
|
||||
)
|
||||
@@ -188,7 +189,32 @@ func DeployCommand(ctx models.CommandCtx, args []string) error {
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
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 {
|
||||
return fmt.Errorf("failed to create config part: %v", err)
|
||||
@@ -246,7 +272,19 @@ func DeployCommand(ctx models.CommandCtx, args []string) error {
|
||||
switch event {
|
||||
case "complete":
|
||||
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
|
||||
case "cmd_output":
|
||||
customWriter.Printf("... %s\n", data.Message)
|
||||
|
||||
@@ -3,14 +3,27 @@ package commands
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/juls0730/flux/cmd/flux/models"
|
||||
"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
|
||||
|
||||
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 _, 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)
|
||||
@@ -32,5 +45,28 @@ func GetProjectName(command string, args []string) (string, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func StartCommand(ctx models.CommandCtx, args []string) error {
|
||||
projectName, err := GetProjectName("start", args)
|
||||
projectName, err := GetProjectId("start", args, ctx.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func StopCommand(ctx models.CommandCtx, args []string) error {
|
||||
projectName, err := GetProjectName("stop", args)
|
||||
projectName, err := GetProjectId("stop", args, ctx.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ func main() {
|
||||
|
||||
http.HandleFunc("POST /deploy", fluxServer.DeployHandler)
|
||||
http.HandleFunc("DELETE /deployments", fluxServer.DeleteAllDeploymentsHandler)
|
||||
http.HandleFunc("DELETE /deployments/{name}", fluxServer.DeleteDeployHandler)
|
||||
http.HandleFunc("POST /start/{name}", fluxServer.StartDeployHandler)
|
||||
http.HandleFunc("POST /stop/{name}", fluxServer.StopDeployHandler)
|
||||
http.HandleFunc("DELETE /deployments/{id}", fluxServer.DeleteDeployHandler)
|
||||
http.HandleFunc("POST /start/{id}", fluxServer.StartDeployHandler)
|
||||
http.HandleFunc("POST /stop/{id}", fluxServer.StopDeployHandler)
|
||||
http.HandleFunc("GET /apps", fluxServer.ListAppsHandler)
|
||||
http.HandleFunc("GET /apps/by-name/{name}", fluxServer.GetAppByNameHandler)
|
||||
http.HandleFunc("GET /heartbeat", fluxServer.DaemonInfoHandler)
|
||||
|
||||
fluxServer.Logger.Info("Fluxd started on http://127.0.0.1:5647")
|
||||
|
||||
5
go.mod
5
go.mod
@@ -9,7 +9,10 @@ require (
|
||||
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 (
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package pkg
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type App struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DeploymentID int64 `json:"deployment_id,omitempty"`
|
||||
DeploymentStatus string `json:"deployment_status,omitempty"`
|
||||
Id uuid.UUID `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DeploymentID int64 `json:"deployment_id,omitempty"`
|
||||
DeploymentStatus string `json:"deployment_status,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: this should be flattened to an int, where 0 = disabled and any other number is the level
|
||||
|
||||
30
pkg/typedmap.go
Normal file
30
pkg/typedmap.go
Normal 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))
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
package pkg
|
||||
|
||||
const Version = "2025.04.13-05"
|
||||
const Version = "2025.04.13-10"
|
||||
|
||||
Reference in New Issue
Block a user