Repository: kurtosis-tech/kurtosis (go)

Issue:
Enable tains/tolerations on k8s
### Background & motivation

In order to enable smart scheduling (e.g placing EL/CL on the same host), it would be great if the service field could be extended with kubernetes [taints/tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/).


### Desired behaviour

Have an extra field in services for tolerations that takes a yaml array. 

### How important is this to you?

Painful; the lack of this feature makes using Kurtosis frictionful.

### What area of the product does this pertain to?

CLI: the Command Line Interface

Pull request:
feat: support passing toleration to kubernetes
We can now pass taints and tolerations to Kubernetes

Closes #2048

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9baca754e..b9d61787a 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -223,6 +223,9 @@ parameters:
   starlark-test-args-file-test-main-star-relative-path:
     type: string
     default: "internal_testsuites/starlark/ci_tests/simple_arg_test/main.star"
+  starlark-test-taints-and-tolerations:
+    type: string
+    default: "internal_testsuites/starlark/taints_and_tolerations/taints_and_toleration_test.star"
   starlark-test-args-file-test-args-json-relative-path:
     type: string
     default: "internal_testsuites/starlark/ci_tests/simple_arg_test/args.json"
@@ -562,6 +565,7 @@ jobs:
           paths:
             - "./<< pipeline.parameters.cli-dist-home-relative-dirpath >>"
             - "./<< pipeline.parameters.startosis-test-script-file-relative-path >>"
+            - "./<< pipeline.parameters.starlark-test-taints-and-tolerations >>"
 
   build_golang_testsuite:
     executor: ubuntu_vm
@@ -909,6 +913,12 @@ jobs:
       # Execute docker compose import
       - run: "${KURTOSIS_BINPATH} import --enclave test-import --env << pipeline.parameters.dockerimport-cli-dotenv-relative-path >> << pipeline.parameters.dockerimport-cli-dockerfile-relative-path >>"
 
+      # This just tests that tolerations don't mess up Docker
+      # TODO add proper tests for Docker and Kubernetes that test
+      # Adding Toleration to a tainted cluster makes things work
+      # Not having toleration fails
+      - run: "${KURTOSIS_BINPATH} run --enclave with-tolerations << pipeline.parameters.workspace-with-cli-binary-and-images-mountpoint >>/<< pipeline.parameters.starlark-test-taints-and-tolerations >>"
+
       # Execute engine functions
       - run: "${KURTOSIS_BINPATH} engine stop"
       - run: "${KURTOSIS_BINPATH} engine stop"
