package appManagerService import ( "context" "database/sql" "fmt" "github.com/google/uuid" "github.com/juls0730/flux/internal/docker" models "github.com/juls0730/flux/internal/models" proxyManagerService "github.com/juls0730/flux/internal/services/proxy" "github.com/juls0730/flux/internal/util" "github.com/juls0730/flux/pkg" "github.com/juls0730/sentinel" "go.uber.org/zap" ) type AppManager struct { util.TypedMap[uuid.UUID, *models.App] nameIndex util.TypedMap[string, uuid.UUID] logger *zap.SugaredLogger proxyManager *sentinel.ProxyManager dockerClient *docker.DockerClient db *sql.DB } func NewAppManager(db *sql.DB, dockerClient *docker.DockerClient, proxyManager *sentinel.ProxyManager, logger *zap.SugaredLogger) *AppManager { return &AppManager{ db: db, dockerClient: dockerClient, proxyManager: proxyManager, logger: logger, } } func (appManager *AppManager) CreateApp(ctx context.Context, imageName string, projectConfig *pkg.ProjectConfig, id uuid.UUID) (*models.App, error) { app := &models.App{ Id: id, } appManager.logger.Debugw("Creating deployment", zap.String("id", app.Id.String())) app.Deployment = models.NewDeployment() if app.Deployment == nil { appManager.logger.Errorw("Failed to create deployment") return nil, fmt.Errorf("failed to create deployment") } if err := appManager.db.QueryRowContext(ctx, "INSERT INTO deployments (url, port) VALUES ($1, $2) RETURNING id, url, port", projectConfig.Url, projectConfig.Port).Scan(&app.Deployment.ID, &app.Deployment.URL, &app.Deployment.Port); err != nil { appManager.logger.Errorw("Failed to create deployment", zap.Error(err)) return nil, err } for _, container := range projectConfig.Containers { // Create a container given a container configuration and a deployment. This will do a few things: // 1. Create the container in the docker daemon // 2. Create the volumes for the container // 3. Insert the container and volumes into the database c, err := models.CreateContainer(ctx, container.ImageName, container.Name, false, container.Environment, container.Volumes, app.Deployment, appManager.logger, appManager.dockerClient, appManager.db) if err != nil { appManager.logger.Errorw("Failed to create container", zap.Error(err)) return nil, fmt.Errorf("failed to create container: %v", err) } c.Start(ctx, true, appManager.db, appManager.dockerClient, appManager.logger) } _, err := models.CreateContainer(ctx, imageName, projectConfig.Name, true, projectConfig.Environment, projectConfig.Volumes, app.Deployment, appManager.logger, appManager.dockerClient, appManager.db) if err != nil { appManager.logger.Errorw("Failed to create container", zap.Error(err)) return nil, fmt.Errorf("failed to create container: %v", err) } err = appManager.db.QueryRowContext(ctx, "INSERT INTO apps (id, name, state, deployment_id) VALUES ($1, $2, $3, $4) RETURNING name, state, deployment_id", app.Id[:], projectConfig.Name, "running", app.Deployment.ID).Scan(&app.Name, &app.State, &app.DeploymentID) if err != nil { appManager.logger.Errorw("Failed to insert app", zap.Error(err)) return nil, fmt.Errorf("failed to insert app: %v", err) } err = app.Deployment.Start(ctx, appManager.dockerClient) if err != nil { appManager.logger.Errorw("Failed to start deployment", zap.Error(err)) return nil, fmt.Errorf("failed to start deployment: %v", err) } deploymentInternalUrl, err := app.Deployment.GetInternalUrl(appManager.dockerClient) if err != nil { appManager.logger.Errorw("Failed to get internal url", zap.Error(err)) return nil, fmt.Errorf("failed to get internal url: %v", err) } newProxy, err := sentinel.NewDeploymentProxy(deploymentInternalUrl.String(), proxyManagerService.GetTransport) if err != nil { appManager.logger.Errorw("Failed to create deployment proxy", zap.Error(err)) return nil, fmt.Errorf("failed to create deployment proxy: %v", err) } appManager.AddApp(app.Id, app) appManager.proxyManager.AddProxy(app.Deployment.URL, newProxy) return app, nil } func (appManager *AppManager) Upgrade(ctx context.Context, appId uuid.UUID, imageName string, projectConfig *pkg.ProjectConfig) error { appManager.logger.Debugw("Upgrading app", zap.String("app_id", appId.String()), zap.String("image_name", imageName)) app := appManager.GetApp(appId) if app == nil { appManager.logger.Errorw("App not found, but upgrade called", zap.String("app_id", appId.String())) return fmt.Errorf("failed to get app") } deploymentStatus, err := app.Deployment.Status(ctx, appManager.dockerClient, appManager.logger) if err != nil { appManager.logger.Errorw("Failed to get deployment status", zap.Error(err)) return fmt.Errorf("failed to get deployment status: %v", err) } if deploymentStatus != "running" { err = app.Deployment.Start(ctx, appManager.dockerClient) if err != nil { appManager.logger.Errorw("Failed to start deployment", zap.Error(err)) return fmt.Errorf("failed to start deployment: %v", err) } } err = app.Deployment.Upgrade(ctx, projectConfig, imageName, appManager.dockerClient, appManager.proxyManager, appManager.db, appManager.logger) if err != nil { appManager.logger.Errorw("Failed to upgrade deployment", zap.Error(err)) return fmt.Errorf("failed to upgrade deployment: %v", err) } return nil } func (am *AppManager) GetAppByName(name string) *models.App { id, ok := am.nameIndex.Load(name) if !ok { return nil } return am.GetApp(id) } func (am *AppManager) GetApp(id uuid.UUID) *models.App { app, exists := am.Load(id) if !exists { return nil } return app } func (am *AppManager) GetAllApps() []*models.App { var apps []*models.App am.Range(func(key uuid.UUID, app *models.App) bool { apps = append(apps, app) return true }) return apps } // removes an app from the app manager func (am *AppManager) RemoveApp(id uuid.UUID) { app, ok := am.Load(id) if !ok { return } am.nameIndex.Delete(app.Name) am.Delete(id) } // add a given app to the app manager func (am *AppManager) AddApp(id uuid.UUID, app *models.App) { if app.Deployment == nil || app.Deployment.Containers() == nil || app.Deployment.Head() == nil || len(app.Deployment.Containers()) == 0 || app.Name == "" { panic("invalid app") } am.nameIndex.Store(app.Name, id) am.Store(id, app) } // nukes an app completely func (am *AppManager) DeleteApp(id uuid.UUID) error { app := am.GetApp(id) if app == nil { return fmt.Errorf("app not found") } am.logger.Debugw("Deleting app", zap.String("id", id.String())) // calls RemoveApp err := app.Remove(context.Background(), am.dockerClient, am.db, am.logger) if err != nil { am.logger.Errorw("Failed to remove app", zap.Error(err)) return err } return nil } // Scan every app in the database, and create in memory structures if the deployment is already running func (am *AppManager) Init() error { am.logger.Info("Initializing deployments") if am.db == nil { am.logger.Panic("DB is nil") } appRows, err := am.db.Query("SELECT id, name, state, deployment_id FROM apps") if err != nil { return fmt.Errorf("failed to get apps: %v", err) } defer appRows.Close() var apps []*models.App for appRows.Next() { var app *models.App = new(models.App) var appIdBlob []byte if err := appRows.Scan(&appIdBlob, &app.Name, &app.State, &app.DeploymentID); err != nil { return fmt.Errorf("failed to scan app: %v", err) } app.Id = uuid.Must(uuid.FromBytes(appIdBlob)) app.Deployment = models.NewDeployment() if app.Deployment == nil { return fmt.Errorf("failed to create deployment") } err := am.db.QueryRow("SELECT id, url, port FROM deployments WHERE id = ?", app.DeploymentID).Scan(&app.Deployment.ID, &app.Deployment.URL, &app.Deployment.Port) if err != nil { return fmt.Errorf("failed to get deployment: %v", err) } am.logger.Debugw("Found deployment", zap.Int64("id", app.Deployment.ID)) containerRows, err := am.db.Query("SELECT id, container_id, friendly_name, deployment_id, head FROM containers WHERE deployment_id = ?", app.DeploymentID) if err != nil { return fmt.Errorf("failed to query containers: %v", err) } defer containerRows.Close() for containerRows.Next() { var container *models.Container = new(models.Container) containerRows.Scan(&container.ID, &container.ContainerID, &container.FriendlyName, &container.DeploymentID, &container.Head) container.Deployment = app.Deployment volumeRows, err := am.db.Query("SELECT id, volume_id, container_id, mountpoint FROM volumes WHERE container_id = ?", container.ID) if err != nil { return fmt.Errorf("failed to query volumes: %v", err) } defer volumeRows.Close() for volumeRows.Next() { volume := new(models.Volume) volumeRows.Scan(&volume.ID, &volume.VolumeID, &volume.ContainerID, &volume.Mountpoint) container.Volumes = append(container.Volumes, volume) } app.Deployment.AppendContainer(container) } // align the state of the deployment with the state of the app switch app.State { case "running": err = app.Deployment.Start(context.Background(), am.dockerClient) if err != nil { return fmt.Errorf("failed to start deployment: %v", err) } case "stopped": err = app.Deployment.Stop(context.Background(), am.dockerClient) if err != nil { return fmt.Errorf("failed to stop deployment: %v", err) } } apps = append(apps, app) } for _, app := range apps { am.AddApp(app.Id, app) am.logger.Debugw("Added app", zap.String("id", app.Id.String())) status, err := app.Deployment.Status(context.Background(), am.dockerClient, am.logger) if err != nil { am.logger.Warnw("Failed to get deployment status", zap.Error(err)) continue } if status != "running" { continue } proxyURL, err := app.Deployment.GetInternalUrl(am.dockerClient) if err != nil { am.logger.Errorw("Failed to parse proxy url", zap.Error(err)) continue } proxy, err := sentinel.NewDeploymentProxy(proxyURL.String(), proxyManagerService.GetTransport) if err != nil { am.logger.Errorw("Failed to create proxy", zap.Error(err)) continue } am.proxyManager.AddProxy(app.Deployment.URL, proxy) am.logger.Debugw("Created proxy", zap.String("id", app.Id.String())) } return nil }