Skip to content

Commit

Permalink
feat: Added ReadyConditions type which is used in ServiceConfig f…
Browse files Browse the repository at this point in the history
…or defining how to check services readiness (#151)
  • Loading branch information
leoporoli committed Mar 16, 2023
1 parent a31821d commit c53d6e2
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 39 deletions.
Expand Up @@ -90,5 +90,6 @@ func KurtosisTypeConstructors() []*starlark.Builtin {
starlark.NewBuiltin(port_spec.PortSpecTypeName, port_spec.NewPortSpecType().CreateBuiltin()),
starlark.NewBuiltin(service_config.ServiceConfigTypeName, service_config.NewServiceConfigType().CreateBuiltin()),
starlark.NewBuiltin(update_service_config.UpdateServiceConfigTypeName, update_service_config.NewUpdateServiceConfigType().CreateBuiltin()),
starlark.NewBuiltin(service_config.ReadyConditionsTypeName, service_config.NewReadyConditionsType().CreateBuiltin()),
}
}
Expand Up @@ -6,6 +6,7 @@ import (
"reflect"
"regexp"
"strings"
"time"
)

func NonEmptyString(value starlark.Value, argNameForLogging string) *startosis_errors.InterpretationError {
Expand Down Expand Up @@ -79,3 +80,21 @@ func StringRegexp(value starlark.Value, argNameForLogging string, mustMatchRegex
valueStr.GoString(),
)
}

func Duration(value starlark.Value, attributeName string) *startosis_errors.InterpretationError {
valueStarlarkStr, ok := value.(starlark.String)
if !ok {
return startosis_errors.NewInterpretationError("The '%s' attribute is not a valid string type (was '%s').", attributeName, reflect.TypeOf(value))
}

if valueStarlarkStr.GoString() == "" {
return nil
}

_, parseErr := time.ParseDuration(valueStarlarkStr.GoString())
if parseErr != nil {
return startosis_errors.WrapWithInterpretationError(parseErr, "The value '%v' of '%s' attribute is not a valid duration string format", valueStarlarkStr.GoString(), attributeName)
}

return nil
}
@@ -0,0 +1,120 @@
package test_engine

import (
"fmt"
"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/kurtosis_types/service_config"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/recipe"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.starlark.net/starlark"
"testing"
"time"
)

type readyConditionsTestCase struct {
*testing.T
}

func newReadyConditionsTestCase(t *testing.T) *readyConditionsTestCase {
return &readyConditionsTestCase{
T: t,
}
}

func (t *readyConditionsTestCase) GetId() string {
return service_config.ReadyConditionsTypeName
}

func (t *readyConditionsTestCase) GetTypeConstructor() *kurtosis_type_constructor.KurtosisTypeConstructor {
return service_config.NewReadyConditionsType()
}

func (t *readyConditionsTestCase) GetStarlarkCode() string {
return fmt.Sprintf("%s(%s=%s(%s=%q, %s=%q, %s=%s), %s=%q, %s=%q, %s=%s, %s=%q, %s=%q)",
service_config.ReadyConditionsTypeName,
service_config.RecipeAttr,
recipe.GetHttpRecipeTypeName,
recipe.PortIdAttr,
TestReadyConditionsRecipePortId,
recipe.EndpointAttr,
TestReadyConditionsRecipeEndpoint,
recipe.ExtractKeyPrefix,
TestReadyConditionsRecipeExtract,
service_config.FieldAttr,
TestReadyConditionsField,
service_config.AssertionAttr,
TestReadyConditionsAssertion,
service_config.TargetAttr,
TestReadyConditionsTarget,
service_config.IntervalAttr,
TestReadyConditionsInterval,
service_config.TimeoutAttr,
TestReadyConditionsTimeout,
)
}

