diff --git a/.gitmodules b/.gitmodules index fcf489ff..4e9fb42d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "unity-cs-manager"] path = unity-cs-manager url = https://github.com/unity-sds/unity-cs-manager.git +[submodule "src/stellar"] + path = src/stellar + url = git@github.com:nasa-jpl/stellar.git diff --git a/backend/internal/database/models/models.go b/backend/internal/database/models/models.go index 1a8b71c5..955bb5b8 100644 --- a/backend/internal/database/models/models.go +++ b/backend/internal/database/models/models.go @@ -44,3 +44,13 @@ type Deployment struct { Creator string CreationDate time.Time } + +type InstalledMarketplaceApplication struct { + gorm.Model + Name string + DeploymentName string + Version string + Source string + Status string + PackageName string +} \ No newline at end of file diff --git a/backend/internal/database/operations.go b/backend/internal/database/operations.go index 6eeaa718..72f5ce7b 100644 --- a/backend/internal/database/operations.go +++ b/backend/internal/database/operations.go @@ -7,6 +7,7 @@ import ( "github.com/unity-sds/unity-management-console/backend/internal/application/config" "github.com/unity-sds/unity-management-console/backend/internal/database/models" "gorm.io/gorm/clause" + "gorm.io/gorm" ) // StoreConfig stores the given configuration in the database. It uses a @@ -133,6 +134,29 @@ func (g GormDatastore) FetchDeploymentIDByName(deploymentID string) (uint, error return deployment.ID, nil } +func (g GormDatastore) GetInstalledApplicationByName(name string) (*models.InstalledMarketplaceApplication, error) { + var application models.InstalledMarketplaceApplication + result := g.db.Where("name = ?", name).Where("status != 'UNINSTALLED'").First(&application) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound){ + return nil, nil + } + + log.WithError(result.Error).Error("Error finding application") + return nil, result.Error + } + return &application, nil +} + +func (g GormDatastore) FetchDeploymentIDByApplicationName(deploymentName string) (uint, error) { + var application models.Application + result := g.db.Where("display_name = ?", deploymentName).First(&application) + if result.Error != nil { + return 0, fmt.Errorf("error finding application: %v", result.Error) + } + return application.DeploymentID, nil +} + func (g GormDatastore) UpdateApplicationStatus(deploymentID uint, targetAppName string, displayName string, newStatus string) error { var deployment models.Deployment result := g.db.Preload("Applications").First(&deployment, deploymentID) @@ -167,6 +191,15 @@ func (g GormDatastore) FetchAllApplicationStatus() ([]models.Deployment, error) return deployments, nil } +func (g GormDatastore) FetchAllInstalledMarketplaceApplications() ([]models.InstalledMarketplaceApplication, error) { + var applications []models.InstalledMarketplaceApplication + result := g.db.Find(&applications) + if result.Error != nil { + return nil, result.Error + } + return applications, nil +} + func (g GormDatastore) FetchAllApplicationStatusByDeployment(deploymentid uint) ([]models.Application, error) { var deployments models.Deployment result := g.db.Preload("Applications").First(&deployments, deploymentid) @@ -223,11 +256,45 @@ func (g GormDatastore) RemoveApplicationByName(deploymentName string, applicatio return fmt.Errorf("error deleting application: %v", err) } - // Delete the deployment - err = g.db.Delete(&deployment).Error + return nil +} + +func (g GormDatastore) StoreInstalledMarketplaceApplication(model models.InstalledMarketplaceApplication) (error) { + if err := g.db.Save(&model).Error; err != nil { + // Handle error for Save + log.WithError(err).Error("Problem saving record to database") + return err + } + return nil +} + +func (g GormDatastore) GetInstalledMarketplaceApplicationStatusByName(appName string, deploymentName string) (*models.InstalledMarketplaceApplication, error) { + var application models.InstalledMarketplaceApplication + err := g.db.Where("Name = ? AND deployment_name = ?", appName, deploymentName).First(&application).Error if err != nil { - return fmt.Errorf("error deleting deployment: %v", err) + log.WithError(err).Error("Problem getting application status") + return nil, err + } + return &application, nil +} + +func (g GormDatastore) UpdateInstalledMarketplaceApplicationStatusByName(appName string, deploymentName string, status string) (error) { + var app models.InstalledMarketplaceApplication + + g.db.Where("name = ? AND deployment_name = ?", appName, deploymentName).First(&app) + app.Status = status + + if err := g.db.Save(&app).Error; err != nil { + // Handle error for Save + log.WithError(err).Error("Problem saving record to database") + return err } + return nil +} +func (g GormDatastore) RemoveInstalledMarketplaceApplicationByName(appName string) (error) { + if err := g.db.Where("name != ?", appName).Delete(&models.InstalledMarketplaceApplication{}).Error; err != nil { + return err + } return nil } \ No newline at end of file diff --git a/backend/internal/database/setup.go b/backend/internal/database/setup.go index f6016db4..5ba8b158 100644 --- a/backend/internal/database/setup.go +++ b/backend/internal/database/setup.go @@ -100,6 +100,12 @@ func NewGormDatastore() (Datastore, error) { if err != nil { return nil, err } + + err = db.AutoMigrate(&models.InstalledMarketplaceApplication{}) + if err != nil { + return nil, err + } + return &GormDatastore{ db: db, }, nil @@ -120,4 +126,11 @@ type Datastore interface { FetchDeploymentNames() ([]string, error) RemoveDeploymentByName(name string) error RemoveApplicationByName(deploymentName string, applicationName string) error + FetchDeploymentIDByApplicationName(deploymentName string) (uint, error) + GetInstalledApplicationByName(name string) (*models.InstalledMarketplaceApplication, error) + StoreInstalledMarketplaceApplication(model models.InstalledMarketplaceApplication) error + UpdateInstalledMarketplaceApplicationStatusByName(appName string,displayName string, status string) error + RemoveInstalledMarketplaceApplicationByName(appName string) error + GetInstalledMarketplaceApplicationStatusByName(appName string, displayName string) (*models.InstalledMarketplaceApplication, error) + FetchAllInstalledMarketplaceApplications() ([]models.InstalledMarketplaceApplication, error) } diff --git a/backend/internal/processes/bootstrap.go b/backend/internal/processes/bootstrap.go index cba3600b..7e6b3d31 100644 --- a/backend/internal/processes/bootstrap.go +++ b/backend/internal/processes/bootstrap.go @@ -84,7 +84,7 @@ func BootstrapEnv(appconf *config.AppConfig) { } return } - + err = installHealthStatusLambda(store, appconf) if err != nil { log.WithError(err).Error("Error installing Health Status ") diff --git a/backend/internal/processes/installer.go b/backend/internal/processes/installer.go index 72c1d8a4..6f6b69f9 100644 --- a/backend/internal/processes/installer.go +++ b/backend/internal/processes/installer.go @@ -11,6 +11,7 @@ import ( "github.com/unity-sds/unity-management-console/backend/internal/database/models" "github.com/unity-sds/unity-management-console/backend/internal/terraform" "github.com/unity-sds/unity-management-console/backend/internal/websocket" + "github.com/unity-sds/unity-management-console/backend/types" "github.com/zclconf/go-cty/cty" "os" "os/exec" @@ -34,6 +35,71 @@ func fetchMandatoryVars() ([]terraform.Varstruct, error) { return vars, nil } + +func InstallMarketplaceApplicationNew(appConfig *config.AppConfig, location string, installParams *types.ApplicationInstallParams, meta *marketplace.MarketplaceMetadata, db database.Datastore) (string, error) { + if meta.Backend == "terraform" { + app := models.Application{ + Name: installParams.Name, + Version: installParams.Version, + DisplayName: installParams.DeploymentName, + PackageName: meta.Name, + Source: meta.Package, + Status: "STAGED", + } + deployment := models.Deployment{ + Name: installParams.DeploymentName, + Applications: []models.Application{app}, + Creator: "admin", + CreationDate: time.Time{}, + } + + deploymentID, err := db.StoreDeployment(deployment) + + if err != nil { + db.UpdateApplicationStatus(deploymentID, installParams.Name, installParams.DeploymentName, "STAGINGFAILED") + return "", err + } + + go func() { + log.Errorf("Application name is: %s", installParams.Name) + err = terraform.AddApplicationToStackNew(appConfig, location, meta, installParams, db, deploymentID) + executeNew(db, appConfig, meta, installParams, deploymentID) + }() + + return fmt.Sprintf("%d", deploymentID), nil + + } else { + return "", errors.New("backend not implemented") + } +} + +func InstallMarketplaceApplicationNewV2(appConfig *config.AppConfig, location string, installParams *types.ApplicationInstallParams, meta *marketplace.MarketplaceMetadata, db database.Datastore) error { + if meta.Backend == "terraform" { + application := models.InstalledMarketplaceApplication{ + Name: installParams.Name, + Version: installParams.Version, + DeploymentName: installParams.DeploymentName, + PackageName: meta.Name, + Source: meta.Package, + Status: "STAGED", + } + + db.StoreInstalledMarketplaceApplication(application) + db.UpdateInstalledMarketplaceApplicationStatusByName(installParams.Name, installParams.DeploymentName, "INSTALLING") + + go func() { + log.Errorf("Application name is: %s", installParams.Name) + terraform.AddApplicationToStackNewV2(appConfig, location, meta, installParams, db) + executeNewV2(db, appConfig, meta, installParams) + }() + + return nil + + } else { + return errors.New("backend not implemented") + } +} + func InstallMarketplaceApplication(conn *websocket.WebSocketManager, userid string, appConfig *config.AppConfig, meta *marketplace.MarketplaceMetadata, location string, install *marketplace.Install, db database.Datastore) error { if meta.Backend == "terraform" { @@ -70,6 +136,12 @@ func InstallMarketplaceApplication(conn *websocket.WebSocketManager, userid stri } func execute(db database.Datastore, appConfig *config.AppConfig, meta *marketplace.MarketplaceMetadata, install *marketplace.Install, deploymentID uint, conn *websocket.WebSocketManager, userid string) error { + // Create install_logs directory if it doesn't exist + logDir := filepath.Join(appConfig.Workdir, "install_logs") + if err := os.MkdirAll(logDir, 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create install_logs directory: %w", err) + } + executor := &terraform.RealTerraformExecutor{} //m, err := fetchMandatoryVars() @@ -104,6 +176,100 @@ func execute(db database.Datastore, appConfig *config.AppConfig, meta *marketpla return nil } + +func executeNew(db database.Datastore, appConfig *config.AppConfig, meta *marketplace.MarketplaceMetadata, installParams *types.ApplicationInstallParams, deploymentID uint) error { + // Create install_logs directory if it doesn't exist + logDir := filepath.Join(appConfig.Workdir, "install_logs") + if err := os.MkdirAll(logDir, 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create install_logs directory: %w", err) + } + + executor := &terraform.RealTerraformExecutor{} + + //m, err := fetchMandatoryVars() + //if err != nil { + // return err + //} + //terraform.WriteTFVars(m, appConfig) + err := runPreInstallNew(appConfig, meta, installParams) + if err != nil { + return err + } + + db.UpdateApplicationStatus(deploymentID, installParams.Name, installParams.DeploymentName, "INSTALLING") + + fetchAllApplications(db) + + logfile := filepath.Join(logDir, fmt.Sprintf("%s_install_log", installParams.DeploymentName)) + err = terraform.RunTerraformLogOutToFile(appConfig, logfile, executor, "") + + if err != nil { + db.UpdateApplicationStatus(deploymentID, installParams.Name, installParams.DeploymentName, "FAILED") + fetchAllApplications(db) + return err + } + db.UpdateApplicationStatus(deploymentID, installParams.Name, installParams.DeploymentName, "INSTALLED") + fetchAllApplications(db) + err = runPostInstallNew(appConfig, meta, installParams) + + if err != nil { + db.UpdateApplicationStatus(deploymentID, installParams.Name, installParams.DeploymentName, "POSTINSTALL FAILED") + fetchAllApplications(db) + + return err + } + db.UpdateApplicationStatus(deploymentID, installParams.Name, installParams.DeploymentName, "COMPLETE") + fetchAllApplications(db) + + return nil +} + +func executeNewV2(db database.Datastore, appConfig *config.AppConfig, meta *marketplace.MarketplaceMetadata, installParams *types.ApplicationInstallParams) error { + // Create install_logs directory if it doesn't exist + logDir := filepath.Join(appConfig.Workdir, "install_logs") + if err := os.MkdirAll(logDir, 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create install_logs directory: %w", err) + } + + executor := &terraform.RealTerraformExecutor{} + + //m, err := fetchMandatoryVars() + //if err != nil { + // return err + //} + //terraform.WriteTFVars(m, appConfig) + err := runPreInstallNew(appConfig, meta, installParams) + if err != nil { + return err + } + + db.UpdateInstalledMarketplaceApplicationStatusByName(installParams.Name, installParams.DeploymentName, "INSTALLING") + + fetchAllApplications(db) + + logfile := filepath.Join(logDir, fmt.Sprintf("%s_%s_install_log", installParams.Name, installParams.DeploymentName)) + err = terraform.RunTerraformLogOutToFile(appConfig, logfile, executor, "") + + if err != nil { + db.UpdateInstalledMarketplaceApplicationStatusByName(installParams.Name, installParams.DeploymentName, "FAILED") + fetchAllApplications(db) + return err + } + db.UpdateInstalledMarketplaceApplicationStatusByName(installParams.Name, installParams.DeploymentName, "INSTALLED") + fetchAllApplications(db) + err = runPostInstallNew(appConfig, meta, installParams) + + if err != nil { + db.UpdateInstalledMarketplaceApplicationStatusByName(installParams.Name, installParams.DeploymentName, "POSTINSTALL FAILED") + fetchAllApplications(db) + + return err + } + db.UpdateInstalledMarketplaceApplicationStatusByName(installParams.Name, installParams.DeploymentName, "COMPLETE") + fetchAllApplications(db) + + return nil +} func runPostInstall(appConfig *config.AppConfig, meta *marketplace.MarketplaceMetadata, install *marketplace.Install) error { if meta.PostInstall != "" { @@ -139,6 +305,41 @@ func runPostInstall(appConfig *config.AppConfig, meta *marketplace.MarketplaceMe return nil } +func runPostInstallNew(appConfig *config.AppConfig, meta *marketplace.MarketplaceMetadata, installParams *types.ApplicationInstallParams) error { + + if meta.PostInstall != "" { + //TODO UNPIN ME + path := filepath.Join(appConfig.Workdir, "terraform", "modules", meta.Name, meta.Version, meta.WorkDirectory, meta.PostInstall) + log.Infof("Post install command path: %s", path) + cmd := exec.Command(path) + cmd.Env = os.Environ() + // for k, v := range install.Applications.Dependencies { + // // Replace hyphens with underscores + // formattedKey := strings.ReplaceAll(k, "-", "_") + + // // Convert to upper case + // upperKey := strings.ToUpper(formattedKey) + + // // Use a regex to keep only alphanumeric characters and underscores + // re := regexp.MustCompile("[^A-Z0-9_]+") + // cleanKey := re.ReplaceAllString(upperKey, "") + + // log.Infof("Adding environment variable: %s = %s", cleanKey, v) + // cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", cleanKey, v)) + // } + cmd.Env = append(cmd.Env, fmt.Sprintf("DEPLOYMENTNAME=%s", installParams.DeploymentName)) + cmd.Env = append(cmd.Env, fmt.Sprintf("WORKDIR=%s", appConfig.Workdir)) + cmd.Env = append(cmd.Env, fmt.Sprintf("DISPLAYNAME=%s", installParams.DisplayName)) + cmd.Env = append(cmd.Env, fmt.Sprintf("APPNAME=%s", installParams.Name)) + + if err := cmd.Run(); err != nil { + log.WithError(err).Error("Error running post install script") + return err + } + } + return nil +} + func runPreInstall(appConfig *config.AppConfig, meta *marketplace.MarketplaceMetadata, install *marketplace.Install) error { if meta.PreInstall != "" { // TODO UNPIN ME @@ -172,6 +373,39 @@ func runPreInstall(appConfig *config.AppConfig, meta *marketplace.MarketplaceMet return nil } +func runPreInstallNew(appConfig *config.AppConfig, meta *marketplace.MarketplaceMetadata, installParams *types.ApplicationInstallParams) error { + if meta.PreInstall != "" { + // TODO UNPIN ME + path := filepath.Join(appConfig.Workdir, "terraform", "modules", meta.Name, meta.Version, meta.WorkDirectory, meta.PreInstall) + log.Infof("Pre install command path: %s", path) + cmd := exec.Command(path) + cmd.Env = os.Environ() + // for k, v := range install.Applications.Dependencies { + // // Replace hyphens with underscores + // formattedKey := strings.ReplaceAll(k, "-", "_") + + // // Convert to upper case + // upperKey := strings.ToUpper(formattedKey) + + // // Use a regex to keep only alphanumeric characters and underscores + // re := regexp.MustCompile("[^A-Z0-9_]+") + // cleanKey := re.ReplaceAllString(upperKey, "") + + // log.Infof("Adding environment variable: %s = %s", cleanKey, v) + // cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", cleanKey, v)) + // } + cmd.Env = append(cmd.Env, fmt.Sprintf("DEPLOYMENTNAME=%s", installParams.DeploymentName)) + cmd.Env = append(cmd.Env, fmt.Sprintf("WORKDIR=%s", appConfig.Workdir)) + cmd.Env = append(cmd.Env, fmt.Sprintf("DISPLAYNAME=%s", installParams.DisplayName)) + cmd.Env = append(cmd.Env, fmt.Sprintf("APPNAME=%s", installParams.Name)) + if err := cmd.Run(); err != nil { + log.WithError(err).Error("Error running pre install script") + return err + } + } + return nil +} + func TriggerInstall(wsManager *websocket.WebSocketManager, userid string, store database.Datastore, received *marketplace.Install, conf *config.AppConfig) error { t := received.Applications diff --git a/backend/internal/processes/marketplace.go b/backend/internal/processes/marketplace.go index 56f1274d..0ef65066 100644 --- a/backend/internal/processes/marketplace.go +++ b/backend/internal/processes/marketplace.go @@ -15,7 +15,7 @@ import ( "strings" ) -func fetchMarketplaceMetadata(name string, version string, appConfig *config.AppConfig) (marketplace.MarketplaceMetadata, error) { +func FetchMarketplaceMetadata(name string, version string, appConfig *config.AppConfig) (marketplace.MarketplaceMetadata, error) { log.Infof("Fetching marketplace metadata for, %s, %s", name, version) url := fmt.Sprintf("%sunity-sds/unity-marketplace/main/applications/%s/%s/metadata.json", appConfig.MarketplaceBaseUrl, name, version) diff --git a/backend/internal/processes/uninstaller.go b/backend/internal/processes/uninstaller.go index 7ce53896..e1ba3228 100644 --- a/backend/internal/processes/uninstaller.go +++ b/backend/internal/processes/uninstaller.go @@ -15,6 +15,7 @@ import ( "path" "strconv" "strings" + "fmt" ) type UninstallPayload struct { @@ -59,7 +60,7 @@ func UninstallApplication(appname string, deploymentname string, displayname str } for _, file := range files { - log.Infof("Checking file %s has prefix: %s", file.Name(), appname) + log.Infof("Checking file %s has prefix: %s", file.Name(), appname) if strings.HasPrefix(file.Name(), appname) { log.Infof("File was a match") // Open the file @@ -123,6 +124,182 @@ func UninstallApplication(appname string, deploymentname string, displayname str return nil } +func UninstallApplicationNew(appname string, deploymentname string, displayname string, conf *config.AppConfig, store database.Datastore) error { + // Create uninstall_logs directory if it doesn't exist + logDir := path.Join(conf.Workdir, "uninstall_logs") + if err := os.MkdirAll(logDir, 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create install_logs directory: %w", err) + } + + executor := &terraform.RealTerraformExecutor{} + + filepath := path.Join(conf.Workdir, "workspace") + + files, err := ioutil.ReadDir(filepath) + if err != nil { + return err + } + + for _, file := range files { + log.Infof("Checking file %s has prefix: %s", file.Name(), appname) + if strings.HasPrefix(file.Name(), appname) { + log.Infof("File was a match") + // Open the file + f, err := os.Open(path.Join(filepath, file.Name())) + if err != nil { + return err + } + + // Read comments at the top + scanner := bufio.NewScanner(f) + metadata := make(map[string]string) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "#") { + break + } + // Parsing the comments + parts := strings.SplitN(strings.TrimPrefix(line, "# "), ": ", 2) + if len(parts) == 2 { + key := parts[0] + value := strings.TrimSpace(parts[1]) + metadata[key] = value + } + } + f.Close() + + // Check applicationName from the comments and delete the file if it matches + log.Infof("Check if appname %s == %s", metadata["applicationName"], displayname) + if metadata["applicationName"] == displayname { + p := path.Join(filepath, file.Name()) + log.Infof("Attempting to delete file: %s", p) + err = os.Remove(p) + if err != nil { + id, err := store.FetchDeploymentIDByName(deploymentname) + log.WithError(err).Error("Failed to fetch deployment ID by name when removing application") + err = store.UpdateApplicationStatus(id, appname, displayname, "UNINSTALL FAILED") + log.WithError(err).Error("Failed to update application status removing application") + return err + } + logfile := path.Join(logDir, fmt.Sprintf("%s_uninstall_log", deploymentname)) + err = terraform.RunTerraformLogOutToFile(conf, logfile, executor, "") + if err != nil { + log.WithError(err).Error("Failed to uninstall application") + return err + } + + // err := store.RemoveApplicationByName(deploymentname, appname) + // if err != nil { + // id, err := store.FetchDeploymentIDByName(deploymentname) + // log.WithError(err).Error("Failed to fetch deployment ID by name when removing application") + // err = store.UpdateApplicationStatus(id, appname, displayname, "UNINSTALL FAILED") + // log.WithError(err).Error("Failed to update application status removing application") + // return err + // } + id, err := store.FetchDeploymentIDByName(deploymentname) + err = store.UpdateApplicationStatus(id, appname, displayname, "UNINSTALLED") + err = fetchAllApplications(store) + if err != nil { + return err + } + + return nil + } + } + } + + return nil +} + +func UninstallApplicationNewV2(appName string, version string, deploymentName string, conf *config.AppConfig, store database.Datastore) error { + // Create uninstall_logs directory if it doesn't exist + logDir := path.Join(conf.Workdir, "uninstall_logs") + if err := os.MkdirAll(logDir, 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create install_logs directory: %w", err) + } + + executor := &terraform.RealTerraformExecutor{} + + filepath := path.Join(conf.Workdir, "workspace") + + files, err := ioutil.ReadDir(filepath) + if err != nil { + return err + } + + for _, file := range files { + log.Infof("Checking file %s has prefix: %s", file.Name(), appName) + if strings.HasPrefix(file.Name(), appName) { + log.Infof("File was a match") + // Open the file + f, err := os.Open(path.Join(filepath, file.Name())) + if err != nil { + return err + } + + // Read comments at the top + scanner := bufio.NewScanner(f) + metadata := make(map[string]string) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "#") { + break + } + // Parsing the comments + parts := strings.SplitN(strings.TrimPrefix(line, "# "), ": ", 2) + if len(parts) == 2 { + key := parts[0] + value := strings.TrimSpace(parts[1]) + metadata[key] = value + } + } + f.Close() + + // Check applicationName from the comments and delete the file if it matches + log.Infof("Check if appname %s == %s", metadata["applicationName"], deploymentName) + if metadata["applicationName"] == deploymentName { + p := path.Join(filepath, file.Name()) + log.Infof("Attempting to delete file: %s", p) + err = os.Remove(p) + // if err != nil { + // log.WithError(err).Error("Failed to fetch deployment ID by name when removing application") + // err = store.UpdateApplicationStatus(id, appname, deploymentName, "UNINSTALL FAILED") + // log.WithError(err).Error("Failed to update application status removing application") + // return err + // } + store.UpdateInstalledMarketplaceApplicationStatusByName(appName, deploymentName, "STARTING UNINSTALL") + logfile := path.Join(logDir, fmt.Sprintf("%s_%s_uninstall_log", appName, deploymentName)) + err = terraform.RunTerraformLogOutToFile(conf, logfile, executor, "") + if err != nil { + log.WithError(err).Error("Failed to uninstall application") + return err + } + + err = store.RemoveInstalledMarketplaceApplicationByName(appName) + + // err := store.RemoveApplicationByName(deploymentname, appname) + // if err != nil { + // id, err := store.FetchDeploymentIDByName(deploymentname) + // log.WithError(err).Error("Failed to fetch deployment ID by name when removing application") + // err = store.UpdateApplicationStatus(id, appname, deploymentName, "UNINSTALL FAILED") + // log.WithError(err).Error("Failed to update application status removing application") + // return err + // } + // id, err := store.FetchDeploymentIDByName(deploymentname) + err = store.UpdateInstalledMarketplaceApplicationStatusByName(appName, deploymentName, "UNINSTALLED") + err = fetchAllApplications(store) + if err != nil { + return err + } + + return nil + } + } + } + + return nil +} + func ReapplyApplication(payload string, conf *config.AppConfig, store database.Datastore, wsmgr *websocket.WebSocketManager, userid string) error { filepath := path.Join(conf.Workdir, "workspace") var uninstall UninstallPayload diff --git a/backend/internal/processes/validation.go b/backend/internal/processes/validation.go index fe0fca62..8d35c865 100644 --- a/backend/internal/processes/validation.go +++ b/backend/internal/processes/validation.go @@ -19,7 +19,7 @@ func ValidateMarketplaceInstallation(name string, version string, appConfig *con // Validate installation // Check Marketplace Installation Exists - meta, err := fetchMarketplaceMetadata(name, version, appConfig) + meta, err := FetchMarketplaceMetadata(name, version, appConfig) if err != nil { return false, meta, err } diff --git a/backend/internal/terraform/management.go b/backend/internal/terraform/management.go index 203b94c9..e18d44fa 100644 --- a/backend/internal/terraform/management.go +++ b/backend/internal/terraform/management.go @@ -10,6 +10,7 @@ import ( "github.com/unity-sds/unity-cs-manager/marketplace" "github.com/unity-sds/unity-management-console/backend/internal/application/config" "github.com/unity-sds/unity-management-console/backend/internal/database" + "github.com/unity-sds/unity-management-console/backend/types" "github.com/zclconf/go-cty/cty" "math/rand" "os" @@ -103,7 +104,21 @@ func generateMetadataHeader(cloudenv *hclwrite.Body, id string, application stri cloudenv.AppendUnstructuredTokens(comment) } -func generateRandomString(length int) string { + +func generateMetadataHeaderNew(cloudenv *hclwrite.Body, id string, application string, applicationName string, version string, creator string) { + currentTime := time.Now() + dateString := currentTime.Format("2006-01-02") + comment := hclwrite.Tokens{ + &hclwrite.Token{ + Type: hclsyntax.TokenComment, + Bytes: []byte(fmt.Sprintf("# id: %v\n# application: %v\n# applicationName: %v\n# version: %v\n# creator: %v\n# creationDate: %v\n", id, application, applicationName, version, creator, dateString)), + SpacesBefore: 0, + }, + } + cloudenv.AppendUnstructuredTokens(comment) +} + +func GenerateRandomString(length int) string { charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" b := make([]byte, length) for i := range b { @@ -150,7 +165,7 @@ func AddApplicationToStack(appConfig *config.AppConfig, location string, meta *m log.Infof("Adding application to stack. Location: %v, meta %v, install: %v, deploymentID: %v", location, meta, install, deploymentID) rand.Seed(time.Now().UnixNano()) - s := generateRandomString(8) + s := GenerateRandomString(8) hclFile := hclwrite.NewEmptyFile() directory := filepath.Join(appConfig.Workdir, "workspace") @@ -220,6 +235,149 @@ func AddApplicationToStack(appConfig *config.AppConfig, location string, meta *m return nil } + +// AddApplicationToStack adds the given application configuration to the stack. +// It takes care of creating the necessary workspace directory, generating the +// HCL file, and writing the required attributes. +func AddApplicationToStackNew(appConfig *config.AppConfig, location string, meta *marketplace.MarketplaceMetadata, installParams *types.ApplicationInstallParams, db database.Datastore, deploymentID uint) error { + log.Infof("Adding application to stack. Location: %v, meta %v, install: %v, deploymentID: %v", location, meta, installParams, deploymentID) + rand.Seed(time.Now().UnixNano()) + + s := GenerateRandomString(8) + hclFile := hclwrite.NewEmptyFile() + + directory := filepath.Join(appConfig.Workdir, "workspace") + log.Errorf("Application name: %s", installParams.Name) + filename := fmt.Sprintf("%v%v%v", installParams.Name, s, ".tf") + + log.Errorf("Creating file with the name: %s", filename) + tfFile, err := createFile(directory, filename, 0755) + if err != nil { + log.WithError(err).Error("Problem creating tf file") + return err + } + + path := filepath.Join(location, meta.WorkDirectory) + // initialize the body of the new file object + rootBody := hclFile.Body() + + u, err := uuid.NewRandom() + if err != nil { + log.WithError(err).Error("Failed to generate UUID") + return err + } + + log.Info("Generating header") + generateMetadataHeader(rootBody, u.String(), meta.Name, installParams.DeploymentName, installParams.Version, "admin", deploymentID) + + log.Info("adding attributes") + attributes := map[string]cty.Value{ + "deployment_name": cty.StringVal(installParams.DeploymentName), + "tags": cty.MapValEmpty(cty.String), // Example of setting an empty map + "project": cty.StringVal(appConfig.Project), + "venue": cty.StringVal(appConfig.Venue), + "installprefix": cty.StringVal(appConfig.InstallPrefix), + } + + log.Info("Organising variable replacement") + if installParams.Variables != nil { + for key, element := range installParams.Variables { + log.Infof("Adding variable: %s, %s", key, element) + attributes[key] = cty.StringVal(element) + } + } + // log.Info("Parsing advanced vars") + // parseAdvancedVariables(install, &attributes) + rand.Seed(time.Now().UnixNano()) + chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + randomChars := make([]byte, 5) + for i, v := range rand.Perm(52)[:5] { + randomChars[i] = chars[v] + } + log.Info("Appending block to body") + appendBlockToBody(rootBody, "module", []string{fmt.Sprintf("%s-%s", installParams.DeploymentName, string(randomChars))}, path, attributes) + + log.Info("Writing hcl file.") + _, err = tfFile.Write(hclFile.Bytes()) + if err != nil { + log.WithError(err).Error("error writing hcl file") + return err + } + + return nil +} + +// AddApplicationToStack adds the given application configuration to the stack. +// It takes care of creating the necessary workspace directory, generating the +// HCL file, and writing the required attributes. +func AddApplicationToStackNewV2(appConfig *config.AppConfig, location string, meta *marketplace.MarketplaceMetadata, installParams *types.ApplicationInstallParams, db database.Datastore) error { + log.Infof("Adding application to stack. Location: %v, meta %v, install: %v, deploymentID: %v", location, meta, installParams) + rand.Seed(time.Now().UnixNano()) + + s := GenerateRandomString(8) + hclFile := hclwrite.NewEmptyFile() + + directory := filepath.Join(appConfig.Workdir, "workspace") + log.Errorf("Application name: %s", installParams.Name) + filename := fmt.Sprintf("%v%v%v", installParams.Name, s, ".tf") + + log.Errorf("Creating file with the name: %s", filename) + tfFile, err := createFile(directory, filename, 0755) + if err != nil { + log.WithError(err).Error("Problem creating tf file") + return err + } + + path := filepath.Join(location, meta.WorkDirectory) + // initialize the body of the new file object + rootBody := hclFile.Body() + + u, err := uuid.NewRandom() + if err != nil { + log.WithError(err).Error("Failed to generate UUID") + return err + } + + log.Info("Generating header") + generateMetadataHeaderNew(rootBody, u.String(), meta.Name, installParams.DeploymentName, installParams.Version, "admin") + + log.Info("adding attributes") + attributes := map[string]cty.Value{ + "deployment_name": cty.StringVal(installParams.DeploymentName), + "tags": cty.MapValEmpty(cty.String), // Example of setting an empty map + "project": cty.StringVal(appConfig.Project), + "venue": cty.StringVal(appConfig.Venue), + "installprefix": cty.StringVal(appConfig.InstallPrefix), + } + + log.Info("Organising variable replacement") + if installParams.Variables != nil { + for key, element := range installParams.Variables { + log.Infof("Adding variable: %s, %s", key, element) + attributes[key] = cty.StringVal(element) + } + } + // log.Info("Parsing advanced vars") + // parseAdvancedVariables(install, &attributes) + rand.Seed(time.Now().UnixNano()) + chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + randomChars := make([]byte, 5) + for i, v := range rand.Perm(52)[:5] { + randomChars[i] = chars[v] + } + log.Info("Appending block to body") + appendBlockToBody(rootBody, "module", []string{fmt.Sprintf("%s-%s", installParams.DeploymentName, string(randomChars))}, path, attributes) + + log.Info("Writing hcl file.") + _, err = tfFile.Write(hclFile.Bytes()) + if err != nil { + log.WithError(err).Error("error writing hcl file") + return err + } + + return nil +} + func lookUpVariablePointer(element string, inst *marketplace.Install) (string, error) { val, err := lookUpFromDependencies(element, inst.Applications) if err != nil { diff --git a/backend/internal/terraform/runner.go b/backend/internal/terraform/runner.go index 46f5212a..3dcb525e 100644 --- a/backend/internal/terraform/runner.go +++ b/backend/internal/terraform/runner.go @@ -194,6 +194,80 @@ func RunTerraform(appconf *config.AppConfig, wsmgr *ws.WebSocketManager, id stri return nil } +func RunTerraformLogOutToFile(appconf *config.AppConfig, logfile string, executor TerraformExecutor, target string) error { + bucket := fmt.Sprintf("bucket=%s", appconf.BucketName) + key := fmt.Sprintf("key=%s-%s-tfstate", appconf.Project, appconf.Venue) + region := fmt.Sprintf("region=%s", appconf.AWSRegion) + + p := filepath.Join(appconf.Workdir, "workspace") + tf, err := executor.NewTerraform(p, "/usr/local/bin/terraform") + if err != nil { + log.Fatalf("error running NewTerraform: %s", err) + } + + // Open the log file in append mode + file, err := os.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Fatalf("error opening log file: %s", err) + } + defer file.Close() + + // Create a multi-writer that writes to both file and stdout + writerStdout := io.MultiWriter(os.Stdout, file) + writerStderr := io.MultiWriter(os.Stderr, file) + + tf.SetStdout(writerStdout) + tf.SetStderr(writerStderr) + tf.SetLogger(log.StandardLogger()) + + file.WriteString("Starting Terraform") + + err = executor.Init(context.Background(), tfexec.Upgrade(true), tfexec.BackendConfig(bucket), tfexec.BackendConfig(key), tfexec.BackendConfig(region)) + + if err != nil { + log.WithError(err).Error("error initialising terraform") + file.WriteString(fmt.Sprintf("Error initialising terraform: %s\n", err)) + return err + } + + file.WriteString("Waiting 60 seconds for the state to settle\n") + time.Sleep(60 * time.Second) + + change := false + if target != "" { + log.Infof("Running terraform with target: %s", target) + file.WriteString(fmt.Sprintf("Running terraform with target: %s\n", target)) + change, err = executor.Plan(context.Background(), tfexec.Target(target)) + } else { + change, err = executor.Plan(context.Background()) + } + + if err != nil { + log.WithError(err).Error("error running plan") + file.WriteString(fmt.Sprintf("Error running plan: %s\n", err)) + return err + } + + if change { + if target != "" { + log.Infof("Running terraform with target: %s", target) + file.WriteString(fmt.Sprintf("Running terraform with target: %s\n", target)) + err = executor.Apply(context.Background(), tfexec.Target(target)) + } else { + err = executor.Apply(context.Background()) + } + + if err != nil { + log.WithError(err).Error("error running apply") + file.WriteString(fmt.Sprintf("Error running apply: %s\n", err)) + return err + } + } + + file.WriteString("Terraform operation completed successfully\n") + return nil +} + func DestroyAllTerraform(appconf *config.AppConfig, wsmgr *ws.WebSocketManager, id string, executor TerraformExecutor) error { p := filepath.Join(appconf.Workdir, "workspace") diff --git a/backend/internal/web/api.go b/backend/internal/web/api.go index f394fcc2..bd193167 100644 --- a/backend/internal/web/api.go +++ b/backend/internal/web/api.go @@ -1,81 +1,217 @@ package web import ( + "fmt" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/unity-sds/unity-cs-manager/marketplace" "github.com/unity-sds/unity-management-console/backend/internal/application/config" "github.com/unity-sds/unity-management-console/backend/internal/aws" + "github.com/unity-sds/unity-management-console/backend/internal/database" "github.com/unity-sds/unity-management-console/backend/internal/processes" + "github.com/unity-sds/unity-management-console/backend/types" "net/http" + "os" + "path/filepath" + // "strconv" ) -func handleHealthChecks(c *gin.Context, appConfig config.AppConfig) { - bucketname := viper.Get("bucketname").(string) +func handleHealthChecks(appConfig config.AppConfig) func(c *gin.Context) { + return func(c *gin.Context) { + bucketname := viper.Get("bucketname").(string) - // Get the latest health check file - result, err := aws.GetObject(nil, &appConfig, bucketname, "health_check_latest.json") - if err != nil { - log.Errorf("Error getting health check file: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve health check data"}) - return + // Get the latest health check file + result, err := aws.GetObject(nil, &appConfig, bucketname, "health_check_latest.json") + if err != nil { + log.Errorf("Error getting health check file: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve health check data"}) + return + } + c.Data(http.StatusOK, "application/json", result) } - c.Data(http.StatusOK, "application/json", result) } -func handleUninstall(c *gin.Context, appConfig config.AppConfig) { - uninstallStatus := viper.Get("uninstallStatus") +func handleUninstall(appConfig config.AppConfig) func(c *gin.Context) { + return func(c *gin.Context) { + uninstallStatus := viper.Get("uninstallStatus") - if uninstallStatus != nil { - c.JSON(http.StatusOK, gin.H{"uninstall_status": uninstallStatus}) - return - } + if uninstallStatus != nil { + c.JSON(http.StatusOK, gin.H{"uninstall_status": uninstallStatus}) + return + } - var uninstallOptions struct { - DeleteBucket *bool `form:"delete_bucket" json:"delete_bucket"` - } - err := c.BindJSON(&uninstallOptions) + var uninstallOptions struct { + DeleteBucket *bool `form:"delete_bucket" json:"delete_bucket"` + } + err := c.BindJSON(&uninstallOptions) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bad input posted."}) + return + } + + deleteBucket := false + if uninstallOptions.DeleteBucket != nil { + deleteBucket = *uninstallOptions.DeleteBucket + } + + received := &marketplace.Uninstall{ + DeleteBucket: deleteBucket, + } - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bad input posted."}) - return + go processes.UninstallAll(&conf, nil, "restAPIUser", received) + viper.Set("uninstallStatus", "in progress") + c.JSON(http.StatusOK, gin.H{"uninstall_status": "in progress"}) } +} + +func handleApplicationInstall(appConfig config.AppConfig, db database.Datastore) func(c *gin.Context) { + return func(c *gin.Context) { + var applicationInstallParams types.ApplicationInstallParams + err := c.BindJSON(&applicationInstallParams) + + if err != nil { + log.Errorf("Error parsing JSON: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Bad JSON"}) + return + } + + log.Errorf("Got JSON: %v", applicationInstallParams) + + // First check if this application is already installed. + existingApplication, err := db.GetInstalledMarketplaceApplicationStatusByName(applicationInstallParams.Name, applicationInstallParams.DeploymentName) + if err != nil { + log.WithError(err).Error("Error finding applications") + c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to search applcation list"}) + } + + if existingApplication != nil && existingApplication.Status != "UNINSTALLED" { + errMsg := fmt.Sprintf("Application with name %s already exists. Status: %s", applicationInstallParams.Name, existingApplication.Status) + c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg}) + return + } + + // OK to start installing, get the metadata for this application + metadata, err := processes.FetchMarketplaceMetadata(applicationInstallParams.Name, applicationInstallParams.Version, &appConfig) + if err != nil { + log.Errorf("Unable to fetch metadata for application: %s, %v", applicationInstallParams.Name, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to fetch package"}) + return + } + + // Install the application package files + location, err := processes.FetchPackage(&metadata, &appConfig) + if err != nil { + log.Errorf("Unable to fetch package for application: %s, %v", applicationInstallParams.Name, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to fetch package"}) + return + } + + err = processes.InstallMarketplaceApplicationNewV2(&appConfig, location, &applicationInstallParams, &metadata, db) + + c.Status(http.StatusOK) - deleteBucket := false - if uninstallOptions.DeleteBucket != nil { - deleteBucket = *uninstallOptions.DeleteBucket } +} + +func handleGetInstallLogs(appConfig config.AppConfig, db database.Datastore, uninstall bool) func(c *gin.Context) { + return func(c *gin.Context) { + appName := c.Param("appName") + deploymentName := c.Param("deploymentName") + + // deploymentID, err := db.FetchDeploymentIDByApplicationName(deploymentName) + // if err != nil { + // log.Errorf("Error getting deployment ID: %v", err) + // c.JSON(http.StatusInternalServerError, gin.H{"error": "Error reading application status"}) + // return + // } + + var logDir string + if uninstall { + logDir = filepath.Join(appConfig.Workdir, "uninstall_logs") + } else { + logDir = filepath.Join(appConfig.Workdir, "install_logs") + } + + var logfile string + if uninstall { + logfile = filepath.Join(logDir, fmt.Sprintf("%s_%s_uninstall_log", appName, deploymentName)) + } else { + logfile = filepath.Join(logDir, fmt.Sprintf("%s_%s_install_log", appName, deploymentName)) + } + + // Read the log file + content, err := os.ReadFile(logfile) + if err != nil { + log.Errorf("Error reading log file: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read log file"}) + return + } - received := &marketplace.Uninstall{ - DeleteBucket: deleteBucket, + // Return the file contents + c.Data(http.StatusOK, "text/plain", content) } +} + +func handleUninstallApplication(appConfig config.AppConfig, db database.Datastore) func(c *gin.Context) { + return func(c *gin.Context) { + appName := c.Param("appName") + version := c.Param("version") + deploymentName := c.Param("deploymentName") - go processes.UninstallAll(&conf, nil, "restAPIUser", received) - viper.Set("uninstallStatus", "in progress") - c.JSON(http.StatusOK, gin.H{"uninstall_status": "in progress"}) + go processes.UninstallApplicationNewV2(appName, version, deploymentName, &conf, db) + } } -func handleGetAPICall(appConfig config.AppConfig) gin.HandlerFunc { - fn := func(c *gin.Context) { - switch endpoint := c.Param("endpoint"); endpoint { - case "health_checks": - handleHealthChecks(c, appConfig) - default: - handleNoRoute(c) +func handleGetApplicationInstallStatusByName(appConfig config.AppConfig, db database.Datastore) func(c *gin.Context) { + return func(c *gin.Context) { + appName := c.Param("appName") + deploymentName := c.Param("deploymentName") + app, err := db.GetInstalledMarketplaceApplicationStatusByName(appName, deploymentName) + + if err != nil { + log.Errorf("Error reading application status: %v", err) + c.Status(http.StatusNotFound) + return } + + c.JSON(http.StatusOK, app) } - return gin.HandlerFunc(fn) } -func handlePostAPICall(appConfig config.AppConfig) gin.HandlerFunc { - fn := func(c *gin.Context) { - switch endpoint := c.Param("endpoint"); endpoint { - case "uninstall": - handleUninstall(c, appConfig) - default: - handleNoRoute(c) +func getInstalledApplications(appConfig config.AppConfig, db database.Datastore) func(c *gin.Context) { + return func(c *gin.Context) { + applications, err := db.FetchAllInstalledMarketplaceApplications() + if err != nil { + log.Errorf("Error getting application list: %v", err) + c.Status(http.StatusInternalServerError) + return } + c.JSON(http.StatusOK, applications) } - return gin.HandlerFunc(fn) } + +// func handleGetAPICall(appConfig config.AppConfig) gin.HandlerFunc { +// fn := func(c *gin.Context) { +// switch endpoint := c.Param("endpoint"); endpoint { +// case "health_checks": +// handleHealthChecks(c, appConfig) +// default: +// handleNoRoute(c) +// } +// } +// return gin.HandlerFunc(fn) +// } + +// func handlePostAPICall(appConfig config.AppConfig) gin.HandlerFunc { +// fn := func(c *gin.Context) { +// switch endpoint := c.Param("endpoint"); endpoint { +// case "uninstall": +// handleUninstall(c, appConfig) +// default: +// handleNoRoute(c) +// } +// } +// return gin.HandlerFunc(fn) +// } diff --git a/backend/internal/web/router.go b/backend/internal/web/router.go index c50404ee..1b3eac67 100644 --- a/backend/internal/web/router.go +++ b/backend/internal/web/router.go @@ -86,6 +86,10 @@ func DefineRoutes(appConfig config.AppConfig) *gin.Engine { router.RedirectTrailingSlash = false conf = appConfig + store, err := database.NewGormDatastore() + if err != nil { + log.WithError(err).Error("Unable to create datastore") + } /*authorized := router.Group("/", gin.BasicAuth(gin.Accounts{ "admin": "unity", "user": "unity", @@ -97,9 +101,19 @@ func DefineRoutes(appConfig config.AppConfig) *gin.Engine { }) router.StaticFS("/ui/", http.Dir("./build")) router.GET("/ws", handleWebsocket) + + api := router.Group("/api") + { + api.GET("/health_checks", gin.HandlerFunc(handleHealthChecks(appConfig))) + api.GET("/installed_applications", gin.HandlerFunc(getInstalledApplications(appConfig, store))) + api.POST("/uninstall", gin.HandlerFunc(handleUninstall(appConfig))) + api.POST("/install_application", gin.HandlerFunc(handleApplicationInstall(appConfig, store))) + api.GET("/install_application/logs/:appName/:deploymentName", gin.HandlerFunc(handleGetInstallLogs(appConfig, store, false))) + api.GET("/uninstall_application/logs/:appName/:deploymentName", gin.HandlerFunc(handleGetInstallLogs(appConfig, store, true))) + api.GET("/uninstall_application/:appName/:version/:deploymentName", gin.HandlerFunc(handleUninstallApplication(appConfig, store))) + api.GET("/install_application/status/:appName/:version/:deploymentName", gin.HandlerFunc(handleGetApplicationInstallStatusByName(appConfig, store))) + } router.GET("/debug/pprof/*profile", gin.WrapF(pprof.Index)) - router.GET("/api/:endpoint", handleGetAPICall(appConfig)) - router.POST("/api/:endpoint", handlePostAPICall(appConfig)) //router.Use(EnsureTrailingSlash()) router.Use(LoggingMiddleware()) diff --git a/backend/types/marketplace.go b/backend/types/marketplace.go new file mode 100644 index 00000000..0bb65d7c --- /dev/null +++ b/backend/types/marketplace.go @@ -0,0 +1,9 @@ +package types + +type ApplicationInstallParams struct { + Name string + Version string + DisplayName string + DeploymentName string + Variables map[string]string +} diff --git a/go.mod b/go.mod index 005b8e92..5d1fa453 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.2 github.com/thomaspoignant/go-feature-flag v1.10.2 - github.com/unity-sds/unity-cs-manager v0.0.0-20240702183238-3867e1c6c2c4 + github.com/unity-sds/unity-cs-manager v0.0.0-20240916163316-5b79347ceb6c github.com/zclconf/go-cty v1.13.0 golang.org/x/tools v0.7.0 google.golang.org/protobuf v1.30.0 diff --git a/go.sum b/go.sum index 0bcc5f0a..51259cc3 100644 --- a/go.sum +++ b/go.sum @@ -431,6 +431,10 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/unity-sds/unity-cs-manager v0.0.0-20240702183238-3867e1c6c2c4 h1:cTbn+4dgbkvsQOUtXGO2a1pgzURizGvUqPqy7m8z+pA= github.com/unity-sds/unity-cs-manager v0.0.0-20240702183238-3867e1c6c2c4/go.mod h1:2AW0iNfvHGRdrb3u6PuheZP0zbV4W2eBIW0+7/6RCfE= +github.com/unity-sds/unity-cs-manager v0.0.0-20240916151921-503ac3eb268e h1:ZLv2GcY4CN0IXgDC82wNMG4+uK2hvZWtt3JFV5FhyI4= +github.com/unity-sds/unity-cs-manager v0.0.0-20240916151921-503ac3eb268e/go.mod h1:2AW0iNfvHGRdrb3u6PuheZP0zbV4W2eBIW0+7/6RCfE= +github.com/unity-sds/unity-cs-manager v0.0.0-20240916163316-5b79347ceb6c h1:1RxN6frUkV8bOhUH44y2K7eTFEWBiuPw2bWd0LNielQ= +github.com/unity-sds/unity-cs-manager v0.0.0-20240916163316-5b79347ceb6c/go.mod h1:2AW0iNfvHGRdrb3u6PuheZP0zbV4W2eBIW0+7/6RCfE= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= diff --git a/package-lock.json b/package-lock.json index 5aca7577..bc492b13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@nasa-jpl/stellar": "^1.1.4", + "@nasa-jpl/stellar": "^1.1.18", "@tailwindcss/forms": "^0.5.6", "atob": "^2.1.2", "axios": "^1.4.0", @@ -728,9 +728,9 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "node_modules/@nasa-jpl/stellar": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@nasa-jpl/stellar/-/stellar-1.1.4.tgz", - "integrity": "sha512-tWgawwlIHa211hEvHWY2yRHTUzW0DlK8HqZEcHjJMhtsrbjB7v9YMFZhTTE9XD1YL1AJ2oB3K1eKL/b7eiHgXw==" + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@nasa-jpl/stellar/-/stellar-1.1.18.tgz", + "integrity": "sha512-e+26M01HFrGBZBQwsxxoJ8OSbRKn/zdUatwRryuPaEIm9RNJwDiejYqIoWCLn9s04hVeYr4PRe6IXa0nPR95vg==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -8813,9 +8813,9 @@ } }, "@nasa-jpl/stellar": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@nasa-jpl/stellar/-/stellar-1.1.4.tgz", - "integrity": "sha512-tWgawwlIHa211hEvHWY2yRHTUzW0DlK8HqZEcHjJMhtsrbjB7v9YMFZhTTE9XD1YL1AJ2oB3K1eKL/b7eiHgXw==" + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@nasa-jpl/stellar/-/stellar-1.1.18.tgz", + "integrity": "sha512-e+26M01HFrGBZBQwsxxoJ8OSbRKn/zdUatwRryuPaEIm9RNJwDiejYqIoWCLn9s04hVeYr4PRe6IXa0nPR95vg==" }, "@nodelib/fs.scandir": { "version": "2.1.5", diff --git a/package.json b/package.json index 571b23e7..6a46ff6c 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ }, "type": "module", "dependencies": { - "@nasa-jpl/stellar": "^1.1.4", + "@nasa-jpl/stellar": "^1.1.18", "@tailwindcss/forms": "^0.5.6", "atob": "^2.1.2", "axios": "^1.4.0", diff --git a/src/components/ApplicationPanelItem.svelte b/src/components/ApplicationPanelItem.svelte index 04eeb366..61234a81 100644 --- a/src/components/ApplicationPanelItem.svelte +++ b/src/components/ApplicationPanelItem.svelte @@ -1,74 +1,243 @@ -
-
-
- {title} -
- Installation Status: - {#if status === 'COMPLETE'} - Done - {:else} - {status} - {/if} -
-
-
- {#if isUninstalling} +
+
+
+ {title} + Application: {appName} +
+ Installation Status: + {#if combinedStatus === 'COMPLETE'} + Done + {:else} + {combinedStatus} + {/if} +
+
+ {#if combinedStatus !== 'UNINSTALLED'} +
+ + Explore + + {#if uninstallInProgress} + + {:else if uninstallError} + + {:else if !uninstallComplete} + + {/if} + + + {#if uninstallComplete} +
Uninstall Complete!
+ {/if} + + +
+ {/if} +
+ + +

+ Install Logs for {title} +

+ + {#if logs} +
+  {logs}
+
+ {/if}
> diff --git a/src/components/ProductItem.svelte b/src/components/ProductItem.svelte index d88d5c6a..762c59b5 100644 --- a/src/components/ProductItem.svelte +++ b/src/components/ProductItem.svelte @@ -62,13 +62,13 @@
- + -->
diff --git a/src/components/common/Modal.svelte b/src/components/common/Modal.svelte new file mode 100644 index 00000000..3dee6f2f --- /dev/null +++ b/src/components/common/Modal.svelte @@ -0,0 +1,64 @@ + + + + (showModal = false)} + on:click|self={() => dialog.close()} +> + +
+ +
+ +
+ + +
+
+ + diff --git a/src/data/unity-cs-manager/protobuf/extensions.ts b/src/data/unity-cs-manager/protobuf/extensions.ts index b0a90826..cf3487cf 100644 --- a/src/data/unity-cs-manager/protobuf/extensions.ts +++ b/src/data/unity-cs-manager/protobuf/extensions.ts @@ -13,6 +13,7 @@ export interface UnityWebsocketMessage { logs?: LogLine | undefined; deployments?: Deployments | undefined; uninstall?: Uninstall | undefined; + uninstallstatus?: UninstallStatus | undefined; } export interface Application { @@ -128,6 +129,12 @@ export interface LogLine { type: string; } +export interface UninstallStatus { + DeploymentName: string; + Application: string; + DisplayName: string; +} + function createBaseUnityWebsocketMessage(): UnityWebsocketMessage { return { install: undefined, @@ -138,6 +145,7 @@ function createBaseUnityWebsocketMessage(): UnityWebsocketMessage { logs: undefined, deployments: undefined, uninstall: undefined, + uninstallstatus: undefined, }; } @@ -167,6 +175,9 @@ export const UnityWebsocketMessage = { if (message.uninstall !== undefined) { Uninstall.encode(message.uninstall, writer.uint32(66).fork()).ldelim(); } + if (message.uninstallstatus !== undefined) { + UninstallStatus.encode(message.uninstallstatus, writer.uint32(74).fork()).ldelim(); + } return writer; }, @@ -233,6 +244,13 @@ export const UnityWebsocketMessage = { message.uninstall = Uninstall.decode(reader, reader.uint32()); continue; + case 9: + if (tag !== 74) { + break; + } + + message.uninstallstatus = UninstallStatus.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -252,6 +270,7 @@ export const UnityWebsocketMessage = { logs: isSet(object.logs) ? LogLine.fromJSON(object.logs) : undefined, deployments: isSet(object.deployments) ? Deployments.fromJSON(object.deployments) : undefined, uninstall: isSet(object.uninstall) ? Uninstall.fromJSON(object.uninstall) : undefined, + uninstallstatus: isSet(object.uninstallstatus) ? UninstallStatus.fromJSON(object.uninstallstatus) : undefined, }; }, @@ -270,6 +289,8 @@ export const UnityWebsocketMessage = { (obj.deployments = message.deployments ? Deployments.toJSON(message.deployments) : undefined); message.uninstall !== undefined && (obj.uninstall = message.uninstall ? Uninstall.toJSON(message.uninstall) : undefined); + message.uninstallstatus !== undefined && + (obj.uninstallstatus = message.uninstallstatus ? UninstallStatus.toJSON(message.uninstallstatus) : undefined); return obj; }, @@ -301,6 +322,9 @@ export const UnityWebsocketMessage = { message.uninstall = (object.uninstall !== undefined && object.uninstall !== null) ? Uninstall.fromPartial(object.uninstall) : undefined; + message.uninstallstatus = (object.uninstallstatus !== undefined && object.uninstallstatus !== null) + ? UninstallStatus.fromPartial(object.uninstallstatus) + : undefined; return message; }, }; @@ -1987,6 +2011,90 @@ export const LogLine = { }, }; +function createBaseUninstallStatus(): UninstallStatus { + return { DeploymentName: "", Application: "", DisplayName: "" }; +} + +export const UninstallStatus = { + encode(message: UninstallStatus, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.DeploymentName !== "") { + writer.uint32(10).string(message.DeploymentName); + } + if (message.Application !== "") { + writer.uint32(18).string(message.Application); + } + if (message.DisplayName !== "") { + writer.uint32(26).string(message.DisplayName); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): UninstallStatus { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUninstallStatus(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.DeploymentName = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.Application = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.DisplayName = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): UninstallStatus { + return { + DeploymentName: isSet(object.DeploymentName) ? String(object.DeploymentName) : "", + Application: isSet(object.Application) ? String(object.Application) : "", + DisplayName: isSet(object.DisplayName) ? String(object.DisplayName) : "", + }; + }, + + toJSON(message: UninstallStatus): unknown { + const obj: any = {}; + message.DeploymentName !== undefined && (obj.DeploymentName = message.DeploymentName); + message.Application !== undefined && (obj.Application = message.Application); + message.DisplayName !== undefined && (obj.DisplayName = message.DisplayName); + return obj; + }, + + create, I>>(base?: I): UninstallStatus { + return UninstallStatus.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): UninstallStatus { + const message = createBaseUninstallStatus(); + message.DeploymentName = object.DeploymentName ?? ""; + message.Application = object.Application ?? ""; + message.DisplayName = object.DisplayName ?? ""; + return message; + }, +}; + type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; export type DeepPartial = T extends Builtin ? T diff --git a/src/routes/applications/+page.svelte b/src/routes/applications/+page.svelte index 5e960fb1..66c1f62f 100644 --- a/src/routes/applications/+page.svelte +++ b/src/routes/applications/+page.svelte @@ -9,8 +9,43 @@ let project = ''; + type InstalledMarketplaceApplication = { + DeploymentName: string; + PackageName: string; + Name: string; + Source: string; + Version: string; + Status: string; + }; + + let applications: InstalledMarketplaceApplication[] = []; + + async function getInstalledApplications() { + const res = await fetch('../api/installed_applications'); + if (!res.ok) { + console.warn('Unable to get application list!'); + return; + } + applications = await res.json(); + // json.forEach((app: InstalledMarketplaceApplication[] = []) => { + // const newCard: CardItem = { + // title: app.displayName, + // packageName: app.PackageName, + // applicationName: app.Name, + // source: app.Source, + // version: app.Version, + // status: app.Status, + // link: '', + // deploymentName: app.DisplayName + // }; + // cardData = cardData.concat([newCard]); + // }); + // console.log(json); + } + onMount(async () => { - await fetchDeployedApplications(); + await getInstalledApplications(); + // await fetchDeployedApplications(); }); type CardItem = { @@ -85,16 +120,22 @@
Installed Applications
-
- {#each cardData as card, index (card.title)} +
+ {#each applications as card, index (card.DeploymentName)} {/each} diff --git a/src/routes/install/+page.svelte b/src/routes/install/+page.svelte index 60284f6b..f263a5fc 100644 --- a/src/routes/install/+page.svelte +++ b/src/routes/install/+page.svelte @@ -1,29 +1,231 @@
-
-
- {#if product} -

{product.DisplayName} Installation

- +
+
+ Installing Marketplace Application: {product.Name} +
+
+
+
+ {#if steps[currentStepIndex] === 'deploymentDetails'} +
Deployment Details
+
+
+ Deployment Name (this should be a unique identifier for this installation of the + Marketplace item) +
+ +
+ {:else if steps[currentStepIndex] === 'variables'} +
Variables
+
+ {#each Object.entries(Variables) as [key, value]} +
+
+ {key} +
+ +
+ {/each} +
+ {:else if steps[currentStepIndex] === 'summary'} +
Installation Summary
+
+
+
Version: 
+
{product.Version}
+
+
+
+
Variables
+ {#each Object.entries(applicationMetadata.Variables) as [key, value]} +
+
{key}: 
+
{value}
+
+ {/each} +
+
+ {/if} +
+
+ {#if currentStepIndex > 0} + + {/if} + {#if installInProgress} + + {:else if installFailed} + + {:else if installComplete} + + {:else if currentStepIndex === steps.length - 1} + {:else} -

Loading product...

+ + {/if} + + {#if installInProgress || installComplete} + {/if}
+ {#if showLogs && logs} +
+
+
+      {logs}
+    
+
+ {/if}
+ + diff --git a/src/routes/marketplace/+page.svelte b/src/routes/marketplace/+page.svelte index a755bd18..70f25257 100644 --- a/src/routes/marketplace/+page.svelte +++ b/src/routes/marketplace/+page.svelte @@ -3,10 +3,11 @@ import ProductItem from '../../components/ProductItem.svelte'; import CategoryList from '../../components/CategoryList.svelte'; import Header from '../../components/Header.svelte'; - import { marketplaceStore, selectedCategory, order } from '../../store/stores'; + import { marketplaceStore, selectedCategory, order, productInstall } from '../../store/stores'; import type { MarketplaceMetadata } from '../../data/unity-cs-manager/protobuf/marketplace'; import type { OrderLine } from '../../data/entities'; import { fade, slide } from 'svelte/transition'; + import { goto } from '$app/navigation'; $: categories = ['All', ...new Set($marketplaceStore.map((p) => p.Category))]; $: filteredProducts = $marketplaceStore.filter( @@ -60,6 +61,14 @@ } }; } + + function handleStartInstall(name: string) { + return () => { + const product = selectedVersionsForProducts[name]; + productInstall.set(product); + goto('/management/ui/install', { replaceState: true }); + }; + }
@@ -84,6 +93,7 @@ {/each} +