almost there

This commit is contained in:
Zoe
2024-12-07 02:35:38 -06:00
parent f1ad13a216
commit d27cc71f1d
12 changed files with 707 additions and 586 deletions

View File

@@ -11,13 +11,15 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/juls0730/fluxd/models" "github.com/juls0730/fluxd/pkg"
) )
//go:embed config.json //go:embed config.json
@@ -98,7 +100,7 @@ func getProjectName() string {
} }
defer fluxConfigFile.Close() defer fluxConfigFile.Close()
var config models.ProjectConfig var config pkg.ProjectConfig
if err := json.NewDecoder(fluxConfigFile).Decode(&config); err != nil { if err := json.NewDecoder(fluxConfigFile).Decode(&config); err != nil {
fmt.Printf("Failed to decode flux.json: %v\n", err) fmt.Printf("Failed to decode flux.json: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -113,11 +115,23 @@ func getProjectName() string {
} }
func main() { func main() {
loadingSpinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
if len(os.Args) < 2 { if len(os.Args) < 2 {
fmt.Println("Usage: flux <command>") fmt.Println("Usage: flux <command>")
os.Exit(1) os.Exit(1)
} }
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-signalChannel
if loadingSpinner.Active() {
loadingSpinner.Stop()
}
os.Exit(0)
}()
command := os.Args[1] command := os.Args[1]
if _, err := os.Stat(filepath.Join(configPath, "config.json")); err != nil { if _, err := os.Stat(filepath.Join(configPath, "config.json")); err != nil {
@@ -151,7 +165,6 @@ func main() {
os.Exit(1) os.Exit(1)
} }
loadingSpinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
loadingSpinner.Suffix = " Deploying" loadingSpinner.Suffix = " Deploying"
loadingSpinner.Start() loadingSpinner.Start()
@@ -293,6 +306,59 @@ func main() {
fmt.Printf("Successfully started %s\n", projectName) fmt.Printf("Successfully started %s\n", projectName)
case "delete": case "delete":
if len(os.Args) == 3 {
if os.Args[2] == "all" {
var response string
fmt.Print("Are you sure you want to delete all projects? this will delete all volumes and containers associated and cannot be undone. \n[y/N] ")
fmt.Scanln(&response)
if strings.ToLower(response) != "y" {
fmt.Println("Aborting...")
os.Exit(0)
}
response = ""
fmt.Printf("Are you really sure you want to delete all projects? \n[y/N] ")
fmt.Scanln(&response)
if strings.ToLower(response) != "y" {
fmt.Println("Aborting...")
os.Exit(0)
}
req, err := http.NewRequest("DELETE", config.DeamonURL+"/deployments", nil)
if err != nil {
fmt.Printf("Failed to delete deployments: %v\n", err)
os.Exit(1)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("Failed to delete deployments: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("error reading response body: %v\n", err)
os.Exit(1)
}
if len(responseBody) > 0 && responseBody[len(responseBody)-1] == '\n' {
responseBody = responseBody[:len(responseBody)-1]
}
fmt.Printf("Delete failed: %s\n", responseBody)
os.Exit(1)
}
fmt.Printf("Successfully deleted all projects\n")
return
}
}
projectName := getProjectName() projectName := getProjectName()
// ask for confirmation // ask for confirmation
@@ -340,7 +406,22 @@ func main() {
os.Exit(1) os.Exit(1)
} }
var apps []models.App if resp.StatusCode != http.StatusOK {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("error reading response body: %v\n", err)
os.Exit(1)
}
if len(responseBody) > 0 && responseBody[len(responseBody)-1] == '\n' {
responseBody = responseBody[:len(responseBody)-1]
}
fmt.Printf("List failed: %s\n", responseBody)
os.Exit(1)
}
var apps []pkg.App
if err := json.NewDecoder(resp.Body).Decode(&apps); err != nil { if err := json.NewDecoder(resp.Body).Decode(&apps); err != nil {
fmt.Printf("Failed to decode apps: %v\n", err) fmt.Printf("Failed to decode apps: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -355,7 +436,7 @@ func main() {
fmt.Printf("%s (%s)\n", app.Name, app.DeploymentStatus) fmt.Printf("%s (%s)\n", app.Name, app.DeploymentStatus)
} }
case "init": case "init":
var projectConfig models.ProjectConfig var projectConfig pkg.ProjectConfig
var response string var response string
if len(os.Args) > 2 { if len(os.Args) > 2 {
@@ -379,7 +460,8 @@ func main() {
fmt.Println("What port does your project listen to?") fmt.Println("What port does your project listen to?")
fmt.Scanln(&response) fmt.Scanln(&response)
projectConfig.Port, err = strconv.Atoi(response) port, err := strconv.ParseUint(response, 10, 16)
projectConfig.Port = uint16(port)
if err != nil || projectConfig.Port < 1 || projectConfig.Port > 65535 { if err != nil || projectConfig.Port < 1 || projectConfig.Port > 65535 {
fmt.Println("That doesnt look like a valid port", err) fmt.Println("That doesnt look like a valid port", err)
os.Exit(1) os.Exit(1)

View File

@@ -9,11 +9,13 @@ import (
func main() { func main() {
fluxServer := server.NewServer() fluxServer := server.NewServer()
server.InitReverseProxy()
go fluxServer.Proxy.Start() // go fluxServer.Proxy.Start()
http.HandleFunc("POST /deploy", fluxServer.DeployHandler) http.HandleFunc("POST /deploy", fluxServer.DeployHandler)
http.HandleFunc("DELETE /deploy/{name}", fluxServer.DeleteDeployHandler) http.HandleFunc("DELETE /deployments", fluxServer.DeleteAllDeploymentsHandler)
http.HandleFunc("DELETE /deployments/{name}", fluxServer.DeleteDeployHandler)
http.HandleFunc("POST /start/{name}", fluxServer.StartDeployHandler) http.HandleFunc("POST /start/{name}", fluxServer.StartDeployHandler)
http.HandleFunc("POST /stop/{name}", fluxServer.StopDeployHandler) http.HandleFunc("POST /stop/{name}", fluxServer.StopDeployHandler)
http.HandleFunc("GET /apps", fluxServer.ListAppsHandler) http.HandleFunc("GET /apps", fluxServer.ListAppsHandler)

View File

@@ -1,20 +0,0 @@
package models
type ProjectConfig struct {
Name string `json:"name,omitempty"`
Url string `json:"url,omitempty"`
Port int `json:"port,omitempty"`
EnvFile string `json:"env_file,omitempty"`
Environment []string `json:"environment,omitempty"`
}
type App struct {
ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Image string `json:"image,omitempty"`
ProjectPath string `json:"project_path,omitempty"`
ProjectConfig ProjectConfig `json:"project_config,omitempty"`
DeploymentID int64 `json:"deployment_id,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
DeploymentStatus string `json:"deployment_status,omitempty"`
}

View File

@@ -1,15 +0,0 @@
package models
type Containers struct {
ID string `json:"id"`
Head bool `json:"head"` // if the container is the head of the deployment
ContainerID string `json:"container_id"`
DeploymentID int64 `json:"deployment_id"`
CreatedAt string `json:"created_at"`
}
type Deployments struct {
ID int64 `json:"id"`
URL string `json:"url"`
CreatedAt string `json:"created_at"`
}

9
pkg/config.go Normal file
View File

@@ -0,0 +1,9 @@
package pkg
type ProjectConfig struct {
Name string `json:"name,omitempty"`
Url string `json:"url,omitempty"`
Port uint16 `json:"port,omitempty"`
EnvFile string `json:"env_file,omitempty"`
Environment []string `json:"environment,omitempty"`
}

8
pkg/responses.go Normal file
View File

@@ -0,0 +1,8 @@
package pkg
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"`
}

View File

@@ -15,25 +15,30 @@ import (
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/juls0730/fluxd/models" "github.com/juls0730/fluxd/pkg"
) )
type ContainerManager struct { var dockerClient *client.Client
dockerClient *client.Client
type Container struct {
ID int64 `json:"id"`
Head bool `json:"head"` // if the container is the head of the deployment
Deployment *Deployment
ContainerID [64]byte `json:"container_id"`
DeploymentID int64 `json:"deployment_id"`
} }
func NewContainerManager() *ContainerManager { func init() {
dockerClient, err := client.NewClientWithOpts(client.FromEnv) log.Printf("Initializing Docker client...\n")
var err error
dockerClient, err = client.NewClientWithOpts(client.FromEnv)
if err != nil { if err != nil {
log.Fatalf("Failed to create Docker client: %v", err) log.Fatalf("Failed to create Docker client: %v", err)
} }
return &ContainerManager{
dockerClient: dockerClient,
}
} }
func (cm *ContainerManager) CreateContainer(ctx context.Context, imageName, projectPath string, projectConfig models.ProjectConfig) (string, error) { func CreateDockerContainer(ctx context.Context, imageName, projectPath string, projectConfig pkg.ProjectConfig) (string, error) {
log.Printf("Deploying container with image %s\n", imageName) log.Printf("Deploying container with image %s\n", imageName)
containerName := fmt.Sprintf("%s-%s", projectConfig.Name, time.Now().Format("20060102-150405")) containerName := fmt.Sprintf("%s-%s", projectConfig.Name, time.Now().Format("20060102-150405"))
@@ -55,10 +60,10 @@ func (cm *ContainerManager) CreateContainer(ctx context.Context, imageName, proj
} }
} }
vol, err := cm.dockerClient.VolumeCreate(ctx, volume.CreateOptions{ vol, err := dockerClient.VolumeCreate(ctx, volume.CreateOptions{
Driver: "local", Driver: "local",
DriverOpts: map[string]string{}, DriverOpts: map[string]string{},
Name: fmt.Sprintf("%s-volume", projectConfig.Name), Name: fmt.Sprintf("flux_%s-volume", projectConfig.Name),
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("Failed to create volume: %v", err) return "", fmt.Errorf("Failed to create volume: %v", err)
@@ -67,25 +72,15 @@ func (cm *ContainerManager) CreateContainer(ctx context.Context, imageName, proj
log.Printf("Volume %s created at %s\n", vol.Name, vol.Mountpoint) log.Printf("Volume %s created at %s\n", vol.Name, vol.Mountpoint)
log.Printf("Creating container %s...\n", containerName) log.Printf("Creating container %s...\n", containerName)
resp, err := cm.dockerClient.ContainerCreate(ctx, &container.Config{ resp, err := dockerClient.ContainerCreate(ctx, &container.Config{
Image: imageName, Image: imageName,
Env: projectConfig.Environment, Env: projectConfig.Environment,
// ExposedPorts: nat.PortSet{
// nat.Port(fmt.Sprintf("%d/tcp", projectConfig.Port)): {},
// },
Volumes: map[string]struct{}{ Volumes: map[string]struct{}{
vol.Name: {}, vol.Name: {},
}, },
}, },
&container.HostConfig{ &container.HostConfig{
// PortBindings: nat.PortMap{ NetworkMode: "bridge",
// nat.Port(fmt.Sprintf("%d/tcp", projectConfig.Port)): []nat.PortBinding{
// {
// HostIP: "0.0.0.0",
// HostPort: strconv.Itoa(projectConfig.Port),
// },
// },
// },
Mounts: []mount.Mount{ Mounts: []mount.Mount{
{ {
Type: mount.TypeVolume, Type: mount.TypeVolume,
@@ -107,28 +102,37 @@ func (cm *ContainerManager) CreateContainer(ctx context.Context, imageName, proj
return resp.ID, nil return resp.ID, nil
} }
func (cm *ContainerManager) StartContainer(ctx context.Context, containerID string) error { func (c *Container) Start(ctx context.Context) error {
return cm.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}) return dockerClient.ContainerStart(ctx, string(c.ContainerID[:]), container.StartOptions{})
} }
func (cm *ContainerManager) StopContainer(ctx context.Context, containerID string) error { func (c *Container) Stop(ctx context.Context) error {
return cm.dockerClient.ContainerStop(ctx, containerID, container.StopOptions{}) return dockerClient.ContainerStop(ctx, string(c.ContainerID[:]), container.StopOptions{})
}
func (c *Container) Remove(ctx context.Context) error {
return RemoveDockerContainer(ctx, string(c.ContainerID[:]))
}
func (c *Container) Wait(ctx context.Context, port uint16) error {
return WaitForDockerContainer(ctx, string(c.ContainerID[:]), port)
} }
// RemoveContainer stops and removes a container, but be warned that this will not remove the container from the database // RemoveContainer stops and removes a container, but be warned that this will not remove the container from the database
func (cm *ContainerManager) RemoveContainer(ctx context.Context, containerID string) error { func RemoveDockerContainer(ctx context.Context, containerID string) error {
if err := cm.dockerClient.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil { if err := dockerClient.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil {
return fmt.Errorf("Failed to stop existing container: %v", err) return fmt.Errorf("Failed to stop existing container: %v", err)
} }
if err := cm.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{}); err != nil { if err := dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{}); err != nil {
return fmt.Errorf("Failed to remove existing container: %v", err) return fmt.Errorf("Failed to remove existing container: %v", err)
} }
return nil return nil
} }
func (cm *ContainerManager) WaitForContainer(ctx context.Context, containerID string, containerPort int) error { // scuffed af "health check" for docker containers
func WaitForDockerContainer(ctx context.Context, containerID string, containerPort uint16) error {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel() defer cancel()
@@ -138,7 +142,7 @@ func (cm *ContainerManager) WaitForContainer(ctx context.Context, containerID st
return fmt.Errorf("container failed to become ready in time") return fmt.Errorf("container failed to become ready in time")
default: default:
containerJSON, err := cm.dockerClient.ContainerInspect(ctx, containerID) containerJSON, err := dockerClient.ContainerInspect(ctx, containerID)
if err != nil { if err != nil {
return err return err
} }
@@ -155,9 +159,9 @@ func (cm *ContainerManager) WaitForContainer(ctx context.Context, containerID st
} }
} }
func (cm *ContainerManager) GracefullyRemoveContainer(ctx context.Context, containerID string) error { func GracefullyRemoveDockerContainer(ctx context.Context, containerID string) error {
timeout := 30 timeout := 30
err := cm.dockerClient.ContainerStop(ctx, containerID, container.StopOptions{ err := dockerClient.ContainerStop(ctx, containerID, container.StopOptions{
Timeout: &timeout, Timeout: &timeout,
}) })
if err != nil { if err != nil {
@@ -170,15 +174,15 @@ func (cm *ContainerManager) GracefullyRemoveContainer(ctx context.Context, conta
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return cm.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{}) return dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{})
default: default:
containerJSON, err := cm.dockerClient.ContainerInspect(ctx, containerID) containerJSON, err := dockerClient.ContainerInspect(ctx, containerID)
if err != nil { if err != nil {
return err return err
} }
if !containerJSON.State.Running { if !containerJSON.State.Running {
return cm.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{}) return dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{})
} }
time.Sleep(time.Second) time.Sleep(time.Second)
@@ -186,26 +190,26 @@ func (cm *ContainerManager) GracefullyRemoveContainer(ctx context.Context, conta
} }
} }
func (cm *ContainerManager) RemoveVolume(ctx context.Context, volumeID string) error { func RemoveVolume(ctx context.Context, volumeID string) error {
if err := cm.dockerClient.VolumeRemove(ctx, volumeID, true); err != nil { if err := dockerClient.VolumeRemove(ctx, volumeID, true); err != nil {
return fmt.Errorf("Failed to remove existing volume: %v", err) return fmt.Errorf("Failed to remove existing volume: %v", err)
} }
return nil return nil
} }
func (cm *ContainerManager) findExistingContainers(ctx context.Context, containerPrefix string) ([]string, error) { func findExistingDockerContainers(ctx context.Context, containerPrefix string) (map[string]bool, error) {
containers, err := cm.dockerClient.ContainerList(ctx, container.ListOptions{ containers, err := dockerClient.ContainerList(ctx, container.ListOptions{
All: true, All: true,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
var existingContainers []string var existingContainers map[string]bool = make(map[string]bool)
for _, container := range containers { for _, container := range containers {
if strings.HasPrefix(container.Names[0], fmt.Sprintf("/%s-", containerPrefix)) { if strings.HasPrefix(container.Names[0], fmt.Sprintf("/%s-", containerPrefix)) {
existingContainers = append(existingContainers, container.ID) existingContainers[container.ID] = true
} }
} }

View File

@@ -1,14 +1,21 @@
package server package server
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"os/exec" "os/exec"
"path/filepath"
"github.com/juls0730/fluxd/models" "github.com/juls0730/fluxd/pkg"
)
var (
appInsertStmt *sql.Stmt
) )
type DeployRequest struct { type DeployRequest struct {
@@ -17,7 +24,7 @@ type DeployRequest struct {
} }
type DeployResponse struct { type DeployResponse struct {
App models.App `json:"app"` App App `json:"app"`
} }
func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) { func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
@@ -44,25 +51,15 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
} }
defer deployRequest.Code.Close() defer deployRequest.Code.Close()
var projectConfig models.ProjectConfig var projectConfig pkg.ProjectConfig
if err := json.NewDecoder(deployRequest.Config).Decode(&projectConfig); err != nil { if err := json.NewDecoder(deployRequest.Config).Decode(&projectConfig); err != nil {
log.Printf("Failed to decode config: %v\n", err) log.Printf("Failed to decode config: %v\n", err)
http.Error(w, "Invalid flux.json", http.StatusBadRequest) http.Error(w, "Invalid flux.json", http.StatusBadRequest)
return return
} }
if projectConfig.Name == "" { if projectConfig.Name == "" || projectConfig.Url == "" || projectConfig.Port == 0 {
http.Error(w, "No project name specified", http.StatusBadRequest) http.Error(w, "Invalid flux.json, a name, url, and port must be specified", http.StatusBadRequest)
return
}
if projectConfig.Url == "" {
http.Error(w, "No deployment url specified", http.StatusBadRequest)
return
}
if projectConfig.Port == 0 {
http.Error(w, "No port specified", http.StatusBadRequest)
return return
} }
@@ -86,7 +83,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
} }
log.Printf("Building image for project %s...\n", projectConfig.Name) log.Printf("Building image for project %s...\n", projectConfig.Name)
imageName := fmt.Sprintf("%s-image", projectConfig.Name) imageName := fmt.Sprintf("flux_%s-image", projectConfig.Name)
buildCmd := exec.Command("pack", "build", imageName, "--builder", s.config.Builder) buildCmd := exec.Command("pack", "build", imageName, "--builder", s.config.Builder)
buildCmd.Dir = projectPath buildCmd.Dir = projectPath
err = buildCmd.Run() err = buildCmd.Run()
@@ -96,59 +93,61 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
var app models.App app := Apps.GetApp(projectConfig.Name)
s.db.QueryRow("SELECT id, name, deployment_id FROM apps WHERE name = ?", projectConfig.Name).Scan(&app.ID, &app.Name, &app.DeploymentID)
if app.ID == 0 { if app == nil {
configBytes, err := json.Marshal(projectConfig) app = &App{
if err != nil { Name: projectConfig.Name,
log.Printf("Failed to marshal project config: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
log.Printf("Creating deployment %s...\n", app.Name)
containerID, err := s.containerManager.CreateContainer(r.Context(), imageName, projectPath, projectConfig) containerID, err := CreateDockerContainer(r.Context(), imageName, projectPath, projectConfig)
if err != nil { if err != nil {
log.Printf("Failed to create container: %v\n", err) log.Printf("Failed to create container: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
deploymentID, err := s.CreateDeployment(r.Context(), projectConfig, containerID) deployment, err := CreateDeployment(containerID, projectConfig.Port, projectConfig.Url, s.db)
app.Deployment = deployment
if err != nil { if err != nil {
log.Printf("Failed to create deployment: %v\n", err) log.Printf("Failed to create deployment: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if appInsertStmt == nil {
appInsertStmt, err = s.db.Prepare("INSERT INTO apps (name, deployment_id) VALUES ($1, $2) RETURNING id, name, deployment_id")
if err != nil {
log.Printf("Failed to prepare statement: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// create app in the database // create app in the database
appResult, err := s.db.Exec("INSERT INTO apps (name, image, project_path, project_config, deployment_id) VALUES (?, ?, ?, ?, ?)", projectConfig.Name, imageName, projectPath, configBytes, deploymentID) err = appInsertStmt.QueryRow(projectConfig.Name, deployment.ID).Scan(&app.ID, &app.Name, &app.DeploymentID)
if err != nil { if err != nil {
log.Printf("Failed to insert app: %v\n", err) log.Printf("Failed to insert app: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
appID, err := appResult.LastInsertId() err = deployment.Start(r.Context())
if err != nil {
log.Printf("Failed to get app id: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s.db.QueryRow("SELECT id, name, deployment_id FROM apps WHERE id = ?", appID).Scan(&app.ID, &app.Name, &app.DeploymentID)
err = s.StartDeployment(r.Context(), app.DeploymentID)
if err != nil { if err != nil {
log.Printf("Failed to start deployment: %v\n", err) log.Printf("Failed to start deployment: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
Apps.AddApp(app.Name, app)
} else { } else {
log.Printf("Upgrading deployment %s...\n", app.Name)
// if deploy is not started, start it // if deploy is not started, start it
deploymentStatus, err := s.GetDeploymentStatus(r.Context(), app.DeploymentID) deploymentStatus, err := app.Deployment.Status(r.Context())
if deploymentStatus != "started" || err != nil { if deploymentStatus != "running" || err != nil {
err = s.StartDeployment(r.Context(), app.DeploymentID) err = app.Deployment.Start(r.Context())
if err != nil { if err != nil {
log.Printf("Failed to start deployment: %v\n", err) log.Printf("Failed to start deployment: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -156,7 +155,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
err = s.UpgradeDeployment(r.Context(), app.DeploymentID, projectConfig, imageName, projectPath) err = app.Deployment.Upgrade(r.Context(), projectConfig, imageName, projectPath, s)
if err != nil { if err != nil {
log.Printf("Failed to upgrade deployment: %v\n", err) log.Printf("Failed to upgrade deployment: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -167,26 +166,31 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("App %s deployed successfully!\n", app.Name) log.Printf("App %s deployed successfully!\n", app.Name)
json.NewEncoder(w).Encode(DeployResponse{ json.NewEncoder(w).Encode(DeployResponse{
App: app, App: *app,
}) })
} }
func (s *FluxServer) StartDeployHandler(w http.ResponseWriter, r *http.Request) { func (s *FluxServer) StartDeployHandler(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name") name := r.PathValue("name")
var app struct { app := Apps.GetApp(name)
id int64 if app == nil {
name string
deployment_id int64
}
s.db.QueryRow("SELECT id, name, deployment_id FROM apps WHERE name = ?", name).Scan(&app.id, &app.name, &app.deployment_id)
if app.id == 0 {
http.Error(w, "App not found", http.StatusNotFound) http.Error(w, "App not found", http.StatusNotFound)
return return
} }
err := s.StartDeployment(r.Context(), app.deployment_id) status, err := app.Deployment.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if status == "running" {
http.Error(w, "App is already running", http.StatusBadRequest)
return
}
err = app.Deployment.Start(r.Context())
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -198,19 +202,24 @@ 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") name := r.PathValue("name")
var app struct { app := Apps.GetApp(name)
id int64 if app == nil {
name string
deployment_id int64
}
s.db.QueryRow("SELECT id, name, deployment_id FROM apps WHERE name = ?", name).Scan(&app.id, &app.name, &app.deployment_id)
if app.id == 0 {
http.Error(w, "App not found", http.StatusNotFound) http.Error(w, "App not found", http.StatusNotFound)
return return
} }
err := s.StopDeployment(r.Context(), app.deployment_id) status, err := app.Deployment.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if status == "stopped" {
http.Error(w, "App is already stopped", http.StatusBadRequest)
return
}
err = app.Deployment.Stop(r.Context())
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -221,46 +230,31 @@ 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") name := r.PathValue("name")
var err error
var app struct { app := Apps.GetApp(name)
id int if app == nil {
name string
deployment_id int
}
s.db.QueryRow("SELECT id, name, deployment_id FROM apps WHERE name = ?", name).Scan(&app.id, &app.name, &app.deployment_id)
if app.id == 0 {
http.Error(w, "App not found", http.StatusNotFound) http.Error(w, "App not found", http.StatusNotFound)
return return
} }
var containerId []string
rows, err := s.db.Query("SELECT container_id FROM containers WHERE deployment_id = ?", app.deployment_id)
if err != nil {
log.Printf("Failed to query containers: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
for rows.Next() {
var newContainerId string
if err := rows.Scan(&newContainerId); err != nil {
log.Printf("Failed to scan container id: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
containerId = append(containerId, newContainerId)
}
log.Printf("Deleting deployment %s...\n", name) log.Printf("Deleting deployment %s...\n", name)
for _, container := range containerId { for _, container := range app.Deployment.Containers {
s.containerManager.RemoveContainer(r.Context(), container) err = RemoveDockerContainer(r.Context(), string(container.ContainerID[:]))
if err != nil {
log.Printf("Failed to remove container: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} }
s.containerManager.RemoveVolume(r.Context(), fmt.Sprintf("%s-volume", name)) err = RemoveVolume(r.Context(), fmt.Sprintf("flux_%s-volume", name))
if err != nil {
log.Printf("Failed to remove volume: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
@@ -269,7 +263,7 @@ func (s *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request)
return return
} }
_, err = tx.Exec("DELETE FROM deployments WHERE id = ?", app.deployment_id) _, err = tx.Exec("DELETE FROM deployments WHERE id = ?", app.DeploymentID)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
log.Printf("Failed to delete deployment: %v\n", err) log.Printf("Failed to delete deployment: %v\n", err)
@@ -277,7 +271,7 @@ func (s *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request)
return return
} }
_, err = tx.Exec("DELETE FROM containers WHERE deployment_id = ?", app.deployment_id) _, err = tx.Exec("DELETE FROM containers WHERE deployment_id = ?", app.DeploymentID)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
log.Printf("Failed to delete containers: %v\n", err) log.Printf("Failed to delete containers: %v\n", err)
@@ -285,7 +279,7 @@ func (s *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request)
return return
} }
_, err = tx.Exec("DELETE FROM apps WHERE id = ?", app.id) _, err = tx.Exec("DELETE FROM apps WHERE id = ?", app.ID)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
log.Printf("Failed to delete app: %v\n", err) log.Printf("Failed to delete app: %v\n", err)
@@ -299,48 +293,109 @@ func (s *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request)
return return
} }
projectPath := filepath.Join(s.rootDir, "apps", name)
err = os.RemoveAll(projectPath)
if err != nil {
log.Printf("Failed to remove project directory: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
Apps.DeleteApp(name)
w.WriteHeader(http.StatusOK)
}
func (s *FluxServer) DeleteAllDeploymentsHandler(w http.ResponseWriter, r *http.Request) {
var err error
for _, app := range Apps.GetAllApps() {
for _, container := range app.Deployment.Containers {
err = RemoveDockerContainer(r.Context(), string(container.ContainerID[:]))
if err != nil {
log.Printf("Failed to remove container: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
err = RemoveVolume(r.Context(), fmt.Sprintf("flux_%s-volume", app.Name))
if err != nil {
log.Printf("Failed to remove volume: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
tx, err := s.db.Begin()
if err != nil {
log.Printf("Failed to begin transaction: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = tx.Exec("DELETE FROM deployments")
if err != nil {
tx.Rollback()
log.Printf("Failed to delete deployments: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = tx.Exec("DELETE FROM containers")
if err != nil {
tx.Rollback()
log.Printf("Failed to delete containers: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = tx.Exec("DELETE FROM apps")
if err != nil {
tx.Rollback()
log.Printf("Failed to delete apps: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := tx.Commit(); err != nil {
log.Printf("Failed to commit transaction: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := os.RemoveAll(filepath.Join(s.rootDir, "apps")); err != nil {
log.Printf("Failed to remove apps directory: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := os.RemoveAll(filepath.Join(s.rootDir, "deployments")); err != nil {
log.Printf("Failed to remove deployments directory: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
func (s *FluxServer) ListAppsHandler(w http.ResponseWriter, r *http.Request) { func (s *FluxServer) ListAppsHandler(w http.ResponseWriter, r *http.Request) {
var apps []models.App
rows, err := s.db.Query("SELECT * FROM apps")
if err != nil {
log.Printf("Failed to query apps: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
for rows.Next() {
var app models.App
var configBytes string
if err := rows.Scan(&app.ID, &app.Name, &app.Image, &app.ProjectPath, &configBytes, &app.DeploymentID, &app.CreatedAt); err != nil {
log.Printf("Failed to scan app: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.Unmarshal([]byte(configBytes), &app.ProjectConfig)
if err != nil {
log.Printf("Failed to unmarshal project config: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
apps = append(apps, app)
}
// for each app, get the deployment status // for each app, get the deployment status
for i, app := range apps { var apps []*pkg.App
deploymentStatus, err := s.GetDeploymentStatus(r.Context(), app.DeploymentID) for _, app := range Apps.GetAllApps() {
var extApp pkg.App
deploymentStatus, err := app.Deployment.Status(r.Context())
if err != nil { if err != nil {
log.Printf("Failed to get deployment status: %v\n", err) log.Printf("Failed to get deployment status: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
apps[i].DeploymentStatus = deploymentStatus extApp.ID = app.ID
extApp.Name = app.Name
extApp.DeploymentID = app.DeploymentID
extApp.DeploymentStatus = deploymentStatus
apps = append(apps, &extApp)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")

View File

@@ -2,98 +2,291 @@ package server
import ( import (
"context" "context"
"encoding/json" "database/sql"
"fmt" "fmt"
"log" "log"
"sync"
"time"
"github.com/juls0730/fluxd/models" "github.com/juls0730/fluxd/pkg"
) )
var (
Apps *AppManager = new(AppManager)
deploymentInsertStmt *sql.Stmt
containerInsertStmt *sql.Stmt
)
type AppManager struct {
sync.Map
}
type App struct {
ID int64 `json:"id,omitempty"`
Deployment Deployment `json:"-"`
Name string `json:"name,omitempty"`
DeploymentID int64 `json:"deployment_id,omitempty"`
}
type Deployment struct {
ID int64 `json:"id"`
Containers []Container `json:"-"`
Proxy *DeploymentProxy `json:"-"`
URL string `json:"url"`
Port uint16 `json:"port"`
}
func (am *AppManager) GetApp(name string) *App {
app, exists := am.Load(name)
if !exists {
return nil
}
return app.(*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)
}
return true
})
return apps
}
func (am *AppManager) AddApp(name string, app *App) {
am.Store(name, app)
}
func (am *AppManager) DeleteApp(name string) {
am.Delete(name)
}
func (am *AppManager) Init() {
log.Printf("Initializing deployments...\n")
if DB == nil {
log.Panicf("DB is nil")
}
rows, err := DB.Query("SELECT id, name, deployment_id FROM apps")
if err != nil {
log.Printf("Failed to query apps: %v\n", err)
return
}
defer rows.Close()
var apps []App
for rows.Next() {
var app App
if err := rows.Scan(&app.ID, &app.Name, &app.DeploymentID); err != nil {
log.Printf("Failed to scan app: %v\n", err)
return
}
apps = append(apps, app)
}
for _, app := range apps {
var deployment Deployment
var headContainer *Container
DB.QueryRow("SELECT id, url, port FROM deployments WHERE id = ?", app.DeploymentID).Scan(&deployment.ID, &deployment.URL, &deployment.Port)
deployment.Containers = make([]Container, 0)
rows, err := DB.Query("SELECT id, container_id, deployment_id, head FROM containers WHERE deployment_id = ?", app.DeploymentID)
if err != nil {
log.Printf("Failed to query containers: %v\n", err)
return
}
for rows.Next() {
var container Container
var containerIDString string
rows.Scan(&container.ID, &containerIDString, &container.DeploymentID, &container.Head)
container.Deployment = &deployment
copy(container.ContainerID[:], containerIDString)
if container.Head {
headContainer = &container
}
deployment.Containers = append(deployment.Containers, container)
}
deployment.Proxy = &DeploymentProxy{
deployment: &deployment,
currentHead: headContainer,
gracePeriod: time.Second * 30,
activeRequests: 0,
}
app.Deployment = deployment
Apps.AddApp(app.Name, &app)
}
}
// Creates a deployment and containers in the database // Creates a deployment and containers in the database
func (s *FluxServer) CreateDeployment(ctx context.Context, projectConfig models.ProjectConfig, containerID string) (int64, error) { func CreateDeployment(containerID string, port uint16, appUrl string, db *sql.DB) (Deployment, error) {
deploymentResult, err := s.db.Exec("INSERT INTO deployments (url) VALUES (?)", projectConfig.Url) var deployment Deployment
var err error
if deploymentInsertStmt == nil {
deploymentInsertStmt, err = db.Prepare("INSERT INTO deployments (url, port) VALUES ($1, $2) RETURNING id, url, port")
if err != nil {
log.Printf("Failed to prepare statement: %v\n", err)
return Deployment{}, err
}
}
err = deploymentInsertStmt.QueryRow(appUrl, port).Scan(&deployment.ID, &deployment.URL, &deployment.Port)
if err != nil { if err != nil {
log.Printf("Failed to insert deployment: %v\n", err) log.Printf("Failed to insert deployment: %v\n", err)
return 0, err return Deployment{}, err
} }
deploymentID, err := deploymentResult.LastInsertId() var container Container
if containerInsertStmt == nil {
containerInsertStmt, err = db.Prepare("INSERT INTO containers (container_id, deployment_id, head) VALUES ($1, $2, $3) RETURNING id, container_id, deployment_id, head")
if err != nil { if err != nil {
log.Printf("Failed to get deployment id: %v\n", err) log.Printf("Failed to prepare statement: %v\n", err)
return 0, err return Deployment{}, err
}
} }
_, err = s.db.Exec("INSERT INTO containers (container_id, deployment_id, head) VALUES (?, ?, ?)", containerID, deploymentID, true) var containerIDString string
err = containerInsertStmt.QueryRow(containerID, deployment.ID, true).Scan(&container.ID, &containerIDString, &container.DeploymentID, &container.Head)
if err != nil { if err != nil {
log.Printf("Failed to get container id: %v\n", err) log.Printf("Failed to get container id: %v\n", err)
return 0, err return Deployment{}, err
}
copy(container.ContainerID[:], containerIDString)
deployment.Proxy = &DeploymentProxy{
deployment: &deployment,
currentHead: &container,
gracePeriod: time.Second * 30,
activeRequests: 0,
} }
return deploymentID, nil container.Deployment = &deployment
deployment.Containers = append(deployment.Containers, container)
ReverseProxy.AddDeployment(&deployment)
return deployment, nil
} }
func (s *FluxServer) UpgradeDeployment(ctx context.Context, deploymentID int64, projectConfig models.ProjectConfig, imageName string, projectPath string) error { func (deployment *Deployment) Upgrade(ctx context.Context, projectConfig pkg.ProjectConfig, imageName string, projectPath string, s *FluxServer) error {
configBytes, err := json.Marshal(projectConfig) existingContainers, err := findExistingDockerContainers(ctx, projectConfig.Name)
if err != nil {
log.Printf("Failed to marshal project config: %v\n", err)
return err
}
existingContainers, err := s.containerManager.findExistingContainers(ctx, projectConfig.Name)
if err != nil { if err != nil {
return fmt.Errorf("Failed to find existing containers: %v", err) return fmt.Errorf("Failed to find existing containers: %v", err)
} }
fmt.Printf("There are %d existing containers\n", len(existingContainers))
// Deploy new container before deleting old one // Deploy new container before deleting old one
containerID, err := s.containerManager.CreateContainer(ctx, imageName, projectPath, projectConfig) containerID, err := CreateDockerContainer(ctx, imageName, projectPath, projectConfig)
if err != nil { if err != nil {
log.Printf("Failed to create container: %v\n", err) log.Printf("Failed to create container: %v\n", err)
return err return err
} }
// calls AddContainer in proxy var container Container
err = s.containerManager.StartContainer(ctx, containerID) if containerInsertStmt == nil {
containerInsertStmt, err = DB.Prepare("INSERT INTO containers (container_id, deployment_id, head) VALUES ($1, $2, $3) RETURNING id, container_id, deployment_id, head")
if err != nil {
log.Printf("Failed to prepare statement: %v\n", err)
return err
}
}
var containerIDString string
err = containerInsertStmt.QueryRow(containerID, deployment.ID, true).Scan(&container.ID, &containerIDString, &container.DeploymentID, &container.Head)
if err != nil {
log.Printf("Failed to get container id: %v\n", err)
return err
}
container.Deployment = deployment
copy(container.ContainerID[:], containerIDString)
deployment.Containers = append(deployment.Containers, container)
log.Printf("Starting container %s...\n", containerID)
err = container.Start(ctx)
if err != nil { if err != nil {
log.Printf("Failed to start container: %v\n", err) log.Printf("Failed to start container: %v\n", err)
return err return err
} }
if err := s.containerManager.WaitForContainer(ctx, containerID, projectConfig.Port); err != nil { if err := container.Wait(ctx, projectConfig.Port); err != nil {
log.Printf("Failed to wait for container: %v\n", err) log.Printf("Failed to wait for container: %v\n", err)
return err return err
} }
if _, err := s.db.Exec("INSERT INTO containers (container_id, deployment_id, head) VALUES (?, ?, ?)", containerID, deploymentID, true); err != nil {
log.Printf("Failed to insert container: %v\n", err)
return err
}
// update app in the database
if _, err := s.db.Exec("UPDATE apps SET project_config = ?, deployment_id = ? WHERE name = ?", configBytes, deploymentID, projectConfig.Name); err != nil {
log.Printf("Failed to update app: %v\n", err)
return err
}
// TODO: swap containers if they are running and have the same image so that we can have a constant uptime
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
log.Printf("Failed to begin transaction: %v\n", err) log.Printf("Failed to begin transaction: %v\n", err)
return err return err
} }
for _, existingContainer := range existingContainers { if _, err := tx.Exec("UPDATE deployments SET url = ?, port = ? WHERE id = ?", projectConfig.Url, projectConfig.Port, deployment.ID); err != nil {
log.Printf("Stopping existing container: %s\n", existingContainer[0:12]) log.Printf("Failed to update deployment: %v\n", err)
tx.Rollback()
return err
}
if _, err := tx.Exec("UPDATE apps SET deployment_id = ? WHERE name = ?", deployment.ID, projectConfig.Name); err != nil {
log.Printf("Failed to update app: %v\n", err)
tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
log.Printf("Failed to commit transaction: %v\n", err)
return err
}
tx, err = s.db.Begin()
if err != nil {
log.Printf("Failed to begin transaction: %v\n", err)
return err
}
// Create a new proxy that points to the new head, and replace the old one, but ensure that the old one is gracefully shutdown
oldProxy := deployment.Proxy
deployment.Proxy = &DeploymentProxy{
deployment: deployment,
currentHead: &container,
gracePeriod: time.Second * 30,
activeRequests: 0,
}
var containers []Container
var oldContainers []*Container
for _, container := range deployment.Containers {
if existingContainers[string(container.ContainerID[:])] {
log.Printf("Stopping existing container: %s\n", container.ContainerID[0:12])
_, err = tx.Exec("DELETE FROM containers WHERE container_id = ?", string(container.ContainerID[:]))
oldContainers = append(oldContainers, &container)
tx.Exec("DELETE FROM containers WHERE container_id = ?", existingContainer)
err = s.containerManager.GracefullyRemoveContainer(ctx, existingContainer)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
return err return err
} }
continue
} }
containers = append(containers, container)
}
if oldProxy != nil {
go oldProxy.GracefulShutdown(oldContainers)
}
deployment.Containers = containers
ReverseProxy.AddDeployment(deployment)
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
log.Printf("Failed to commit transaction: %v\n", err) log.Printf("Failed to commit transaction: %v\n", err)
return err return err
@@ -102,112 +295,42 @@ func (s *FluxServer) UpgradeDeployment(ctx context.Context, deploymentID int64,
return nil return nil
} }
func (s *FluxServer) StartDeployment(ctx context.Context, deploymentID int64) error { func arrayContains(arr []string, str string) bool {
var containerIds []string for _, a := range arr {
rows, err := s.db.Query("SELECT container_id FROM containers WHERE deployment_id = ?", deploymentID) if a == str {
if err != nil { return true
log.Printf("Failed to query containers: %v\n", err)
return err
} }
defer rows.Close()
for rows.Next() {
var newContainerId string
if err := rows.Scan(&newContainerId); err != nil {
log.Printf("Failed to scan container id: %v\n", err)
return err
} }
containerIds = append(containerIds, newContainerId) return false
} }
var projectConfigStr []byte func (d *Deployment) Start(ctx context.Context) error {
s.db.QueryRow("SELECT project_config FROM apps WHERE deployment_id = ?", deploymentID).Scan(&projectConfigStr) for _, container := range d.Containers {
var projectConfig models.ProjectConfig err := container.Start(ctx)
if err := json.Unmarshal(projectConfigStr, &projectConfig); err != nil {
return err
}
if projectConfig.Name == "" {
return fmt.Errorf("No project config found for deployment")
}
for _, containerId := range containerIds {
err := s.containerManager.StartContainer(ctx, containerId)
s.Proxy.AddContainer(projectConfig, containerId)
if err != nil { if err != nil {
log.Printf("Failed to start container: %v\n", err) log.Printf("Failed to start container: %v\n", err)
return err return err
} }
} }
tx, err := s.db.Begin()
if err != nil {
log.Printf("Failed to begin transaction: %v\n", err)
return err
}
if err := tx.Commit(); err != nil {
log.Printf("Failed to commit transaction: %v\n", err)
return err
}
return nil return nil
} }
func (s *FluxServer) StopDeployment(ctx context.Context, deploymentID int64) error { func (d *Deployment) Stop(ctx context.Context) error {
var containerIds []string for _, container := range d.Containers {
rows, err := s.db.Query("SELECT container_id FROM containers WHERE deployment_id = ?", deploymentID) err := container.Stop(ctx)
if err != nil {
log.Printf("Failed to query containers: %v\n", err)
return err
}
defer rows.Close()
for rows.Next() {
var newContainerId string
if err := rows.Scan(&newContainerId); err != nil {
log.Printf("Failed to scan container id: %v\n", err)
return err
}
containerIds = append(containerIds, newContainerId)
}
var projectConfigStr []byte
s.db.QueryRow("SELECT project_config FROM apps WHERE deployment_id = ?", deploymentID).Scan(&projectConfigStr)
var projectConfig models.ProjectConfig
if err := json.Unmarshal(projectConfigStr, &projectConfig); err != nil {
return err
}
if projectConfig.Name == "" {
return fmt.Errorf("No project config found for deployment")
}
for _, containerId := range containerIds {
err := s.containerManager.StopContainer(ctx, containerId)
s.Proxy.RemoveContainer(containerId)
if err != nil { if err != nil {
log.Printf("Failed to start container: %v\n", err) log.Printf("Failed to start container: %v\n", err)
return err return err
} }
} }
tx, err := s.db.Begin()
if err != nil {
log.Printf("Failed to begin transaction: %v\n", err)
return err
}
if err := tx.Commit(); err != nil {
log.Printf("Failed to commit transaction: %v\n", err)
return err
}
return nil return nil
} }
func (s *FluxServer) GetStatus(ctx context.Context, containerID string) (string, error) { func (c *Container) GetStatus(ctx context.Context) (string, error) {
containerJSON, err := s.containerManager.dockerClient.ContainerInspect(ctx, containerID) containerJSON, err := dockerClient.ContainerInspect(ctx, string(c.ContainerID[:]))
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -215,31 +338,20 @@ func (s *FluxServer) GetStatus(ctx context.Context, containerID string) (string,
return containerJSON.State.Status, nil return containerJSON.State.Status, nil
} }
func (s *FluxServer) GetDeploymentStatus(ctx context.Context, deploymentID int64) (string, error) { func (d *Deployment) Status(ctx context.Context) (string, error) {
var deployment models.Deployments
s.db.QueryRow("SELECT id, url FROM deployments WHERE id = ?", deploymentID).Scan(&deployment.ID, &deployment.URL)
var containerIds []string
rows, err := s.db.Query("SELECT container_id FROM containers WHERE deployment_id = ?", deploymentID)
if err != nil {
log.Printf("Failed to query containers: %v\n", err)
return "", err
}
defer rows.Close()
for rows.Next() {
var newContainerId string
if err := rows.Scan(&newContainerId); err != nil {
log.Printf("Failed to scan container id: %v\n", err)
return "", err
}
containerIds = append(containerIds, newContainerId)
}
var status string var status string
for _, containerId := range containerIds { if d == nil {
containerStatus, err := s.GetStatus(ctx, containerId) fmt.Printf("Deployment is nil\n")
return "stopped", nil
}
if d.Containers == nil {
fmt.Printf("Containers are nil\n")
return "stopped", nil
}
for _, container := range d.Containers {
containerStatus, err := container.GetStatus(ctx)
if err != nil { if err != nil {
log.Printf("Failed to get container status: %v\n", err) log.Printf("Failed to get container status: %v\n", err)
return "", err return "", err

View File

@@ -2,7 +2,6 @@ package server
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@@ -12,209 +11,119 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/juls0730/fluxd/models"
) )
type ContainerProxy struct { var ReverseProxy *Proxy
routes *RouteCache
db *sql.DB type Proxy struct {
cm *ContainerManager deployments sync.Map
activeConns int64
} }
type RouteCache struct { func (p *Proxy) AddDeployment(deployment *Deployment) {
m sync.Map log.Printf("Adding deployment %s\n", deployment.URL)
p.deployments.Store(deployment.URL, deployment)
} }
type containerRoute struct { func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
containerID string host := r.Host
port int
url string
proxy *httputil.ReverseProxy
isActive bool
}
func (rc *RouteCache) GetRoute(appUrl string) *containerRoute { deployment, ok := p.deployments.Load(host)
if !ok {
container, exists := rc.m.Load(appUrl) http.Error(w, "Not found", http.StatusNotFound)
if !exists {
return nil
}
return container.(*containerRoute)
}
func (rc *RouteCache) SetRoute(appUrl string, container *containerRoute) {
rc.m.Store(appUrl, container)
}
func (rc *RouteCache) DeleteRoute(appUrl string) {
rc.m.Delete(appUrl)
}
func (cp *ContainerProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Extract app name from host
appUrl := r.Host
container := cp.routes.GetRoute(appUrl)
if container == nil {
http.Error(w, "Container not found", http.StatusNotFound)
return return
} }
container.proxy.ServeHTTP(w, r) atomic.AddInt64(&deployment.(*Deployment).Proxy.activeRequests, 1)
}
func (cp *ContainerProxy) AddContainer(projectConfig models.ProjectConfig, containerID string) error { container := deployment.(*Deployment).Proxy.currentHead
containerJSON, err := cp.cm.dockerClient.ContainerInspect(context.Background(), containerID) if container == nil {
if err != nil { http.Error(w, "No active container found", http.StatusNotFound)
log.Printf("Failed to inspect container: %v\n", err)
return err
}
containerUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", containerJSON.NetworkSettings.IPAddress, projectConfig.Port))
if err != nil {
log.Printf("Failed to parse URL: %v\n", err)
return err
}
proxy := cp.createProxy(containerUrl)
newRoute := &containerRoute{
url: projectConfig.Url,
proxy: proxy,
port: projectConfig.Port,
isActive: true,
}
cp.routes.SetRoute(projectConfig.Url, newRoute)
return nil
}
func (cp *ContainerProxy) createProxy(url *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(url)
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
atomic.AddInt64(&cp.activeConns, 1)
// Validate URL before directing
if url == nil {
log.Printf("URL is nil")
return return
} }
originalDirector(req) containerJSON, err := dockerClient.ContainerInspect(context.Background(), string(container.ContainerID[:]))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
proxy.ModifyResponse = func(resp *http.Response) error { containerUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", containerJSON.NetworkSettings.IPAddress, container.Deployment.Port))
atomic.AddInt64(&cp.activeConns, -1) if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL = containerUrl
req.Host = containerUrl.Host
},
ModifyResponse: func(resp *http.Response) error {
atomic.AddInt64(&deployment.(*Deployment).Proxy.activeRequests, -1)
return nil return nil
},
} }
// Handle errors proxy.ServeHTTP(w, r)
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
atomic.AddInt64(&cp.activeConns, -1)
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
// Ensure request body is closed
if r.Body != nil {
r.Body.Close()
}
} }
return proxy type DeploymentProxy struct {
deployment *Deployment
currentHead *Container
gracePeriod time.Duration
activeRequests int64
} }
func (cp *ContainerProxy) RemoveContainer(containerID string) error { func (dp *DeploymentProxy) GracefulShutdown(oldContainers []*Container) {
var deploymentID int64 ctx, cancel := context.WithTimeout(context.Background(), dp.gracePeriod)
if err := cp.db.QueryRow("SELECT deployment_id FROM containers WHERE id = ?", containerID).Scan(&deploymentID); err != nil {
return err
}
var url string
if err := cp.db.QueryRow("SELECT url FROM deployments WHERE id = ?", deploymentID).Scan(&url); err != nil {
return err
}
container := cp.routes.GetRoute(url)
if container == nil {
return fmt.Errorf("container not found")
}
container.isActive = false
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
// Create a channel to signal when wait group is done
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
cp.routes.DeleteRoute(url) break
return nil
default: default:
if atomic.LoadInt64(&cp.activeConns) == 0 { if atomic.LoadInt64(&dp.activeRequests) == 0 {
cp.routes.DeleteRoute(url) break
return nil
} }
time.Sleep(time.Second)
} }
if atomic.LoadInt64(&dp.activeRequests) == 0 || ctx.Err() != nil {
break
} }
} }
func (cp *ContainerProxy) ScanRoutes() { for _, container := range oldContainers {
rows, err := cp.db.Query("SELECT url, id FROM deployments") err := RemoveDockerContainer(context.Background(), string(container.ContainerID[:]))
if err != nil { if err != nil {
log.Printf("Failed to query deployments: %v\n", err) log.Printf("Failed to remove container: %v\n", err)
return
}
defer rows.Close()
var containers []models.Containers
for rows.Next() {
var url string
var deploymentID int64
if err := rows.Scan(&url, &deploymentID); err != nil {
log.Printf("Failed to scan deployment: %v\n", err)
return
}
rows, err := cp.db.Query("SELECT * FROM containers WHERE deployment_id = ?", deploymentID)
if err != nil {
log.Printf("Failed to query containers: %v\n", err)
return
}
defer rows.Close()
for rows.Next() {
var container models.Containers
if err := rows.Scan(&container.ID, &container.ContainerID, &container.Head, &container.DeploymentID, &container.CreatedAt); err != nil {
log.Printf("Failed to scan container: %v\n", err)
return
}
fmt.Printf("Found container: %s\n", container.ContainerID)
containers = append(containers, container)
} }
} }
} }
func (cp *ContainerProxy) Start() { func InitProxy(apps *AppManager) {
cp.ScanRoutes() ReverseProxy = &Proxy{}
apps.Range(func(key, value interface{}) bool {
app := value.(*App)
ReverseProxy.AddDeployment(&app.Deployment)
return true
})
}
func InitReverseProxy() {
InitProxy(Apps)
port := os.Getenv("FLUXD_PROXY_PORT") port := os.Getenv("FLUXD_PROXY_PORT")
if port == "" { if port == "" {
port = "7465" port = "7465"
} }
server := &http.Server{
Addr: fmt.Sprintf(":%s", port),
Handler: cp,
}
go func() { go func() {
log.Printf("Proxy server starting on http://127.0.0.1:%s\n", port) log.Printf("Proxy server starting on http://127.0.0.1:%s\n", port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := http.ListenAndServe(fmt.Sprintf(":%s", port), ReverseProxy); err != nil && err != http.ErrServerClosed {
log.Fatalf("Proxy server error: %v", err) log.Fatalf("Proxy server error: %v", err)
} }
}() }()

View File

@@ -1,11 +1,13 @@
CREATE TABLE IF NOT EXISTS deployments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
port INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS apps ( CREATE TABLE IF NOT EXISTS apps (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
image TEXT NOT NULL,
project_path TEXT NOT NULL,
project_config TEXT NOT NULL,
deployment_id INTEGER, deployment_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(deployment_id) REFERENCES deployments(id) FOREIGN KEY(deployment_id) REFERENCES deployments(id)
); );
@@ -14,12 +16,5 @@ CREATE TABLE IF NOT EXISTS containers (
container_id TEXT NOT NULL, container_id TEXT NOT NULL,
head BOOLEAN NOT NULL, head BOOLEAN NOT NULL,
deployment_id INTEGER NOT NULL, deployment_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(deployment_id) REFERENCES deployments(id) FOREIGN KEY(deployment_id) REFERENCES deployments(id)
); );
CREATE TABLE IF NOT EXISTS deployments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -4,48 +4,38 @@ import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"database/sql" "database/sql"
"embed"
"encoding/json" "encoding/json"
"io" "io"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"github.com/juls0730/fluxd/models" _ "embed"
"github.com/juls0730/fluxd/pkg"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
var (
//go:embed schema.sql //go:embed schema.sql
var schema embed.FS schemaBytes []byte
DefaultConfig = FluxServerConfig{
var DefaultConfig = FluxServerConfig{
Builder: "paketobuildpacks/builder-jammy-tiny", Builder: "paketobuildpacks/builder-jammy-tiny",
} }
DB *sql.DB
)
type FluxServerConfig struct { type FluxServerConfig struct {
Builder string `json:"builder"` Builder string `json:"builder"`
} }
type FluxServer struct { type FluxServer struct {
containerManager *ContainerManager
config FluxServerConfig config FluxServerConfig
db *sql.DB db *sql.DB
Proxy *ContainerProxy
rootDir string rootDir string
} }
// var rootDir string
// func init() {
// rootDir = os.Getenv("FLUXD_ROOT_DIR")
// if rootDir == "" {
// rootDir = "/var/fluxd"
// }
// }
func NewServer() *FluxServer { func NewServer() *FluxServer {
containerManager := NewContainerManager()
var serverConfig FluxServerConfig var serverConfig FluxServerConfig
rootDir := os.Getenv("FLUXD_ROOT_DIR") rootDir := os.Getenv("FLUXD_ROOT_DIR")
@@ -84,36 +74,26 @@ func NewServer() *FluxServer {
log.Fatalf("Failed to create apps directory: %v\n", err) log.Fatalf("Failed to create apps directory: %v\n", err)
} }
db, err := sql.Open("sqlite3", filepath.Join(rootDir, "fluxd.db")) DB, err = sql.Open("sqlite3", filepath.Join(rootDir, "fluxd.db"))
if err != nil { if err != nil {
log.Fatalf("Failed to open database: %v\n", err) log.Fatalf("Failed to open database: %v\n", err)
} }
// create database schema _, err = DB.Exec(string(schemaBytes))
schemaBytes, err := schema.ReadFile("schema.sql")
if err != nil {
log.Fatalf("Failed to read schema file: %v\n", err)
}
_, err = db.Exec(string(schemaBytes))
if err != nil { if err != nil {
log.Fatalf("Failed to create database schema: %v\n", err) log.Fatalf("Failed to create database schema: %v\n", err)
} }
Apps.Init()
return &FluxServer{ return &FluxServer{
containerManager: containerManager,
config: serverConfig, config: serverConfig,
db: db, db: DB,
Proxy: &ContainerProxy{
routes: &RouteCache{},
db: db,
cm: containerManager,
},
rootDir: rootDir, rootDir: rootDir,
} }
} }
func (s *FluxServer) UploadAppCode(code io.Reader, projectConfig models.ProjectConfig) (string, error) { func (s *FluxServer) UploadAppCode(code io.Reader, projectConfig pkg.ProjectConfig) (string, error) {
projectPath := filepath.Join(s.rootDir, "apps", projectConfig.Name) projectPath := filepath.Join(s.rootDir, "apps", projectConfig.Name)
if err := os.MkdirAll(projectPath, 0755); err != nil { if err := os.MkdirAll(projectPath, 0755); err != nil {
log.Printf("Failed to create project directory: %v\n", err) log.Printf("Failed to create project directory: %v\n", err)