diff --git a/core/server/api_container/main.go b/core/server/api_container/main.go index 578470031e..268b664184 100644 --- a/core/server/api_container/main.go +++ b/core/server/api_container/main.go @@ -8,6 +8,7 @@ package main import ( "context" "fmt" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/interpretation_time_value_store" "net" "os" "path" @@ -203,6 +204,11 @@ func runMain() error { return stacktrace.Propagate(err, "An error occurred creating the runtime value store") } + interpretationTimeValueStore, err := interpretation_time_value_store.CreateInterpretationTimeValueStore(enclaveDb, starlarkValueSerde) + if err != nil { + return stacktrace.Propagate(err, "an error occurred while creating the interpretation time value store") + } + // Load the current enclave plan, in case the enclave is being restarted enclavePlan, err := enclave_plan_persistence.Load(enclaveDb) if err != nil { @@ -211,7 +217,7 @@ func runMain() error { // TODO: Consolidate Interpreter, Validator and Executor into a single interface startosisRunner := startosis_engine.NewStartosisRunner( - startosis_engine.NewStartosisInterpreter(serviceNetwork, gitPackageContentProvider, runtimeValueStore, starlarkValueSerde, serverArgs.EnclaveEnvVars), + startosis_engine.NewStartosisInterpreter(serviceNetwork, gitPackageContentProvider, runtimeValueStore, starlarkValueSerde, serverArgs.EnclaveEnvVars, interpretationTimeValueStore), startosis_engine.NewStartosisValidator(&kurtosisBackend, serviceNetwork, filesArtifactStore), startosis_engine.NewStartosisExecutor(starlarkValueSerde, runtimeValueStore, enclavePlan, enclaveDb)) diff --git a/core/server/api_container/server/startosis_engine/interpretation_time_value_store/interpretation_time_value_store.go b/core/server/api_container/server/startosis_engine/interpretation_time_value_store/interpretation_time_value_store.go new file mode 100644 index 0000000000..af89cd79cc --- /dev/null +++ b/core/server/api_container/server/startosis_engine/interpretation_time_value_store/interpretation_time_value_store.go @@ -0,0 +1,36 @@ +package interpretation_time_value_store + +import ( + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/database_accessors/enclave_db" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_types" + "github.com/kurtosis-tech/stacktrace" +) + +type InterpretationTimeValueStore struct { + serviceValues *serviceInterpretationValueRepository + serde *kurtosis_types.StarlarkValueSerde +} + +func CreateInterpretationTimeValueStore(enclaveDb *enclave_db.EnclaveDB, serde *kurtosis_types.StarlarkValueSerde) (*InterpretationTimeValueStore, error) { + serviceValuesRepository, err := getOrCreateNewServiceInterpretationTimeValueRepository(enclaveDb, serde) + if err != nil { + return nil, stacktrace.Propagate(err, "An error occurred creating interpretation time value store") + } + return &InterpretationTimeValueStore{serviceValues: serviceValuesRepository, serde: serde}, nil +} + +func (itvs *InterpretationTimeValueStore) PutService(name service.ServiceName, service *kurtosis_types.Service) error { + if err := itvs.serviceValues.PutService(name, service); err != nil { + return stacktrace.Propagate(err, "An error occurred while adding value '%v' for service '%v' to db", service, name) + } + return nil +} + +func (itvs *InterpretationTimeValueStore) GetService(name service.ServiceName) (*kurtosis_types.Service, error) { + serviceStarlark, err := itvs.serviceValues.GetService(name) + if err != nil { + return nil, stacktrace.Propagate(err, "An error occurred fetching interpretation time value for '%v' from db", name) + } + return serviceStarlark, nil +} diff --git a/core/server/api_container/server/startosis_engine/interpretation_time_value_store/service_value_repository.go b/core/server/api_container/server/startosis_engine/interpretation_time_value_store/service_value_repository.go new file mode 100644 index 0000000000..f3c44bbe54 --- /dev/null +++ b/core/server/api_container/server/startosis_engine/interpretation_time_value_store/service_value_repository.go @@ -0,0 +1,113 @@ +package interpretation_time_value_store + +import ( + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/database_accessors/enclave_db" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_types" + "github.com/kurtosis-tech/stacktrace" + "github.com/sirupsen/logrus" + bolt "go.etcd.io/bbolt" +) + +var ( + serviceInterpretationValueBucketName = []byte("service-interpretation-value") + emptyValue = []byte{} +) + +type serviceInterpretationValueRepository struct { + enclaveDb *enclave_db.EnclaveDB + starlarkValueSerde *kurtosis_types.StarlarkValueSerde +} + +func getOrCreateNewServiceInterpretationTimeValueRepository( + enclaveDb *enclave_db.EnclaveDB, + starlarkValueSerde *kurtosis_types.StarlarkValueSerde, +) (*serviceInterpretationValueRepository, error) { + if err := enclaveDb.Update(func(tx *bolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(serviceInterpretationValueBucketName) + if err != nil { + return stacktrace.Propagate(err, "An error occurred while creating the bucket for the service interpretation time value repository") + } + logrus.Debugf("Service value interpretation time store bucket: '%+v'", bucket) + + return nil + }); err != nil { + return nil, stacktrace.Propagate(err, "An error occurred while building service interpretation time value repository") + } + + repository := &serviceInterpretationValueRepository{ + enclaveDb: enclaveDb, + starlarkValueSerde: starlarkValueSerde, + } + + return repository, nil +} + +func (repository *serviceInterpretationValueRepository) PutService(name service.ServiceName, service *kurtosis_types.Service) error { + logrus.Debugf("Saving service interpretation value '%v' for service with name '%v' to", service, name) + if err := repository.enclaveDb.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket(serviceInterpretationValueBucketName) + + serviceNameKey := getKey(name) + serializedValue := repository.starlarkValueSerde.Serialize(service) + + // save it to disk + if err := bucket.Put(serviceNameKey, []byte(serializedValue)); err != nil { + return stacktrace.Propagate(err, "An error occurred while saving service interpretation time value '%v' for service '%v'", serializedValue, serviceNameKey) + } + return nil + }); err != nil { + return stacktrace.Propagate(err, "An error occurred while saving service interpretation time value '%v' for service '%v'", service, name) + } + logrus.Debugf("Succesfully saved service '%v'", name) + return nil +} + +func (repository *serviceInterpretationValueRepository) GetService(name service.ServiceName) (*kurtosis_types.Service, error) { + logrus.Debugf("Getting service interpretation time value for service '%v'", name) + var value *kurtosis_types.Service + + if err := repository.enclaveDb.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket(serviceInterpretationValueBucketName) + + serviceNameKey := getKey(name) + + serviceSerializedValue := bucket.Get(serviceNameKey) + + // check for existence + if serviceSerializedValue == nil { + return stacktrace.NewError("Service '%v' doesn't exist in the repository", name) + } + + isEmptyValue := len(serviceSerializedValue) == len(emptyValue) + + serviceSerializedValueStr := string(serviceSerializedValue) + + // if an empty value was found we return an error + if isEmptyValue { + return stacktrace.NewError("An empty value was found for service '%v'; this is a bug in Kurtosis", name) + } + + deserializedValue, interpretationErr := repository.starlarkValueSerde.Deserialize(serviceSerializedValueStr) + if interpretationErr != nil { + return stacktrace.Propagate(interpretationErr, "an error occurred while deserializing object associated with service '%v' in repository", name) + } + + var ok bool + value, ok = deserializedValue.(*kurtosis_types.Service) + if !ok { + return stacktrace.NewError("An error occurred converting the deserialized value '%v' into required internal type", deserializedValue) + } + + return nil + }); err != nil { + return nil, stacktrace.Propagate(err, "An error occurred while getting service '%v' from db", name) + } + logrus.Debugf("Successfully got value for '%v'", name) + return value, nil + +} + +func getKey(name service.ServiceName) []byte { + return []byte(name) +} diff --git a/core/server/api_container/server/startosis_engine/interpretation_time_value_store/service_value_repository_test.go b/core/server/api_container/server/startosis_engine/interpretation_time_value_store/service_value_repository_test.go new file mode 100644 index 0000000000..9b7db03a01 --- /dev/null +++ b/core/server/api_container/server/startosis_engine/interpretation_time_value_store/service_value_repository_test.go @@ -0,0 +1,102 @@ +package interpretation_time_value_store + +import ( + port_spec_core "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/port_spec" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/database_accessors/enclave_db" + "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/port_spec" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" + "os" + "testing" +) + +const ( + starlarkThreadName = "thread-for-db-test" + serviceName = service.ServiceName("datastore-1") + serviceNameStarlarkStr = starlark.String(serviceName) + hostName = serviceNameStarlarkStr + ipAddress = starlark.String("172.23.34.44") +) + +func TestPutGet_Succeed(t *testing.T) { + repository := getServiceInterpretationTimeValueRepository(t) + require.NotNil(t, repository) + + applicationProtocol := "" + maybeUrl := "" + + port, interpretationErr := port_spec.CreatePortSpecUsingGoValues( + string(serviceName), + uint16(443), + port_spec_core.TransportProtocol_TCP, + &applicationProtocol, + "10s", + &maybeUrl, + ) + require.Nil(t, interpretationErr) + ports := starlark.NewDict(1) + require.NoError(t, ports.SetKey(starlark.String("http"), port)) + + expectedService, interpretationErr := kurtosis_types.CreateService(serviceNameStarlarkStr, hostName, ipAddress, ports) + require.Nil(t, interpretationErr) + + err := repository.PutService(serviceName, expectedService) + require.Nil(t, err) + + actualService, err := repository.GetService(serviceName) + require.Nil(t, err) + require.Equal(t, expectedService.AttrNames(), actualService.AttrNames()) + require.Equal(t, expectedService.String(), actualService.String()) +} + +func TestPutGetFail_ForMissingServiceName(t *testing.T) { + repository := getServiceInterpretationTimeValueRepository(t) + require.NotNil(t, repository) + + actualService, err := repository.GetService(serviceName) + require.Error(t, err) + require.Nil(t, actualService) +} + +func getServiceInterpretationTimeValueRepository(t *testing.T) *serviceInterpretationValueRepository { + file, err := os.CreateTemp("/tmp", "*.db") + defer func() { + err = os.Remove(file.Name()) + require.NoError(t, err) + }() + + require.NoError(t, err) + db, err := bolt.Open(file.Name(), 0666, nil) + require.NoError(t, err) + enclaveDb := &enclave_db.EnclaveDB{ + DB: db, + } + + dummySerde := newDummyStarlarkValueSerDeForTest() + + repository, err := getOrCreateNewServiceInterpretationTimeValueRepository(enclaveDb, dummySerde) + require.NoError(t, err) + + return repository +} + +func newDummyStarlarkValueSerDeForTest() *kurtosis_types.StarlarkValueSerde { + thread := &starlark.Thread{ + Name: starlarkThreadName, + Print: nil, + Load: nil, + OnMaxSteps: nil, + Steps: 0, + } + starlarkEnv := starlark.StringDict{ + starlarkstruct.Default.GoString(): starlark.NewBuiltin(starlarkstruct.Default.GoString(), starlarkstruct.Make), + + kurtosis_types.ServiceTypeName: starlark.NewBuiltin(kurtosis_types.ServiceTypeName, kurtosis_types.NewServiceType().CreateBuiltin()), + port_spec.PortSpecTypeName: starlark.NewBuiltin(port_spec.PortSpecTypeName, port_spec.NewPortSpecType().CreateBuiltin()), + } + return kurtosis_types.NewStarlarkValueSerde(thread, starlarkEnv) +} diff --git a/core/server/api_container/server/startosis_engine/kurtosis_builtins.go b/core/server/api_container/server/startosis_engine/kurtosis_builtins.go index fc8df636b6..3152a6feb2 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_builtins.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_builtins.go @@ -6,8 +6,10 @@ import ( "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/builtins/import_module" "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/builtins/read_file" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/interpretation_time_value_store" "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/exec" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/get_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" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_instruction/render_templates" @@ -60,10 +62,12 @@ func KurtosisPlanInstructions( packageContentProvider startosis_packages.PackageContentProvider, packageReplaceOptions map[string]string, nonBlockingMode bool, + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore, ) []*kurtosis_plan_instruction.KurtosisPlanInstruction { return []*kurtosis_plan_instruction.KurtosisPlanInstruction{ - add_service.NewAddService(serviceNetwork, runtimeValueStore, packageId, packageContentProvider, packageReplaceOptions), - add_service.NewAddServices(serviceNetwork, runtimeValueStore, packageId, packageContentProvider, packageReplaceOptions), + add_service.NewAddService(serviceNetwork, runtimeValueStore, packageId, packageContentProvider, packageReplaceOptions, interpretationTimeValueStore), + add_service.NewAddServices(serviceNetwork, runtimeValueStore, packageId, packageContentProvider, packageReplaceOptions, interpretationTimeValueStore), + get_service.NewGetService(interpretationTimeValueStore), verify.NewVerify(runtimeValueStore), exec.NewExec(serviceNetwork, runtimeValueStore), kurtosis_print.NewPrint(serviceNetwork, runtimeValueStore), diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service.go index a3686e9a6d..1fbef6e632 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service.go @@ -7,6 +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/enclave_plan_persistence" "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/interpretation_time_value_store" "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_plan_instruction" @@ -32,7 +33,8 @@ func NewAddService( runtimeValueStore *runtime_value_store.RuntimeValueStore, packageId string, packageContentProvider startosis_packages.PackageContentProvider, - packageReplaceOptions map[string]string) *kurtosis_plan_instruction.KurtosisPlanInstruction { + packageReplaceOptions map[string]string, + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore) *kurtosis_plan_instruction.KurtosisPlanInstruction { return &kurtosis_plan_instruction.KurtosisPlanInstruction{ KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{ Name: AddServiceBuiltinName, @@ -74,6 +76,8 @@ func NewAddService( resultUuid: "", // populated at interpretation time readyCondition: nil, // populated at interpretation time + + interpretationTimeValueStore: interpretationTimeValueStore, } }, @@ -97,6 +101,8 @@ type AddServiceCapabilities struct { packageContentProvider startosis_packages.PackageContentProvider packageReplaceOptions map[string]string + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore + resultUuid string } @@ -134,6 +140,11 @@ func (builtin *AddServiceCapabilities) Interpret(locatorOfModuleInWhichThisBuilt if interpretationErr != nil { return nil, interpretationErr } + + err = builtin.interpretationTimeValueStore.PutService(builtin.serviceName, returnValue) + if err != nil { + return nil, startosis_errors.WrapWithInterpretationError(err, "An error occurred while persisting return value for service '%v'", serviceName) + } return returnValue, nil } diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_services.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_services.go index a99eaa6ee2..035ee5a15d 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_services.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_services.go @@ -7,6 +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/enclave_plan_persistence" "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/interpretation_time_value_store" "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_plan_instruction" @@ -35,7 +36,8 @@ func NewAddServices( runtimeValueStore *runtime_value_store.RuntimeValueStore, packageId string, packageContentProvider startosis_packages.PackageContentProvider, - packageReplaceOptions map[string]string) *kurtosis_plan_instruction.KurtosisPlanInstruction { + packageReplaceOptions map[string]string, + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore) *kurtosis_plan_instruction.KurtosisPlanInstruction { return &kurtosis_plan_instruction.KurtosisPlanInstruction{ KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{ Name: AddServicesBuiltinName, @@ -59,12 +61,13 @@ func NewAddServices( Capabilities: func() kurtosis_plan_instruction.KurtosisPlanInstructionCapabilities { return &AddServicesCapabilities{ - serviceNetwork: serviceNetwork, - runtimeValueStore: runtimeValueStore, - packageId: packageId, - packageContentProvider: packageContentProvider, - packageReplaceOptions: packageReplaceOptions, - serviceConfigs: nil, // populated at interpretation time + serviceNetwork: serviceNetwork, + runtimeValueStore: runtimeValueStore, + packageId: packageId, + packageContentProvider: packageContentProvider, + packageReplaceOptions: packageReplaceOptions, + serviceConfigs: nil, // populated at interpretation time + interpretationTimeValueStore: interpretationTimeValueStore, resultUuids: map[service.ServiceName]string{}, // populated at interpretation time readyConditions: nil, // populated at interpretation time @@ -80,11 +83,12 @@ func NewAddServices( } type AddServicesCapabilities struct { - serviceNetwork service_network.ServiceNetwork - runtimeValueStore *runtime_value_store.RuntimeValueStore - packageId string - packageContentProvider startosis_packages.PackageContentProvider - packageReplaceOptions map[string]string + serviceNetwork service_network.ServiceNetwork + runtimeValueStore *runtime_value_store.RuntimeValueStore + packageId string + packageContentProvider startosis_packages.PackageContentProvider + packageReplaceOptions map[string]string + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore serviceConfigs map[service.ServiceName]*service.ServiceConfig @@ -112,7 +116,7 @@ func (builtin *AddServicesCapabilities) Interpret(locatorOfModuleInWhichThisBuil builtin.serviceConfigs = serviceConfigs builtin.readyConditions = readyConditions - resultUuids, returnValue, interpretationErr := makeAddServicesInterpretationReturnValue(builtin.serviceConfigs, builtin.runtimeValueStore) + resultUuids, returnValue, interpretationErr := makeAndPersistAddServicesInterpretationReturnValue(builtin.serviceConfigs, builtin.runtimeValueStore, builtin.interpretationTimeValueStore) if interpretationErr != nil { return nil, interpretationErr } @@ -202,7 +206,7 @@ func (builtin *AddServicesCapabilities) Execute(ctx context.Context, _ *builtin_ serviceMsg := fmt.Sprintf("Service '%v' error:\n%v\n", serviceName, serviceErr) allServiceChecksErrMsg = allServiceChecksErrMsg + serviceMsg } - return "", stacktrace.NewError("An error occurred while checking al service, these are the errors by service:\n%s", allServiceChecksErrMsg) + return "", stacktrace.NewError("An error occurred while checking all service, these are the errors by service:\n%s", allServiceChecksErrMsg) } defer func() { if shouldDeleteAllStartedServices { @@ -450,7 +454,7 @@ func validateAndConvertConfigsAndReadyConditions( return convertedServiceConfigs, readyConditionsByServiceName, nil } -func makeAddServicesInterpretationReturnValue(serviceConfigs map[service.ServiceName]*service.ServiceConfig, runtimeValueStore *runtime_value_store.RuntimeValueStore) (map[service.ServiceName]string, *starlark.Dict, *startosis_errors.InterpretationError) { +func makeAndPersistAddServicesInterpretationReturnValue(serviceConfigs map[service.ServiceName]*service.ServiceConfig, runtimeValueStore *runtime_value_store.RuntimeValueStore, interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore) (map[service.ServiceName]string, *starlark.Dict, *startosis_errors.InterpretationError) { servicesObjectDict := starlark.NewDict(len(serviceConfigs)) resultUuids := map[service.ServiceName]string{} var err error @@ -467,6 +471,9 @@ func makeAddServicesInterpretationReturnValue(serviceConfigs map[service.Service if err := servicesObjectDict.SetKey(serviceNameStr, serviceObject); err != nil { return nil, nil, startosis_errors.WrapWithInterpretationError(err, "Unable to generate the object that should be returned by the '%s' builtin", AddServicesBuiltinName) } + if err = interpretationTimeValueStore.PutService(serviceName, serviceObject); err != nil { + return nil, nil, startosis_errors.WrapWithInterpretationError(err, "An error occurred while persisting the return value for service with name '%v'", serviceName) + } } return resultUuids, servicesObjectDict, nil } diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/get_service/get_service.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/get_service/get_service.go new file mode 100644 index 0000000000..868034c0bb --- /dev/null +++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/get_service/get_service.go @@ -0,0 +1,99 @@ +package get_service + +import ( + "context" + "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/enclave_plan_persistence" + "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/interpretation_time_value_store" + "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_plan_instruction" + "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" + "go.starlark.net/starlark" +) + +const ( + GetServiceBuiltinName = "get_service" + ServiceNameArgName = "name" +) + +func NewGetService(interpretationTimeStore *interpretation_time_value_store.InterpretationTimeValueStore) *kurtosis_plan_instruction.KurtosisPlanInstruction { + return &kurtosis_plan_instruction.KurtosisPlanInstruction{ + KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{ + Name: GetServiceBuiltinName, + Arguments: []*builtin_argument.BuiltinArgument{ + { + Name: ServiceNameArgName, + IsOptional: false, + ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String], + Validator: func(value starlark.Value) *startosis_errors.InterpretationError { + return builtin_argument.NonEmptyString(value, ServiceNameArgName) + }, + }, + }, + Deprecation: nil, + }, + Capabilities: func() kurtosis_plan_instruction.KurtosisPlanInstructionCapabilities { + return &GetServiceCapabilities{interpretationTimeStore: interpretationTimeStore, serviceName: ""} + }, + DefaultDisplayArguments: map[string]bool{ + ServiceNameArgName: true, + }, + } +} + +type GetServiceCapabilities struct { + interpretationTimeStore *interpretation_time_value_store.InterpretationTimeValueStore + serviceName service.ServiceName +} + +func (builtin *GetServiceCapabilities) Interpret(_ string, arguments *builtin_argument.ArgumentValuesSet) (starlark.Value, *startosis_errors.InterpretationError) { + serviceNameArgumentValue, err := builtin_argument.ExtractArgumentValue[starlark.String](arguments, ServiceNameArgName) + if err != nil { + return nil, startosis_errors.WrapWithInterpretationError(err, "Unable to extract value for '%s' argument", ServiceNameArgName) + } + serviceName := service.ServiceName(serviceNameArgumentValue.GoString()) + + builtin.serviceName = serviceName + + serviceStarlarkValue, err := builtin.interpretationTimeStore.GetService(serviceName) + if err != nil { + return nil, startosis_errors.WrapWithInterpretationError(err, "An error occurred while fetching service '%v' from the store", serviceName) + } + + return serviceStarlarkValue, nil +} + +func (builtin *GetServiceCapabilities) Validate(_ *builtin_argument.ArgumentValuesSet, validatorEnvironment *startosis_validator.ValidatorEnvironment) *startosis_errors.ValidationError { + if exists := validatorEnvironment.DoesServiceNameExist(builtin.serviceName); exists == startosis_validator.ComponentNotFound { + return startosis_errors.NewValidationError("Service '%v' required by '%v' instruction doesn't exist", builtin.serviceName, GetServiceBuiltinName) + } + return nil +} + +func (builtin *GetServiceCapabilities) Execute(_ context.Context, _ *builtin_argument.ArgumentValuesSet) (string, error) { + // Note this is a no-op. + // Perhaps this instruction should be like `read_file` instead and not a part of any plan + // But that shouldn't be done outside a function; so it's here for now + return fmt.Sprintf("Fetched service '%v'", builtin.serviceName), nil +} + +func (builtin *GetServiceCapabilities) TryResolveWith(instructionsAreEqual bool, _ *enclave_plan_persistence.EnclavePlanInstruction, enclaveComponents *enclave_structure.EnclaveComponents) enclave_structure.InstructionResolutionStatus { + if instructionsAreEqual && enclaveComponents.HasServiceBeenUpdated(builtin.serviceName) { + return enclave_structure.InstructionIsUpdate + } else if instructionsAreEqual { + return enclave_structure.InstructionIsEqual + } + return enclave_structure.InstructionIsUnknown +} + +func (builtin *GetServiceCapabilities) FillPersistableAttributes(builder *enclave_plan_persistence.EnclavePlanInstructionBuilder) { + builder.SetType(GetServiceBuiltinName).AddServiceName(builtin.serviceName) +} + +func (builtin *GetServiceCapabilities) Description() string { + return fmt.Sprintf("Fetched service '%v'", builtin.serviceName) +} diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_service_framework_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_service_framework_test.go index adb5eb05ac..774ace3ec0 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_service_framework_test.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_service_framework_test.go @@ -2,6 +2,7 @@ package test_engine import ( "fmt" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/interpretation_time_value_store" "testing" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_packages/mock_package_content_provider" @@ -22,9 +23,10 @@ import ( type addServiceTestCase struct { *testing.T - serviceNetwork *service_network.MockServiceNetwork - runtimeValueStore *runtime_value_store.RuntimeValueStore - packageContentProvider *mock_package_content_provider.MockPackageContentProvider + serviceNetwork *service_network.MockServiceNetwork + runtimeValueStore *runtime_value_store.RuntimeValueStore + packageContentProvider *mock_package_content_provider.MockPackageContentProvider + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore } func (suite *KurtosisPlanInstructionTestSuite) TestAddService() { @@ -67,10 +69,11 @@ func (suite *KurtosisPlanInstructionTestSuite) TestAddService() { ) suite.run(&addServiceTestCase{ - T: suite.T(), - serviceNetwork: suite.serviceNetwork, - runtimeValueStore: suite.runtimeValueStore, - packageContentProvider: suite.packageContentProvider, + T: suite.T(), + serviceNetwork: suite.serviceNetwork, + runtimeValueStore: suite.runtimeValueStore, + packageContentProvider: suite.packageContentProvider, + interpretationTimeValueStore: suite.interpretationTimeValueStore, }) } @@ -80,7 +83,8 @@ func (t *addServiceTestCase) GetInstruction() *kurtosis_plan_instruction.Kurtosi t.runtimeValueStore, testModulePackageId, t.packageContentProvider, - testNoPackageReplaceOptions) + testNoPackageReplaceOptions, + t.interpretationTimeValueStore) } func (t *addServiceTestCase) GetStarlarkCode() string { diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_services_framework_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_services_framework_test.go index 7912c700a0..e70931009e 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_services_framework_test.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_services_framework_test.go @@ -2,6 +2,7 @@ package test_engine import ( "fmt" + "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/interpretation_time_value_store" "io" "net/http" "net/url" @@ -25,9 +26,10 @@ import ( type addServicesTestCase struct { *testing.T - serviceNetwork *service_network.MockServiceNetwork - runtimeValueStore *runtime_value_store.RuntimeValueStore - packageContentProvider *mock_package_content_provider.MockPackageContentProvider + serviceNetwork *service_network.MockServiceNetwork + runtimeValueStore *runtime_value_store.RuntimeValueStore + packageContentProvider *mock_package_content_provider.MockPackageContentProvider + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore } func (suite *KurtosisPlanInstructionTestSuite) TestAddServices() { @@ -201,10 +203,11 @@ func (suite *KurtosisPlanInstructionTestSuite) TestAddServices() { }, nil) suite.run(&addServicesTestCase{ - T: suite.T(), - serviceNetwork: suite.serviceNetwork, - runtimeValueStore: suite.runtimeValueStore, - packageContentProvider: suite.packageContentProvider, + T: suite.T(), + serviceNetwork: suite.serviceNetwork, + runtimeValueStore: suite.runtimeValueStore, + packageContentProvider: suite.packageContentProvider, + interpretationTimeValueStore: suite.interpretationTimeValueStore, }) } @@ -214,7 +217,8 @@ func (t *addServicesTestCase) GetInstruction() *kurtosis_plan_instruction.Kurtos t.runtimeValueStore, testModulePackageId, t.packageContentProvider, - testNoPackageReplaceOptions) + testNoPackageReplaceOptions, + t.interpretationTimeValueStore) } func (t *addServicesTestCase) GetStarlarkCode() string { diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/suite_kurtosis_plan_instruction_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/suite_kurtosis_plan_instruction_test.go index 31db6acde1..468618d602 100644 --- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/suite_kurtosis_plan_instruction_test.go +++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/suite_kurtosis_plan_instruction_test.go @@ -6,6 +6,7 @@ import ( "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/interpretation_time_value_store" "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/runtime_value_store" @@ -26,9 +27,10 @@ type KurtosisPlanInstructionTestSuite struct { starlarkThread *starlark.Thread starlarkEnv starlark.StringDict - serviceNetwork *service_network.MockServiceNetwork - runtimeValueStore *runtime_value_store.RuntimeValueStore - packageContentProvider *mock_package_content_provider.MockPackageContentProvider + serviceNetwork *service_network.MockServiceNetwork + runtimeValueStore *runtime_value_store.RuntimeValueStore + packageContentProvider *mock_package_content_provider.MockPackageContentProvider + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore } func TestKurtosisPlanInstructionSuite(t *testing.T) { @@ -47,6 +49,10 @@ func (suite *KurtosisPlanInstructionTestSuite) SetupTest() { suite.Require().NoError(err) suite.runtimeValueStore = runtimeValueStoreForTest + interpretationTimeValueStore, err := interpretation_time_value_store.CreateInterpretationTimeValueStore(enclaveDb, serde) + suite.Require().NoError(err) + suite.interpretationTimeValueStore = interpretationTimeValueStore + suite.packageContentProvider = mock_package_content_provider.NewMockPackageContentProvider() } 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 ed23c5bd73..91740fe74d 100644 --- a/core/server/api_container/server/startosis_engine/startosis_interpreter.go +++ b/core/server/api_container/server/startosis_engine/startosis_interpreter.go @@ -11,6 +11,7 @@ import ( "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/interpretation_time_value_store" "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/kurtosis_types" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/package_io" @@ -56,21 +57,23 @@ type StartosisInterpreter struct { serviceNetwork service_network.ServiceNetwork recipeExecutor *runtime_value_store.RuntimeValueStore // TODO AUTH there will be a leak here in case people with different repo visibility access a module - packageContentProvider startosis_packages.PackageContentProvider - starlarkValueSerde *kurtosis_types.StarlarkValueSerde - enclaveEnvVars string + packageContentProvider startosis_packages.PackageContentProvider + starlarkValueSerde *kurtosis_types.StarlarkValueSerde + enclaveEnvVars string + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore } type SerializedInterpretationOutput string -func NewStartosisInterpreter(serviceNetwork service_network.ServiceNetwork, packageContentProvider startosis_packages.PackageContentProvider, runtimeValueStore *runtime_value_store.RuntimeValueStore, starlarkValueSerde *kurtosis_types.StarlarkValueSerde, enclaveVarEnvs string) *StartosisInterpreter { +func NewStartosisInterpreter(serviceNetwork service_network.ServiceNetwork, packageContentProvider startosis_packages.PackageContentProvider, runtimeValueStore *runtime_value_store.RuntimeValueStore, starlarkValueSerde *kurtosis_types.StarlarkValueSerde, enclaveVarEnvs string, interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore) *StartosisInterpreter { return &StartosisInterpreter{ - mutex: &sync.Mutex{}, - serviceNetwork: serviceNetwork, - recipeExecutor: runtimeValueStore, - packageContentProvider: packageContentProvider, - enclaveEnvVars: enclaveVarEnvs, - starlarkValueSerde: starlarkValueSerde, + mutex: &sync.Mutex{}, + serviceNetwork: serviceNetwork, + recipeExecutor: runtimeValueStore, + packageContentProvider: packageContentProvider, + enclaveEnvVars: enclaveVarEnvs, + starlarkValueSerde: starlarkValueSerde, + interpretationTimeValueStore: interpretationTimeValueStore, } } @@ -270,7 +273,7 @@ func (interpreter *StartosisInterpreter) Interpret( if mainFuncParamsNum >= minimumParamsRequiredForPlan { firstParamName, _ := mainFunction.Param(planParamIndex) if firstParamName == planParamName { - kurtosisPlanInstructions := KurtosisPlanInstructions(packageId, interpreter.serviceNetwork, interpreter.recipeExecutor, interpreter.packageContentProvider, packageReplaceOptions, nonBlockingMode) + kurtosisPlanInstructions := KurtosisPlanInstructions(packageId, interpreter.serviceNetwork, interpreter.recipeExecutor, interpreter.packageContentProvider, packageReplaceOptions, nonBlockingMode, interpreter.interpretationTimeValueStore) planModule := plan_module.PlanModule(newInstructionsPlan, enclaveComponents, interpreter.starlarkValueSerde, instructionsPlanMask, kurtosisPlanInstructions) argsTuple = append(argsTuple, planModule) } diff --git a/core/server/api_container/server/startosis_engine/startosis_interpreter_idempotent_test.go b/core/server/api_container/server/startosis_engine/startosis_interpreter_idempotent_test.go index 76407536c5..f60f45a376 100644 --- a/core/server/api_container/server/startosis_engine/startosis_interpreter_idempotent_test.go +++ b/core/server/api_container/server/startosis_engine/startosis_interpreter_idempotent_test.go @@ -9,6 +9,7 @@ import ( "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/interpretation_time_value_store" "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/port_spec" "github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/runtime_value_store" @@ -62,12 +63,15 @@ func (suite *StartosisInterpreterIdempotentTestSuite) SetupTest() { runtimeValueStore, err := runtime_value_store.CreateRuntimeValueStore(starlarkValueSerde, enclaveDb) require.NoError(suite.T(), err) + interpretationTimeValueStore, err := interpretation_time_value_store.CreateInterpretationTimeValueStore(enclaveDb, starlarkValueSerde) + require.Nil(suite.T(), err) + serviceNetwork := service_network.NewMockServiceNetwork(suite.T()) serviceNetwork.EXPECT().GetApiContainerInfo().Maybe().Return( service_network.NewApiContainerInfo(net.IPv4(0, 0, 0, 0), uint16(1234), "0.0.0"), ) serviceNetwork.EXPECT().GetEnclaveUuid().Maybe().Return(enclaveUuid) - suite.interpreter = NewStartosisInterpreter(serviceNetwork, suite.packageContentProvider, runtimeValueStore, starlarkValueSerde, "") + suite.interpreter = NewStartosisInterpreter(serviceNetwork, suite.packageContentProvider, runtimeValueStore, starlarkValueSerde, "", interpretationTimeValueStore) } func TestRunStartosisInterpreterIdempotentTestSuite(t *testing.T) { 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 4f560c25b3..c585fc3717 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 @@ -13,6 +13,7 @@ import ( "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/interpretation_time_value_store" "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" @@ -53,9 +54,10 @@ const ( type StartosisInterpreterTestSuite struct { suite.Suite - serviceNetwork *service_network.MockServiceNetwork - packageContentProvider *mock_package_content_provider.MockPackageContentProvider - runtimeValueStore *runtime_value_store.RuntimeValueStore + serviceNetwork *service_network.MockServiceNetwork + packageContentProvider *mock_package_content_provider.MockPackageContentProvider + runtimeValueStore *runtime_value_store.RuntimeValueStore + interpretationTimeValueStore *interpretation_time_value_store.InterpretationTimeValueStore interpreter *StartosisInterpreter } @@ -71,7 +73,12 @@ func (suite *StartosisInterpreterTestSuite) SetupTest() { suite.runtimeValueStore = runtimeValueStore suite.serviceNetwork = service_network.NewMockServiceNetwork(suite.T()) - suite.interpreter = NewStartosisInterpreter(suite.serviceNetwork, suite.packageContentProvider, suite.runtimeValueStore, nil, "") + interpretationTimeValueStore, err := interpretation_time_value_store.CreateInterpretationTimeValueStore(enclaveDb, dummySerde) + require.NoError(suite.T(), err) + suite.interpretationTimeValueStore = interpretationTimeValueStore + require.NotNil(suite.T(), interpretationTimeValueStore) + + suite.interpreter = NewStartosisInterpreter(suite.serviceNetwork, suite.packageContentProvider, suite.runtimeValueStore, nil, "", suite.interpretationTimeValueStore) service.NewServiceRegistration( testServiceName, diff --git a/docs/docs/api-reference/starlark-reference/plan.md b/docs/docs/api-reference/starlark-reference/plan.md index c41ec9e5d9..d9ad45d683 100644 --- a/docs/docs/api-reference/starlark-reference/plan.md +++ b/docs/docs/api-reference/starlark-reference/plan.md @@ -97,6 +97,22 @@ services will be rolled back and the instruction will return an execution error. ::: +get_service +----------- + +The `get_service` instruction allows you to get a [Service][service-starlark-reference] object from a service name. This is +useful in situations if you don't have access to the [Service][service-starlark-reference] returned by the `add_service` or `add_services` +instructions anymore; perhaps you are in a different function or have imported and run another Kurtosis package. + +```python +# Returns a Service object (see the Service page in the sidebar) +service = plan.get_service( + # The name of the service to get + # MANDATORY + name = "my-service" +) +``` + verify ------ diff --git a/internal_testsuites/golang/testsuite/startosis_add_service_test/startosis_add_service_test.go b/internal_testsuites/golang/testsuite/startosis_add_service_test/startosis_add_service_test.go index 809a8a6c20..cbd8fad27b 100644 --- a/internal_testsuites/golang/testsuite/startosis_add_service_test/startosis_add_service_test.go +++ b/internal_testsuites/golang/testsuite/startosis_add_service_test/startosis_add_service_test.go @@ -36,6 +36,9 @@ def run(plan): datastore_1 = plan.add_service(name = SERVICE_NAME, config = config) datastore_2 = plan.add_service(name = SERVICE_NAME_2, config = config) + ds1_through_get = plan.get_service(SERVICE_NAME) + plan.print(ds1_through_get) + test_hostname_cmd = "nc -zv {0} {1}".format(datastore_1.hostname, GRPC_PORT) connection_result = plan.exec( recipe = ExecRecipe( @@ -67,6 +70,8 @@ func (suite *StartosisAddServiceTestSuite) TestAddTwoServicesAndTestConnection() expectedScriptOutput := `Adding services ` + serviceName + ` and ` + serviceName2 + ` Service '` + serviceName + `' added with service UUID '[a-z-0-9]+' Service '` + serviceName2 + `' added with service UUID '[a-z-0-9]+' +Fetched service '` + serviceName + `' +Service\(name="datastore-1", hostname="datastore-1", ip_address="[0-9\.]+", ports=\{"grpc": PortSpec\(number=1323, transport_protocol="TCP", wait="2m0s"\)\}\) Command returned with exit code '0' and the following output: -------------------- [a-z-0-9]+ \([0-9\.]+:1323\) open