func (t *readyConditionsTestCase) Assert(typeValue starlark.Value) {
receivedReadyConditions, ok := typeValue.(*service_config.ReadyConditions)
require.True(t, ok)

uncastedRecipe, err := receivedReadyConditions.GetRecipe()
if assert.Nil(t, err) {
castedRecipe, ok := uncastedRecipe.(*recipe.HttpRequestRecipe)
require.True(t, ok)

portIdAttrValue, err := castedRecipe.Attr(recipe.PortIdAttr)
if assert.Nil(t, err) {
portId, ok := portIdAttrValue.(starlark.String)
require.True(t, ok)
require.Equal(t, TestReadyConditionsRecipePortId, portId.GoString())
}

endpointAttrValue, err := castedRecipe.Attr(recipe.EndpointAttr)
if assert.Nil(t, err) {
endpoint, ok := endpointAttrValue.(starlark.String)
require.True(t, ok)
require.Equal(t, TestReadyConditionsRecipeEndpoint, endpoint.GoString())
}

extractAttrValue, err := castedRecipe.Attr(recipe.ExtractKeyPrefix)
if assert.Nil(t, err) {
extract, ok := extractAttrValue.(*starlark.Dict)
require.True(t, ok)
expectedExtractLen := 0
require.Equal(t, expectedExtractLen, extract.Len())
}
}

field, err := receivedReadyConditions.GetField()
if assert.Nil(t, err) {
require.Equal(t, TestReadyConditionsField, field)
}

assertion, err := receivedReadyConditions.GetAssertion()
if assert.Nil(t, err) {
require.Equal(t, TestReadyConditionsAssertion, assertion)
}

target, err := receivedReadyConditions.GetTarget()
if assert.Nil(t, err) {
require.Equal(t, TestReadyConditionsTarget, target.String())
}

interval, err := receivedReadyConditions.GetInterval()
if assert.Nil(t, err) {
expectedInterval, err := time.ParseDuration(TestReadyConditionsInterval)
if assert.Nil(t, err) {
require.Equal(t, expectedInterval, interval)
}
}

timeout, err := receivedReadyConditions.GetTimeout()
if assert.Nil(t, err) {
expectedTimeout, err := time.ParseDuration(TestReadyConditionsTimeout)
if assert.Nil(t, err) {
require.Equal(t, expectedTimeout, timeout)
}
}

}
Expand Up @@ -57,6 +57,7 @@ func TestAllRegisteredBuiltins(t *testing.T) {
testKurtosisTypeConstructor(t, newServiceConfigFullTestCase(t))
testKurtosisTypeConstructor(t, newUniformPacketDelayDistributionTestCase(t))
testKurtosisTypeConstructor(t, newUpdateServiceConfigTestCase(t))
testKurtosisTypeConstructor(t, newReadyConditionsTestCase(t))
}

func testKurtosisPlanInstruction(t *testing.T, builtin KurtosisPlanInstructionBaseTest) {
Expand Down
Expand Up @@ -66,4 +66,13 @@ var (
TestCpuAllocation = uint64(2000)

TestMemoryAllocation = uint64(1024)

TestReadyConditionsRecipePortId = "http"
TestReadyConditionsRecipeEndpoint = "/endpoint?input=data"
TestReadyConditionsRecipeExtract = "{}"
TestReadyConditionsField = "code"
TestReadyConditionsAssertion = "=="
TestReadyConditionsTarget = "200"
TestReadyConditionsInterval = "1s"
TestReadyConditionsTimeout = "100ms"
)
@@ -0,0 +1,207 @@
package service_config

import (
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/assert"
"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/recipe"
"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors"
"go.starlark.net/starlark"
"reflect"
"time"
)

const (
ReadyConditionsTypeName = "ReadyConditions"

RecipeAttr = "recipe"
FieldAttr = "field"
AssertionAttr = "assertion"
TargetAttr = "target_value"
IntervalAttr = "interval"
TimeoutAttr = "timeout"

defaultInterval = 1 * time.Second
defaultTimeout = 15 * time.Minute //TODO we could move these two to the service helpers method
)

func NewReadyConditionsType() *kurtosis_type_constructor.KurtosisTypeConstructor {
return &kurtosis_type_constructor.KurtosisTypeConstructor{
KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{
Name: ReadyConditionsTypeName,
Arguments: []*builtin_argument.BuiltinArgument{
{
Name: RecipeAttr,
IsOptional: false,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.Value],
Validator: validateRecipe,
},
{
Name: FieldAttr,
IsOptional: false,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
return builtin_argument.NonEmptyString(value, FieldAttr)
},
},
{
Name: AssertionAttr,
IsOptional: false,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
Validator: assert.ValidateAssertionToken,
},
{
Name: TargetAttr,
IsOptional: false,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.Comparable],
Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
return builtin_argument.NonEmptyString(value, FieldAttr)
},
},
{
Name: IntervalAttr,
IsOptional: true,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
return builtin_argument.Duration(value, IntervalAttr)
},
},
{
Name: TimeoutAttr,
IsOptional: true,
ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
return builtin_argument.Duration(value, TimeoutAttr)
},
},
},
},
Instantiate: instantiateReadyConditions,
}
}

