diff --git a/backend/internal/processes/bootstrap.go b/backend/internal/processes/bootstrap.go index 9856e5b8..def82821 100644 --- a/backend/internal/processes/bootstrap.go +++ b/backend/internal/processes/bootstrap.go @@ -2,13 +2,13 @@ package processes import ( "fmt" + "path/filepath" + "strings" log "github.com/sirupsen/logrus" "github.com/spf13/afero" // "github.com/unity-sds/unity-cs-manager/marketplace" - "path/filepath" - "strings" "github.com/unity-sds/unity-management-console/backend/internal/application" "github.com/unity-sds/unity-management-console/backend/internal/application/config" @@ -86,10 +86,10 @@ func BootstrapEnv(appconf *config.AppConfig) error { // log.WithError(err).Error("Problem updating ssm config") //} - log.Infof("Setting Up HTTPD Gateway from Marketplace") - err = installGateway(store, appconf) + log.Infof("Setting Up HTTPD Gateway and API Gateway from Marketplace in parallel") + err = installGatewayAndApiGateway(store, appconf) if err != nil { - log.WithError(err).Error("Error installing HTTPD Gateway") + log.WithError(err).Error("Error installing Gateways") err = store.AddToAudit(application.Bootstrap_Unsuccessful, "test") if err != nil { log.WithError(err).Error("Problem writing to auditlog") @@ -97,32 +97,10 @@ func BootstrapEnv(appconf *config.AppConfig) error { return err } - log.Infof("Setting Up Health Status Lambda") - err = installHealthStatusLambda(store, appconf) + log.Infof("Setting Up Health Status Lambda and Unity UI from Marketplace in parallel") + err = installHealthStatusLambdaAndUnityUi(store, appconf) if err != nil { - log.WithError(err).Error("Error installing Health Status") - err = store.AddToAudit(application.Bootstrap_Unsuccessful, "test") - if err != nil { - log.WithError(err).Error("Problem writing to auditlog") - } - return err - } - - log.Infof("Setting Up Basic API Gateway from Marketplace") - err = installBasicAPIGateway(store, appconf) - if err != nil { - log.WithError(err).Error("Error installing API Gateway") - err = store.AddToAudit(application.Bootstrap_Unsuccessful, "test") - if err != nil { - log.WithError(err).Error("Problem writing to auditlog") - } - return err - } - - log.Infof("Setting Up Unity UI from Marketplace") - err = installUnityUi(store, appconf) - if err != nil { - log.WithError(err).Error("Error installing unity-portal") + log.WithError(err).Error("Error installing Health Status Lambda and Unity UI") err = store.AddToAudit(application.Bootstrap_Unsuccessful, "test") if err != nil { log.WithError(err).Error("Problem writing to auditlog") @@ -184,13 +162,14 @@ required_providers { } } backend "s3" { - dynamodb_table = "%s-%s-terraform-state" + use_lockfile = true } } provider "aws" { region = "us-west-2" -}`, appConfig.Project, appConfig.Venue) +} +`) err := fs.MkdirAll(filepath.Join(appConfig.Workdir, "workspace"), 0755) if err != nil { @@ -233,89 +212,110 @@ func storeDefaultSSMParameters(appConfig *config.AppConfig, store database.Datas return nil } -func installGateway(store database.Datastore, appConfig *config.AppConfig) error { +func installGatewayAndApiGateway(store database.Datastore, appConfig *config.AppConfig) error { // Find the marketplace item for unity-proxy - var name, version string + var proxyName, proxyVersion string for _, item := range appConfig.MarketplaceItems { if item.Name == "unity-proxy" { - name = item.Name - version = item.Version + proxyName = item.Name + proxyVersion = item.Version break } } - - // Print the name and version - log.Infof("Found marketplace item - Name: %s, Version: %s", name, version) - - // If the item wasn't found, log an error and return - if name == "" || version == "" { + if proxyName == "" || proxyVersion == "" { log.Error("unity-proxy not found in MarketplaceItems") return fmt.Errorf("unity-proxy not found in MarketplaceItems") } - - simplevars := make(map[string]string) - simplevars["mgmt_dns"] = appConfig.ConsoleHost - // variables := marketplace.Install_Variables{Values: simplevars} - // applications := marketplace.Install_Applications{ - // Name: name, - // Version: version, - // Variables: &variables, - // Displayname: fmt.Sprintf("%s-%s", appConfig.InstallPrefix, name), - // } - // install := marketplace.Install{ - // Applications: &applications, - // DeploymentName: "Core Mgmt Gateway", - // } - - installParams := types.ApplicationInstallParams{ - Name: name, - Version: version, - Variables: simplevars, - DisplayName: "Unity Health Status Lambda", - DeploymentName: fmt.Sprintf("default-%s", name), - } - err := TriggerInstall(store, &installParams, appConfig, true) - if err != nil { - log.WithError(err).Error("Issue installing Mgmt Gateway") - return err - } - return nil -} - -func installBasicAPIGateway(store database.Datastore, appConfig *config.AppConfig) error { + // Find the marketplace item for unity-apigateway - var name, version string + var apiName, apiVersion string for _, item := range appConfig.MarketplaceItems { if item.Name == "unity-apigateway" { - name = item.Name - version = item.Version + apiName = item.Name + apiVersion = item.Version break } } - - // Print the name and version - log.Infof("Found marketplace item - Name: %s, Version: %s", name, version) - - // If the item wasn't found, log an error and return - if name == "" || version == "" { + if apiName == "" || apiVersion == "" { log.Error("unity-apigateway not found in MarketplaceItems") return fmt.Errorf("unity-apigateway not found in MarketplaceItems") } - - installParams := types.ApplicationInstallParams{ - Name: name, - Version: version, - Variables: nil, - DisplayName: "Core API Gateway", - DeploymentName: fmt.Sprintf("default-%s", name), + + // Set up variables for unity-proxy + proxyVars := make(map[string]string) + proxyVars["mgmt_dns"] = appConfig.ConsoleHost + + // Create installation parameters for both applications + params := []*types.ApplicationInstallParams{ + { + Name: proxyName, + Version: proxyVersion, + Variables: proxyVars, + DisplayName: "Core Mgmt Gateway", + DeploymentName: fmt.Sprintf("default-%s", proxyName), + }, + { + Name: apiName, + Version: apiVersion, + Variables: nil, + DisplayName: "Core API Gateway", + DeploymentName: fmt.Sprintf("default-%s", apiName), + }, } + + // Install both applications in a single batch operation + return BatchTriggerInstall(store, params, appConfig) +} - err := TriggerInstall(store, &installParams, appConfig, true) - if err != nil { - log.WithError(err).Error("Issue installing API Gateway") - return err +func installHealthStatusLambdaAndUnityUi(store database.Datastore, appConfig *config.AppConfig) error { + // Find the marketplace item for health status lambda + var lambdaName, lambdaVersion string + for _, item := range appConfig.MarketplaceItems { + if item.Name == "unity-cs-monitoring-lambda" { + lambdaName = item.Name + lambdaVersion = item.Version + break + } } - return nil + if lambdaName == "" || lambdaVersion == "" { + log.Error("unity-cs-monitoring-lambda not found in MarketplaceItems") + return fmt.Errorf("unity-cs-monitoring-lambda not found in MarketplaceItems") + } + + // Find the marketplace item for unity-portal + var uiName, uiVersion string + for _, item := range appConfig.MarketplaceItems { + if item.Name == "unity-portal" { + uiName = item.Name + uiVersion = item.Version + break + } + } + if uiName == "" || uiVersion == "" { + log.Error("unity-portal not found in MarketplaceItems") + return fmt.Errorf("unity-portal not found in MarketplaceItems") + } + + // Create installation parameters for both applications + params := []*types.ApplicationInstallParams{ + { + Name: lambdaName, + Version: lambdaVersion, + Variables: nil, + DisplayName: "Unity Health Status Lambda", + DeploymentName: fmt.Sprintf("default-%s", lambdaName), + }, + { + Name: uiName, + Version: uiVersion, + Variables: nil, + DisplayName: "Unity Navbar UI", + DeploymentName: fmt.Sprintf("default-%s", uiName), + }, + } + + // Install both applications in a single batch operation + return BatchTriggerInstall(store, params, appConfig) } func installUnityCloudEnv(store database.Datastore, appConfig *config.AppConfig) error { @@ -396,88 +396,3 @@ func installUnityCloudEnv(store database.Datastore, appConfig *config.AppConfig) } return nil } - -func installHealthStatusLambda(store database.Datastore, appConfig *config.AppConfig) error { - - // Find the marketplace item for the health status lambda - var name, version string - for _, item := range appConfig.MarketplaceItems { - if item.Name == "unity-cs-monitoring-lambda" { - name = item.Name - version = item.Version - break - } - } - - // Print the name and version - log.Infof("Found marketplace item - Name: %s, Version: %s", name, version) - - // If the item wasn't found, log an error and return - if name == "" || version == "" { - log.Error("unity-cs-monitoring-lambda not found in MarketplaceItems") - return fmt.Errorf("unity-cs-monitoring-lambda not found in MarketplaceItems") - } - - // applications := marketplace.Install_Applications{ - // Name: name, - // Version: version, - // Variables: nil, - // Displayname: fmt.Sprintf("%s-%s", appConfig.InstallPrefix, name), - // } - // install := marketplace.Install{ - // Applications: &applications, - // DeploymentName: "Unity Health Status Lambda", - // } - - installParams := types.ApplicationInstallParams{ - Name: name, - Version: version, - Variables: nil, - DisplayName: "Unity Health Status Lambda", - DeploymentName: fmt.Sprintf("default-%s", name), - } - - err := TriggerInstall(store, &installParams, appConfig, true) - if err != nil { - log.WithError(err).Error("Issue installing Unity Health Status Lambda") - return err - } - return nil -} - -func installUnityUi(store database.Datastore, appConfig *config.AppConfig) error { - - // Find the marketplace item for the unity-portal - var name, version string - for _, item := range appConfig.MarketplaceItems { - if item.Name == "unity-portal" { - name = item.Name - version = item.Version - break - } - } - - // Print the name and version - log.Infof("Found marketplace item - Name: %s, Version: %s", name, version) - - // If the item wasn't found, log an error and return - if name == "" || version == "" { - log.Error("unity-portal not found in MarketplaceItems") - return fmt.Errorf("unity-portal not found in MarketplaceItems") - } - - installParams := types.ApplicationInstallParams{ - Name: name, - Version: version, - Variables: nil, - DisplayName: "Unity Navbar UI", - DeploymentName: fmt.Sprintf("default-%s", name), - } - - err := TriggerInstall(store, &installParams, appConfig, true) - if err != nil { - log.WithError(err).Error("Issue installing Unity Navbar UI") - return err - } - return nil -} diff --git a/backend/internal/processes/installer.go b/backend/internal/processes/installer.go index b7d17c0a..1e3427aa 100644 --- a/backend/internal/processes/installer.go +++ b/backend/internal/processes/installer.go @@ -267,6 +267,137 @@ func TriggerInstall(store database.Datastore, applicationInstallParams *types.Ap return InstallMarketplaceApplication(conf, location, applicationInstallParams, &metadata, store, sync) } +// BatchTriggerInstall handles the installation of multiple applications in a single Terraform operation +// to avoid state locking issues when installing multiple modules in parallel +func BatchTriggerInstall(store database.Datastore, params []*types.ApplicationInstallParams, conf *config.AppConfig) error { + if len(params) == 0 { + return nil + } + + applications := make([]*types.InstalledMarketplaceApplication, 0, len(params)) + metadatas := make([]*marketplace.MarketplaceMetadata, 0, len(params)) + locations := make([]string, 0, len(params)) + + // First pass: validate and prepare all applications + for _, param := range params { + // Check if application is already installed + existingApplication, err := store.GetInstalledMarketplaceApplication(param.Name, param.DeploymentName) + if err != nil { + log.WithError(err).Error("Error finding applications") + return errors.New("Unable to search application list") + } + + if existingApplication != nil && existingApplication.Status != "UNINSTALLED" { + errMsg := fmt.Sprintf("Application with name %s already exists. Status: %s", param.Name, existingApplication.Status) + return errors.New(errMsg) + } + + // Fetch metadata and package + metadata, err := FetchMarketplaceMetadata(param.Name, param.Version, conf) + if err != nil { + log.Errorf("Unable to fetch metadata for application: %s, %v", param.Name, err) + return errors.New("Unable to fetch package") + } + + location, err := FetchPackage(&metadata, conf) + if err != nil { + log.Errorf("Unable to fetch package for application: %s, %v", param.Name, err) + return errors.New("Unable to fetch package") + } + + // Generate unique module name + rand.Seed(time.Now().UnixNano()) + chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + randomChars := make([]byte, 5) + for i, v := range rand.Perm(52)[:5] { + randomChars[i] = chars[v] + } + terraformModuleName := fmt.Sprintf("%s-%s", param.DeploymentName, string(randomChars)) + + // Create application record + application := &types.InstalledMarketplaceApplication{ + Name: param.Name, + Version: param.Version, + DeploymentName: param.DeploymentName, + PackageName: metadata.Name, + Source: metadata.Package, + Status: "STAGED", + TerraformModuleName: terraformModuleName, + Variables: param.Variables, + } + + // Store application record + store.StoreInstalledMarketplaceApplication(application) + + // Update status to INSTALLING + application.Status = "INSTALLING" + store.UpdateInstalledMarketplaceApplication(application) + + // Add to our tracking lists + applications = append(applications, application) + metadatas = append(metadatas, &metadata) + locations = append(locations, location) + } + + // Second pass: prepare all Terraform configurations + for i, application := range applications { + // Run pre-install script if needed + err := runPreInstall(conf, metadatas[i], application) + if err != nil { + log.WithError(err).Errorf("Error running pre-install for %s", application.Name) + application.Status = "FAILED" + store.UpdateInstalledMarketplaceApplication(application) + return err + } + + // Add application to Terraform stack + err = terraform.AddApplicationToStack(conf, locations[i], metadatas[i], application, store) + if err != nil { + log.WithError(err).Errorf("Error adding application to stack: %s", application.Name) + application.Status = "FAILED" + store.UpdateInstalledMarketplaceApplication(application) + return err + } + } + + // Third pass: run a single Terraform apply for all modules + executor := &terraform.RealTerraformExecutor{} + logDir := filepath.Join(conf.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) + } + + logfile := filepath.Join(logDir, "batch_install_log") + err := terraform.RunTerraformLogOutToFile(conf, logfile, executor, "") + if err != nil { + // Mark all applications as failed + for _, application := range applications { + application.Status = "FAILED" + store.UpdateInstalledMarketplaceApplication(application) + } + return err + } + + // Fourth pass: run post-install scripts and update status + for i, application := range applications { + application.Status = "INSTALLED" + store.UpdateInstalledMarketplaceApplication(application) + + err := runPostInstallNew(conf, metadatas[i], application) + if err != nil { + log.WithError(err).Errorf("Error running post-install for %s", application.Name) + application.Status = "POSTINSTALL FAILED" + store.UpdateInstalledMarketplaceApplication(application) + return err + } + + application.Status = "COMPLETE" + store.UpdateInstalledMarketplaceApplication(application) + } + + return nil +} + func TriggerUninstall(wsManager *websocket.WebSocketManager, userid string, store database.Datastore, received *marketplace.Uninstall, conf *config.AppConfig) error { if received.All == true { return UninstallAll(conf, wsManager, userid, received)