diff --git a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/engine_functions/create_engine.go b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/engine_functions/create_engine.go
index ebafaf1d8..67bccf3a4 100644
--- a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/engine_functions/create_engine.go
+++ b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/engine_functions/create_engine.go
@@ -32,6 +32,9 @@ const (
 
 var noWait *port_spec.Wait = nil
 
+// TODO add support for passing toleration to Engine
+var noToleration []apiv1.Toleration = nil
+
 func CreateEngine(
 	ctx context.Context,
 	imageOrgAndRepo string,
@@ -470,6 +473,7 @@ func createEnginePod(
 		serviceAccountName,
 		// Engine doesn't auto restart
 		apiv1.RestartPolicyNever,
+		noToleration,
 	)
 	if err != nil {
 		return nil, nil, stacktrace.Propagate(err, "An error occurred while creating the pod with name '%s' in namespace '%s' with image '%s'", enginePodName, namespace, containerImageAndTag)
diff --git a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/kubernetes_kurtosis_backend_api_container_functions.go b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/kubernetes_kurtosis_backend_api_container_functions.go
index cabd190b1..814e50ebf 100644
--- a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/kubernetes_kurtosis_backend_api_container_functions.go
+++ b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/kubernetes_kurtosis_backend_api_container_functions.go
@@ -43,6 +43,9 @@ const (
 
 var noWait *port_spec.Wait = nil
 
+// TODO add support for passing toleration to APIC
+var noTolerations []apiv1.Toleration = nil
+
 // TODO: MIGRATE THIS FOLDER TO USE STRUCTURE OF USER_SERVICE_FUNCTIONS MODULE
 
 // Any of these values being nil indicates that the resource doesn't exist
@@ -477,6 +480,7 @@ func (backend *KubernetesKurtosisBackend) CreateAPIContainer(
 		apiContainerVolumes,
 		apiContainerServiceAccountName,
 		apiContainerRestartPolicy,
+		noTolerations,
 	)
 	if err != nil {
 		errMsg := fmt.Sprintf("An error occurred while creating the pod with name '%s' in namespace '%s' with image '%s'", apiContainerPodName, enclaveNamespaceName, image)
diff --git a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/user_services_functions/start_user_services.go b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/user_services_functions/start_user_services.go
index 8d85648b9..577bd3f8c 100644
--- a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/user_services_functions/start_user_services.go
+++ b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/user_services_functions/start_user_services.go
@@ -308,6 +308,7 @@ func createStartServiceOperation(
 		minCpuAllocationMilliCpus := serviceConfig.GetMinCPUAllocationMillicpus()
 		minMemoryAllocationMegabytes := serviceConfig.GetMinMemoryAllocationMegabytes()
 		user := serviceConfig.GetUser()
+		tolerations := serviceConfig.GetTolerations()
 
 		matchingObjectAndResources, found := servicesObjectsAndResources[serviceUuid]
 		if !found {
@@ -416,6 +417,7 @@ func createStartServiceOperation(
 			podVolumes,
 			userServiceServiceAccountName,
 			restartPolicy,
+			tolerations,
 		)
 		if err != nil {
 			return nil, stacktrace.Propagate(err, "An error occurred creating pod '%v' using image '%v'", podName, containerImageName)
diff --git a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_manager/kubernetes_manager.go b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_manager/kubernetes_manager.go
index a5f56e6a8..2b37cd6f6 100644
--- a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_manager/kubernetes_manager.go
+++ b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_manager/kubernetes_manager.go
@@ -1048,6 +1048,7 @@ func (manager *KubernetesManager) CreatePod(
 	podVolumes []apiv1.Volume,
 	podServiceAccountName string,
 	restartPolicy apiv1.RestartPolicy,
+	tolerations []apiv1.Toleration,
 ) (*apiv1.Pod, error) {
 	podClient := manager.kubernetesClientSet.CoreV1().Pods(namespaceName)
 
@@ -1096,7 +1097,7 @@ func (manager *KubernetesManager) CreatePod(
 		Subdomain:                 "",
 		Affinity:                  nil,
 		SchedulerName:             "",
-		Tolerations:               nil,
+		Tolerations:               tolerations,
 		HostAliases:               nil,
 		PriorityClassName:         "",
 		Priority:                  nil,
diff --git a/container-engine-lib/lib/backend_interface/objects/service/service_config.go b/container-engine-lib/lib/backend_interface/objects/service/service_config.go
index ff9f4d8bb..0b7b56b80 100644
--- a/container-engine-lib/lib/backend_interface/objects/service/service_config.go
+++ b/container-engine-lib/lib/backend_interface/objects/service/service_config.go
@@ -8,6 +8,7 @@ import (
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_directory"
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_user"
 	"github.com/kurtosis-tech/stacktrace"
+	v1 "k8s.io/api/core/v1"
 )
 
 // Config options for the underlying container of a service
@@ -57,6 +58,9 @@ type privateServiceConfig struct {
 	Labels map[string]string
 
 	User *service_user.ServiceUser
+
+	// TODO replace this with an abstraction that we own
+	Tolerations []v1.Toleration
 }
 
 func CreateServiceConfig(
@@ -77,6 +81,7 @@ func CreateServiceConfig(
 	minMemoryMegaBytes uint64,
 	labels map[string]string,
 	user *service_user.ServiceUser,
+	tolerations []v1.Toleration,
 ) (*ServiceConfig, error) {
 
 	if err := ValidateServiceConfigLabels(labels); err != nil {
@@ -102,6 +107,7 @@ func CreateServiceConfig(
 		MinMemoryAllocationMegabytes: minMemoryMegaBytes,
 		Labels:                       labels,
 		User:                         user,
+		Tolerations:                  tolerations,
 	}
 	return &ServiceConfig{internalServiceConfig}, nil
 }
@@ -176,6 +182,10 @@ func (serviceConfig *ServiceConfig) GetLabels() map[string]string {
 	return serviceConfig.privateServiceConfig.Labels
 }
 
+func (serviceConfig *ServiceConfig) GetTolerations() []v1.Toleration {
+	return serviceConfig.privateServiceConfig.Tolerations
+}
+
 func (serviceConfig *ServiceConfig) MarshalJSON() ([]byte, error) {
 	return json.Marshal(serviceConfig.privateServiceConfig)
 }
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 70eb4fb7e..e8a4f6a8a 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_builtins.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_builtins.go
@@ -114,5 +114,6 @@ func KurtosisTypeConstructors() []*starlark.Builtin {
 		starlark.NewBuiltin(service_config.ImageBuildSpecTypeName, service_config.NewImageBuildSpecType().CreateBuiltin()),
 		starlark.NewBuiltin(service_config.ImageRegistrySpecTypeName, service_config.NewImageRegistrySpec().CreateBuiltin()),
 		starlark.NewBuiltin(service_config.UserTypeName, service_config.NewUserType().CreateBuiltin()),
+		starlark.NewBuiltin(service_config.TolerationTypeName, service_config.NewTolerationType().CreateBuiltin()),
 	}
 }
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared.go
index 93bea3a64..da3210942 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared.go
@@ -216,6 +216,7 @@ func replaceMagicStrings(
 		serviceConfig.GetMinMemoryAllocationMegabytes(),
 		serviceConfig.GetLabels(),
 		serviceConfig.GetUser(),
+		serviceConfig.GetTolerations(),
 	)
 	if err != nil {
 		return "", nil, stacktrace.Propagate(err, "An error occurred creating a service config")
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/tasks/tasks_shared.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/tasks/tasks_shared.go
index e8e245cd2..e8addad38 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/tasks/tasks_shared.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/tasks/tasks_shared.go
@@ -272,6 +272,7 @@ func getServiceConfig(image string, filesArtifactExpansion *service_directory.Fi
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	if err != nil {
 		return nil, stacktrace.Propagate(err, "An error occurred creating service config")
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_config.go b/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_config.go
index 1b12a0654..e043956d9 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_config.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_config.go
@@ -21,6 +21,7 @@ import (
 	"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"
 	"go.starlark.net/starlark"
+	v1 "k8s.io/api/core/v1"
 	"math"
 	"path"
 )
@@ -45,6 +46,7 @@ const (
 	MaxMemoryMegaBytesAttr          = "max_memory"
 	LabelsAttr                      = "labels"
 	UserAttr                        = "user"
+	TolerationsAttr                 = "tolerations"
 
 	DefaultPrivateIPAddrPlaceholder = "KURTOSIS_IP_ADDR_PLACEHOLDER"
 
@@ -189,6 +191,14 @@ func NewServiceConfigType() *kurtosis_type_constructor.KurtosisTypeConstructor {
 					ZeroValueProvider: builtin_argument.ZeroValueProvider[*User],
 					Validator:         nil,
 				},
+				{
+					Name:              TolerationsAttr,
+					IsOptional:        true,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[*starlark.List],
+					Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
+						return nil
+					},
+				},
 			},
 		},
 
@@ -454,6 +464,18 @@ func (config *ServiceConfig) ToKurtosisType(
 		}
 	}
 
+	var tolerations []v1.Toleration
+	tolerationsStarlarkList, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[*starlark.List](config.KurtosisValueTypeDefault, TolerationsAttr)
+	if interpretationErr != nil {
+		return nil, interpretationErr
+	}
+	if found {
+		tolerations, interpretationErr = convertTolerations(tolerationsStarlarkList)
+		if interpretationErr != nil {
+			return nil, interpretationErr
+		}
+	}
+
 	serviceConfig, err := service.CreateServiceConfig(
 		imageName,
 		maybeImageBuildSpec,
@@ -472,6 +494,7 @@ func (config *ServiceConfig) ToKurtosisType(
 		minMemory,
 		labels,
 		serviceUser,
+		tolerations,
 	)
 	if err != nil {
 		return nil, startosis_errors.WrapWithInterpretationError(err, "An error occurred creating a service config")
@@ -654,3 +677,27 @@ func convertImage(
 		return imageName, nil, nil, nil
 	}
 }
+
+func convertTolerations(tolerationsList *starlark.List) ([]v1.Toleration, *startosis_errors.InterpretationError) {
+	var outputValue []v1.Toleration
+	iterator := tolerationsList.Iterate()
+	defer iterator.Done()
+	var item starlark.Value
+
+	var index = 0
+	for iterator.Next(&item) {
+		toleration, ok := item.(*Toleration)
+		if !ok {
+			return nil, startosis_errors.NewInterpretationError("Expected item at index '%v' of the tolerations list passed via '%v' attr to be a '%v' but it wasn't", index, TolerationsAttr, TolerationTypeName)
+		}
+
+		tolerationKubeType, err := toleration.ToKubeType()
+		if err != nil {
+			return nil, startosis_errors.WrapWithInterpretationError(err, "Error occurred while converting object at '%v' of '%v' list to internal type", index, TolerationsAttr)
+		}
+		outputValue = append(outputValue, *tolerationKubeType)
+		index += 1
+	}
+
+	return outputValue, nil
+}
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/toleration.go b/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/toleration.go
new file mode 100644
index 000000000..559b242a6
--- /dev/null
+++ b/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/toleration.go
@@ -0,0 +1,218 @@
+package service_config
+
+import (
+	"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"
+	v1 "k8s.io/api/core/v1"
+)
+
+const (
+	TolerationTypeName = "Toleration"
+
+	KeyAttr               = "key"
+	OperatorAttr          = "operator"
+	ValueAttr             = "value"
+	EffectAttr            = "effect"
+	TolerationSecondsAttr = "toleration_seconds"
+)
+
+var allowedOperatorValues = []string{string(v1.TolerationOpExists), string(v1.TolerationOpEqual)}
+var allowedEffectValues = []string{string(v1.TaintEffectNoSchedule), string(v1.TaintEffectNoExecute), string(v1.TaintEffectPreferNoSchedule)}
+
+func NewTolerationType() *kurtosis_type_constructor.KurtosisTypeConstructor {
+	return &kurtosis_type_constructor.KurtosisTypeConstructor{
+		KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{
+			Name: TolerationTypeName,
+			Arguments: []*builtin_argument.BuiltinArgument{
+				{
+					Name:              KeyAttr,
+					IsOptional:        true,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
+					Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
+						return builtin_argument.NonEmptyString(value, KeyAttr)
+					},
+				},
+				{
+					Name:              OperatorAttr,
+					IsOptional:        true,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
+					Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
+						return builtin_argument.StringValues(value, OperatorAttr, allowedOperatorValues)
+					},
+				},
+				{
+					Name:              ValueAttr,
+					IsOptional:        true,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
+				},
+				{
+					Name:              EffectAttr,
+					IsOptional:        true,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.String],
+					Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
+						return builtin_argument.StringValues(value, EffectAttr, allowedEffectValues)
+					},
+				},
+				{
+					Name:              TolerationSecondsAttr,
+					IsOptional:        true,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.Int],
+					Validator:         nil,
+				},
+			},
+			Deprecation: nil,
+		},
+		Instantiate: instantiateToleration,
+	}
+}
+
+func instantiateToleration(arguments *builtin_argument.ArgumentValuesSet) (builtin_argument.KurtosisValueType, *startosis_errors.InterpretationError) {
+	kurtosisValueType, interpretationErr := kurtosis_type_constructor.CreateKurtosisStarlarkTypeDefault(TolerationTypeName, arguments)
+	if interpretationErr != nil {
+		return nil, interpretationErr
+	}
+	return &Toleration{
+		kurtosisValueType,
+	}, nil
+}
+
+type Toleration struct {
+	*kurtosis_type_constructor.KurtosisValueTypeDefault
+}
+
+func (toleration *Toleration) Copy() (builtin_argument.KurtosisValueType, error) {
+	copiedValueType, err := toleration.KurtosisValueTypeDefault.Copy()
+	if err != nil {
+		return nil, err
+	}
+	return &Toleration{
+		KurtosisValueTypeDefault: copiedValueType,
+	}, nil
+}
+
+func (toleration *Toleration) GetKeyIfSet() (string, bool, *startosis_errors.InterpretationError) {
+	keyValue, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](
+		toleration.KurtosisValueTypeDefault, KeyAttr)
+	if interpretationErr != nil {
+		return "", false, interpretationErr
+	}
+	if !found {
+		return "", false, nil
+	}
+	return keyValue.GoString(), true, nil
+}
+
+func (toleration *Toleration) GetOperatorIfSet() (string, bool, *startosis_errors.InterpretationError) {
+	attrValue, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](
+		toleration.KurtosisValueTypeDefault, OperatorAttr)
+	if interpretationErr != nil {
+		return "", false, interpretationErr
+	}
+	if !found {
+		return "", false, nil
+	}
+	return attrValue.GoString(), true, nil
+}
+
+func (toleration *Toleration) GetValueIfExists() (string, bool, *startosis_errors.InterpretationError) {
+	attrValue, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](
+		toleration.KurtosisValueTypeDefault, ValueAttr)
+	if interpretationErr != nil {
+		return "", false, interpretationErr
+	}
+	if !found {
+		return "", false, nil
+	}
+	return attrValue.GoString(), true, nil
+}
+
+func (toleration *Toleration) GetEffectIfExist() (string, bool, *startosis_errors.InterpretationError) {
+	attrValue, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.String](
+		toleration.KurtosisValueTypeDefault, EffectAttr)
+	if interpretationErr != nil {
+		return "", false, interpretationErr
+	}
+	if !found {
+		return "", false, nil
+	}
+	return attrValue.GoString(), true, nil
+}
+
+func (toleration *Toleration) GetTolerationSecondsIfExists() (int64, bool, *startosis_errors.InterpretationError) {
+	attrValue, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.Int](
+		toleration.KurtosisValueTypeDefault, TolerationSecondsAttr)
+	if interpretationErr != nil {
+		return 0, false, interpretationErr
+	}
+	if !found {
+		return 0, false, nil
+	}
+	attr, ok := attrValue.Int64()
+	if !ok {
+		return 0, false, startosis_errors.NewInterpretationError("Couldn't convert '%v' '%v' to int64", TolerationSecondsAttr, attr)
+	}
+	return attr, true, nil
+}
+
+func (toleration *Toleration) ToKubeType() (*v1.Toleration, *startosis_errors.InterpretationError) {
+	//nolint :exhaustruct
+	returnValue := &v1.Toleration{}
+
+	key, keyFound, err := toleration.GetKeyIfSet()
+	if err != nil {
+		return nil, err
+	}
+
+	if keyFound {
+		returnValue.Key = key
+	}
+
+	operator, operatorFound, err := toleration.GetOperatorIfSet()
+	if err != nil {
+		return nil, err
+	}
+
+	if !keyFound && (!operatorFound || operator != string(v1.TolerationOpExists)) {
+		return nil, startosis_errors.NewInterpretationError("'%v' expects either '%v' to be set or for '%v' to be '%v'", TolerationTypeName, KeyAttr, OperatorAttr, v1.TolerationOpExists)
+	}
+
+	if operatorFound {
+		returnValue.Operator = v1.TolerationOperator(operator)
+	}
+
+	value, valueFound, err := toleration.GetValueIfExists()
+	if err != nil {
+		return nil, err
+	}
+
+	if operatorFound && operator == string(v1.TolerationOpExists) && valueFound && value != "" {
+		return nil, startosis_errors.NewInterpretationError("'%v' cannot have non empty value '%v' for '%v' if '%v' is '%v'", TolerationTypeName, value, ValueAttr, OperatorAttr, operator)
+	}
+
+	if valueFound {
+		returnValue.Value = value
+	}
+
+	effect, effectFound, err := toleration.GetEffectIfExist()
+	if err != nil {
+		return nil, err
+	}
+
+	if effectFound {
+		returnValue.Effect = v1.TaintEffect(effect)
+	}
+
+	tolerationSeconds, tolerationSecondsFound, err := toleration.GetTolerationSecondsIfExists()
+	if err != nil {
+		return nil, err
+	}
+
+	if tolerationSecondsFound {
+		returnValue.TolerationSeconds = &tolerationSeconds
+	}
+
+	return returnValue, nil
+}
diff --git a/docs/docs/api-reference/starlark-reference/service-config.md b/docs/docs/api-reference/starlark-reference/service-config.md
index 5d4a5534e..b11a2edff 100644
--- a/docs/docs/api-reference/starlark-reference/service-config.md
+++ b/docs/docs/api-reference/starlark-reference/service-config.md
@@ -159,6 +159,21 @@ config = ServiceConfig(
     # Note that the `gid` field is optional
     # OPTIONAL
     user = User(uid=0, gid=0),
+    
+    # An array of Toleration 
+    # This refers to Kubernetes Tolerations https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
+    # This has no effect on Docker
+    # As of 2024-01-24 Taints and Tolerations to work with Kubernetes you need at least one untainted node
+    # OPTIONAL
+    tolerations = [
+        Toleration(
+            key = "test-key",
+            value = "test-value",
+            operator = "Equal",
+            effect = "NoSchedule",
+            toleration_seconds = 64,
+        )
+    ]
 )
 ```
 Note that `ImageBuildSpec` can only be used in packages and not standalone scripts as it relies on build context in package.
@@ -209,6 +224,8 @@ labels:
 
 The `user` field expects a `User`[user] object being passed.
 
+The `tolerations` field expects a list of `toleration[toleration]` object being passed.
+
 <!--------------- ONLY LINKS BELOW THIS POINT ---------------------->
 [add-service-reference]: ./plan.md#add_service
 [directory]: ./directory.md
@@ -217,3 +234,4 @@ The `user` field expects a `User`[user] object being passed.
 [locators]: ../../advanced-concepts/locators.md
 [package]: ../../advanced-concepts/packages.md
 [user]: ./user.md
+[toleration]: ./toleration.md
diff --git a/docs/docs/api-reference/starlark-reference/toleration.md b/docs/docs/api-reference/starlark-reference/toleration.md
new file mode 100644
index 000000000..2c50ba83a
--- /dev/null
+++ b/docs/docs/api-reference/starlark-reference/toleration.md
@@ -0,0 +1,24 @@
+---
+title: Toleration
+sidebar_label: Toleration
+---
+
+The `Toleration` constructor creates a `Toleration` object that represents a Kubernetes [Toleration](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) that
+can be used with a [ServiceConfig][service-config] object.
+
+```python
+toleration = Toleration(
+    key = "key",
+    operator = "Equal",
+    value = "value",
+    effect = "NoSchedule",
+    toleration_seconds = 64,
+)
+```
+
+Note all fields are completely optional and follow the rules as laid out in the Kubernetes doc linked above.
+
+Note you need at least one untainted node to use Kurtosis with Kubernetes.
+
+<!--------------- ONLY LINKS BELOW THIS POINT ---------------------->
+[service-config]: ./service-config.md
\ No newline at end of file
diff --git a/enclave-manager/web/packages/app/src/emui/Navbar.tsx b/enclave-manager/web/packages/app/src/emui/Navbar.tsx
index 409801a08..3176b6c93 100644
--- a/enclave-manager/web/packages/app/src/emui/Navbar.tsx
+++ b/enclave-manager/web/packages/app/src/emui/Navbar.tsx
@@ -1,5 +1,22 @@
-import { NavButton, Navigation } from "kurtosis-ui-components";
+import {
+  Button,
+  Input,
+  InputGroup,
+  InputRightElement,
+  Modal,
+  ModalBody,
+  ModalCloseButton,
+  ModalContent,
+  ModalFooter,
+  ModalHeader,
+  ModalOverlay,
+  Text,
+} from "@chakra-ui/react";
+import { CopyButton, NavButton, Navigation, NavigationDivider } from "kurtosis-ui-components";
+import { useState } from "react";
 import { FiHome, FiPackage } from "react-icons/fi";
+import { GoBug } from "react-icons/go";
+import { MdInfoOutline } from "react-icons/md";
 import { PiLinkSimpleBold } from "react-icons/pi";
 import { Link, useLocation } from "react-router-dom";
 import { KURTOSIS_CLOUD_CONNECT_URL } from "../client/constants";
@@ -8,6 +25,8 @@ import { useKurtosisClient } from "../client/enclaveManager/KurtosisClientContex
 export const Navbar = () => {
   const location = useLocation();
   const kurtosisClient = useKurtosisClient();
+  const [showAboutDialog, setShowAboutDialog] = useState(false);
+  const kurtosisVersion = process.env.REACT_APP_VERSION || "Unknown";
 
   return (
     <Navigation>
@@ -26,6 +45,47 @@ export const Navbar = () => {
           <NavButton label={"Link your CLI"} Icon={<PiLinkSimpleBold />} />
         </Link>
       )}
+      <NavigationDivider />
+      <Link
+        to={`https://github.com/kurtosis-tech/kurtosis/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml&version=${kurtosisVersion}`}
+        target={"_blank"}
+      >
+        <NavButton label={"Report a Bug"} Icon={<GoBug />} />
+      </Link>
+      <NavButton label={"About"} Icon={<MdInfoOutline />} onClick={() => setShowAboutDialog(true)} />
+
+      <Modal isOpen={showAboutDialog} onClose={() => setShowAboutDialog(false)}>
+        <ModalOverlay />
+        <ModalContent>
+          <ModalHeader>About your Kurtosis Engine</ModalHeader>
+          <ModalCloseButton />
+          <ModalBody>
+            <Text>Your Kurtosis engine version is:</Text>
+            <InputGroup size={"lg"} variant={"solid"}>
+              <Input
+                value={kurtosisVersion}
+                textOverflow={"ellipsis"}
+                fontFamily={"Inconsolata"}
+                bgColor={"gray.850"}
+                readOnly
+              />
+              <InputRightElement>
+                <CopyButton
+                  contentName={"version"}
+                  isIconButton
+                  aria-label={"Click to copy this version"}
+                  valueToCopy={kurtosisVersion}
+                />
+              </InputRightElement>
+            </InputGroup>
+            <Text></Text>
+          </ModalBody>
+
+          <ModalFooter>
+            <Button onClick={() => setShowAboutDialog(false)}>Close</Button>
+          </ModalFooter>
+        </ModalContent>
+      </Modal>
     </Navigation>
   );
 };
