diff --git a/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan.go b/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan.go new file mode 100644 index 0000000000..4036c3511b --- /dev/null +++ b/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan.go @@ -0,0 +1,69 @@ +package instructions_plan + +import ( + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/uuid_generator" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors" + "github.com/kurtosis-tech/stacktrace" + "go.starlark.net/starlark" +) + +// InstructionsPlan is the object to store a sequence of instructions which forms a "plan" for the enclave. +// Right now, the object is fairly simple in the sense of it just stores literally the sequence of instructions, and +// a bit of metadata about each instruction (i.e. whether it has been executed of not, for example) +// The plan is "append-only", i.e. when an instruction is added, it cannot be removed. +// The only read method is GeneratePlan unwraps the plan into an actual list of instructions that can be submitted to +// the executor. +type InstructionsPlan struct { + scheduledInstructionsIndex map[ScheduledInstructionUuid]*ScheduledInstruction + + instructionsSequence []ScheduledInstructionUuid +} + +func NewInstructionsPlan() *InstructionsPlan { + return &InstructionsPlan{ + scheduledInstructionsIndex: map[ScheduledInstructionUuid]*ScheduledInstruction{}, + instructionsSequence: []ScheduledInstructionUuid{}, + } +} + +func (plan *InstructionsPlan) AddInstruction(instruction kurtosis_instruction.KurtosisInstruction, returnedValue starlark.Value) error { + generatedUuid, err := uuid_generator.GenerateUUIDString() + if err != nil { + return stacktrace.Propagate(err, "Unable to generate a random UUID for instruction '%s' to add it to the plan", instruction.String()) + } + + scheduledInstructionUuid := ScheduledInstructionUuid(generatedUuid) + scheduledInstruction := NewScheduledInstruction(scheduledInstructionUuid, instruction, returnedValue) + + plan.scheduledInstructionsIndex[scheduledInstructionUuid] = scheduledInstruction + plan.instructionsSequence = append(plan.instructionsSequence, scheduledInstructionUuid) + return nil +} + +func (plan *InstructionsPlan) AddScheduledInstruction(scheduledInstruction *ScheduledInstruction) *ScheduledInstruction { + newScheduledInstructionUuid := scheduledInstruction.uuid + newScheduledInstruction := NewScheduledInstruction(newScheduledInstructionUuid, scheduledInstruction.kurtosisInstruction, scheduledInstruction.returnedValue) + newScheduledInstruction.Executed(scheduledInstruction.IsExecuted()) + + plan.scheduledInstructionsIndex[newScheduledInstructionUuid] = newScheduledInstruction + plan.instructionsSequence = append(plan.instructionsSequence, newScheduledInstructionUuid) + return newScheduledInstruction +} + +// GeneratePlan unwraps the plan into a list of instructions +func (plan *InstructionsPlan) GeneratePlan() ([]*ScheduledInstruction, *startosis_errors.InterpretationError) { + var generatedPlan []*ScheduledInstruction + for _, instructionUuid := range plan.instructionsSequence { + instruction, found := plan.scheduledInstructionsIndex[instructionUuid] + if !found { + return nil, startosis_errors.NewInterpretationError("Unexpected error generating the Kurtosis Instructions plan. Instruction with UUID '%s' was scheduled but could not be found in Kurtosis instruction index", instructionUuid) + } + generatedPlan = append(generatedPlan, instruction) + } + return generatedPlan, nil +} + +func (plan *InstructionsPlan) Size() int { + return len(plan.instructionsSequence) +} diff --git a/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan_test.go b/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan_test.go new file mode 100644 index 0000000000..aa867a895a --- /dev/null +++ b/core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan_test.go @@ -0,0 +1,86 @@ +package instructions_plan + +import ( + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/mock_instruction" + "github.com/stretchr/testify/require" + "go.starlark.net/starlark" + "testing" +) + +func TestAddInstruction(t *testing.T) { + plan := NewInstructionsPlan() + + instruction1 := mock_instruction.NewMockKurtosisInstruction(t) + instruction1ReturnedValue := starlark.None + require.NoError(t, plan.AddInstruction(instruction1, instruction1ReturnedValue)) + + require.Len(t, plan.instructionsSequence, 1) + scheduledInstructionUuid := plan.instructionsSequence[0] + require.Contains(t, plan.scheduledInstructionsIndex, scheduledInstructionUuid) + scheduledInstruction, found := plan.scheduledInstructionsIndex[scheduledInstructionUuid] + require.True(t, found) + require.Equal(t, scheduledInstruction.GetInstruction(), instruction1) + require.False(t, scheduledInstruction.IsExecuted()) + require.Equal(t, scheduledInstruction.GetReturnedValue(), instruction1ReturnedValue) +} + +func TestAddScheduledInstruction(t *testing.T) { + plan := NewInstructionsPlan() + + instruction1Uuid := ScheduledInstructionUuid("instruction1") + instruction1 := mock_instruction.NewMockKurtosisInstruction(t) + instruction1ReturnedValue := starlark.MakeInt(1) + scheduleInstruction := NewScheduledInstruction(instruction1Uuid, instruction1, instruction1ReturnedValue) + scheduleInstruction.executed = true + + plan.AddScheduledInstruction(scheduleInstruction) + + require.Len(t, plan.instructionsSequence, 1) + scheduledInstructionUuid := plan.instructionsSequence[0] + require.Equal(t, instruction1Uuid, scheduledInstructionUuid) + require.Contains(t, plan.scheduledInstructionsIndex, scheduledInstructionUuid) + addedScheduledInstruction, found := plan.scheduledInstructionsIndex[scheduledInstructionUuid] + require.True(t, found) + require.NotSame(t, scheduleInstruction, addedScheduledInstruction) // validate the instruction was cloned + require.Equal(t, addedScheduledInstruction.GetInstruction(), instruction1) + require.True(t, addedScheduledInstruction.IsExecuted()) + require.Equal(t, addedScheduledInstruction.GetReturnedValue(), instruction1ReturnedValue) +} + +func TestGeneratePlan(t *testing.T) { + plan := NewInstructionsPlan() + + // add instruction1 which is marked as executed + instruction1Uuid := ScheduledInstructionUuid("instruction1") + instruction1 := mock_instruction.NewMockKurtosisInstruction(t) + instruction1ReturnedValue := starlark.None + scheduleInstruction1 := NewScheduledInstruction(instruction1Uuid, instruction1, instruction1ReturnedValue) + scheduleInstruction1.Executed(true) + + plan.scheduledInstructionsIndex[instruction1Uuid] = scheduleInstruction1 + plan.instructionsSequence = append(plan.instructionsSequence, instruction1Uuid) + + // add instruction2 which by default is not executed + instruction2Uuid := ScheduledInstructionUuid("instruction2") + instruction2 := mock_instruction.NewMockKurtosisInstruction(t) + instruction2ReturnedValue := starlark.MakeInt(1) + scheduleInstruction2 := NewScheduledInstruction(instruction2Uuid, instruction2, instruction2ReturnedValue) + + plan.scheduledInstructionsIndex[instruction2Uuid] = scheduleInstruction2 + plan.instructionsSequence = append(plan.instructionsSequence, instruction2Uuid) + + // generate plan and validate it + instructionsSequence, err := plan.GeneratePlan() + require.Nil(t, err) + + require.Len(t, instructionsSequence, 2) + scheduledInstruction1 := instructionsSequence[0] + require.Equal(t, scheduledInstruction1.GetInstruction(), instruction1) + require.True(t, scheduledInstruction1.IsExecuted()) + require.Equal(t, scheduledInstruction1.GetReturnedValue(), instruction1ReturnedValue) + + scheduledInstruction2 := instructionsSequence[1] + require.Equal(t, scheduledInstruction2.GetInstruction(), instruction2) + require.False(t, scheduledInstruction2.IsExecuted()) + require.Equal(t, scheduledInstruction2.GetReturnedValue(), instruction2ReturnedValue) +} diff --git a/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction.go b/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction.go new file mode 100644 index 0000000000..149da462d5 --- /dev/null +++ b/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction.go @@ -0,0 +1,49 @@ +package instructions_plan + +import ( + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "go.starlark.net/starlark" +) + +type ScheduledInstructionUuid string + +// ScheduledInstruction is a wrapper around a KurtosisInstruction to specify that the instruction is part of an +// InstructionPlan. The instruction plan can either be the current enclave plan (which has been executed) or a newly +// generated plan from the latest interpretation. +// In any case, the ScheduledInstructionUuid stores the result object from the interpretation of the instruction, +// as well as a flag to track whether this instruction was already executed or not. +type ScheduledInstruction struct { + uuid ScheduledInstructionUuid + + kurtosisInstruction kurtosis_instruction.KurtosisInstruction + + returnedValue starlark.Value + + executed bool +} + +func NewScheduledInstruction(uuid ScheduledInstructionUuid, kurtosisInstruction kurtosis_instruction.KurtosisInstruction, returnedValue starlark.Value) *ScheduledInstruction { + return &ScheduledInstruction{ + uuid: uuid, + kurtosisInstruction: kurtosisInstruction, + returnedValue: returnedValue, + executed: false, + } +} + +func (instruction *ScheduledInstruction) GetInstruction() kurtosis_instruction.KurtosisInstruction { + return instruction.kurtosisInstruction +} + +func (instruction *ScheduledInstruction) GetReturnedValue() starlark.Value { + return instruction.returnedValue +} + +func (instruction *ScheduledInstruction) Executed(isExecuted bool) *ScheduledInstruction { + instruction.executed = isExecuted + return instruction +} + +func (instruction *ScheduledInstruction) IsExecuted() bool { + return instruction.executed +} diff --git a/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction_test.go b/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction_test.go new file mode 100644 index 0000000000..597bba176c --- /dev/null +++ b/core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction_test.go @@ -0,0 +1,19 @@ +package instructions_plan + +import ( + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/mock_instruction" + "github.com/stretchr/testify/require" + "go.starlark.net/starlark" + "testing" +) + +func TestExecutedDefaultAndUpdate(t *testing.T) { + instruction1Uuid := ScheduledInstructionUuid("instruction1") + instruction1 := mock_instruction.NewMockKurtosisInstruction(t) + instruction1ReturnedValue := starlark.MakeInt(1) + scheduleInstruction := NewScheduledInstruction(instruction1Uuid, instruction1, instruction1ReturnedValue) + + require.False(t, scheduleInstruction.executed) + scheduleInstruction.Executed(true) + require.True(t, scheduleInstruction.executed) +} diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module/plan_module.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module/plan_module.go index c7b04cf431..637287cbdc 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module/plan_module.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module/plan_module.go @@ -1,7 +1,7 @@ package plan_module import ( - "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" @@ -12,12 +12,12 @@ const ( ) func PlanModule( - instructionsQueue *[]kurtosis_instruction.KurtosisInstruction, + instructionsPlan *instructions_plan.InstructionsPlan, kurtosisPlanInstructions []*kurtosis_plan_instruction.KurtosisPlanInstruction, ) *starlarkstruct.Module { moduleBuiltins := starlark.StringDict{} for _, planInstruction := range kurtosisPlanInstructions { - wrappedPlanInstruction := kurtosis_plan_instruction.NewKurtosisPlanInstructionWrapper(planInstruction, instructionsQueue) + wrappedPlanInstruction := kurtosis_plan_instruction.NewKurtosisPlanInstructionWrapper(planInstruction, instructionsPlan) moduleBuiltins[planInstruction.GetName()] = starlark.NewBuiltin(planInstruction.GetName(), wrappedPlanInstruction.CreateBuiltin()) } diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction/kurtosis_plan_instruction.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction/kurtosis_plan_instruction.go index d50c54c206..55942dd391 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction/kurtosis_plan_instruction.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction/kurtosis_plan_instruction.go @@ -1,8 +1,9 @@ package kurtosis_plan_instruction import ( - "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors" "go.starlark.net/starlark" ) @@ -20,13 +21,13 @@ type KurtosisPlanInstructionWrapper struct { *KurtosisPlanInstruction // TODO: This can be changed to KurtosisPlanInstructionInternal when we get rid of KurtosisInstruction - instructionQueue *[]kurtosis_instruction.KurtosisInstruction + instructionsPlan *instructions_plan.InstructionsPlan } -func NewKurtosisPlanInstructionWrapper(instruction *KurtosisPlanInstruction, instructionQueue *[]kurtosis_instruction.KurtosisInstruction) *KurtosisPlanInstructionWrapper { +func NewKurtosisPlanInstructionWrapper(instruction *KurtosisPlanInstruction, instructionsPlan *instructions_plan.InstructionsPlan) *KurtosisPlanInstructionWrapper { return &KurtosisPlanInstructionWrapper{ KurtosisPlanInstruction: instruction, - instructionQueue: instructionQueue, + instructionsPlan: instructionsPlan, } } @@ -44,8 +45,12 @@ func (builtin *KurtosisPlanInstructionWrapper) CreateBuiltin() func(thread *star } // before returning, automatically add instruction to queue - *builtin.instructionQueue = append(*builtin.instructionQueue, instructionWrapper) - + if err := builtin.instructionsPlan.AddInstruction(instructionWrapper, returnedFutureValue); err != nil { + return nil, startosis_errors.WrapWithInterpretationError(err, + "Unable to add Kurtosis instruction '%s' at position '%s' to the current plan being assembled. This is a Kurtosis internal bug", + instructionWrapper.String(), + instructionWrapper.GetPositionInOriginalScript().String()) + } return returnedFutureValue, nil } } diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/starlark_framework_engine_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/starlark_framework_engine_test.go index 711d63935e..2f11b25d7c 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/starlark_framework_engine_test.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/starlark_framework_engine_test.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins" - "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/builtin_argument" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_constants" @@ -73,13 +73,13 @@ func TestAllRegisteredBuiltins(t *testing.T) { func testKurtosisPlanInstruction(t *testing.T, builtin KurtosisPlanInstructionBaseTest) { testId := builtin.GetId() - var instructionQueue []kurtosis_instruction.KurtosisInstruction + instructionsPlan := instructions_plan.NewInstructionsPlan() thread := newStarlarkThread("framework-testing-engine") predeclared := getBasePredeclaredDict(t) // Add the KurtosisPlanInstruction that is being tested instructionFromBuiltin := builtin.GetInstruction() - instructionWrapper := kurtosis_plan_instruction.NewKurtosisPlanInstructionWrapper(instructionFromBuiltin, &instructionQueue) + instructionWrapper := kurtosis_plan_instruction.NewKurtosisPlanInstructionWrapper(instructionFromBuiltin, instructionsPlan) predeclared[instructionWrapper.GetName()] = starlark.NewBuiltin(instructionWrapper.GetName(), instructionWrapper.CreateBuiltin()) starlarkCode := builtin.GetStarlarkCode() @@ -87,8 +87,10 @@ func testKurtosisPlanInstruction(t *testing.T, builtin KurtosisPlanInstructionBa require.Nil(t, err, "Error interpreting Starlark code for instruction '%s'", testId) interpretationResult := extractResultValue(t, globals) - require.Len(t, instructionQueue, 1) - instructionToExecute := instructionQueue[0] + require.Equal(t, 1, instructionsPlan.Size()) + instructionsSequence, err := instructionsPlan.GeneratePlan() + require.Nil(t, err) + instructionToExecute := instructionsSequence[0].GetInstruction() // execute the instruction and run custom builtin assertions executionResult, err := instructionToExecute.Execute(context.WithValue(context.Background(), "PARALLELISM", 1)) diff --git a/core/server/api_container/server/startosis_engine/startosis_executor.go b/core/server/api_container/server/startosis_engine/startosis_executor.go index 5a37267b72..2e0355e092 100644 --- a/core/server/api_container/server/startosis_engine/startosis_executor.go +++ b/core/server/api_container/server/startosis_engine/startosis_executor.go @@ -4,10 +4,11 @@ import ( "context" "github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings" "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/binding_constructors" - "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/shared_helpers/magic_string_helper" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/runtime_value_store" "github.com/kurtosis-tech/stacktrace" + "github.com/sirupsen/logrus" "sync" ) @@ -16,8 +17,13 @@ const ( ParallelismParam = "PARALLELISM" ) +var ( + skippedInstructionOutput = "SKIPPED" +) + type StartosisExecutor struct { mutex *sync.Mutex + enclavePlan *instructions_plan.InstructionsPlan runtimeValueStore *runtime_value_store.RuntimeValueStore } @@ -28,6 +34,7 @@ type ExecutionError struct { func NewStartosisExecutor(runtimeValueStore *runtime_value_store.RuntimeValueStore) *StartosisExecutor { return &StartosisExecutor{ mutex: &sync.Mutex{}, + enclavePlan: instructions_plan.NewInstructionsPlan(), runtimeValueStore: runtimeValueStore, } } @@ -39,7 +46,7 @@ func NewStartosisExecutor(runtimeValueStore *runtime_value_store.RuntimeValueSto // - A regular KurtosisInstruction that was successfully executed // - A KurtosisExecutionError if the execution failed // - A ProgressInfo to update the current "state" of the execution -func (executor *StartosisExecutor) Execute(ctx context.Context, dryRun bool, parallelism int, instructions []kurtosis_instruction.KurtosisInstruction, serializedScriptOutput string) <-chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine { +func (executor *StartosisExecutor) Execute(ctx context.Context, dryRun bool, parallelism int, instructionsSequence []*instructions_plan.ScheduledInstruction, serializedScriptOutput string) <-chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine { executor.mutex.Lock() starlarkRunResponseLineStream := make(chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine) ctxWithParallelism := context.WithValue(ctx, ParallelismParam, parallelism) @@ -49,43 +56,59 @@ func (executor *StartosisExecutor) Execute(ctx context.Context, dryRun bool, par close(starlarkRunResponseLineStream) }() - totalNumberOfInstructions := uint32(len(instructions)) - for index, instruction := range instructions { + // TODO: for now the plan is append only, as each Starlark run happens on top of whatever exists in the enclave + logrus.Debugf("Current enclave plan contains %d instuctions. About to process a new plan with %d instructions (dry-run: %v)", + executor.enclavePlan.Size(), len(instructionsSequence), dryRun) + totalNumberOfInstructions := uint32(len(instructionsSequence)) + for index, scheduledInstruction := range instructionsSequence { instructionNumber := uint32(index + 1) progress := binding_constructors.NewStarlarkRunResponseLineFromSinglelineProgressInfo( progressMsg, instructionNumber, totalNumberOfInstructions) starlarkRunResponseLineStream <- progress + instruction := scheduledInstruction.GetInstruction() canonicalInstruction := binding_constructors.NewStarlarkRunResponseLineFromInstruction(instruction.GetCanonicalInstruction()) starlarkRunResponseLineStream <- canonicalInstruction if !dryRun { - instructionOutput, err := instruction.Execute(ctxWithParallelism) + var err error + var instructionOutput *string + if scheduledInstruction.IsExecuted() { + // instruction already executed within this enclave. Do not run it + instructionOutput = &skippedInstructionOutput + } else { + instructionOutput, err = instruction.Execute(ctxWithParallelism) + } if err != nil { - - propagatedError := stacktrace.Propagate(err, "An error occurred executing instruction (number %d) at %v:\n%v", instructionNumber, instruction.GetPositionInOriginalScript().String(), instruction.String()) - serializedError := binding_constructors.NewStarlarkExecutionError(propagatedError.Error()) - starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromExecutionError(serializedError) - starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent() + sendErrorAndFail(starlarkRunResponseLineStream, err, "An error occurred executing instruction (number %d) at %v:\n%v", instructionNumber, instruction.GetPositionInOriginalScript().String(), instruction.String()) return } if instructionOutput != nil { starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromInstructionResult(*instructionOutput) } + // mark the instruction as executed and add it to the current instruction plan + executor.enclavePlan.AddScheduledInstruction(scheduledInstruction).Executed(true) } } if !dryRun { scriptWithValuesReplaced, err := magic_string_helper.ReplaceRuntimeValueInString(serializedScriptOutput, executor.runtimeValueStore) if err != nil { - propagatedErr := stacktrace.Propagate(err, "An error occurred while replacing the runtime values in the output of the script") - serializedError := binding_constructors.NewStarlarkExecutionError(propagatedErr.Error()) - starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromExecutionError(serializedError) - starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent() + sendErrorAndFail(starlarkRunResponseLineStream, err, "An error occurred while replacing the runtime values in the output of the script") return } starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromRunSuccessEvent(scriptWithValuesReplaced) + logrus.Debugf("Current enclave plan has been updated. It now contains %d instructions", executor.enclavePlan.Size()) + } else { + logrus.Debugf("Current enclave plan remained the same as the it was a dry-run. It contains %d instructions", executor.enclavePlan.Size()) } }() return starlarkRunResponseLineStream } + +func sendErrorAndFail(starlarkRunResponseLineStream chan<- *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine, err error, msg string, msgArgs ...interface{}) { + propagatedErr := stacktrace.Propagate(err, msg, msgArgs...) + serializedError := binding_constructors.NewStarlarkExecutionError(propagatedErr.Error()) + starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromExecutionError(serializedError) + starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent() +} diff --git a/core/server/api_container/server/startosis_engine/startosis_executor_test.go b/core/server/api_container/server/startosis_engine/startosis_executor_test.go index ea9847406a..aacd279c34 100644 --- a/core/server/api_container/server/startosis_engine/startosis_executor_test.go +++ b/core/server/api_container/server/startosis_engine/startosis_executor_test.go @@ -5,12 +5,13 @@ import ( "errors" "github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings" "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/binding_constructors" - "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/mock_instruction" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/runtime_value_store" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "go.starlark.net/starlark" "strings" "testing" ) @@ -35,14 +36,21 @@ func TestExecuteKurtosisInstructions_ExecuteForReal_Success(t *testing.T) { executor := NewStartosisExecutor(runtime_value_store.NewRuntimeValueStore()) + instructionsPlan := instructions_plan.NewInstructionsPlan() + instruction0 := createMockInstruction(t, "instruction0", executeSuccessfully) + scheduledInstruction0 := instructions_plan.NewScheduledInstruction("instruction0", instruction0, starlark.None).Executed(true) + instructionsPlan.AddScheduledInstruction(scheduledInstruction0) + instruction1 := createMockInstruction(t, "instruction1", executeSuccessfully) instruction2 := createMockInstruction(t, "instruction2", executeSuccessfully) - instructions := []kurtosis_instruction.KurtosisInstruction{ - instruction1, - instruction2, - } + require.NoError(t, instructionsPlan.AddInstruction(instruction1, starlark.None)) + require.NoError(t, instructionsPlan.AddInstruction(instruction2, starlark.None)) - _, serializedInstruction, err := executeSynchronously(t, executor, executeForReal, instructions) + require.Equal(t, executor.enclavePlan.Size(), 0) // check that the enclave plan is empty prior to execution + + _, serializedInstruction, err := executeSynchronously(t, executor, executeForReal, instructionsPlan) + instruction0.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1) + instruction0.AssertNumberOfCalls(t, "Execute", 0) // not executed as it was already executed instruction1.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1) instruction1.AssertNumberOfCalls(t, "Execute", 1) instruction2.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1) @@ -51,10 +59,12 @@ func TestExecuteKurtosisInstructions_ExecuteForReal_Success(t *testing.T) { require.Nil(t, err) expectedSerializedInstructions := []*kurtosis_core_rpc_api_bindings.StarlarkInstruction{ + binding_constructors.NewStarlarkInstruction(dummyPosition.ToAPIType(), "instruction0", "instruction0()", noInstructionArgsForTesting), binding_constructors.NewStarlarkInstruction(dummyPosition.ToAPIType(), "instruction1", "instruction1()", noInstructionArgsForTesting), binding_constructors.NewStarlarkInstruction(dummyPosition.ToAPIType(), "instruction2", "instruction2()", noInstructionArgsForTesting), } require.Equal(t, expectedSerializedInstructions, serializedInstruction) + require.Equal(t, executor.enclavePlan.Size(), 3) // check that the enclave plan now contains the 3 instructions } func TestExecuteKurtosisInstructions_ExecuteForReal_FailureHalfWay(t *testing.T) { @@ -63,13 +73,12 @@ func TestExecuteKurtosisInstructions_ExecuteForReal_FailureHalfWay(t *testing.T) instruction1 := createMockInstruction(t, "instruction1", executeSuccessfully) instruction2 := createMockInstruction(t, "instruction2", throwOnExecute) instruction3 := createMockInstruction(t, "instruction3", executeSuccessfully) - instructions := []kurtosis_instruction.KurtosisInstruction{ - instruction1, - instruction2, - instruction3, - } + instructionsPlan := instructions_plan.NewInstructionsPlan() + require.NoError(t, instructionsPlan.AddInstruction(instruction1, starlark.None)) + require.NoError(t, instructionsPlan.AddInstruction(instruction2, starlark.None)) + require.NoError(t, instructionsPlan.AddInstruction(instruction3, starlark.None)) - _, serializedInstruction, executionError := executeSynchronously(t, executor, executeForReal, instructions) + _, serializedInstruction, executionError := executeSynchronously(t, executor, executeForReal, instructionsPlan) instruction1.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1) instruction1.AssertNumberOfCalls(t, "Execute", 1) instruction2.AssertNumberOfCalls(t, "String", 1) @@ -99,12 +108,11 @@ func TestExecuteKurtosisInstructions_DoDryRun(t *testing.T) { instruction1 := createMockInstruction(t, "instruction1", executeSuccessfully) instruction2 := createMockInstruction(t, "instruction2", executeSuccessfully) - instructions := []kurtosis_instruction.KurtosisInstruction{ - instruction1, - instruction2, - } + instructionsPlan := instructions_plan.NewInstructionsPlan() + require.NoError(t, instructionsPlan.AddInstruction(instruction1, starlark.None)) + require.NoError(t, instructionsPlan.AddInstruction(instruction2, starlark.None)) - _, serializedInstruction, err := executeSynchronously(t, executor, doDryRun, instructions) + _, serializedInstruction, err := executeSynchronously(t, executor, doDryRun, instructionsPlan) instruction1.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1) instruction2.AssertNumberOfCalls(t, "GetCanonicalInstruction", 1) // both execute never called because dry run = true @@ -139,11 +147,14 @@ func createMockInstruction(t *testing.T, instructionName string, executeSuccessf return instruction } -func executeSynchronously(t *testing.T, executor *StartosisExecutor, dryRun bool, instructions []kurtosis_instruction.KurtosisInstruction) (string, []*kurtosis_core_rpc_api_bindings.StarlarkInstruction, *kurtosis_core_rpc_api_bindings.StarlarkExecutionError) { +func executeSynchronously(t *testing.T, executor *StartosisExecutor, dryRun bool, instructionsPlan *instructions_plan.InstructionsPlan) (string, []*kurtosis_core_rpc_api_bindings.StarlarkInstruction, *kurtosis_core_rpc_api_bindings.StarlarkExecutionError) { scriptOutput := strings.Builder{} var serializedInstructions []*kurtosis_core_rpc_api_bindings.StarlarkInstruction - executionResponseLines := executor.Execute(context.Background(), dryRun, noParallelism, instructions, noScriptOutputObject) + scheduledInstructions, err := instructionsPlan.GeneratePlan() + require.Nil(t, err) + + executionResponseLines := executor.Execute(context.Background(), dryRun, noParallelism, scheduledInstructions, noScriptOutputObject) for executionResponseLine := range executionResponseLines { if executionResponseLine.GetError() != nil { return scriptOutput.String(), serializedInstructions, executionResponseLine.GetError().GetExecutionError() diff --git a/core/server/api_container/server/startosis_engine/startosis_interpreter.go b/core/server/api_container/server/startosis_engine/startosis_interpreter.go index 48e0ffe547..74bcad6a89 100644 --- a/core/server/api_container/server/startosis_engine/startosis_interpreter.go +++ b/core/server/api_container/server/startosis_engine/startosis_interpreter.go @@ -7,7 +7,7 @@ import ( "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/service_network" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins/print_builtin" - "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/plan_module" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/package_io" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/runtime_value_store" @@ -84,17 +84,17 @@ func (interpreter *StartosisInterpreter) Interpret( mainFunctionName string, serializedStarlark string, serializedJsonParams string, -) (string, []kurtosis_instruction.KurtosisInstruction, *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError) { +) (string, *instructions_plan.InstructionsPlan, *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError) { interpreter.mutex.Lock() defer interpreter.mutex.Unlock() - var instructionsQueue []kurtosis_instruction.KurtosisInstruction + newInstructionsPlan := instructions_plan.NewInstructionsPlan() logrus.Debugf("Interpreting package '%v' with contents '%v' and params '%v'", packageId, serializedStarlark, serializedJsonParams) - globalVariables, interpretationErr := interpreter.interpretInternal(packageId, serializedStarlark, &instructionsQueue) + globalVariables, interpretationErr := interpreter.interpretInternal(packageId, serializedStarlark, newInstructionsPlan) if interpretationErr != nil { return startosis_constants.NoOutputObject, nil, interpretationErr.ToAPIType() } - logrus.Debugf("Successfully interpreted Starlark code into instruction queue: \n%s", instructionsQueue) + logrus.Debugf("Successfully interpreted Starlark code into %d instructions", newInstructionsPlan.Size()) var isUsingDefaultMainFunction bool // if the user sends "" or "run" we isUsingDefaultMainFunction to true @@ -104,13 +104,13 @@ func (interpreter *StartosisInterpreter) Interpret( } if !globalVariables.Has(mainFunctionName) { - return missingMainFunctionReturnValue(packageId, mainFunctionName) + return "", nil, missingMainFunctionError(packageId, mainFunctionName) } mainFunction, ok := globalVariables[mainFunctionName].(*starlark.Function) - // if there is a element with the `mainFunctionName` but it isn't a function we have to error as well + // if there is an element with the `mainFunctionName` but it isn't a function we have to error as well if !ok { - return missingMainFunctionReturnValue(packageId, mainFunctionName) + return "", nil, missingMainFunctionError(packageId, mainFunctionName) } runFunctionExecutionThread := newStarlarkThread(starlarkGoThreadName) @@ -129,7 +129,7 @@ func (interpreter *StartosisInterpreter) Interpret( firstParamName, _ := mainFunction.Param(planParamIndex) if firstParamName == planParamName { kurtosisPlanInstructions := KurtosisPlanInstructions(interpreter.serviceNetwork, interpreter.recipeExecutor, interpreter.moduleContentProvider) - planModule := plan_module.PlanModule(&instructionsQueue, kurtosisPlanInstructions) + planModule := plan_module.PlanModule(newInstructionsPlan, kurtosisPlanInstructions) argsTuple = append(argsTuple, planModule) } @@ -174,17 +174,17 @@ func (interpreter *StartosisInterpreter) Interpret( if interpretationError != nil { return startosis_constants.NoOutputObject, nil, interpretationError.ToAPIType() } - return serializedOutputObject, instructionsQueue, nil + return serializedOutputObject, newInstructionsPlan, nil } - return startosis_constants.NoOutputObject, instructionsQueue, nil + return startosis_constants.NoOutputObject, newInstructionsPlan, nil } -func (interpreter *StartosisInterpreter) interpretInternal(packageId string, serializedStarlark string, instructionsQueue *[]kurtosis_instruction.KurtosisInstruction) (starlark.StringDict, *startosis_errors.InterpretationError) { +func (interpreter *StartosisInterpreter) interpretInternal(packageId string, serializedStarlark string, instructionPlan *instructions_plan.InstructionsPlan) (starlark.StringDict, *startosis_errors.InterpretationError) { // We spin up a new thread for every call to interpreterInternal such that the stacktrace provided by the Starlark // Go interpreter is relative to each individual thread, and we don't keep accumulating stacktrace entries from the // previous calls inside the same thread thread := newStarlarkThread(packageId) - predeclared, interpretationErr := interpreter.buildBindings(instructionsQueue) + predeclared, interpretationErr := interpreter.buildBindings(instructionPlan) if interpretationErr != nil { return nil, interpretationErr } @@ -197,9 +197,9 @@ func (interpreter *StartosisInterpreter) interpretInternal(packageId string, ser return globalVariables, nil } -func (interpreter *StartosisInterpreter) buildBindings(instructionsQueue *[]kurtosis_instruction.KurtosisInstruction) (*starlark.StringDict, *startosis_errors.InterpretationError) { +func (interpreter *StartosisInterpreter) buildBindings(instructionPlan *instructions_plan.InstructionsPlan) (*starlark.StringDict, *startosis_errors.InterpretationError) { recursiveInterpretForModuleLoading := func(moduleId string, serializedStartosis string) (starlark.StringDict, *startosis_errors.InterpretationError) { - result, err := interpreter.interpretInternal(moduleId, serializedStartosis, instructionsQueue) + result, err := interpreter.interpretInternal(moduleId, serializedStartosis, instructionPlan) if err != nil { return nil, err } @@ -305,9 +305,9 @@ func generateInterpretationError(err error) *startosis_errors.InterpretationErro return startosis_errors.NewInterpretationError("UnknownError: %s\n", err.Error()) } -func missingMainFunctionReturnValue(packageId string, mainFunctionName string) (string, []kurtosis_instruction.KurtosisInstruction, *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError) { +func missingMainFunctionError(packageId string, mainFunctionName string) *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError { if packageId == startosis_constants.PackageIdPlaceholderForStandaloneScript { - return "", nil, startosis_errors.NewInterpretationError( + return startosis_errors.NewInterpretationError( "No '%s' function found in the script; a '%s' entrypoint function with the signature `%s(plan, args)` or `%s()` is required in the Kurtosis script", mainFunctionName, mainFunctionName, @@ -316,7 +316,7 @@ func missingMainFunctionReturnValue(packageId string, mainFunctionName string) ( ).ToAPIType() } - return "", nil, startosis_errors.NewInterpretationError( + return startosis_errors.NewInterpretationError( "No '%s' function found in the main file of package '%s'; a '%s' entrypoint function with the signature `%s(plan, args)` or `%s()` is required in the main file of the Kurtosis package", mainFunctionName, packageId, diff --git a/core/server/api_container/server/startosis_engine/startosis_interpreter_test.go b/core/server/api_container/server/startosis_engine/startosis_interpreter_test.go index 291b9283ee..f64193ce8a 100644 --- a/core/server/api_container/server/startosis_engine/startosis_interpreter_test.go +++ b/core/server/api_container/server/startosis_engine/startosis_interpreter_test.go @@ -8,7 +8,7 @@ import ( "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/service_network" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins/print_builtin" - "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/kurtosis_print" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/remove_service" @@ -49,13 +49,13 @@ def run(plan): plan.print("` + testString + `") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 1) // Only the print statement + require.Equal(t, 1, instructionsPlan.Size()) // Only the print statement expectedOutput := testString + ` ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_RandomMainFunctionAndParamsWithPlan(t *testing.T) { @@ -76,14 +76,14 @@ def deploy_contract(plan,service_name,contract_name,init_message,args): mainFunctionName := "deploy_contract" inputArgs := `{"service_name": "my-service", "contract_name": "my-contract", "init_message": "Init message", "args": {"arg1": "arg1-value", "arg2": "arg2-value"}}` - result, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, mainFunctionName, script, inputArgs) + result, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, mainFunctionName, script, inputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 3) // The three print functions + require.Equal(t, 3, instructionsPlan.Size()) // The three print functions require.NotNil(t, result) expectedResult := "\"arg1-value:arg2-value\"" require.Equal(t, expectedResult, result) expectedOutput := "Service name: service_name\nContract name: contract_name\nInit message: init_message\n" - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_RandomMainFunctionAndParams(t *testing.T) { @@ -103,9 +103,9 @@ def my_func(my_arg1, my_arg2, args): mainFunctionName := "my_func" inputArgs := `{"my_arg1": "foo", "my_arg2": "bar", "args": {"arg1": "arg1-value", "arg2": "arg2-value"}}` - result, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, mainFunctionName, script, inputArgs) + result, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, mainFunctionName, script, inputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 0) // There are no instructions to execute + require.Equal(t, 0, instructionsPlan.Size()) // There are no instructions to execute require.NotNil(t, result) expectedResult := "\"foo--bar--arg1-value:arg2-value\"" require.Equal(t, expectedResult, result) @@ -125,14 +125,14 @@ def run(plan): plan.print(my_dict) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Len(t, instructions, 2) // Only the print statement + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) + require.Equal(t, 2, instructionsPlan.Size()) expectedOutput := `{} {"hello": "world"} ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_ScriptFailingSingleError(t *testing.T) { @@ -147,8 +147,7 @@ def run(plan): unknownInstruction() ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Empty(t, instructions) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) expectedError := startosis_errors.NewInterpretationErrorWithCustomMsg( []startosis_errors.CallFrame{ @@ -157,6 +156,7 @@ unknownInstruction() multipleInterpretationErrorMsg, ).ToAPIType() require.Equal(t, expectedError, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_ScriptFailingMultipleErrors(t *testing.T) { @@ -172,8 +172,7 @@ unknownVariable unknownInstruction2() ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Empty(t, instructions) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) expectedError := startosis_errors.NewInterpretationErrorWithCustomMsg( []startosis_errors.CallFrame{ @@ -184,6 +183,7 @@ unknownInstruction2() multipleInterpretationErrorMsg, ).ToAPIType() require.Equal(t, expectedError, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_ScriptFailingSyntaxError(t *testing.T) { @@ -198,8 +198,7 @@ def run(): load("otherScript.start") # fails b/c load takes in at least 2 args ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Empty(t, instructions) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) expectedError := startosis_errors.NewInterpretationErrorFromStacktrace( []startosis_errors.CallFrame{ @@ -207,6 +206,7 @@ load("otherScript.start") # fails b/c load takes in at least 2 args }, ).ToAPIType() require.Equal(t, expectedError, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_ValidSimpleScriptWithInstruction(t *testing.T) { @@ -234,18 +234,18 @@ def run(plan): plan.print("The grpc transport protocol is " + datastore_service.ports["grpc"].transport_protocol) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, fmt.Sprintf(script, testServiceName), startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, fmt.Sprintf(script, testServiceName), startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 5) + require.Equal(t, 5, instructionsPlan.Size()) - assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 15, 38) + assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 15, 38) expectedOutput := `Starting Startosis script! Adding service example-datastore-server The grpc port is 1323 The grpc transport protocol is TCP ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_ValidSimpleScriptWithApplicationProtocol(t *testing.T) { @@ -273,9 +273,9 @@ def run(plan): plan.print("The transport protocol is " + datastore_service.ports["grpc"].transport_protocol) plan.print("The application protocol is " + datastore_service.ports["grpc"].application_protocol) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, fmt.Sprintf(script, testServiceName), startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, fmt.Sprintf(script, testServiceName), startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 6) + require.Equal(t, 6, instructionsPlan.Size()) expectedOutput := `Starting Startosis script! Adding service example-datastore-server @@ -283,7 +283,7 @@ The port is 1323 The transport protocol is TCP The application protocol is http ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_ValidSimpleScriptWithInstructionMissingContainerName(t *testing.T) { @@ -307,8 +307,7 @@ def run(plan): plan.add_service(name = service_name, config = config) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Empty(t, instructions) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) expectedError := startosis_errors.NewInterpretationErrorWithCauseAndCustomMsg( errors.New("ServiceConfig: missing argument for image"), @@ -319,6 +318,7 @@ def run(plan): "Evaluation error: Cannot construct 'ServiceConfig' from the provided arguments.", ).ToAPIType() require.Equal(t, expectedError, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_ValidSimpleScriptWithInstructionTypoInProtocol(t *testing.T) { @@ -342,8 +342,7 @@ def run(plan): plan.add_service(name = service_name, config = config) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Empty(t, instructions) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) expectedError := startosis_errors.NewInterpretationErrorWithCauseAndCustomMsg( startosis_errors.NewInterpretationError(`The following argument(s) could not be parsed or did not pass validation: {"transport_protocol":"Invalid argument value for 'transport_protocol': 'TCPK'. Valid values are TCP, SCTP, UDP"}`), []startosis_errors.CallFrame{ @@ -353,6 +352,7 @@ def run(plan): "Evaluation error: Cannot construct 'PortSpec' from the provided arguments.", ).ToAPIType() require.Equal(t, expectedError, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_ValidSimpleScriptWithInstructionPortNumberAsString(t *testing.T) { @@ -376,8 +376,7 @@ def run(plan): plan.add_service(name = service_name, config = config) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Empty(t, instructions) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) expectedError := startosis_errors.NewInterpretationErrorWithCauseAndCustomMsg( startosis_errors.NewInterpretationError(`The following argument(s) could not be parsed or did not pass validation: {"number":"Value for 'number' was expected to be an integer between 1 and 65535, but it was 'starlark.String'"}`), []startosis_errors.CallFrame{ @@ -387,6 +386,7 @@ def run(plan): "Evaluation error: Cannot construct 'PortSpec' from the provided arguments.", ).ToAPIType() require.Equal(t, expectedError, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_ValidScriptWithMultipleInstructions(t *testing.T) { @@ -420,13 +420,13 @@ def run(plan): plan.print("Done!") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 8) + require.Equal(t, 8, instructionsPlan.Size()) - assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19) - assertInstructionTypeAndPosition(t, instructions[4], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19) - assertInstructionTypeAndPosition(t, instructions[6], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19) + assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19) + assertInstructionTypeAndPosition(t, instructionsPlan, 4, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19) + assertInstructionTypeAndPosition(t, instructionsPlan, 6, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 19, 19) expectedOutput := `Starting Startosis script! Adding service example-datastore-server-0 @@ -434,7 +434,7 @@ Adding service example-datastore-server-1 Adding service example-datastore-server-2 Done! ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_LoadStatementIsDisallowedInKurtosis(t *testing.T) { @@ -452,7 +452,7 @@ load("` + barModulePath + `", "a") def run(plan): plan.print("Hello " + a) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) expectedError := startosis_errors.NewInterpretationErrorWithCustomMsg( []startosis_errors.CallFrame{ *startosis_errors.NewCallFrame("", startosis_errors.NewScriptPosition(startosis_constants.PackageIdPlaceholderForStandaloneScript, 2, 1)), @@ -461,7 +461,7 @@ def run(plan): ).ToAPIType() require.Equal(t, expectedError, interpretationError) - require.Empty(t, instructions) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_SimpleImport(t *testing.T) { @@ -479,13 +479,13 @@ my_module = import_module("` + barModulePath + `") def run(plan): plan.print("Hello " + my_module.a) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Len(t, instructions, 1) // Only the print statement + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) + require.Equal(t, 1, instructionsPlan.Size()) // Only the print statement expectedOutput := `Hello World! ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_TransitiveLoading(t *testing.T) { @@ -508,13 +508,13 @@ def run(plan): ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Len(t, instructions, 1) // Only the print statement + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) + require.Equal(t, 1, instructionsPlan.Size()) expectedOutput := `Hello World! ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_FailsOnCycle(t *testing.T) { @@ -536,8 +536,7 @@ def run(plan): plan.print(module_doo.b) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Empty(t, instructions) // No kurtosis instruction + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) expectedError := startosis_errors.NewInterpretationErrorWithCustomMsg( []startosis_errors.CallFrame{ *startosis_errors.NewCallFrame("", startosis_errors.NewScriptPosition(moduleBarLoadsModuleDoo, 1, 27)), @@ -547,6 +546,7 @@ def run(plan): "Evaluation error: There's a cycle in the import_module calls", ).ToAPIType() require.Equal(t, expectedError, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_FailsOnNonExistentModule(t *testing.T) { @@ -561,8 +561,7 @@ def run(plan): plan.print(my_module.b) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Empty(t, instructions) // No kurtosis instruction + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) errorMsg := `Evaluation error: An error occurred while loading the module '` + nonExistentModule + `' Caused by: Package '` + nonExistentModule + `' not found` @@ -573,6 +572,7 @@ def run(plan): errorMsg, ).ToAPIType() require.Equal(t, expectedError, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_ImportingAValidModuleThatPreviouslyFailedToLoadSucceeds(t *testing.T) { @@ -588,19 +588,19 @@ def run(plan): ` // assert that first load fails - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Nil(t, instructions) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.NotNil(t, interpretationError) + require.Nil(t, instructionsPlan) barModuleContents := "a=\"World!\"" require.Nil(t, packageContentProvider.AddFileContent(barModulePath, barModuleContents)) expectedOutput := `Hello World! ` // assert that second load succeeds - _, instructions, interpretationError = interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError = interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 1) // The print statement - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + require.Equal(t, 1, instructionsPlan.Size()) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_ValidSimpleScriptWithImportedStruct(t *testing.T) { @@ -628,16 +628,16 @@ def run(plan): plan.add_service(name = module_bar.service_name, config = module_bar.config) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Len(t, instructions, 3) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) + require.Equal(t, 3, instructionsPlan.Size()) - assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 6, 18) + assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 6, 18) expectedOutput := `Starting Startosis script! Adding service example-datastore-server ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_ValidScriptWithFunctionsImportedFromOtherModule(t *testing.T) { @@ -676,13 +676,13 @@ def run(plan): plan.print("Done!") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 8) + require.Equal(t, 8, instructionsPlan.Size()) - assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, moduleBar, 18, 25) - assertInstructionTypeAndPosition(t, instructions[4], add_service.AddServiceBuiltinName, moduleBar, 18, 25) - assertInstructionTypeAndPosition(t, instructions[6], add_service.AddServiceBuiltinName, moduleBar, 18, 25) + assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, moduleBar, 18, 25) + assertInstructionTypeAndPosition(t, instructionsPlan, 4, add_service.AddServiceBuiltinName, moduleBar, 18, 25) + assertInstructionTypeAndPosition(t, instructionsPlan, 6, add_service.AddServiceBuiltinName, moduleBar, 18, 25) expectedOutput := `Starting Startosis script! Adding service example-datastore-server-0 @@ -690,7 +690,7 @@ Adding service example-datastore-server-1 Adding service example-datastore-server-2 Done! ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_ImportModuleWithNoGlobalVariables(t *testing.T) { @@ -709,13 +709,13 @@ def run(plan): plan.print("World!") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Len(t, instructions, 1) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) + require.Equal(t, 1, instructionsPlan.Size()) expectedOutput := `World! ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_TestInstructionQueueAndOutputBufferDontHaveDupesInterpretingAnotherScript(t *testing.T) { @@ -751,11 +751,11 @@ Adding service example-datastore-server Starting Startosis script! ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, scriptA, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, scriptA, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 4) - assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, moduleBar, 12, 18) - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutputFromScriptA) + require.Equal(t, 4, instructionsPlan.Size()) + assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, moduleBar, 12, 18) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutputFromScriptA) scriptB := ` def run(plan): @@ -776,11 +776,11 @@ def run(plan): Adding service example-datastore-server ` - _, instructions, interpretationError = interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, scriptB, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError = interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, scriptB, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 3) - assertInstructionTypeAndPosition(t, instructions[2], add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 14, 18) - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutputFromScriptB) + require.Equal(t, 3, instructionsPlan.Size()) + assertInstructionTypeAndPosition(t, instructionsPlan, 2, add_service.AddServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 14, 18) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutputFromScriptB) } func TestStartosisInterpreter_ReadFileFromGithub(t *testing.T) { @@ -800,14 +800,14 @@ def run(plan): plan.print(file_contents) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 2) + require.Equal(t, 2, instructionsPlan.Size()) expectedOutput := `Reading file from GitHub! this is a test string ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_RenderTemplates(t *testing.T) { @@ -838,16 +838,16 @@ def run(plan): plan.print(artifact_name) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 3) + require.Equal(t, 3, instructionsPlan.Size()) - assertInstructionTypeAndPosition(t, instructions[1], render_templates.RenderTemplatesBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 20, 39) + assertInstructionTypeAndPosition(t, instructionsPlan, 1, render_templates.RenderTemplatesBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 20, 39) expectedOutput := fmt.Sprintf(`Rendering template to disk! %v `, testArtifactName) - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_ThreeLevelNestedInstructionPositionTest(t *testing.T) { @@ -884,17 +884,17 @@ def run(plan): plan.print(uuid) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 4) + require.Equal(t, 4, instructionsPlan.Size()) - assertInstructionTypeAndPosition(t, instructions[2], store_service_files.StoreServiceFilesBuiltinName, storeFileDefinitionPath, 4, 40) + assertInstructionTypeAndPosition(t, instructionsPlan, 2, store_service_files.StoreServiceFilesBuiltinName, storeFileDefinitionPath, 4, 40) expectedOutput := fmt.Sprintf(`In the module that calls store.star In the store files instruction %v `, testArtifactName) - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_ValidSimpleRemoveService(t *testing.T) { @@ -910,16 +910,16 @@ def run(plan): plan.print("The service example-datastore-server has been removed") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Len(t, instructions, 3) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) + require.Equal(t, 3, instructionsPlan.Size()) - assertInstructionTypeAndPosition(t, instructions[1], remove_service.RemoveServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 5, 21) + assertInstructionTypeAndPosition(t, instructionsPlan, 1, remove_service.RemoveServiceBuiltinName, startosis_constants.PackageIdPlaceholderForStandaloneScript, 5, 21) expectedOutput := `Starting Startosis script! The service example-datastore-server has been removed ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_NoPanicIfUploadIsPassedAPathNotOnDisk(t *testing.T) { @@ -932,9 +932,9 @@ func TestStartosisInterpreter_NoPanicIfUploadIsPassedAPathNotOnDisk(t *testing.T def run(plan): plan.upload_files("` + filePath + `") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) - require.Nil(t, instructions) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.NotNil(t, interpretationError) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_RunWithoutArgsNoArgsPassed(t *testing.T) { @@ -947,13 +947,13 @@ def run(plan): plan.print("Hello World!") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 1) + require.Equal(t, 1, instructionsPlan.Size()) expectedOutput := `Hello World! ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_RunWithoutArgsArgsPassed(t *testing.T) { @@ -966,13 +966,13 @@ def run(plan): plan.print("Hello World!") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, `{"number": 4}`) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, `{"number": 4}`) require.Nil(t, interpretationError) - require.Len(t, instructions, 1) + require.Equal(t, 1, instructionsPlan.Size()) expectedOutput := `Hello World! ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_RunWithArgsArgsPassed(t *testing.T) { @@ -985,13 +985,13 @@ def run(plan, args): plan.print("My favorite number is {0}".format(args["number"])) ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, `{"number": 4}`) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, `{"number": 4}`) require.Nil(t, interpretationError) - require.Len(t, instructions, 1) + require.Equal(t, 1, instructionsPlan.Size()) expectedOutput := `My favorite number is 4 ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_RunWithArgsNoArgsPassed(t *testing.T) { @@ -1007,13 +1007,13 @@ def run(plan, args): plan.print("Sorry no args!") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.Nil(t, interpretationError) - require.Len(t, instructions, 1) + require.Equal(t, 1, instructionsPlan.Size()) expectedOutput := `Sorry no args! ` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + validateScriptOutputFromPrintInstructions(t, instructionsPlan, expectedOutput) } func TestStartosisInterpreter_RunWithMoreThanExpectedParams(t *testing.T) { @@ -1026,13 +1026,11 @@ def run(plan, args, invalid_arg): plan.print("this wouldn't interpret so the text here doesnt matter") ` - _, instructions, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) + _, instructionsPlan, interpretationError := interpreter.Interpret(context.Background(), startosis_constants.PackageIdPlaceholderForStandaloneScript, useDefaultMainFunctionName, script, startosis_constants.EmptyInputArgs) require.NotNil(t, interpretationError) expectedError := fmt.Sprintf("The 'run' entrypoint function can have at most '%v' argument got '%v'", maximumParamsAllowedForRunFunction, 3) require.Equal(t, expectedError, interpretationError.GetErrorMessage()) - - expectedOutput := `` - validateScriptOutputFromPrintInstructions(t, instructions, expectedOutput) + require.Nil(t, instructionsPlan) } func TestStartosisInterpreter_PrintWithoutPlanErrorsNicely(t *testing.T) { @@ -1055,10 +1053,15 @@ def run(plan): // TEST HELPERS // // ##################################################################################################################### -func validateScriptOutputFromPrintInstructions(t *testing.T, instructions []kurtosis_instruction.KurtosisInstruction, expectedOutput string) { +func validateScriptOutputFromPrintInstructions(t *testing.T, instructionsPlan *instructions_plan.InstructionsPlan, expectedOutput string) { scriptOutput := strings.Builder{} - for _, instruction := range instructions { - + scheduledInstructions, err := instructionsPlan.GeneratePlan() + require.Nil(t, err) + for _, scheduledInstruction := range scheduledInstructions { + if scheduledInstruction.IsExecuted() { + continue + } + instruction := scheduledInstruction.GetInstruction() switch instruction.GetCanonicalInstruction().InstructionName { case kurtosis_print.PrintBuiltinName: instructionOutput, err := instruction.Execute(context.Background()) @@ -1072,7 +1075,11 @@ func validateScriptOutputFromPrintInstructions(t *testing.T, instructions []kurt require.Equal(t, expectedOutput, scriptOutput.String()) } -func assertInstructionTypeAndPosition(t *testing.T, instruction kurtosis_instruction.KurtosisInstruction, expectedInstructionName string, filename string, expectedLine int32, expectedCol int32) { +func assertInstructionTypeAndPosition(t *testing.T, instructionsPlan *instructions_plan.InstructionsPlan, idxInPlan int, expectedInstructionName string, filename string, expectedLine int32, expectedCol int32) { + scheduledInstructions, err := instructionsPlan.GeneratePlan() + require.Nil(t, err) + instruction := scheduledInstructions[idxInPlan].GetInstruction() + canonicalInstruction := instruction.GetCanonicalInstruction() require.Equal(t, expectedInstructionName, canonicalInstruction.GetInstructionName()) expectedPosition := binding_constructors.NewStarlarkInstructionPosition(filename, expectedLine, expectedCol) diff --git a/core/server/api_container/server/startosis_engine/startosis_runner.go b/core/server/api_container/server/startosis_engine/startosis_runner.go index 2892137faa..51951977d2 100644 --- a/core/server/api_container/server/startosis_engine/startosis_runner.go +++ b/core/server/api_container/server/startosis_engine/startosis_runner.go @@ -73,22 +73,29 @@ func (runner *StartosisRunner) Run( startingInterpretationMsg, defaultCurrentStepNumber, defaultTotalStepsNumber) starlarkRunResponseLines <- progressInfo - serializedScriptOutput, instructionsList, interpretationError := runner.startosisInterpreter.Interpret(ctx, packageId, mainFunctionName, serializedStartosis, serializedParams) + serializedScriptOutput, instructionsPlan, interpretationError := runner.startosisInterpreter.Interpret(ctx, packageId, mainFunctionName, serializedStartosis, serializedParams) if interpretationError != nil { starlarkRunResponseLines <- binding_constructors.NewStarlarkRunResponseLineFromInterpretationError(interpretationError) starlarkRunResponseLines <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent() return } - totalNumberOfInstructions := uint32(len(instructionsList)) - logrus.Debugf("Successfully interpreted Starlark script into a series of Kurtosis instructions: \n%v", - instructionsList) + totalNumberOfInstructions := uint32(instructionsPlan.Size()) + logrus.Debugf("Successfully interpreted Starlark script into a series of %d Kurtosis instructions", + totalNumberOfInstructions) + + instructionsSequence, interpretationErr := instructionsPlan.GeneratePlan() + if interpretationErr != nil { + starlarkRunResponseLines <- binding_constructors.NewStarlarkRunResponseLineFromInterpretationError(interpretationErr.ToAPIType()) + starlarkRunResponseLines <- binding_constructors.NewStarlarkRunResponseLineFromRunFailureEvent() + return + } // Validation starts > send progress info progressInfo = binding_constructors.NewStarlarkRunResponseLineFromSinglelineProgressInfo( startingValidationMsg, defaultCurrentStepNumber, totalNumberOfInstructions) starlarkRunResponseLines <- progressInfo - validationErrorsChan := runner.startosisValidator.Validate(ctx, instructionsList) + validationErrorsChan := runner.startosisValidator.Validate(ctx, instructionsSequence) if isRunFinished := forwardKurtosisResponseLineChannelUntilSourceIsClosed(validationErrorsChan, starlarkRunResponseLines); isRunFinished { return } @@ -99,11 +106,11 @@ func (runner *StartosisRunner) Run( startingExecutionMsg, defaultCurrentStepNumber, totalNumberOfInstructions) starlarkRunResponseLines <- progressInfo - executionResponseLinesChan := runner.startosisExecutor.Execute(ctx, dryRun, parallelism, instructionsList, serializedScriptOutput) + executionResponseLinesChan := runner.startosisExecutor.Execute(ctx, dryRun, parallelism, instructionsSequence, serializedScriptOutput) if isRunFinished := forwardKurtosisResponseLineChannelUntilSourceIsClosed(executionResponseLinesChan, starlarkRunResponseLines); !isRunFinished { logrus.Warnf("Execution finished but no 'RunFinishedEvent' was received through the stream. This is unexpected as every execution should be terminal.") } - logrus.Debugf("Successfully executed the list of %d Kurtosis instructions", len(instructionsList)) + logrus.Debugf("Successfully executed the list of %d Kurtosis instructions", totalNumberOfInstructions) }() return starlarkRunResponseLines diff --git a/core/server/api_container/server/startosis_engine/startosis_validator.go b/core/server/api_container/server/startosis_engine/startosis_validator.go index 9070770d30..16d6a8033d 100644 --- a/core/server/api_container/server/startosis_engine/startosis_validator.go +++ b/core/server/api_container/server/startosis_engine/startosis_validator.go @@ -8,7 +8,7 @@ import ( "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface" "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/service_network" - "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_validator" "github.com/kurtosis-tech/kurtosis/core/server/commons/enclave_data_directory" @@ -36,7 +36,7 @@ func NewStartosisValidator(kurtosisBackend *backend_interface.KurtosisBackend, s } } -func (validator *StartosisValidator) Validate(ctx context.Context, instructions []kurtosis_instruction.KurtosisInstruction) <-chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine { +func (validator *StartosisValidator) Validate(ctx context.Context, instructionsSequence []*instructions_plan.ScheduledInstruction) <-chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine { starlarkRunResponseLineStream := make(chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine) go func() { defer close(starlarkRunResponseLineStream) @@ -61,7 +61,7 @@ func (validator *StartosisValidator) Validate(ctx context.Context, instructions serviceNamePortIdMapping) isValidationFailure = isValidationFailure || - validator.validateAndUpdateEnvironment(instructions, environment, starlarkRunResponseLineStream) + validator.validateAndUpdateEnvironment(instructionsSequence, environment, starlarkRunResponseLineStream) logrus.Debug("Finished validating environment. Validating and downloading container images.") isValidationFailure = isValidationFailure || @@ -77,12 +77,20 @@ func (validator *StartosisValidator) Validate(ctx context.Context, instructions return starlarkRunResponseLineStream } -func (validator *StartosisValidator) validateAndUpdateEnvironment(instructions []kurtosis_instruction.KurtosisInstruction, environment *startosis_validator.ValidatorEnvironment, starlarkRunResponseLineStream chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine) bool { +func (validator *StartosisValidator) validateAndUpdateEnvironment(instructionsSequence []*instructions_plan.ScheduledInstruction, environment *startosis_validator.ValidatorEnvironment, starlarkRunResponseLineStream chan *kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine) bool { isValidationFailure := false - for _, instruction := range instructions { + for _, scheduledInstruction := range instructionsSequence { + if scheduledInstruction.IsExecuted() { + // no need to validate the instruction as it won't be executed in this round + continue + } + instruction := scheduledInstruction.GetInstruction() err := instruction.ValidateAndUpdateEnvironment(environment) if err != nil { - wrappedValidationError := startosis_errors.WrapWithValidationError(err, "Error while validating instruction %v. The instruction can be found at %v", instruction.String(), instruction.GetPositionInOriginalScript().String()) + wrappedValidationError := startosis_errors.WrapWithValidationError(err, + "Error while validating instruction %v. The instruction can be found at %v", + instruction.String(), + instruction.GetPositionInOriginalScript().String()) starlarkRunResponseLineStream <- binding_constructors.NewStarlarkRunResponseLineFromValidationError(wrappedValidationError.ToAPIType()) isValidationFailure = true }