small fixes and deploy event streaming
This commit is contained in:
@@ -2,7 +2,6 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -14,7 +13,7 @@ import (
|
||||
|
||||
type App struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Deployment Deployment `json:"-"`
|
||||
Deployment Deployment `json:"deployment,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DeploymentID int64 `json:"deployment_id,omitempty"`
|
||||
}
|
||||
@@ -161,14 +160,14 @@ func (am *AppManager) DeleteApp(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AppManager) Init(db *sql.DB) {
|
||||
func (am *AppManager) Init() {
|
||||
log.Printf("Initializing deployments...\n")
|
||||
|
||||
if db == nil {
|
||||
if Flux.db == nil {
|
||||
log.Panicf("DB is nil")
|
||||
}
|
||||
|
||||
rows, err := db.Query("SELECT id, name, deployment_id FROM apps")
|
||||
rows, err := Flux.db.Query("SELECT id, name, deployment_id FROM apps")
|
||||
if err != nil {
|
||||
log.Printf("Failed to query apps: %v\n", err)
|
||||
return
|
||||
@@ -188,10 +187,10 @@ func (am *AppManager) Init(db *sql.DB) {
|
||||
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)
|
||||
Flux.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)
|
||||
rows, err = Flux.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
|
||||
@@ -214,7 +213,7 @@ func (am *AppManager) Init(db *sql.DB) {
|
||||
|
||||
for i, container := range deployment.Containers {
|
||||
var volumes []Volume
|
||||
rows, err := db.Query("SELECT id, volume_id, container_id FROM volumes WHERE container_id = ?", container.ID)
|
||||
rows, err := Flux.db.Query("SELECT id, volume_id, container_id FROM volumes WHERE container_id = ?", container.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to query volumes: %v\n", err)
|
||||
return
|
||||
|
||||
@@ -13,13 +13,10 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/juls0730/flux/pkg"
|
||||
)
|
||||
|
||||
var dockerClient *client.Client
|
||||
|
||||
type Volume struct {
|
||||
ID int64 `json:"id"`
|
||||
VolumeID string `json:"volume_id"`
|
||||
@@ -27,26 +24,16 @@ type Volume struct {
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
ID int64 `json:"id"`
|
||||
Head bool `json:"head"` // if the container is the head of the deployment
|
||||
Deployment *Deployment
|
||||
Volumes []Volume `json:"volumes"`
|
||||
ContainerID [64]byte `json:"container_id"`
|
||||
DeploymentID int64 `json:"deployment_id"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.Printf("Initializing Docker client...\n")
|
||||
|
||||
var err error
|
||||
dockerClient, err = client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Docker client: %v", err)
|
||||
}
|
||||
ID int64 `json:"id"`
|
||||
Head bool `json:"head"` // if the container is the head of the deployment
|
||||
Deployment *Deployment `json:"-"`
|
||||
Volumes []Volume `json:"volumes"`
|
||||
ContainerID [64]byte `json:"container_id"`
|
||||
DeploymentID int64 `json:"deployment_id"`
|
||||
}
|
||||
|
||||
func CreateDockerVolume(ctx context.Context, name string) (vol *Volume, err error) {
|
||||
dockerVolume, err := dockerClient.VolumeCreate(ctx, volume.CreateOptions{
|
||||
dockerVolume, err := Flux.dockerClient.VolumeCreate(ctx, volume.CreateOptions{
|
||||
Driver: "local",
|
||||
DriverOpts: map[string]string{},
|
||||
Name: name,
|
||||
@@ -89,7 +76,7 @@ func CreateDockerContainer(ctx context.Context, imageName, projectPath string, p
|
||||
vol, err := CreateDockerVolume(ctx, fmt.Sprintf("flux_%s-volume", projectConfig.Name))
|
||||
|
||||
log.Printf("Creating container %s...\n", containerName)
|
||||
resp, err := dockerClient.ContainerCreate(ctx, &container.Config{
|
||||
resp, err := Flux.dockerClient.ContainerCreate(ctx, &container.Config{
|
||||
Image: imageName,
|
||||
Env: projectConfig.Environment,
|
||||
Volumes: map[string]struct{}{
|
||||
@@ -126,11 +113,11 @@ func CreateDockerContainer(ctx context.Context, imageName, projectPath string, p
|
||||
}
|
||||
|
||||
func (c *Container) Start(ctx context.Context) error {
|
||||
return dockerClient.ContainerStart(ctx, string(c.ContainerID[:]), container.StartOptions{})
|
||||
return Flux.dockerClient.ContainerStart(ctx, string(c.ContainerID[:]), container.StartOptions{})
|
||||
}
|
||||
|
||||
func (c *Container) Stop(ctx context.Context) error {
|
||||
return dockerClient.ContainerStop(ctx, string(c.ContainerID[:]), container.StopOptions{})
|
||||
return Flux.dockerClient.ContainerStop(ctx, string(c.ContainerID[:]), container.StopOptions{})
|
||||
}
|
||||
|
||||
func (c *Container) Remove(ctx context.Context) error {
|
||||
@@ -178,7 +165,7 @@ func (c *Container) Wait(ctx context.Context, port uint16) error {
|
||||
}
|
||||
|
||||
func (c *Container) Status(ctx context.Context) (string, error) {
|
||||
containerJSON, err := dockerClient.ContainerInspect(ctx, string(c.ContainerID[:]))
|
||||
containerJSON, err := Flux.dockerClient.ContainerInspect(ctx, string(c.ContainerID[:]))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -188,11 +175,11 @@ func (c *Container) Status(ctx context.Context) (string, error) {
|
||||
|
||||
// RemoveContainer stops and removes a container, but be warned that this will not remove the container from the database
|
||||
func RemoveDockerContainer(ctx context.Context, containerID string) error {
|
||||
if err := dockerClient.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil {
|
||||
if err := Flux.dockerClient.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil {
|
||||
return fmt.Errorf("Failed to stop container (%s): %v", containerID[:12], err)
|
||||
}
|
||||
|
||||
if err := dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{}); err != nil {
|
||||
if err := Flux.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{}); err != nil {
|
||||
return fmt.Errorf("Failed to remove container (%s): %v", containerID[:12], err)
|
||||
}
|
||||
|
||||
@@ -210,7 +197,7 @@ func WaitForDockerContainer(ctx context.Context, containerID string, containerPo
|
||||
return fmt.Errorf("container failed to become ready in time")
|
||||
|
||||
default:
|
||||
containerJSON, err := dockerClient.ContainerInspect(ctx, containerID)
|
||||
containerJSON, err := Flux.dockerClient.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -229,7 +216,7 @@ func WaitForDockerContainer(ctx context.Context, containerID string, containerPo
|
||||
|
||||
func GracefullyRemoveDockerContainer(ctx context.Context, containerID string) error {
|
||||
timeout := 30
|
||||
err := dockerClient.ContainerStop(ctx, containerID, container.StopOptions{
|
||||
err := Flux.dockerClient.ContainerStop(ctx, containerID, container.StopOptions{
|
||||
Timeout: &timeout,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -242,15 +229,15 @@ func GracefullyRemoveDockerContainer(ctx context.Context, containerID string) er
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{})
|
||||
return Flux.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{})
|
||||
default:
|
||||
containerJSON, err := dockerClient.ContainerInspect(ctx, containerID)
|
||||
containerJSON, err := Flux.dockerClient.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !containerJSON.State.Running {
|
||||
return dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{})
|
||||
return Flux.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
@@ -261,7 +248,7 @@ func GracefullyRemoveDockerContainer(ctx context.Context, containerID string) er
|
||||
func RemoveVolume(ctx context.Context, volumeID string) error {
|
||||
log.Printf("Removed volume %s\n", volumeID)
|
||||
|
||||
if err := dockerClient.VolumeRemove(ctx, volumeID, true); err != nil {
|
||||
if err := Flux.dockerClient.VolumeRemove(ctx, volumeID, true); err != nil {
|
||||
return fmt.Errorf("Failed to remove volume (%s): %v", volumeID, err)
|
||||
}
|
||||
|
||||
@@ -269,7 +256,7 @@ func RemoveVolume(ctx context.Context, volumeID string) error {
|
||||
}
|
||||
|
||||
func findExistingDockerContainers(ctx context.Context, containerPrefix string) (map[string]bool, error) {
|
||||
containers, err := dockerClient.ContainerList(ctx, container.ListOptions{
|
||||
containers, err := Flux.dockerClient.ContainerList(ctx, container.ListOptions{
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
293
server/deploy.go
293
server/deploy.go
@@ -1,13 +1,17 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"github.com/juls0730/flux/pkg"
|
||||
)
|
||||
@@ -25,7 +29,59 @@ type DeployResponse struct {
|
||||
App App `json:"app"`
|
||||
}
|
||||
|
||||
type DeploymentLock struct {
|
||||
mu sync.Mutex
|
||||
deployed map[string]context.CancelFunc
|
||||
}
|
||||
|
||||
func NewDeploymentLock() *DeploymentLock {
|
||||
return &DeploymentLock{
|
||||
deployed: make(map[string]context.CancelFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (dt *DeploymentLock) StartDeployment(appName string, ctx context.Context) (context.Context, error) {
|
||||
dt.mu.Lock()
|
||||
defer dt.mu.Unlock()
|
||||
|
||||
// Check if the app is already being deployed
|
||||
if _, exists := dt.deployed[appName]; exists {
|
||||
return nil, fmt.Errorf("app %s is already being deployed", appName)
|
||||
}
|
||||
|
||||
// Create a context that can be cancelled
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
// Store the cancel function
|
||||
dt.deployed[appName] = cancel
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (dt *DeploymentLock) CompleteDeployment(appName string) {
|
||||
dt.mu.Lock()
|
||||
defer dt.mu.Unlock()
|
||||
|
||||
// Remove the app from deployed tracking
|
||||
if cancel, exists := dt.deployed[appName]; exists {
|
||||
// Cancel the context
|
||||
cancel()
|
||||
// Remove from map
|
||||
delete(dt.deployed, appName)
|
||||
}
|
||||
}
|
||||
|
||||
var deploymentLock = NewDeploymentLock()
|
||||
|
||||
func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if Flux.appManager == nil {
|
||||
panic("App manager is nil")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "test/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
err := r.ParseMultipartForm(10 << 30) // 10 GiB
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse multipart form: %v\n", err)
|
||||
@@ -33,7 +89,6 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// bind to DeployRequest struct
|
||||
var deployRequest DeployRequest
|
||||
deployRequest.Config, _, err = r.FormFile("config")
|
||||
if err != nil {
|
||||
@@ -42,21 +97,81 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer deployRequest.Config.Close()
|
||||
|
||||
var projectConfig pkg.ProjectConfig
|
||||
if err := json.NewDecoder(deployRequest.Config).Decode(&projectConfig); err != nil {
|
||||
log.Printf("Failed to decode config: %v\n", err)
|
||||
|
||||
http.Error(w, "Invalid flux.json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err := deploymentLock.StartDeployment(projectConfig.Name, r.Context())
|
||||
if err != nil {
|
||||
// This will happen if the app is already being deployed
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
defer deploymentLock.CompleteDeployment(projectConfig.Name)
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
eventChannel := make(chan pkg.DeploymentEvent, 10)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-eventChannel:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
eventJSON, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "data: %s\n\n", err.Error())
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "data: %s\n\n", eventJSON)
|
||||
flusher.Flush()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "start",
|
||||
Message: "Uploading code",
|
||||
}
|
||||
|
||||
deployRequest.Code, _, err = r.FormFile("code")
|
||||
if err != nil {
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: "No code archive found",
|
||||
Error: err.Error(),
|
||||
}
|
||||
http.Error(w, "No code archive found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer deployRequest.Code.Close()
|
||||
|
||||
var projectConfig pkg.ProjectConfig
|
||||
if err := json.NewDecoder(deployRequest.Config).Decode(&projectConfig); err != nil {
|
||||
log.Printf("Failed to decode config: %v\n", err)
|
||||
http.Error(w, "Invalid flux.json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if projectConfig.Name == "" || projectConfig.Url == "" || projectConfig.Port == 0 {
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: "Invalid flux.json, a name, url, and port must be specified",
|
||||
Error: "Invalid flux.json, a name, url, and port must be specified",
|
||||
}
|
||||
http.Error(w, "Invalid flux.json, a name, url, and port must be specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -66,58 +181,198 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
|
||||
projectPath, err := s.UploadAppCode(deployRequest.Code, projectConfig)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upload code: %v\n", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: "Failed to upload code",
|
||||
Error: err.Error(),
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
streamPipe := func(pipe io.ReadCloser) {
|
||||
scanner := bufio.NewScanner(pipe)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "cmd_output",
|
||||
Message: fmt.Sprintf("%s", line),
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to read pipe: %s", err),
|
||||
}
|
||||
log.Printf("Error reading pipe: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Preparing project %s...\n", projectConfig.Name)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "preparing",
|
||||
Message: "Preparing project",
|
||||
}
|
||||
|
||||
prepareCmd := exec.Command("go", "generate")
|
||||
prepareCmd.Dir = projectPath
|
||||
cmdOut, err := prepareCmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get stdout pipe: %v\n", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to get stdout pipe: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, fmt.Sprintf("Failed to get stdout pipe: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cmdErr, err := prepareCmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get stderr pipe: %v\n", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to get stderr pipe: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, fmt.Sprintf("Failed to get stderr pipe: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
go streamPipe(cmdOut)
|
||||
go streamPipe(cmdErr)
|
||||
|
||||
err = prepareCmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("Failed to prepare project: %s\n", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to prepare project: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, fmt.Sprintf("Failed to prepare project: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cmdOut.Close()
|
||||
cmdErr.Close()
|
||||
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "building",
|
||||
Message: "Building project image",
|
||||
}
|
||||
|
||||
log.Printf("Building image for project %s...\n", projectConfig.Name)
|
||||
imageName := fmt.Sprintf("flux_%s-image", projectConfig.Name)
|
||||
buildCmd := exec.Command("pack", "build", imageName, "--builder", s.config.Builder)
|
||||
buildCmd.Dir = projectPath
|
||||
err = buildCmd.Run()
|
||||
cmdOut, err = buildCmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("Failed to build image: %s\n", err)
|
||||
http.Error(w, fmt.Sprintf("Failed to build image: %s", err), http.StatusInternalServerError)
|
||||
log.Printf("Failed to get stdout pipe: %v\n", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to get stdout pipe: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, fmt.Sprintf("Failed to get stdout pipe: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cmdErr, err = buildCmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get stderr pipe: %v\n", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to get stderr pipe: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, fmt.Sprintf("Failed to get stderr pipe: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if Flux.appManager == nil {
|
||||
panic("App manager is nil")
|
||||
go streamPipe(cmdOut)
|
||||
go streamPipe(cmdErr)
|
||||
|
||||
err = buildCmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("Failed to build image: %s\n", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to build image: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, fmt.Sprintf("Failed to build image: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cmdOut.Close()
|
||||
cmdErr.Close()
|
||||
|
||||
app := Flux.appManager.GetApp(projectConfig.Name)
|
||||
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "creating",
|
||||
Message: "Creating deployment",
|
||||
}
|
||||
|
||||
if app == nil {
|
||||
app, err = CreateApp(r.Context(), imageName, projectPath, projectConfig)
|
||||
app, err = CreateApp(ctx, imageName, projectPath, projectConfig)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create app: %v", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to create app: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = app.Upgrade(r.Context(), projectConfig, imageName, projectPath)
|
||||
err = app.Upgrade(ctx, projectConfig, imageName, projectPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade deployment: %v", err)
|
||||
log.Printf("Failed to upgrade app: %v", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to upgrade app: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("App %s deployed successfully!\n", app.Name)
|
||||
|
||||
json.NewEncoder(w).Encode(DeployResponse{
|
||||
responseJSON, err := json.Marshal(DeployResponse{
|
||||
App: *app,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal deploy response: %v\n", err)
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "error",
|
||||
Message: fmt.Sprintf("Failed to marshal deploy response: %s", err),
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
eventChannel <- pkg.DeploymentEvent{
|
||||
Stage: "complete",
|
||||
Message: fmt.Sprintf("%s", responseJSON),
|
||||
}
|
||||
|
||||
log.Printf("App %s deployed successfully!\n", app.Name)
|
||||
|
||||
close(eventChannel)
|
||||
|
||||
// make sure all the events are flushed
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (s *FluxServer) StartDeployHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -18,7 +18,7 @@ var (
|
||||
|
||||
type Deployment struct {
|
||||
ID int64 `json:"id"`
|
||||
Containers []Container `json:"-"`
|
||||
Containers []Container `json:"containers,omitempty"`
|
||||
Proxy *DeploymentProxy `json:"-"`
|
||||
URL string `json:"url"`
|
||||
Port uint16 `json:"port"`
|
||||
|
||||
@@ -44,7 +44,11 @@ type DeploymentProxy struct {
|
||||
}
|
||||
|
||||
func NewDeploymentProxy(deployment *Deployment, head *Container) (*DeploymentProxy, error) {
|
||||
containerJSON, err := dockerClient.ContainerInspect(context.Background(), string(head.ContainerID[:]))
|
||||
if deployment == nil {
|
||||
return nil, fmt.Errorf("Deployment is nil")
|
||||
}
|
||||
|
||||
containerJSON, err := Flux.dockerClient.ContainerInspect(context.Background(), string(head.ContainerID[:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/juls0730/flux/pkg"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
@@ -37,25 +40,28 @@ type FluxServerConfig struct {
|
||||
}
|
||||
|
||||
type FluxServer struct {
|
||||
config FluxServerConfig
|
||||
db *sql.DB
|
||||
proxy *Proxy
|
||||
rootDir string
|
||||
appManager *AppManager
|
||||
config FluxServerConfig
|
||||
db *sql.DB
|
||||
proxy *Proxy
|
||||
rootDir string
|
||||
appManager *AppManager
|
||||
dockerClient *client.Client
|
||||
}
|
||||
|
||||
func NewServer() *FluxServer {
|
||||
Flux = new(FluxServer)
|
||||
|
||||
var serverConfig FluxServerConfig
|
||||
|
||||
rootDir := os.Getenv("FLUXD_ROOT_DIR")
|
||||
if rootDir == "" {
|
||||
rootDir = "/var/fluxd"
|
||||
Flux.rootDir = os.Getenv("FLUXD_ROOT_DIR")
|
||||
if Flux.rootDir == "" {
|
||||
Flux.rootDir = "/var/fluxd"
|
||||
}
|
||||
|
||||
// parse config, if it doesnt exist, create it and use the default config
|
||||
configPath := filepath.Join(rootDir, "config.json")
|
||||
configPath := filepath.Join(Flux.rootDir, "config.json")
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
if err := os.MkdirAll(rootDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(Flux.rootDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create fluxd directory: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -79,28 +85,47 @@ func NewServer() *FluxServer {
|
||||
log.Fatalf("Failed to parse config file: %v\n", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(rootDir, "apps"), 0755); err != nil {
|
||||
Flux.config = serverConfig
|
||||
|
||||
Flux.dockerClient, err = client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create docker client: %v\n", err)
|
||||
}
|
||||
|
||||
log.Printf("Pulling builder image %s, this may take a while...\n", serverConfig.Builder)
|
||||
|
||||
events, err := Flux.dockerClient.ImagePull(context.Background(), fmt.Sprintf("%s:latest", serverConfig.Builder), image.PullOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to pull builder image: %v\n", err)
|
||||
}
|
||||
|
||||
// wait for the iamge to be pulled
|
||||
io.Copy(io.Discard, events)
|
||||
|
||||
log.Printf("Successfully pulled builder image %s\n", serverConfig.Builder)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(Flux.rootDir, "apps"), 0755); err != nil {
|
||||
log.Fatalf("Failed to create apps directory: %v\n", err)
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", filepath.Join(rootDir, "fluxd.db"))
|
||||
Flux.db, err = sql.Open("sqlite3", filepath.Join(Flux.rootDir, "fluxd.db"))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open database: %v\n", err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(string(schemaBytes))
|
||||
_, err = Flux.db.Exec(string(schemaBytes))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create database schema: %v\n", err)
|
||||
}
|
||||
|
||||
appManager := new(AppManager)
|
||||
appManager.Init(db)
|
||||
Flux.appManager = new(AppManager)
|
||||
Flux.appManager.Init()
|
||||
|
||||
proxy := &Proxy{}
|
||||
Flux.proxy = &Proxy{}
|
||||
|
||||
appManager.Range(func(key, value interface{}) bool {
|
||||
Flux.appManager.Range(func(key, value interface{}) bool {
|
||||
app := value.(*App)
|
||||
proxy.AddDeployment(&app.Deployment)
|
||||
Flux.proxy.AddDeployment(&app.Deployment)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -111,19 +136,11 @@ func NewServer() *FluxServer {
|
||||
|
||||
go func() {
|
||||
log.Printf("Proxy server starting on http://127.0.0.1:%s\n", port)
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%s", port), proxy); err != nil && err != http.ErrServerClosed {
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%s", port), Flux.proxy); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Proxy server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
Flux = &FluxServer{
|
||||
config: serverConfig,
|
||||
db: db,
|
||||
proxy: proxy,
|
||||
appManager: appManager,
|
||||
rootDir: rootDir,
|
||||
}
|
||||
|
||||
return Flux
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user