diff --git a/enclave-manager/web/packages/components/src/Navigation.tsx b/enclave-manager/web/packages/components/src/Navigation.tsx
index 1e6a738ab..b7163f40c 100644
--- a/enclave-manager/web/packages/components/src/Navigation.tsx
+++ b/enclave-manager/web/packages/components/src/Navigation.tsx
@@ -1,4 +1,4 @@
-import { Flex, IconButton, IconButtonProps, Image, Tooltip } from "@chakra-ui/react";
+import { Box, Flex, IconButton, IconButtonProps, Image, Tooltip } from "@chakra-ui/react";
 import { PropsWithChildren } from "react";
 import { useHref } from "react-router-dom";
 
@@ -22,13 +22,17 @@ export const Navigation = ({ children }: PropsWithChildren & NavigationProps) =>
       <Flex width={"40px"} height={"40px"} alignItems={"center"}>
         <Image src={logoHref} />
       </Flex>
-      <Flex flexDirection={"column"} gap={"16px"}>
+      <Flex flexDirection={"column"} gap={"16px"} flex={"1"}>
         {children}
       </Flex>
     </Flex>
   );
 };
 
+export const NavigationDivider = () => {
+  return <Box flex={"1"} />;
+};
+
 type NavButtonProps = Omit<IconButtonProps, "aria-label"> & {
   label: string;
   Icon: React.ReactElement;


Test patch:
diff --git a/container-engine-lib/lib/backend_interface/objects/service/service_config_test.go b/container-engine-lib/lib/backend_interface/objects/service/service_config_test.go
index ad6087b4c..5a2acb97d 100644
--- a/container-engine-lib/lib/backend_interface/objects/service/service_config_test.go
+++ b/container-engine-lib/lib/backend_interface/objects/service/service_config_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_directory"
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_user"
 	"github.com/stretchr/testify/require"
+	v1 "k8s.io/api/core/v1"
 	"testing"
 	"time"
 )