func instantiateReadyConditions(arguments *builtin_argument.ArgumentValuesSet) (kurtosis_type_constructor.KurtosisValueType, *startosis_errors.InterpretationError) {
kurtosisValueType, err := kurtosis_type_constructor.CreateKurtosisStarlarkTypeDefault(ReadyConditionsTypeName, arguments)
if err != nil {
return nil, err
}
return &ReadyConditions{
KurtosisValueTypeDefault: kurtosisValueType,
}, nil
}

// ReadyConditions is a starlark.Value that holds all the information needed for ensuring service readiness
type ReadyConditions struct {
*kurtosis_type_constructor.KurtosisValueTypeDefault
}

func (readyConditions *ReadyConditions) GetRecipe() (recipe.Recipe, *startosis_errors.InterpretationError) {
var genericRecipe recipe.Recipe

httpRecipe, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[*recipe.HttpRequestRecipe](readyConditions.KurtosisValueTypeDefault, RecipeAttr)
genericRecipe = httpRecipe
if !found {
return nil, startosis_errors.NewInterpretationError("Required attribute '%s' could not be found on type '%s'",
RecipeAttr, ReadyConditionsTypeName)
}
//TODO we should rework the recipe types to inherit a single common type, this will avoid the double parsing here.
if interpretationErr != nil {
execRecipe, _, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[*recipe.ExecRecipe](readyConditions.KurtosisValueTypeDefault, RecipeAttr)
if interpretationErr != nil {
return nil, interpretationErr
}
genericRecipe = execRecipe
}

return genericRecipe, nil
}

func (readyConditions *ReadyConditions) GetField() (string, *startosis_errors.InterpretationError) {
field, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](readyConditions.KurtosisValueTypeDefault, FieldAttr)
if interpretationErr != nil {
return "", interpretationErr
}
if !found {
return "", startosis_errors.NewInterpretationError("Required attribute '%s' could not be found on type '%s'",
FieldAttr, ReadyConditionsTypeName)
}
fieldStr := field.GoString()

return fieldStr, nil
}

func (readyConditions *ReadyConditions) GetAssertion() (string, *startosis_errors.InterpretationError) {
assertion, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](readyConditions.KurtosisValueTypeDefault, AssertionAttr)
if interpretationErr != nil {
return "", interpretationErr
}
if !found {
return "", startosis_errors.NewInterpretationError("Required attribute '%s' could not be found on type '%s'",
AssertionAttr, ReadyConditionsTypeName)
}
assertionStr := assertion.GoString()

return assertionStr, nil
}

func (readyConditions *ReadyConditions) GetTarget() (starlark.Comparable, *startosis_errors.InterpretationError) {
target, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.Comparable](readyConditions.KurtosisValueTypeDefault, TargetAttr)
if interpretationErr != nil {
return nil, interpretationErr
}
if !found {
return nil, startosis_errors.NewInterpretationError("Required attribute '%s' could not be found on type '%s'",
TargetAttr, ReadyConditionsTypeName)
}

return target, nil
}

func (readyConditions *ReadyConditions) GetInterval() (time.Duration, *startosis_errors.InterpretationError) {
interval := defaultInterval

intervalStr, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](readyConditions.KurtosisValueTypeDefault, IntervalAttr)
if interpretationErr != nil {
return interval, interpretationErr
}
if found {
parsedInterval, parseErr := time.ParseDuration(intervalStr.GoString())
if parseErr != nil {
return interval, startosis_errors.WrapWithInterpretationError(parseErr, "An error occurred when parsing interval '%v'", intervalStr.GoString())
}
interval = parsedInterval
}

return interval, nil
}

func (readyConditions *ReadyConditions) GetTimeout() (time.Duration, *startosis_errors.InterpretationError) {
timeout := defaultTimeout

timeoutStr, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](readyConditions.KurtosisValueTypeDefault, TimeoutAttr)
if interpretationErr != nil {
return timeout, interpretationErr
}
if found {
parsedTimeout, parseErr := time.ParseDuration(timeoutStr.GoString())
if parseErr != nil {
return timeout, startosis_errors.WrapWithInterpretationError(parseErr, "An error occurred when parsing timeout '%v'", timeoutStr.GoString())
}
timeout = parsedTimeout
}

return timeout, nil
}

func validateRecipe(value starlark.Value) *startosis_errors.InterpretationError {
_, ok := value.(*recipe.HttpRequestRecipe)
if !ok {
//TODO we should rework the recipe types to inherit a single common type, this will avoid the double parsing here.
_, ok := value.(*recipe.ExecRecipe)
if !ok {
return startosis_errors.NewInterpretationError("The '%s' attribute is not a Recipe (was '%s').", RecipeAttr, reflect.TypeOf(value))
}
}
return nil
}

0 comments on commit c53d6e2

Please sign in to comment.