Repository: kurtosis-tech/kurtosis
Pull request #2367 (2024-04-10 13:48:46)

feat: get all services
## Description
```
lots_of_services = import_module(".../main.star")

def run(plan.args):
   lots_of_services.run(plan, args)
   services = plan.get_services()
   for service in services:
        <do smth>
```

## Is this change user facing?
YES

### References
https://github.com/kurtosis-tech/kurtosis/issues/2349




Patch:
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
index af89cd79c..a6bb42bb2 100644
--- 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
@@ -34,3 +34,19 @@ func (itvs *InterpretationTimeValueStore) GetService(name service.ServiceName) (
 	}
 	return serviceStarlark, nil
 }
+
+func (itvs *InterpretationTimeValueStore) GetServices() ([]*kurtosis_types.Service, error) {
+	servicesStarlark, err := itvs.serviceValues.GetServices()
+	if err != nil {
+		return nil, stacktrace.Propagate(err, "An error occurred fetching interpretation time service objects from db")
+	}
+	return servicesStarlark, nil
+}
+
+func (itvs *InterpretationTimeValueStore) RemoveService(name service.ServiceName) error {
+	err := itvs.serviceValues.RemoveService(name)
+	if err != nil {
+		return stacktrace.Propagate(err, "An error occurred removing interpretation time service object for service '%v'", name)
+	}
+	return 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
index f3c44bbe5..84be9f704 100644
--- 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
@@ -105,7 +105,49 @@ func (repository *serviceInterpretationValueRepository) GetService(name service.
 	}
 	logrus.Debugf("Successfully got value for '%v'", name)
 	return value, nil
+}
+
+func (repository *serviceInterpretationValueRepository) GetServices() ([]*kurtosis_types.Service, error) {
+	logrus.Debug("Getting all known interpretation time service values.")
+	var services []*kurtosis_types.Service
+
+	if err := repository.enclaveDb.View(func(tx *bolt.Tx) error {
+		bucket := tx.Bucket(serviceInterpretationValueBucketName)
 
+		return bucket.ForEach(func(serviceName, serializedValue []byte) error {
+			deserializedValue, interpretationErr := repository.starlarkValueSerde.Deserialize(string(serializedValue))
+			if interpretationErr != nil {
+				return stacktrace.Propagate(interpretationErr, "an error occurred while deserializing object associated with service '%v' in repository", serviceName)
+			}
+
+			kurtosisServiceValue, ok := deserializedValue.(*kurtosis_types.Service)
+			if !ok {
+				return stacktrace.NewError("an error occurred casting repository service value to kurtosis service value for service: %v", serviceName)
+			}
+
+			services = append(services, kurtosisServiceValue)
+			return nil
+		})
+	}); err != nil {
+		return nil, stacktrace.Propagate(err, "An error occurred while getting services values from repository.")
+	}
+	logrus.Debugf("Successfully retrieved interpretation time service values.")
+	return services, nil
+}
+
+func (repository *serviceInterpretationValueRepository) RemoveService(name service.ServiceName) error {
+	logrus.Debugf("Removing service value for '%v' from service value repository...", name)
+	if err := repository.enclaveDb.Update(func(tx *bolt.Tx) error {
+		bucket := tx.Bucket(serviceInterpretationValueBucketName)
+
+		serviceNameKey := getKey(name)
+
+		return bucket.Delete(serviceNameKey)
+	}); err != nil {
+		return stacktrace.Propagate(err, "An error occurred while removing service '%v' from service value repository", name)
+	}
+	logrus.Debugf("Successfully removed service value for '%v'", name)
+	return nil
 }
 
 func getKey(name service.ServiceName) []byte {
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 4224f5e5b..9a318b22c 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_builtins.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_builtins.go
@@ -12,6 +12,7 @@ import (
 	"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_files_artifact"
 	"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/get_services"
 	"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"
@@ -71,11 +72,12 @@ func KurtosisPlanInstructions(
 		add_service.NewAddService(serviceNetwork, runtimeValueStore, packageId, packageContentProvider, packageReplaceOptions, interpretationTimeValueStore, imageDownloadMode),
 		add_service.NewAddServices(serviceNetwork, runtimeValueStore, packageId, packageContentProvider, packageReplaceOptions, interpretationTimeValueStore, imageDownloadMode),
 		get_service.NewGetService(interpretationTimeValueStore),
+		get_services.NewGetServices(interpretationTimeValueStore),
 		get_files_artifact.NewGetFilesArtifact(),
 		verify.NewVerify(runtimeValueStore),
 		exec.NewExec(serviceNetwork, runtimeValueStore),
 		kurtosis_print.NewPrint(serviceNetwork, runtimeValueStore),
-		remove_service.NewRemoveService(serviceNetwork),
+		remove_service.NewRemoveService(serviceNetwork, interpretationTimeValueStore),
 		render_templates.NewRenderTemplatesInstruction(serviceNetwork, runtimeValueStore),
 		request.NewRequest(serviceNetwork, runtimeValueStore),
 		start_service.NewStartService(serviceNetwork),
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/get_services/get_services.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/get_services/get_services.go
new file mode 100644
index 000000000..403f7e177
--- /dev/null
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/get_services/get_services.go
@@ -0,0 +1,101 @@
+package get_services
+
+import (
+	"context"
+	"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/plan_yaml"
+	"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 (
+	GetServicesBuiltinName = "get_services"
+	descriptionStr         = "Fetching services"
+)
+
+func NewGetServices(interpretationTimeStore *interpretation_time_value_store.InterpretationTimeValueStore) *kurtosis_plan_instruction.KurtosisPlanInstruction {
+	return &kurtosis_plan_instruction.KurtosisPlanInstruction{
+		KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{
+			Name:        GetServicesBuiltinName,
+			Arguments:   []*builtin_argument.BuiltinArgument{},
+			Deprecation: nil,
+		},
+		Capabilities: func() kurtosis_plan_instruction.KurtosisPlanInstructionCapabilities {
+			return &GetServicesCapabilities{
+				interpretationTimeStore: interpretationTimeStore,
+				serviceNames:            []service.ServiceName{}, // populated at interpretation time
+				description:             "",                      // populated at interpretation time
+			}
+		},
+		DefaultDisplayArguments: map[string]bool{},
+	}
+}
+
+type GetServicesCapabilities struct {
+	interpretationTimeStore *interpretation_time_value_store.InterpretationTimeValueStore
+	serviceNames            []service.ServiceName
+	description             string
+}
+
+func (builtin *GetServicesCapabilities) Interpret(_ string, arguments *builtin_argument.ArgumentValuesSet) (starlark.Value, *startosis_errors.InterpretationError) {
+	builtin.description = builtin_argument.GetDescriptionOrFallBack(arguments, descriptionStr)
+
+	services, err := builtin.interpretationTimeStore.GetServices()
+	if err != nil {
+		return nil, startosis_errors.WrapWithInterpretationError(err, "An error occurred while fetching service.")
+	}
+	servicesList := &starlark.List{}
+	for _, serviceVal := range services {
+		name, err := serviceVal.GetName()
+		if err != nil {
+			return nil, startosis_errors.WrapWithInterpretationError(err, "An error occurred getting name of service: %v", serviceVal)
+		}
+		builtin.serviceNames = append(builtin.serviceNames, name)
+
+		_ = servicesList.Append(serviceVal)
+	}
+
+	return servicesList, nil
+}
+
+func (builtin *GetServicesCapabilities) Validate(_ *builtin_argument.ArgumentValuesSet, validatorEnvironment *startosis_validator.ValidatorEnvironment) *startosis_errors.ValidationError {
+	// validate if all services exist in the validation environment
+	for _, serviceName := range builtin.serviceNames {
+		if exists := validatorEnvironment.DoesServiceNameExist(serviceName); exists == startosis_validator.ComponentNotFound {
+			return startosis_errors.NewValidationError("Service '%v' required by '%v' instruction doesn't exist", serviceName, GetServicesBuiltinName)
+		}
+	}
+	return nil
+}
+
+func (builtin *GetServicesCapabilities) Execute(_ context.Context, _ *builtin_argument.ArgumentValuesSet) (string, error) {
+	// note: this is a no op
+	return descriptionStr, nil
+}
+
+func (builtin *GetServicesCapabilities) TryResolveWith(instructionsAreEqual bool, _ *enclave_plan_persistence.EnclavePlanInstruction, enclaveComponents *enclave_structure.EnclaveComponents) enclave_structure.InstructionResolutionStatus {
+	if instructionsAreEqual {
+		return enclave_structure.InstructionIsEqual
+	}
+	return enclave_structure.InstructionIsUnknown
+}
+
+func (builtin *GetServicesCapabilities) FillPersistableAttributes(builder *enclave_plan_persistence.EnclavePlanInstructionBuilder) {
+	builder.SetType(GetServicesBuiltinName)
+}
+
+func (builtin *GetServicesCapabilities) UpdatePlan(planYaml *plan_yaml.PlanYaml) error {
+	// get services does not affect the plan
+	return nil
+}
+
+func (builtin *GetServicesCapabilities) Description() string {
+	return builtin.description
+}
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/remove_service/remove_service.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/remove_service/remove_service.go
index e7dc571fd..d905da7e8 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/remove_service/remove_service.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/remove_service/remove_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"
@@ -24,7 +25,7 @@ const (
 	descriptionFormatStr = "Removing service '%v'"
 )
 
-func NewRemoveService(serviceNetwork service_network.ServiceNetwork) *kurtosis_plan_instruction.KurtosisPlanInstruction {
+func NewRemoveService(serviceNetwork service_network.ServiceNetwork, interpretationTimeStore *interpretation_time_value_store.InterpretationTimeValueStore) *kurtosis_plan_instruction.KurtosisPlanInstruction {
 	return &kurtosis_plan_instruction.KurtosisPlanInstruction{
 		KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{
 			Name: RemoveServiceBuiltinName,
@@ -44,7 +45,8 @@ func NewRemoveService(serviceNetwork service_network.ServiceNetwork) *kurtosis_p
 
 		Capabilities: func() kurtosis_plan_instruction.KurtosisPlanInstructionCapabilities {
 			return &RemoveServiceCapabilities{
-				serviceNetwork: serviceNetwork,
+				serviceNetwork:          serviceNetwork,
+				interpretationTimeStore: interpretationTimeStore,
 
 				serviceName: "", // populated at interpretation time
 				description: "", // populated at interpretation time
@@ -58,7 +60,8 @@ func NewRemoveService(serviceNetwork service_network.ServiceNetwork) *kurtosis_p
 }
 
 type RemoveServiceCapabilities struct {
-	serviceNetwork service_network.ServiceNetwork
+	serviceNetwork          service_network.ServiceNetwork
+	interpretationTimeStore *interpretation_time_value_store.InterpretationTimeValueStore
 
 	serviceName service.ServiceName
 	description string
@@ -71,6 +74,10 @@ func (builtin *RemoveServiceCapabilities) Interpret(_ string, arguments *builtin
 	}
 
 	builtin.serviceName = service.ServiceName(serviceName.GoString())
+	err = builtin.interpretationTimeStore.RemoveService(builtin.serviceName)
+	if err != nil {
+		return nil, startosis_errors.WrapWithInterpretationError(err, "An error occurred removing '%v' from interpretation time store", builtin.serviceName)
+	}
 	builtin.description = builtin_argument.GetDescriptionOrFallBack(arguments, fmt.Sprintf(descriptionFormatStr, builtin.serviceName))
 	return starlark.None, nil
 }


Test patch:
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
index 9b7db03a0..1ddb4777b 100644
--- 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
@@ -1,6 +1,7 @@
 package interpretation_time_value_store
 
 import (
+	"fmt"
 	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"
@@ -16,7 +17,7 @@ import (
 
 const (
 	starlarkThreadName     = "thread-for-db-test"
-	serviceName            = service.ServiceName("datastore-1")
+	serviceName            = service.ServiceName("datastore")
 	serviceNameStarlarkStr = starlark.String(serviceName)
 	hostName               = serviceNameStarlarkStr
 	ipAddress              = starlark.String("172.23.34.44")
@@ -62,6 +63,84 @@ func TestPutGetFail_ForMissingServiceName(t *testing.T) {
 	require.Nil(t, actualService)
 }
 
+func TestGetServices(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))
+
+	serviceOneName := fmt.Sprintf("%v-%v", serviceName, "1")
+	serviceOne, interpretationErr := kurtosis_types.CreateService(starlark.String(serviceOneName), hostName, ipAddress, ports)
+	require.Nil(t, interpretationErr)
+
+	serviceTwoName := fmt.Sprintf("%v-%v", serviceName, "2")
+	serviceTwo, interpretationErr := kurtosis_types.CreateService(starlark.String(serviceTwoName), hostName, ipAddress, ports)
+	require.Nil(t, interpretationErr)
+
+	err := repository.PutService(service.ServiceName(serviceOneName), serviceOne)
+	require.Nil(t, err)
+	err = repository.PutService(service.ServiceName(serviceTwoName), serviceTwo)
+	require.Nil(t, err)
+
+	actualServices, err := repository.GetServices()
+	require.NoError(t, err)
+	require.Len(t, actualServices, 2)
+
+	actualServiceOneName, err := actualServices[0].GetName()
+	require.Nil(t, err)
+	require.Equal(t, service.ServiceName(serviceOneName), actualServiceOneName)
+
+	actualServiceTwoName, err := actualServices[1].GetName()
+	require.Nil(t, err)
+	require.Equal(t, service.ServiceName(serviceTwoName), actualServiceTwoName)
+}
+
+func TestRemoveService(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))
+
+	serviceOneName := fmt.Sprintf("%v-%v", serviceName, "1")
+	serviceOne, interpretationErr := kurtosis_types.CreateService(starlark.String(serviceOneName), hostName, ipAddress, ports)
+	require.Nil(t, interpretationErr)
+
+	err := repository.PutService(service.ServiceName(serviceOneName), serviceOne)
+	require.Nil(t, err)
+
+	err = repository.RemoveService(service.ServiceName(serviceOneName))
+	require.Nil(t, err)
+
+	_, err = repository.GetService(service.ServiceName(serviceOneName))
+	require.Error(t, err)
+}
+
 func getServiceInterpretationTimeValueRepository(t *testing.T) *serviceInterpretationValueRepository {
 	file, err := os.CreateTemp("/tmp", "*.db")
 	defer func() {
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/remove_service_framework_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/remove_service_framework_test.go
index cacd60427..5751c635d 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/remove_service_framework_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/remove_service_framework_test.go
@@ -3,6 +3,7 @@ package test_engine
 import (
 	"fmt"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/service_network"
+	"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/remove_service"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_plan_instruction"
 	"github.com/stretchr/testify/mock"
@@ -13,7 +14,8 @@ import (
 
 type removeServiceTestCase struct {
 	*testing.T
-	serviceNetwork *service_network.MockServiceNetwork
+	serviceNetwork          *service_network.MockServiceNetwork
+	interpretationTimeStore *interpretation_time_value_store.InterpretationTimeValueStore
 }
 
 func (suite *KurtosisPlanInstructionTestSuite) TestRemoveService() {
@@ -26,13 +28,14 @@ func (suite *KurtosisPlanInstructionTestSuite) TestRemoveService() {
 	)
 
 	suite.run(&removeServiceTestCase{
-		T:              suite.T(),
-		serviceNetwork: suite.serviceNetwork,
+		T:                       suite.T(),
+		serviceNetwork:          suite.serviceNetwork,
+		interpretationTimeStore: suite.interpretationTimeValueStore,
 	})
 }
 
 func (t *removeServiceTestCase) GetInstruction() *kurtosis_plan_instruction.KurtosisPlanInstruction {
-	return remove_service.NewRemoveService(t.serviceNetwork)
+	return remove_service.NewRemoveService(t.serviceNetwork, t.interpretationTimeStore)
 }
 
 func (t *removeServiceTestCase) GetStarlarkCode() string {