small fixes and deploy event streaming

This commit is contained in:
Zoe
2024-12-10 02:56:44 -06:00
parent 6c035fc391
commit e46bb05b39
10 changed files with 671 additions and 205 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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
}