From 2c3d74e9c88e6b3a980048b6831b23499b4a0a12 Mon Sep 17 00:00:00 2001 From: Guillaume Bouvignies Date: Fri, 23 Jun 2023 16:17:40 +0200 Subject: [PATCH] feat: Persist enclave plan in the Starlark executor memory (#757) ## Description: The Starlark executor now keeps track of all instructions that were executed within this enclave. It doesn't take into account the manual modification that might have been made, such as manual file uploads via the CLI. ## Is this change user facing? NO ## References (if applicable): --- .../instructions_plan/instructions_plan.go | 69 ++++++ .../instructions_plan_test.go | 86 +++++++ .../scheduled_instruction.go | 49 ++++ .../scheduled_instruction_test.go | 19 ++ .../plan_module/plan_module.go | 6 +- .../kurtosis_plan_instruction.go | 17 +- .../starlark_framework_engine_test.go | 12 +- .../startosis_engine/startosis_executor.go | 51 ++-- .../startosis_executor_test.go | 49 ++-- .../startosis_engine/startosis_interpreter.go | 36 +-- .../startosis_interpreter_test.go | 231 +++++++++--------- .../startosis_engine/startosis_runner.go | 21 +- .../startosis_engine/startosis_validator.go | 20 +- 13 files changed, 476 insertions(+), 190 deletions(-) create mode 100644 core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan.go create mode 100644 core/server/api_container/server/startosis_engine/instructions_plan/instructions_plan_test.go create mode 100644 core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction.go create mode 100644 core/server/api_container/server/startosis_engine/instructions_plan/scheduled_instruction_test.go 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 }