diff --git a/cmd/cli/main.go b/cmd/cli/main.go index d69c81f..07290ab 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -12,9 +12,11 @@ import ( "net/http" "os" "path/filepath" + "strings" "time" "github.com/briandowns/spinner" + "github.com/juls0730/fluxd/models" ) //go:embed config.json @@ -202,6 +204,87 @@ func main() { loadingSpinner.Stop() fmt.Println("Deployed successfully!") + case "delete": + var projectName string + + if len(os.Args) < 3 { + if _, err := os.Stat("flux.json"); err != nil { + fmt.Printf("Usage: flux delete , or run flux delete in the project directory\n") + os.Exit(1) + } + + fluxConfigFile, err := os.Open("flux.json") + if err != nil { + fmt.Printf("Failed to open flux.json: %v\n", err) + os.Exit(1) + } + defer fluxConfigFile.Close() + + var config models.ProjectConfig + if err := json.NewDecoder(fluxConfigFile).Decode(&config); err != nil { + fmt.Printf("Failed to decode flux.json: %v\n", err) + os.Exit(1) + } + + projectName = config.Name + } else { + projectName = os.Args[2] + } + + // ask for confirmation + fmt.Printf("Are you sure you want to delete %s? this will delete all volumes and containers associated with the deployment, and cannot be undone. \n[y/N]", projectName) + var response string + fmt.Scanln(&response) + + if strings.ToLower(response) != "y" { + fmt.Println("Aborting...") + os.Exit(0) + } + + req, err := http.NewRequest("DELETE", config.DeamonURL+"/deploy/"+projectName, nil) + if err != nil { + fmt.Printf("Failed to delete app: %v\n", err) + os.Exit(1) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + fmt.Printf("Failed to delete app: %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 %s\n", projectName) + case "list": + resp, err := http.Get(config.DeamonURL + "/apps") + if err != nil { + fmt.Printf("Failed to get apps: %v\n", err) + os.Exit(1) + } + + var apps []models.App + if err := json.NewDecoder(resp.Body).Decode(&apps); err != nil { + fmt.Printf("Failed to decode apps: %v\n", err) + os.Exit(1) + } + + for _, app := range apps { + fmt.Printf("%s\n", app.Name) + } default: fmt.Println("Unknown command:", command) } diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 6422204..0e298c2 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -11,6 +11,7 @@ func main() { fluxServer := server.NewServer() http.HandleFunc("POST /deploy", fluxServer.DeployHandler) + http.HandleFunc("DELETE /deploy/{name}", fluxServer.DeleteDeployHandler) http.HandleFunc("GET /apps", fluxServer.ListAppsHandler) log.Printf("Fluxd started on http://127.0.0.1:5647\n") diff --git a/models/app.go b/models/app.go new file mode 100644 index 0000000..ab66d8a --- /dev/null +++ b/models/app.go @@ -0,0 +1,19 @@ +package models + +type ProjectConfig struct { + Name string `json:"name"` + Urls []string `json:"urls"` + Port int `json:"port"` + EnvFile string `json:"env_file"` + Environment []string `json:"environment"` +} + +type App struct { + ID int64 `json:"id"` + Name string `json:"name"` + Image string `json:"image"` + ProjectPath string `json:"project_path"` + ProjectConfig ProjectConfig `json:"project_config"` + DeploymentID int64 `json:"deployment_id"` + CreatedAt string `json:"created_at"` +} diff --git a/server/deploy.go b/server/deploy.go index efdfcaf..eef8784 100644 --- a/server/deploy.go +++ b/server/deploy.go @@ -9,6 +9,8 @@ import ( "net/http" "os/exec" "strings" + + "github.com/juls0730/fluxd/models" ) type DeployRequest struct { @@ -20,14 +22,6 @@ type DeployResponse struct { AppID int64 `json:"app_id"` } -type ProjectConfig struct { - Name string `json:"name"` - Urls []string `json:"urls"` - Port int `json:"port"` - EnvFile string `json:"env_file"` - Environment []string `json:"environment"` -} - func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(10 << 30) // 10 GiB if err != nil { @@ -52,7 +46,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) { } defer deployRequest.Code.Close() - var projectConfig ProjectConfig + var projectConfig models.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) @@ -132,33 +126,69 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) { return } - appExists := s.db.QueryRow("SELECT * FROM apps WHERE name = ?", projectConfig.Name) + var app struct { + id int + name string + deployment_id int + } + s.db.QueryRow("SELECT id, name, deployment_id FROM apps WHERE name = ?", projectConfig.Name).Scan(&app.id, &app.name, &app.deployment_id) configBytes, err := json.Marshal(projectConfig) if err != nil { log.Printf("Failed to marshal project config: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - var appResult sql.Result - if appExists.Err() == sql.ErrNoRows { + 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 + } + + var appResult sql.Result + if app.id == 0 { // 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) + appResult, err = tx.Exec("INSERT INTO apps (name, image, project_path, project_config, deployment_id) VALUES (?, ?, ?, ?, ?)", projectConfig.Name, imageName, projectPath, configBytes, deploymentID) if err != nil { + tx.Rollback() log.Printf("Failed to insert app: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } } else { - // update app in the database - appResult, err = s.db.Exec("UPDATE apps SET project_config = ?, deployment_id = ? WHERE name = ?", configBytes, deploymentID, projectConfig.Name) + _, err = tx.Exec("DELETE FROM deployments WHERE id = ?", app.deployment_id) if err != nil { + tx.Rollback() + log.Printf("Failed to delete old deployment: %v\n", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, err = tx.Exec("DELETE FROM containers WHERE deployment_id = ?", app.deployment_id) + if err != nil { + tx.Rollback() + log.Printf("Failed to delete old containers: %v\n", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // update app in the database + appResult, err = tx.Exec("UPDATE apps SET project_config = ?, deployment_id = ? WHERE name = ?", configBytes, deploymentID, projectConfig.Name) + if err != nil { + tx.Rollback() log.Printf("Failed to update app: %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 + } + appId, err := appResult.LastInsertId() if err != nil { log.Printf("Failed to get app id: %v\n", err) @@ -171,9 +201,118 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) { }) } +func (s *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + + var app struct { + id int + 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) + 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) + + for _, container := range containerId { + s.containerManager.RemoveContainer(r.Context(), container) + } + + s.containerManager.RemoveVolume(r.Context(), fmt.Sprintf("%s-volume", name)) + + 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 WHERE id = ?", app.deployment_id) + if err != nil { + tx.Rollback() + log.Printf("Failed to delete deployment: %v\n", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, err = tx.Exec("DELETE FROM containers WHERE deployment_id = ?", app.deployment_id) + 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 WHERE id = ?", app.id) + if err != nil { + tx.Rollback() + log.Printf("Failed to delete app: %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 + } + + w.WriteHeader(http.StatusOK) +} + func (s *FluxServer) ListAppsHandler(w http.ResponseWriter, r *http.Request) { // Implement app listing logic - apps := s.db.QueryRow("SELECT * FROM apps") + 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) + } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(apps) diff --git a/server/docker.go b/server/docker.go index ee6e3d4..35fdc2b 100644 --- a/server/docker.go +++ b/server/docker.go @@ -11,9 +11,12 @@ import ( "time" "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/docker/go-connections/nat" "github.com/joho/godotenv" + "github.com/juls0730/fluxd/models" ) type ContainerManager struct { @@ -31,7 +34,7 @@ func NewContainerManager() *ContainerManager { } } -func (cm *ContainerManager) DeployContainer(ctx context.Context, imageName, containerPrefix, projectPath string, projectConfig ProjectConfig) (string, error) { +func (cm *ContainerManager) DeployContainer(ctx context.Context, imageName, containerPrefix, projectPath string, projectConfig models.ProjectConfig) (string, error) { log.Printf("Deploying container with image %s\n", imageName) containerName := fmt.Sprintf("%s-%s", containerPrefix, time.Now().Format("20060102-150405")) @@ -45,12 +48,9 @@ func (cm *ContainerManager) DeployContainer(ctx context.Context, imageName, cont for _, existingContainer := range existingContainers { log.Printf("Stopping existing container: %s\n", existingContainer) - if err := cm.dockerClient.ContainerStop(ctx, existingContainer, container.StopOptions{}); err != nil { - return "", fmt.Errorf("Failed to stop existing container: %v", err) - } - - if err := cm.dockerClient.ContainerRemove(ctx, existingContainer, container.RemoveOptions{}); err != nil { - return "", fmt.Errorf("Failed to remove existing container: %v", err) + err = cm.RemoveContainer(ctx, existingContainer) + if err != nil { + return "", err } } @@ -71,6 +71,17 @@ func (cm *ContainerManager) DeployContainer(ctx context.Context, imageName, cont } } + vol, err := cm.dockerClient.VolumeCreate(ctx, volume.CreateOptions{ + Driver: "local", + DriverOpts: map[string]string{}, + Name: fmt.Sprintf("%s-volume", containerPrefix), + }) + if err != nil { + return "", fmt.Errorf("Failed to create volume: %v", err) + } + + log.Printf("Volume %s created at %s\n", vol.Name, vol.Mountpoint) + log.Printf("Creating and starting container %s...\n", containerName) resp, err := cm.dockerClient.ContainerCreate(ctx, &container.Config{ Image: imageName, @@ -78,6 +89,9 @@ func (cm *ContainerManager) DeployContainer(ctx context.Context, imageName, cont ExposedPorts: nat.PortSet{ nat.Port(fmt.Sprintf("%d/tcp", projectConfig.Port)): {}, }, + Volumes: map[string]struct{}{ + vol.Name: {}, + }, }, &container.HostConfig{ PortBindings: nat.PortMap{ @@ -88,6 +102,14 @@ func (cm *ContainerManager) DeployContainer(ctx context.Context, imageName, cont }, }, }, + Mounts: []mount.Mount{ + { + Type: mount.TypeVolume, + Source: vol.Name, + Target: "/workspace", + ReadOnly: false, + }, + }, }, nil, nil, @@ -105,6 +127,27 @@ func (cm *ContainerManager) DeployContainer(ctx context.Context, imageName, cont return resp.ID, nil } +// 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 { + if err := cm.dockerClient.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil { + return fmt.Errorf("Failed to stop existing container: %v", err) + } + + if err := cm.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{}); err != nil { + return fmt.Errorf("Failed to remove existing container: %v", err) + } + + return nil +} + +func (cm *ContainerManager) RemoveVolume(ctx context.Context, volumeID string) error { + if err := cm.dockerClient.VolumeRemove(ctx, volumeID, true); err != nil { + return fmt.Errorf("Failed to remove existing volume: %v", err) + } + + return nil +} + func (cm *ContainerManager) findExistingContainers(ctx context.Context, containerPrefix string) ([]string, error) { containers, err := cm.dockerClient.ContainerList(ctx, container.ListOptions{ All: true, diff --git a/server/server.go b/server/server.go index bb16efa..98e0f71 100644 --- a/server/server.go +++ b/server/server.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" + "github.com/juls0730/fluxd/models" _ "github.com/mattn/go-sqlite3" ) @@ -29,22 +30,28 @@ type FluxServer struct { containerManager *ContainerManager config FluxServerConfig db *sql.DB + rootDir string } -var rootDir string +// var rootDir string -func init() { - rootDir = os.Getenv("FLUXD_ROOT_DIR") - if rootDir == "" { - rootDir = "/var/fluxd" - } -} +// func init() { +// rootDir = os.Getenv("FLUXD_ROOT_DIR") +// if rootDir == "" { +// rootDir = "/var/fluxd" +// } +// } func NewServer() *FluxServer { containerManager := NewContainerManager() var serverConfig FluxServerConfig + rootDir := os.Getenv("FLUXD_ROOT_DIR") + if rootDir == "" { + rootDir = "/var/fluxd" + } + // parse config, if it doesnt exist, create it and use the default config configPath := filepath.Join(rootDir, "config.json") if _, err := os.Stat(configPath); err != nil { @@ -96,11 +103,12 @@ func NewServer() *FluxServer { containerManager: containerManager, config: serverConfig, db: db, + rootDir: rootDir, } } -func (s *FluxServer) UploadAppCode(code io.Reader, projectConfig ProjectConfig) (string, error) { - projectPath := filepath.Join(rootDir, "apps", projectConfig.Name) +func (s *FluxServer) UploadAppCode(code io.Reader, projectConfig models.ProjectConfig) (string, error) { + projectPath := filepath.Join(s.rootDir, "apps", projectConfig.Name) if err := os.MkdirAll(projectPath, 0755); err != nil { log.Printf("Failed to create project directory: %v\n", err) return "", err