func()

in internal/engine/execution.go [72:324]


func (e *Engine) ExecuteAndRenderSteps(steps []common.Step, env map[string]string) error {
	var resourceGroupName string = ""
	azureStatus := environments.NewAzureDeploymentStatus()

	err := az.SetSubscription(e.Configuration.Subscription)
	if err != nil {
		logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err)
		azureStatus.SetError(err)
		environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)
		return err
	}

	stepsToExecute := filterDeletionCommands(steps, e.Configuration.DoNotDelete)

	for stepNumber, step := range stepsToExecute {

		azureCodeBlocks := []environments.AzureCodeBlock{}
		for _, block := range step.CodeBlocks {
			azureCodeBlocks = append(azureCodeBlocks, environments.AzureCodeBlock{
				Command:     block.Content,
				Description: block.Description,
			})
		}

		azureStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name), azureCodeBlocks)
	}

	environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)

	for stepNumber, step := range stepsToExecute {
		stepTitle := fmt.Sprintf("%d. %s\n", stepNumber+1, step.Name)
		fmt.Println(ui.StepTitleStyle.Render(stepTitle))
		azureStatus.CurrentStep = stepNumber + 1

		for _, block := range step.CodeBlocks {
			var finalCommandOutput string
			if e.Configuration.RenderValues {
				// Render the codeblock.
				renderedCommand, err := renderCommand(block.Content)
				if err != nil {
					logging.GlobalLogger.Errorf("Failed to render command: %s", err.Error())
					azureStatus.SetError(err)
					environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)
					return err
				}
				finalCommandOutput = ui.IndentMultiLineCommand(renderedCommand.StdOut, 4)
			} else {
				finalCommandOutput = ui.IndentMultiLineCommand(block.Content, 4)
			}

			fmt.Print("    " + finalCommandOutput)

			// execute the command as a goroutine to allow for the spinner to be
			// rendered while the command is executing.
			done := make(chan error)
			var commandOutput shells.CommandOutput

			// If the command is an SSH command, we need to forward the input and
			// output
			interactiveCommand := false
			if patterns.SshCommand.MatchString(block.Content) {
				interactiveCommand = true
			}

			logging.GlobalLogger.WithField("isInteractive", interactiveCommand).
				Infof("Executing command: %s", block.Content)

			var commandErr error
			var frame int = 0

			// If forwarding input/output, don't render the spinner.
			if !interactiveCommand {
				// Grab the number of lines it contains & set the cursor to the
				// beginning of the block.

				lines := strings.Count(finalCommandOutput, "\n")
				terminal.MoveCursorPositionUp(lines)

				// Render the spinner and hide the cursor.
				fmt.Print(ui.SpinnerStyle.Render("  "+string(spinnerFrames[0])) + " ")
				terminal.HideCursor()

				go func(block parsers.CodeBlock) {
					output, err := shells.ExecuteBashCommand(
						block.Content,
						shells.BashCommandConfiguration{
							EnvironmentVariables: lib.CopyMap(env),
							InheritEnvironment:   true,
							InteractiveCommand:   false,
							WriteToHistory:       true,
						},
					)
					logging.GlobalLogger.Infof("Command output to stdout:\n %s", output.StdOut)
					logging.GlobalLogger.Infof("Command output to stderr:\n %s", output.StdErr)
					commandOutput = output
					done <- err
				}(block)
			renderingLoop:
				// While the command is executing, render the spinner.
				for {
					select {
					case commandErr = <-done:
						// Show the cursor, check the result of the command, and display the
						// final status.
						terminal.ShowCursor()

						if commandErr == nil {

							actualOutput := commandOutput.StdOut
							expectedOutput := block.ExpectedOutput.Content
							expectedSimilarity := block.ExpectedOutput.ExpectedSimilarity
							expectedRegex := block.ExpectedOutput.ExpectedRegex
							expectedOutputLanguage := block.ExpectedOutput.Language

							_, outputComparisonError := common.CompareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedRegex, expectedOutputLanguage)

							if outputComparisonError != nil {
								logging.GlobalLogger.Errorf("Error comparing command outputs: %s", outputComparisonError.Error())
								fmt.Printf("\r  %s \n", ui.ErrorStyle.Render("✗"))
								terminal.MoveCursorPositionDown(lines)
								fmt.Printf("  %s\n", ui.ErrorMessageStyle.Render(outputComparisonError.Error()))
								fmt.Printf("	%s\n", lib.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut))

								azureStatus.SetError(outputComparisonError)
								environments.AttachResourceURIsToAzureStatus(
									&azureStatus,
									resourceGroupName,
									e.Configuration.Environment,
								)
								environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)

								return outputComparisonError
							}

							fmt.Printf("\r  %s \n", ui.CheckStyle.Render("✔"))
							terminal.MoveCursorPositionDown(lines)

							fmt.Printf("%s\n", ui.RemoveHorizontalAlign(ui.VerboseStyle.Render(commandOutput.StdOut)))

							// Extract the resource group name from the command output if
							// it's not already set.
							if resourceGroupName == "" && patterns.AzCommand.MatchString(block.Content) {
								logging.GlobalLogger.Info("Attempting to extract resource group name from command output")
								tmpResourceGroup := az.FindResourceGroupName(commandOutput.StdOut)
								if tmpResourceGroup != "" {
									logging.GlobalLogger.WithField("resourceGroup", tmpResourceGroup).Info("Found resource group")
									resourceGroupName = tmpResourceGroup
									azureStatus.AddResourceURI(az.BuildResourceGroupId(e.Configuration.Subscription, resourceGroupName))
								}
							}

							if stepNumber != len(stepsToExecute)-1 {
								environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)
							}

						} else {
							terminal.ShowCursor()
							fmt.Printf("\r  %s \n", ui.ErrorStyle.Render("✗"))
							terminal.MoveCursorPositionDown(lines)
							fmt.Printf("  %s\n", ui.ErrorMessageStyle.Render(commandErr.Error()))

							logging.GlobalLogger.Errorf("Error executing command: %s", commandErr.Error())

							azureStatus.SetError(commandErr)
							environments.AttachResourceURIsToAzureStatus(
								&azureStatus,
								resourceGroupName,
								e.Configuration.Environment,
							)
							environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)

							return commandErr
						}

						break renderingLoop
					default:
						frame = (frame + 1) % len(spinnerFrames)
						fmt.Printf("\r  %s", ui.SpinnerStyle.Render(string(spinnerFrames[frame])))
						time.Sleep(spinnerRefresh)
					}
				}
			} else {
				lines := strings.Count(block.Content, "\n")

				// If we're on the last step and the command is an SSH command, we need
				// to report the status before executing the command. This is needed for
				// one click deployments and does not affect the normal execution flow.
				if stepNumber == len(stepsToExecute)-1 && patterns.SshCommand.MatchString(block.Content) {
					azureStatus.Status = "Succeeded"
					environments.AttachResourceURIsToAzureStatus(&azureStatus, resourceGroupName, e.Configuration.Environment)
					environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)
				}

				output, commandExecutionError := shells.ExecuteBashCommand(
					block.Content,
					shells.BashCommandConfiguration{
						EnvironmentVariables: lib.CopyMap(env),
						InheritEnvironment:   true,
						InteractiveCommand:   true,
						WriteToHistory:       false,
					},
				)

				terminal.ShowCursor()

				if commandExecutionError == nil {
					fmt.Printf("\r  %s \n", ui.CheckStyle.Render("✔"))
					terminal.MoveCursorPositionDown(lines)

					fmt.Printf("  %s\n", ui.VerboseStyle.Render(output.StdOut))

					if stepNumber != len(stepsToExecute)-1 {
						environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)
					}
				} else {
					fmt.Printf("\r  %s \n", ui.ErrorStyle.Render("✗"))
					terminal.MoveCursorPositionDown(lines)
					fmt.Printf("  %s\n", ui.ErrorMessageStyle.Render(commandExecutionError.Error()))

					azureStatus.SetError(commandExecutionError)
					environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)
					return commandExecutionError
				}
			}
		}
	}

	// Report the final status of the deployment (Only applies to one click deployments).
	azureStatus.Status = "Succeeded"
	environments.AttachResourceURIsToAzureStatus(
		&azureStatus,
		resourceGroupName,
		e.Configuration.Environment,
	)
	environments.ReportAzureStatus(azureStatus, e.Configuration.Environment)

	switch e.Configuration.Environment {
	case environments.EnvironmentsAzure, environments.EnvironmentsOCD:
		logging.GlobalLogger.Info(
			"Cleaning environment variable file located at /tmp/env-vars",
		)
		err := lib.CleanEnvironmentStateFile(lib.DefaultEnvironmentStateFile)
		if err != nil {
			logging.GlobalLogger.Errorf("Error cleaning environment variables: %s", err.Error())
			return err
		}

	default:
		lib.DeleteEnvironmentStateFile(lib.DefaultEnvironmentStateFile)
	}

	return nil
}