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**:
|
**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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
5
go.mod
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -30,8 +30,9 @@ 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 {
|
if err != nil {
|
||||||
containerJSON, err := Flux.dockerClient.ContainerInspect(context.Background(), string(container.ContainerID[:]))
|
return nil, err
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hosts = containerJSON.HostConfig.ExtraHosts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
Code multipart.File `form:"code"`
|
Config pkg.ProjectConfig `form:"config"`
|
||||||
}
|
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
|
||||||
|
|||||||
@@ -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,48 +83,13 @@ 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -38,8 +39,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type FluxServerConfig struct {
|
type FluxServerConfig struct {
|
||||||
Builder string `json:"builder"`
|
Builder string `json:"builder"`
|
||||||
Compression pkg.Compression `json:"compression"`
|
DisableDeleteAll bool `json:"disable_delete_all"`
|
||||||
|
Compression pkg.Compression `json:"compression"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FluxServer struct {
|
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
|
// 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this should be flattened to an int, where 0 = disabled and any other number is the level
|
// 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
|
package pkg
|
||||||
|
|
||||||
const Version = "2025.04.13-05"
|
const Version = "2025.04.13-10"
|
||||||
|
|||||||
Reference in New Issue
Block a user