/
startosis_interpreter.go
493 lines (441 loc) · 25.3 KB
/
startosis_interpreter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
package startosis_engine
import (
"context"
"fmt"
"github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings"
"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/instructions_plan"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/instructions_plan/resolver"
"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"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_constants"
"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_packages"
"github.com/sirupsen/logrus"
"go.starlark.net/lib/time"
"go.starlark.net/resolve"
"go.starlark.net/starlark"
"go.starlark.net/starlarkjson"
"go.starlark.net/starlarkstruct"
"go.starlark.net/syntax"
"strings"
"sync"
)
const (
starlarkGoThreadName = "Startosis interpreter thread"
multipleInterpretationErrorMsg = "Multiple errors caught interpreting the Starlark script. Listing each of them below."
evaluationErrorPrefix = "Evaluation error: "
skipImportInstructionInStacktraceValue = "import_module"
runFunctionName = "run"
paramsRequiredForArgs = 2
minimumParamsRequiredForPlan = 1
maximumParamsAllowedForRunFunction = 2
planParamIndex = 0
planParamName = "plan"
argsParamIndex = 1
argsParamName = "args"
unexpectedArgNameError = "Expected argument at index '%v' of run function to be called '%v' got '%v' "
)
var (
noKwargs []starlark.Tuple
)
type StartosisInterpreter struct {
// This is mutex protected as interpreting two different scripts in parallel could potentially cause
// problems with the moduleGlobalsCache & moduleContentProvider. Fixing this is quite complicated, which we decided not to do.
mutex *sync.Mutex
serviceNetwork service_network.ServiceNetwork
recipeExecutor *runtime_value_store.RuntimeValueStore
moduleGlobalsCache map[string]*startosis_packages.ModuleCacheEntry
// TODO AUTH there will be a leak here in case people with different repo visibility access a module
moduleContentProvider startosis_packages.PackageContentProvider
}
type SerializedInterpretationOutput string
func NewStartosisInterpreter(serviceNetwork service_network.ServiceNetwork, moduleContentProvider startosis_packages.PackageContentProvider, runtimeValueStore *runtime_value_store.RuntimeValueStore) *StartosisInterpreter {
return &StartosisInterpreter{
mutex: &sync.Mutex{},
serviceNetwork: serviceNetwork,
recipeExecutor: runtimeValueStore,
moduleGlobalsCache: make(map[string]*startosis_packages.ModuleCacheEntry),
moduleContentProvider: moduleContentProvider,
}
}
// InterpretAndOptimizePlan is an evolution of the Interpret function which takes into account the current enclave
// plan when interpreting the Starlark package in order to not re-run the instructions that have alrady been run
// inside the enclave.
// What it does is that is calls the Interpret function with a mask that swap certain instructions with the ones from
// the current enclave plan abd see what is affected by those swaps. If the new plan produced by the interpretation
// with the swaps is correct, then it returns it. See detailed inline comments for more details on the implementation
//
// Note: the plan generated by this function is necessarily a SUBSET of the currentEnclavePlan passed as a parameter
func (interpreter *StartosisInterpreter) InterpretAndOptimizePlan(
ctx context.Context,
packageId string,
mainFunctionName string,
serializedStarlark string,
serializedJsonParams string,
currentEnclavePlan *instructions_plan.InstructionsPlan,
) (string, *instructions_plan.InstructionsPlan, *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError) {
// run interpretation with no mask at all to generate the list of instructions as if the enclave was empty
emptyPlanInstructionsMask := resolver.NewInstructionsPlanMask(0)
naiveInstructionsPlanSerializedScriptOutput, naiveInstructionsPlan, interpretationErrorApi := interpreter.Interpret(ctx, packageId, mainFunctionName, serializedStarlark, serializedJsonParams, emptyPlanInstructionsMask)
if interpretationErrorApi != nil {
return startosis_constants.NoOutputObject, nil, interpretationErrorApi
}
naiveInstructionsPlanSequence, interpretationErr := naiveInstructionsPlan.GeneratePlan()
if interpretationErr != nil {
return startosis_constants.NoOutputObject, nil, interpretationErr.ToAPIType()
}
logrus.Debugf("First interpretation of package generated %d instructions", len(naiveInstructionsPlanSequence))
currentEnclavePlanSequence, interpretationErr := currentEnclavePlan.GeneratePlan()
if interpretationErr != nil {
return startosis_constants.NoOutputObject, nil, interpretationErr.ToAPIType()
}
logrus.Debugf("Current enclave state contains %d instructions", len(currentEnclavePlanSequence))
logrus.Debugf("Starting iterations to find the best plan to execute given the current state of the enclave")
// We're going to iterate this way:
// 1. Find an instruction in the current enclave plan matching the first instruction of the new plan
// 2. Recopy all instructions prior to the match into the optimized plan
// 3. Recopy all following instruction from the current enclave plan into an Instructions Plan Mask -> the reason
// we're naively recopying all the following instructions, not just the ones that depends on this instruction
// is because right now, we don't have the ability to know which instructions depends on which. We assume that
// all instructions executed AFTER this one will depend on it, to stay on the safe side
// 4. Run the interpretation with the mask.
// - If it's successful, then we've found the optimized plan
// - if it's not successful, then the mask is not compatible with the package. Go back to step 1
firstPossibleIndexForMatchingInstruction := 0
for {
// initialize an empty optimized plan and an empty the mask
potentialMask := resolver.NewInstructionsPlanMask(len(naiveInstructionsPlanSequence))
optimizedPlan := instructions_plan.NewInstructionsPlan()
// find the index of an instruction in the current enclave plan matching the FIRST instruction of our instructions plan generated by the first interpretation
matchingInstructionIdx := findFirstEqualInstructionPastIndex(currentEnclavePlanSequence, naiveInstructionsPlanSequence, firstPossibleIndexForMatchingInstruction)
if matchingInstructionIdx >= 0 {
logrus.Debugf("Found an instruction in enclave state at index %d which matches the first instruction of the new instructions plan", matchingInstructionIdx)
// we found a match
// -> First recopy all enclave state instructions prior to this match to the optimized plan. Those won't
// be executed, but they need to be part of the plan to keep the state of the enclave accurate
logrus.Debugf("Copying %d instructions from current enclave plan to new plan. Those instructions won't be executed but need to be kept in the enclave plan", matchingInstructionIdx)
for i := 0; i < matchingInstructionIdx; i++ {
optimizedPlan.AddScheduledInstruction(currentEnclavePlanSequence[i]).ImportedFromCurrentEnclavePlan(true).Executed(true)
}
// -> Then recopy all instructions past this match from the enclave state to the mask
// Those instructions are the instructions that will mask the instructions for the newly submitted plan
numberOfInstructionCopiedToMask := 0
for copyIdx := matchingInstructionIdx; copyIdx < len(currentEnclavePlanSequence); copyIdx++ {
if numberOfInstructionCopiedToMask >= potentialMask.Size() {
// the mask is already full, can't recopy more instructions, stop here
break
}
potentialMask.InsertAt(numberOfInstructionCopiedToMask, currentEnclavePlanSequence[copyIdx])
numberOfInstructionCopiedToMask += 1
}
logrus.Debugf("Writing %d instruction at the beginning of the plan mask, leaving %d empty at the end", numberOfInstructionCopiedToMask, potentialMask.Size()-numberOfInstructionCopiedToMask)
} else {
// We cannot find any more instructions inside the enclave state matching the first instruction of the plan
for _, currentPlanInstruction := range currentEnclavePlanSequence {
optimizedPlan.AddScheduledInstruction(currentPlanInstruction).ImportedFromCurrentEnclavePlan(true).Executed(true)
}
for _, newPlanInstruction := range naiveInstructionsPlanSequence {
optimizedPlan.AddScheduledInstruction(newPlanInstruction)
}
logrus.Debugf("Exhausted all possibilities. Concatenated the previous enclave plan with the new plan to obtain a %d instructions plan", optimizedPlan.Size())
return naiveInstructionsPlanSerializedScriptOutput, optimizedPlan, nil
}
// Now that we have a potential plan mask, try running interpretation again using this plan mask
attemptSerializedScriptOutput, attemptInstructionsPlan, interpretationErrorApi := interpreter.Interpret(ctx, packageId, mainFunctionName, serializedStarlark, serializedJsonParams, potentialMask)
if interpretationErrorApi != nil {
// Note: there's no real reason why this interpretation would fail with an error, given that the package
// has been interpreted once already (right above). But to be on the safe side, check the error
logrus.Warnf("Interpreting the package again with the plan mask failed, this is an unexpected error. " +
"Ignoring this mask")
firstPossibleIndexForMatchingInstruction += 1
continue
}
if !potentialMask.IsValid() {
// mask has been marks as invalid by the interpreter, we need to find another one
logrus.Infof("Plan mask was marked as invalid after the tentative-interpretation returned. Will " +
"ignore this mask and try to find another one")
firstPossibleIndexForMatchingInstruction += 1
continue
}
// no error happened, it seems we found a good mask
// -> recopy all instructions from the interpretation to the optimized plan
attemptInstructionsPlanSequence, interpretationErr := attemptInstructionsPlan.GeneratePlan()
if interpretationErr != nil {
return startosis_constants.NoOutputObject, nil, interpretationErr.ToAPIType()
}
logrus.Debugf("Interpreting the package again with the plan mask succeeded and generated %d new instructions. Adding them to the new optimized plan", attemptInstructionsPlan.Size())
for _, scheduledInstruction := range attemptInstructionsPlanSequence {
optimizedPlan.AddScheduledInstruction(scheduledInstruction)
}
// there might be still be instructions in the current enclave plan that have not been imported to the
// optimized plan
// for now, we support this only if no new instructions will be executed for this run. If that's not the case
// continue the loop in the hope of finding another mask
if len(currentEnclavePlanSequence) > matchingInstructionIdx+optimizedPlan.Size() {
logrus.Debugf("There are %d instructions remaining in the current state that have not been transferred to the new plan. Transferring them now", len(currentEnclavePlanSequence)-matchingInstructionIdx+optimizedPlan.Size())
atLeastOneInstructionWillBeExecuted := false
for _, instructionThatWillPotentiallyBeRun := range attemptInstructionsPlanSequence {
if !instructionThatWillPotentiallyBeRun.IsExecuted() {
atLeastOneInstructionWillBeExecuted = true
}
}
if atLeastOneInstructionWillBeExecuted {
logrus.Debugf("The remaining instructions in the current enclave plan cannot be transferred to the new plan because this plan contains instructions that will be executed." +
"The remaining instructions might depend on those and Kurtosis cannot re-run them (this is unsupported for now)")
continue
}
// recopy all remaining instructions into the optimized plan
for _, remainingInstructionFromCurrentEnclaveState := range currentEnclavePlanSequence[matchingInstructionIdx+optimizedPlan.Size():] {
optimizedPlan.AddScheduledInstruction(remainingInstructionFromCurrentEnclaveState).ImportedFromCurrentEnclavePlan(true).Executed(true)
}
}
// finally we can return the optimized plan as well as the serialized script output returned by the last
// interpretation attempt
return attemptSerializedScriptOutput, optimizedPlan, nil
}
}
// Interpret interprets the Starlark script and produce different outputs:
// - A potential interpretation error that the writer of the script should be aware of (syntax error in the Startosis
// code, inconsistent). Can be nil if the script was successfully interpreted
// - The list of Kurtosis instructions that was generated based on the interpretation of the script. It can be empty
// if the interpretation of the script failed
func (interpreter *StartosisInterpreter) Interpret(
_ context.Context,
packageId string,
mainFunctionName string,
serializedStarlark string,
serializedJsonParams string,
instructionsPlanMask *resolver.InstructionsPlanMask,
) (string, *instructions_plan.InstructionsPlan, *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError) {
interpreter.mutex.Lock()
defer interpreter.mutex.Unlock()
newInstructionsPlan := instructions_plan.NewInstructionsPlan()
logrus.Debugf("Interpreting package '%v' with contents '%v' and params '%v'", packageId, serializedStarlark, serializedJsonParams)
globalVariables, interpretationErr := interpreter.interpretInternal(packageId, serializedStarlark, newInstructionsPlan)
if interpretationErr != nil {
return startosis_constants.NoOutputObject, nil, interpretationErr.ToAPIType()
}
logrus.Debugf("Successfully interpreted Starlark code into %d instructions", newInstructionsPlan.Size())
var isUsingDefaultMainFunction bool
// if the user sends "" or "run" we isUsingDefaultMainFunction to true
if mainFunctionName == "" || mainFunctionName == runFunctionName {
mainFunctionName = runFunctionName
isUsingDefaultMainFunction = true
}
if !globalVariables.Has(mainFunctionName) {
return "", nil, missingMainFunctionError(packageId, mainFunctionName)
}
mainFunction, ok := globalVariables[mainFunctionName].(*starlark.Function)
// if there is an element with the `mainFunctionName` but it isn't a function we have to error as well
if !ok {
return startosis_constants.NoOutputObject, nil, missingMainFunctionError(packageId, mainFunctionName)
}
runFunctionExecutionThread := newStarlarkThread(starlarkGoThreadName)
var argsTuple starlark.Tuple
var kwArgs []starlark.Tuple
mainFuncParamsNum := mainFunction.NumParams()
// The plan object will always be injected if the first argument name is 'plan'
// If we are on main, 'plan' must be the first argument
if mainFuncParamsNum >= minimumParamsRequiredForPlan {
firstParamName, _ := mainFunction.Param(planParamIndex)
if firstParamName == planParamName {
kurtosisPlanInstructions := KurtosisPlanInstructions(interpreter.serviceNetwork, interpreter.recipeExecutor, interpreter.moduleContentProvider)
planModule := plan_module.PlanModule(newInstructionsPlan, instructionsPlanMask, kurtosisPlanInstructions)
argsTuple = append(argsTuple, planModule)
}
if firstParamName != planParamName && isUsingDefaultMainFunction {
return startosis_constants.NoOutputObject, nil, startosis_errors.NewInterpretationError(unexpectedArgNameError, planParamIndex, planParamName, firstParamName).ToAPIType()
}
}
inputArgs, interpretationError := interpreter.parseInputArgs(runFunctionExecutionThread, serializedJsonParams)
if interpretationError != nil {
return startosis_constants.NoOutputObject, nil, interpretationError.ToAPIType()
}
// For backwards compatibility, deal with case run(plan, args), where args is a generic dictionary
runWithGenericDictArgs := false
if isUsingDefaultMainFunction && mainFuncParamsNum == paramsRequiredForArgs {
if paramName, _ := mainFunction.Param(argsParamIndex); paramName == argsParamName {
logrus.Warnf("Using args dictionary as parameter is deprecated. Consider unpacking the dictionary into individual parameters. For example: run(plan, args) to run(plan, param1, param2, ...)")
argsTuple = append(argsTuple, inputArgs)
kwArgs = noKwargs
runWithGenericDictArgs = true
}
}
if !runWithGenericDictArgs {
argsDict, ok := inputArgs.(*starlark.Dict)
if !ok {
return startosis_constants.NoOutputObject, nil, startosis_errors.NewInterpretationError("An error occurred casting input args '%s' to Starlark Dict", inputArgs).ToAPIType()
}
kwArgs = append(kwArgs, argsDict.Items()...)
}
outputObject, err := starlark.Call(runFunctionExecutionThread, mainFunction, argsTuple, kwArgs)
if err != nil {
return startosis_constants.NoOutputObject, nil, generateInterpretationError(err).ToAPIType()
}
// Serialize and return the output object. It might contain magic strings that should be resolved post-execution
if outputObject != starlark.None {
logrus.Debugf("Starlark output object was: '%s'", outputObject)
serializedOutputObject, interpretationError := package_io.SerializeOutputObject(runFunctionExecutionThread, outputObject)
if interpretationError != nil {
return startosis_constants.NoOutputObject, nil, interpretationError.ToAPIType()
}
return serializedOutputObject, newInstructionsPlan, nil
}
return startosis_constants.NoOutputObject, newInstructionsPlan, nil
}
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(instructionPlan)
if interpretationErr != nil {
return nil, interpretationErr
}
globalVariables, err := starlark.ExecFile(thread, packageId, serializedStarlark, *predeclared)
if err != nil {
return nil, generateInterpretationError(err)
}
return globalVariables, nil
}
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, instructionPlan)
if err != nil {
return nil, err
}
return result, nil
}
kurtosisModule, interpretationErr := builtins.KurtosisModule()
if interpretationErr != nil {
return nil, interpretationErr
}
predeclared := starlark.StringDict{
// go-starlark add-ons
starlarkjson.Module.Name: starlarkjson.Module,
starlarkstruct.Default.GoString(): starlark.NewBuiltin(starlarkstruct.Default.GoString(), starlarkstruct.Make), // extension to build struct in starlark
time.Module.Name: time.Module,
// Kurtosis pre-built module containing Kurtosis constant types
builtins.KurtosisModuleName: kurtosisModule,
}
// Add all Kurtosis helpers
for _, kurtosisHelper := range KurtosisHelpers(recursiveInterpretForModuleLoading, interpreter.moduleContentProvider, interpreter.moduleGlobalsCache) {
predeclared[kurtosisHelper.Name()] = kurtosisHelper
}
// Add all Kurtosis types
for _, kurtosisTypeConstructors := range KurtosisTypeConstructors() {
predeclared[kurtosisTypeConstructors.Name()] = kurtosisTypeConstructors
}
return &predeclared, nil
}
func findFirstEqualInstructionPastIndex(currentEnclaveInstructionsList []*instructions_plan.ScheduledInstruction, naiveInstructionsList []*instructions_plan.ScheduledInstruction, minIndex int) int {
if len(naiveInstructionsList) == 0 {
return -1 // no result as the naiveInstructionsList is empty
}
for i := minIndex; i < len(currentEnclaveInstructionsList); i++ {
if currentEnclaveInstructionsList[i].GetInstruction().String() == naiveInstructionsList[0].GetInstruction().String() {
return i
}
}
return -1 // no match
}
// This method handles the different cases a Startosis module can be executed.
// - If input args are empty it uses empty JSON ({}) as the input args
// - If input args aren't empty it tries to deserialize them
func (interpreter *StartosisInterpreter) parseInputArgs(thread *starlark.Thread, serializedJsonArgs string) (starlark.Value, *startosis_errors.InterpretationError) {
// it is a module, and it has input args -> deserialize the JSON input and add it as a struct to the predeclared
deserializedArgs, interpretationError := package_io.DeserializeArgs(thread, serializedJsonArgs)
if interpretationError != nil {
return nil, interpretationError
}
return deserializedArgs, nil
}
func makeLoadFunction() func(_ *starlark.Thread, packageId string) (starlark.StringDict, error) {
return func(_ *starlark.Thread, _ string) (starlark.StringDict, error) {
return nil, startosis_errors.NewInterpretationError("'load(\"path/to/file.star\", var_in_file=\"var_in_file\")' statement is not available in Kurtosis. Please use instead `module = import(\"path/to/file.star\")` and then `module.var_in_file`")
}
}
func makePrintFunction() func(*starlark.Thread, string) {
return func(_ *starlark.Thread, msg string) {
// the `print` function must be overriden with the custom print builtin in the predeclared map
// which just exists to throw a nice interpretation error as this itself can't
panic(print_builtin.UsePlanFromKurtosisInstructionError)
}
}
func generateInterpretationError(err error) *startosis_errors.InterpretationError {
switch slError := err.(type) {
case resolve.Error:
stacktrace := []startosis_errors.CallFrame{
*startosis_errors.NewCallFrame(slError.Msg, startosis_errors.NewScriptPosition(slError.Pos.Filename(), slError.Pos.Line, slError.Pos.Col)),
}
return startosis_errors.NewInterpretationErrorFromStacktrace(stacktrace)
case syntax.Error:
stacktrace := []startosis_errors.CallFrame{
*startosis_errors.NewCallFrame(slError.Msg, startosis_errors.NewScriptPosition(slError.Pos.Filename(), slError.Pos.Line, slError.Pos.Col)),
}
return startosis_errors.NewInterpretationErrorFromStacktrace(stacktrace)
case resolve.ErrorList:
// TODO(gb): a bit hacky but it's an acceptable way to wrap multiple errors into a single Interpretation
// it's probably not worth adding another level of complexity here to handle InterpretationErrorList
stacktrace := make([]startosis_errors.CallFrame, 0)
for _, slErr := range slError {
if slErr.Msg == skipImportInstructionInStacktraceValue {
continue
}
stacktrace = append(stacktrace, *startosis_errors.NewCallFrame(slErr.Msg, startosis_errors.NewScriptPosition(slErr.Pos.Filename(), slErr.Pos.Line, slErr.Pos.Col)))
}
return startosis_errors.NewInterpretationErrorWithCustomMsg(stacktrace, multipleInterpretationErrorMsg)
case *starlark.EvalError:
stacktrace := make([]startosis_errors.CallFrame, 0)
for _, callStack := range slError.CallStack {
if callStack.Name == skipImportInstructionInStacktraceValue {
continue
}
stacktrace = append(stacktrace, *startosis_errors.NewCallFrame(callStack.Name, startosis_errors.NewScriptPosition(callStack.Pos.Filename(), callStack.Pos.Line, callStack.Pos.Col)))
}
var errorMsg string
// no need to add the evaluation error prefix if the wrapped error already has it
if strings.HasPrefix(slError.Unwrap().Error(), evaluationErrorPrefix) {
errorMsg = slError.Unwrap().Error()
} else {
errorMsg = fmt.Sprintf("%s%s", evaluationErrorPrefix, slError.Unwrap().Error())
}
return startosis_errors.NewInterpretationErrorWithCustomMsg(stacktrace, errorMsg)
case *startosis_errors.InterpretationError:
// If it's already an interpretation error -> nothing to convert
return slError
}
return startosis_errors.NewInterpretationError("UnknownError: %s\n", err.Error())
}
func missingMainFunctionError(packageId string, mainFunctionName string) *kurtosis_core_rpc_api_bindings.StarlarkInterpretationError {
if packageId == startosis_constants.PackageIdPlaceholderForStandaloneScript {
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,
mainFunctionName,
mainFunctionName,
).ToAPIType()
}
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,
mainFunctionName,
mainFunctionName,
mainFunctionName,
).ToAPIType()
}
func newStarlarkThread(threadName string) *starlark.Thread {
return &starlark.Thread{
Name: threadName,
Print: makePrintFunction(),
Load: makeLoadFunction(),
OnMaxSteps: nil,
Steps: 0,
}
}