Skip to content

Commit

Permalink
feat: Add starlark.Value serializer/deserializer for enclave persiste…
Browse files Browse the repository at this point in the history
…nce (#1229)

## Description:
For idempotent runs, we need to store for each instruction that was run
the result object of this instruction, which can be any starlark.Value.
This serde mechanism relies the Starlark code itself to serialize the
value, and runs a mini-interpretation step to deserialize it.

## Is this change user facing?
NO
<!-- If yes, please add the "user facing" label to the PR -->
<!-- If yes, don't forget to include docs changes where relevant -->

## References (if applicable):
<!-- Add relevant Github Issues, Discord threads, or other helpful
information. -->
  • Loading branch information
Guillaume Bouvignies committed Sep 5, 2023
1 parent 94a4b8b commit 45b9330
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 214 deletions.
Expand Up @@ -19,16 +19,29 @@ import (
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/upload_files"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/wait"
"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/kurtosis_types"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_types/directory"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_types/port_spec"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_types/service_config"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/recipe"
"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_errors"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_packages"
starlarkjson "go.starlark.net/lib/json"
"go.starlark.net/lib/time"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)

func Predeclared() starlark.StringDict {
return 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,
}
}

// KurtosisPlanInstructions returns the entire list of KurtosisPlanInstruction.
//
// A KurtosisPlanInstruction is a builtin that adds an operation to the plan.
Expand Down Expand Up @@ -80,6 +93,7 @@ func KurtosisHelpers(recursiveInterpret func(moduleId string, scriptContent stri
// Example: ServiceConfig, PortSpec, etc.
func KurtosisTypeConstructors() []*starlark.Builtin {
return []*starlark.Builtin{
starlark.NewBuiltin(kurtosis_types.ServiceTypeName, kurtosis_types.NewServiceType().CreateBuiltin()),
starlark.NewBuiltin(directory.DirectoryTypeName, directory.NewDirectoryType().CreateBuiltin()),
starlark.NewBuiltin(recipe.ExecRecipeTypeName, recipe.NewExecRecipeType().CreateBuiltin()),
starlark.NewBuiltin(recipe.GetHttpRecipeTypeName, recipe.NewGetHttpRequestRecipeType().CreateBuiltin()),
Expand Down
Expand Up @@ -58,7 +58,10 @@ func makeAddServiceInterpretationReturnValue(serviceName starlark.String, servic
}
ipAddress := starlark.String(fmt.Sprintf(magic_string_helper.RuntimeValueReplacementPlaceholderFormat, resultUuid, ipAddressRuntimeValue))
hostname := starlark.String(fmt.Sprintf(magic_string_helper.RuntimeValueReplacementPlaceholderFormat, resultUuid, hostnameRuntimeValue))
returnValue := kurtosis_types.NewService(serviceName, hostname, ipAddress, portSpecsDict)
returnValue, interpretationErr := kurtosis_types.CreateService(serviceName, hostname, ipAddress, portSpecsDict)
if interpretationErr != nil {
return nil, interpretationErr
}
return returnValue, nil
}

Expand Down
Expand Up @@ -87,7 +87,7 @@ func (t *addServiceTestCase) Assert(interpretationResult starlark.Value, executi
serviceObj, ok := interpretationResult.(*kurtosis_types.Service)
require.True(t, ok, "interpretation result should be a dictionary")
require.NotNil(t, serviceObj)
expectedServiceObj := fmt.Sprintf(`Service\(hostname = "{{kurtosis:[0-9a-f]{32}:hostname.runtime_value}}", ip_address = "{{kurtosis:[0-9a-f]{32}:ip_address.runtime_value}}", name = "%v", ports = {}\)`, TestServiceName)
expectedServiceObj := fmt.Sprintf(`Service\(name="%v", hostname="{{kurtosis:[0-9a-f]{32}:hostname.runtime_value}}", ip_address="{{kurtosis:[0-9a-f]{32}:ip_address.runtime_value}}", ports={}\)`, TestServiceName)
require.Regexp(t, expectedServiceObj, serviceObj.String())

expectedExecutionResult := fmt.Sprintf("Service '%s' added with service UUID '%s'", TestServiceName, TestServiceUuid)
Expand Down
@@ -0,0 +1,59 @@
package test_engine

import (
"fmt"
"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_types"
"github.com/stretchr/testify/require"
"testing"
)

const (
testServiceIpAddress = "192.168.0.43"
testServiceHostname = "test-service-hostname"
testServicePorts = "{}"
)

type serviceTestCase struct {
*testing.T
}

func newServiceTestCase(t *testing.T) *serviceTestCase {
return &serviceTestCase{
T: t,
}
}

func (t serviceTestCase) GetId() string {
return kurtosis_types.ServiceTypeName
}

func (t serviceTestCase) GetStarlarkCode() string {
return fmt.Sprintf("%s(%s=%q, %s=%q, %s=%q, %s=%s)",
kurtosis_types.ServiceTypeName,
kurtosis_types.ServiceNameAttr, TestServiceName,
kurtosis_types.HostnameAttr, testServiceHostname,
kurtosis_types.IpAddressAttr, testServiceIpAddress,
kurtosis_types.PortsAttr, testServicePorts)
}

func (t serviceTestCase) Assert(typeValue builtin_argument.KurtosisValueType) {
serviceStarlark, ok := typeValue.(*kurtosis_types.Service)
require.True(t, ok)

resultServiceName, interpretationErr := serviceStarlark.GetName()
require.Nil(t, interpretationErr)
require.Equal(t, TestServiceName, resultServiceName)

resultServiceHostname, interpretationErr := serviceStarlark.GetHostname()
require.Nil(t, interpretationErr)
require.Equal(t, testServiceHostname, resultServiceHostname)

resultServiceIpAddress, interpretationErr := serviceStarlark.GetIpAddress()
require.Nil(t, interpretationErr)
require.Equal(t, testServiceIpAddress, resultServiceIpAddress)

resultPorts, interpretationErr := serviceStarlark.GetPorts()
require.Nil(t, interpretationErr)
require.Equal(t, 0, resultPorts.Len())
}
Expand Up @@ -4,18 +4,14 @@ import (
"context"
"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/enclave_structure"
"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_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"
"github.com/stretchr/testify/require"
starlarkjson "go.starlark.net/lib/json"
"go.starlark.net/lib/time"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
"reflect"
"testing"
)
Expand Down Expand Up @@ -61,6 +57,7 @@ func TestAllRegisteredBuiltins(t *testing.T) {
testKurtosisTypeConstructor(t, newPostHttpRequestRecipeMinimalTestCase(t))
testKurtosisTypeConstructor(t, newServiceConfigMinimalTestCase(t))
testKurtosisTypeConstructor(t, newServiceConfigFullTestCase(t))
testKurtosisTypeConstructor(t, newServiceTestCase(t))
testKurtosisTypeConstructor(t, newServiceConfigFullTestCaseBackwardCompatible(t))
testKurtosisTypeConstructor(t, newReadyConditionsHttpRecipeTestCase(t))
testKurtosisTypeConstructor(t, newReadyConditionsExecRecipeTestCase(t))
Expand Down Expand Up @@ -150,18 +147,8 @@ func testKurtosisTypeConstructor(t *testing.T, builtin KurtosisTypeConstructorBa
}

func getBasePredeclaredDict(t *testing.T, thread *starlark.Thread) starlark.StringDict {
kurtosisModule, err := builtins.KurtosisModule(thread, "", "")
require.Nil(t, err)
// TODO: refactor this with the one we have in the interpreter
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,
}
predeclared := startosis_engine.Predeclared()

// Add all Kurtosis types
for _, kurtosisTypeConstructor := range startosis_engine.KurtosisTypeConstructors() {
predeclared[kurtosisTypeConstructor.Name()] = kurtosisTypeConstructor
Expand Down
@@ -1,50 +1,143 @@
package kurtosis_types

import (
"fmt"
"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service"
"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/kurtosis_starlark_framework/builtin_argument"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_type_constructor"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
"strings"
)

const (
serviceTypeName = "Service"
ServiceTypeName = "Service"

hostnameAttr = "hostname"
ipAddressAttr = "ip_address"
portsAttr = "ports"
serviceNameAttr = "name"
HostnameAttr = "hostname"
IpAddressAttr = "ip_address"
PortsAttr = "ports"
ServiceNameAttr = "name"
)

// Service is just a wrapper around a regular starlarkstruct.Struct
// It naturally inherits all its function making it a valid starlark.Value
func NewServiceType() *kurtosis_type_constructor.KurtosisTypeConstructor {
return &kurtosis_type_constructor.KurtosisTypeConstructor{
KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{
Name: ServiceTypeName,

Arguments: []*builtin_argument.BuiltinArgument{
{
Name: ServiceNameAttr,
IsOptional: false,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
return builtin_argument.NonEmptyString(value, ServiceNameAttr)
},
},
{
Name: HostnameAttr,
IsOptional: false,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
return builtin_argument.NonEmptyString(value, HostnameAttr)
},
},
{
Name: IpAddressAttr,
IsOptional: false,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
return builtin_argument.NonEmptyString(value, IpAddressAttr)
},
},
{
Name: PortsAttr,
IsOptional: false,
ZeroValueProvider: builtin_argument.ZeroValueProvider[*starlark.Dict],
Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
return nil
},
},
},
},

Instantiate: instantiate,
}
}

func instantiate(arguments *builtin_argument.ArgumentValuesSet) (builtin_argument.KurtosisValueType, *startosis_errors.InterpretationError) {
kurtosisValueType, interpretationErr := kurtosis_type_constructor.CreateKurtosisStarlarkTypeDefault(ServiceTypeName, arguments)
if interpretationErr != nil {
return nil, interpretationErr
}
return &Service{
KurtosisValueTypeDefault: kurtosisValueType,
}, nil
}

type Service struct {
*starlarkstruct.Struct
*kurtosis_type_constructor.KurtosisValueTypeDefault
}

func CreateService(serviceName starlark.String, hostname starlark.String, ipAddress starlark.String, ports *starlark.Dict) (*Service, *startosis_errors.InterpretationError) {
args := []starlark.Value{
serviceName,
hostname,
ipAddress,
ports,
}

argumentDefinitions := NewServiceType().KurtosisBaseBuiltin.Arguments
argumentValuesSet := builtin_argument.NewArgumentValuesSet(argumentDefinitions, args)
kurtosisDefaultValue, interpretationErr := kurtosis_type_constructor.CreateKurtosisStarlarkTypeDefault(ServiceTypeName, argumentValuesSet)
if interpretationErr != nil {
return nil, interpretationErr
}
return &Service{
KurtosisValueTypeDefault: kurtosisDefaultValue,
}, nil
}

func NewService(serviceName starlark.String, hostname starlark.String, ipAddress starlark.String, ports *starlark.Dict) *Service {
structDict := starlark.StringDict{
serviceNameAttr: serviceName,
hostnameAttr: hostname,
ipAddressAttr: ipAddress,
portsAttr: ports,
func (serviceObj *Service) Copy() (builtin_argument.KurtosisValueType, error) {
copiedValueType, err := serviceObj.KurtosisValueTypeDefault.Copy()
if err != nil {
return nil, err
}
return &Service{
Struct: starlarkstruct.FromStringDict(starlark.String(serviceTypeName), structDict),
KurtosisValueTypeDefault: copiedValueType,
}, nil
}

func (serviceObj *Service) GetName() (service.ServiceName, *startosis_errors.InterpretationError) {
serviceName, _, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](
serviceObj.KurtosisValueTypeDefault, ServiceNameAttr)
if interpretationErr != nil {
return "", interpretationErr
}
return service.ServiceName(serviceName.GoString()), nil
}

// String manually overrides the default starlarkstruct.Struct String() function because it is wrong when
// we provide a custom constructor, which we do here
//
// See https://github.com/google/starlark-go/issues/448 for more details
func (service *Service) String() string {
oldInvalid := fmt.Sprintf("\"%s\"(", serviceTypeName)
newValid := fmt.Sprintf("%s(", serviceTypeName)
return strings.Replace(service.Struct.String(), oldInvalid, newValid, 1)
func (serviceObj *Service) GetHostname() (string, *startosis_errors.InterpretationError) {
hostname, _, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](
serviceObj.KurtosisValueTypeDefault, HostnameAttr)
if interpretationErr != nil {
return "", interpretationErr
}
return hostname.GoString(), nil
}

// Type Needs to be overridden as the default for starlarkstruct.Struct always return "struct", which is dumb
func (service *Service) Type() string {
return serviceTypeName
func (serviceObj *Service) GetIpAddress() (string, *startosis_errors.InterpretationError) {
ipAddress, _, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](
serviceObj.KurtosisValueTypeDefault, IpAddressAttr)
if interpretationErr != nil {
return "", interpretationErr
}
return ipAddress.GoString(), nil
}

func (serviceObj *Service) GetPorts() (*starlark.Dict, *startosis_errors.InterpretationError) {
ports, _, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[*starlark.Dict](
serviceObj.KurtosisValueTypeDefault, PortsAttr)
if interpretationErr != nil {
return nil, interpretationErr
}
return ports, nil
}

0 comments on commit 45b9330

Please sign in to comment.