@@ -80,6 +81,7 @@ func getServiceConfigForTest(t *testing.T, imageName string) *ServiceConfig {
 			"test-second-label-key": "test-second-label-value",
 		},
 		testServiceUser(),
+		testToleration(),
 	)
 	require.NoError(t, err)
 	return serviceConfig
@@ -187,3 +189,14 @@ func testServiceUser() *service_user.ServiceUser {
 	su.SetGID(100)
 	return su
 }
+
+func testToleration() []v1.Toleration {
+	tolerationSeconds := int64(6)
+	return []v1.Toleration{{
+		Key:               "testKey",
+		Operator:          v1.TolerationOpEqual,
+		Value:             "testValue",
+		Effect:            v1.TaintEffectNoExecute,
+		TolerationSeconds: &tolerationSeconds,
+	}}
+}
diff --git a/container-engine-lib/lib/database_accessors/enclave_db/service_registration/repository_test.go b/container-engine-lib/lib/database_accessors/enclave_db/service_registration/repository_test.go
index 6ba864fc6..85d497dff 100644
--- a/container-engine-lib/lib/database_accessors/enclave_db/service_registration/repository_test.go
+++ b/container-engine-lib/lib/database_accessors/enclave_db/service_registration/repository_test.go
@@ -323,6 +323,7 @@ func getServiceConfigForTest(t *testing.T, imageName string) *service.ServiceCon
 			"test-second-label-key": "test-second-label-value",
 		},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 	return serviceConfig
