internal/engine/engine.go (162 lines of code) (raw):
package engine
import (
"errors"
"fmt"
"os"
"strings"
"github.com/Azure/InnovationEngine/internal/az"
"github.com/Azure/InnovationEngine/internal/engine/common"
"github.com/Azure/InnovationEngine/internal/engine/environments"
"github.com/Azure/InnovationEngine/internal/engine/interactive"
"github.com/Azure/InnovationEngine/internal/engine/test"
"github.com/Azure/InnovationEngine/internal/lib"
"github.com/Azure/InnovationEngine/internal/lib/fs"
"github.com/Azure/InnovationEngine/internal/logging"
"github.com/Azure/InnovationEngine/internal/ui"
tea "github.com/charmbracelet/bubbletea"
)
// Configuration for the engine.
type EngineConfiguration struct {
Verbose bool
DoNotDelete bool
CorrelationId string
Subscription string
Environment string
WorkingDirectory string
RenderValues bool
ReportFile string
}
type Engine struct {
Configuration EngineConfiguration
}
// / Create a new engine instance.
func NewEngine(configuration EngineConfiguration) (*Engine, error) {
return &Engine{
Configuration: configuration,
}, nil
}
// Executes a markdown scenario.
func (e *Engine) ExecuteScenario(scenario *common.Scenario) error {
return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error {
az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment)
// Execute the steps
fmt.Println(ui.ScenarioTitleStyle.Render(scenario.Name))
err := e.ExecuteAndRenderSteps(scenario.Steps, lib.CopyMap(scenario.Environment))
return err
})
}
// Executes a scenario in testing moe. This mode goes over each code block
// and executes it without user interaction.
func (e *Engine) TestScenario(scenario *common.Scenario) error {
return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error {
az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment)
stepsToExecute := filterDeletionCommands(scenario.Steps, e.Configuration.DoNotDelete)
initialEnvironmentVariables := lib.GetEnvironmentVariables()
model, err := test.NewTestModeModel(
scenario.Name,
e.Configuration.Subscription,
e.Configuration.Environment,
stepsToExecute,
lib.CopyMap(scenario.Environment),
)
if err != nil {
return err
}
var flags []tea.ProgramOption
if environments.EnvironmentsGithubAction == e.Configuration.Environment {
flags = append(
flags,
tea.WithoutRenderer(),
tea.WithOutput(os.Stdout),
tea.WithInput(os.Stdin),
)
} else {
flags = append(flags, tea.WithAltScreen(), tea.WithMouseCellMotion())
}
common.Program = tea.NewProgram(model, flags...)
var finalModel tea.Model
finalModel, err = common.Program.Run()
// TODO(vmarcella): After testing is complete, we should generate a report.
model, ok := finalModel.(test.TestModeModel)
if !ok {
err = errors.Join(err, fmt.Errorf("failed to cast tea.Model to TestModeModel"))
return err
}
if e.Configuration.ReportFile != "" {
allEnvironmentVariables, envErr := lib.LoadEnvironmentStateFile(
lib.DefaultEnvironmentStateFile,
)
if envErr != nil {
logging.GlobalLogger.Errorf("Failed to load environment state file: %s", err)
err = errors.Join(err, fmt.Errorf("failed to load environment state file: %s", err))
return err
}
variablesDeclaredByScenario := lib.DiffMapsByKey(
allEnvironmentVariables,
initialEnvironmentVariables,
)
report := common.BuildReport(scenario.Name)
err = report.
WithProperties(scenario.Properties).
WithEnvironmentVariables(variablesDeclaredByScenario).
WithError(model.GetFailure()).
WithCodeBlocks(model.GetCodeBlocks()).
WriteToJSONFile(e.Configuration.ReportFile)
if err != nil {
err = errors.Join(err, fmt.Errorf("failed to write report to file: %s", err))
return err
}
model.CommandLines = append(
model.CommandLines,
"Report written to "+e.Configuration.ReportFile,
)
}
fmt.Println(strings.Join(model.CommandLines, "\n"))
err = errors.Join(err, model.GetFailure())
if err != nil {
logging.GlobalLogger.Errorf("Failed to run ie test %s", err)
return err
}
return nil
})
}
// Executes a Scenario in interactive mode. This mode goes over each codeblock
// step by step and allows the user to interact with the codeblock.
func (e *Engine) InteractWithScenario(scenario *common.Scenario) error {
return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error {
az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment)
stepsToExecute := filterDeletionCommands(scenario.Steps, e.Configuration.DoNotDelete)
model, err := interactive.NewInteractiveModeModel(
scenario.Name,
e.Configuration.Subscription,
e.Configuration.Environment,
stepsToExecute,
lib.CopyMap(scenario.Environment),
scenario.GetSourceAsString(),
)
if err != nil {
return err
}
common.Program = tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion())
var finalModel tea.Model
var ok bool
finalModel, err = common.Program.Run()
model, ok = finalModel.(interactive.InteractiveModeModel)
if environments.EnvironmentsAzure == e.Configuration.Environment {
if !ok {
return fmt.Errorf("failed to cast tea.Model to InteractiveModeModel")
}
logging.GlobalLogger.Info("Writing session output to stdout")
fmt.Println(strings.Join(model.CommandLines, "\n"))
}
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)
}
if err != nil {
logging.GlobalLogger.Errorf("Failed to run program %s", err)
return err
}
return nil
})
}