diff --git a/core/server/api_container/server/service_network/default_service_network_test.go b/core/server/api_container/server/service_network/default_service_network_test.go
index 72311f04b..f97a53bfe 100644
--- a/core/server/api_container/server/service_network/default_service_network_test.go
+++ b/core/server/api_container/server/service_network/default_service_network_test.go
@@ -1222,6 +1222,7 @@ func testServiceConfig(t *testing.T, imageName string) *service.ServiceConfig {
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 	return serviceConfig
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared_test.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared_test.go
index c63af9e09..430f9701e 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared_test.go
@@ -54,6 +54,7 @@ func TestAddServiceShared_EntryPointArgsRuntimeValueAreReplaced(t *testing.T) {
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 
@@ -98,6 +99,7 @@ func TestAddServiceShared_CmdArgsRuntimeValueAreReplaced(t *testing.T) {
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 
@@ -144,6 +146,7 @@ func TestAddServiceShared_EnvVarsWithRuntimeValueAreReplaced(t *testing.T) {
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 
@@ -191,6 +194,7 @@ func TestAddServiceShared_ServiceNameWithRuntimeValuesAreReplaced(t *testing.T)
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 
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 437ced821..615790499 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
@@ -50,6 +50,7 @@ func (suite *KurtosisPlanInstructionTestSuite) TestAddService() {
 				0,
 				map[string]string{},
 				nil,
+				nil,
 			)
 			require.NoError(suite.T(), err)
 
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 7d8c097d7..87a2247e3 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
@@ -66,6 +66,7 @@ func (suite *KurtosisPlanInstructionTestSuite) TestAddServices() {
 				0,
 				map[string]string{},
 				nil,
+				nil,
 			)
 			require.NoError(suite.T(), err)
 
@@ -90,6 +91,7 @@ func (suite *KurtosisPlanInstructionTestSuite) TestAddServices() {
 				0,
 				map[string]string{},
 				nil,
+				nil,
 			)
 			require.NoError(suite.T(), err)
 
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_build_spec_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_build_spec_test.go
index 8d34280a9..037e9e441 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_build_spec_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_build_spec_test.go
@@ -85,6 +85,7 @@ func (t *serviceConfigImageBuildSpecTestCase) Assert(typeValue builtin_argument.
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 	require.Equal(t, expectedServiceConfig, serviceConfig)
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_registry_spec_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_registry_spec_test.go
index e881817fa..25d05586e 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_registry_spec_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_registry_spec_test.go
@@ -75,6 +75,7 @@ func (t *serviceConfigImageRegistrySpecTest) Assert(typeValue builtin_argument.K
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 	require.Equal(t, expectedServiceConfig, serviceConfig)
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_minimal_framework_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_minimal_framework_test.go
index cedd4374d..a3b955d12 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_minimal_framework_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_minimal_framework_test.go
@@ -62,6 +62,7 @@ func (t *serviceConfigMinimalTestCase) Assert(typeValue builtin_argument.Kurtosi
 		0,
 		map[string]string{},
 		nil,
+		nil,
 	)
 	require.NoError(t, err)
 	require.Equal(t, expectedServiceConfig, serviceConfig)
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_toleration_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_toleration_test.go
new file mode 100644
index 000000000..6780b2c47
--- /dev/null
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_toleration_test.go
@@ -0,0 +1,85 @@
+package test_engine
+
+import (
+	"fmt"
+	"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/core/server/api_container/server/service_network"
+	"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/service_config"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_packages"
+	"github.com/stretchr/testify/require"
+	v1 "k8s.io/api/core/v1"
+	"testing"
+)
+
+type serviceConfigTolerationTest struct {
+	*testing.T
+	serviceNetwork         *service_network.MockServiceNetwork
+	packageContentProvider *startosis_packages.MockPackageContentProvider
+}
+
+func (suite *KurtosisTypeConstructorTestSuite) TestServiceConfigWithTolerationTest() {
+	suite.run(&serviceConfigTolerationTest{
+		T:                      suite.T(),
+		serviceNetwork:         suite.serviceNetwork,
+		packageContentProvider: suite.packageContentProvider,
+	})
+}
+
+func (t *serviceConfigTolerationTest) GetStarlarkCode() string {
+	toleration := fmt.Sprintf("%s(%s=%q, %s=%q, %s=%q, %s=%q, %s=%v)",
+		service_config.TolerationTypeName,
+		service_config.KeyAttr,
+		testTolerationKey,
+		service_config.OperatorAttr,
+		v1.TolerationOpEqual,
+		service_config.ValueAttr,
+		testTolerationValue,
+		service_config.EffectAttr,
+		v1.TaintEffectNoSchedule,
+		service_config.TolerationSecondsAttr,
+		testTolerationSeconds,
+	)
+	return fmt.Sprintf("%s(%s=%q, %s=[%s])",
+		service_config.ServiceConfigTypeName,
+		service_config.ImageAttr, testContainerImageName,
+		service_config.TolerationsAttr, toleration)
+}
+
+func (t *serviceConfigTolerationTest) Assert(typeValue builtin_argument.KurtosisValueType) {
+	serviceConfigStarlark, ok := typeValue.(*service_config.ServiceConfig)
+	require.True(t, ok)
+
+	serviceConfig, interpretationErr := serviceConfigStarlark.ToKurtosisType(
+		t.serviceNetwork,
+		testModuleMainFileLocator,
+		testModulePackageId,
+		t.packageContentProvider,
+		testNoPackageReplaceOptions)
+	require.Nil(t, interpretationErr)
+	expectedTolerations := []v1.Toleration{{Key: testTolerationKey, Operator: v1.TolerationOpEqual, Value: testTolerationValue, Effect: v1.TaintEffectNoSchedule, TolerationSeconds: &testTolerationSeconds}}
+	expectedServiceConfig, err := service.CreateServiceConfig(
+		testContainerImageName,
+		nil,
+		nil,
+		map[string]*port_spec.PortSpec{},
+		map[string]*port_spec.PortSpec{},
+		nil,
+		nil,
+		map[string]string{},
+		nil,
+		nil,
+		0,
+		0,
+		service_config.DefaultPrivateIPAddrPlaceholder,
+		0,
+		0,
+		map[string]string{},
+		nil,
+		expectedTolerations,
+	)
+	require.NoError(t, err)
+	require.Equal(t, expectedServiceConfig, serviceConfig)
+	require.Equal(t, expectedTolerations, serviceConfig.GetTolerations())
+}
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/static_constants.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/static_constants.go
index c32daa7cb..9c3138eea 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/static_constants.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/static_constants.go
@@ -117,4 +117,8 @@ var (
 		testServiceConfigLabelsKey1: testServiceConfigLabelsValue1,
 		testServiceConfigLabelsKey2: testServiceConfigLabelsValue2,
 	}
+
+	testTolerationKey     = "test-key"
+	testTolerationValue   = "test-value"
+	testTolerationSeconds = int64(64)
 )
diff --git a/internal_testsuites/starlark/taints_and_tolerations/taints_and_toleration_test.star b/internal_testsuites/starlark/taints_and_tolerations/taints_and_toleration_test.star
new file mode 100644
index 000000000..4530a4f70
--- /dev/null
+++ b/internal_testsuites/starlark/taints_and_tolerations/taints_and_toleration_test.star
@@ -0,0 +1,21 @@
+def run(plan, with_tolerations=True):
+    if with_tolerations:
+        config = ServiceConfig(
+            image="kurtosistech/example-datastore-server",
+            tolerations=[
+                Toleration(
+                    key="foo", value="bar", operator="Equal", effect="NoSchedule"
+                )
+            ],
+            ports={"grpc": PortSpec(number=1323, transport_protocol="TCP")},
+        )
+    else:
+        config = ServiceConfig(
+            image="kurtosistech/example-datastore-server",
+            ports={"grpc": PortSpec(number=1323, transport_protocol="TCP")},
+        )
+
+    plan.add_service(
+        name="test-data-store",
+        config=config,
+    )